Git status doesn’t know if your local repository is out of date

To setup the scenario that will be demonstrated in this blog article, a new commit has been pushed to the dev branch of my PowerShell repository on GitHub from a computer named PC01. Then I switched over to using to an alternate computer named PC02 that was up to date prior to that latest commit being pushed to GitHub from PC01. This means that the dev branch of the PowerShell repository on PC02 is one commit behind the remote origin on GitHub.

You would think that checking the status of the dev branch for the PowerShell repository on PC02 would show it as being out of date by one commit, but it doesn't. It says "Your branch is up-to-date with 'origin/dev'":

1git status

git-outofdate1a.png

Everything looks correct when double checking the settings for the remote origin:

1git remote -v

git-outofdate2a.png

The simplest way to know if the branch is out of date is to run the following command:

1git remote show origin

git-outofdate3a.png

You could also use the fetch command with the dry run option, but I prefer the previous command.

1git fetch -v --dry-run

git-outofdate4a.png

I'm going to disable the network card on PC02 to show you a secret about checking the status of your Git repository:

1Get-NetAdapter -Name vEthernet* | Disable-NetAdapter -PassThru -Confirm:$false

git-outofdate21a.png

Most Git commands run against your local repository which is one of the reasons that Git is so fast and it's also one of the strengths of Git due to its ability to allow you to work while being disconnected from the network. The problem this creates though, is that checking the status of your Git repository doesn't actually communicate with the remote origin to tell you what the current status is. The status is based off of the last time a fetch or pull was performed which is why checking the status doesn't necessarily provide accurate information.

Notice that no errors are generated when checking the status of my PowerShell repository while being disconnected from the network:

1git status

git-outofdate1a.png

An error is generated when I run the command that I previously recommended that actually reports the correct status of whether or not my local repository is out of date with the remote origin one:

1git remote show origin

git-outofdate22a.png

If you're like me, you want to know a little more about what's going on under the hood. I fired up process monitor and used the file monitor portion of it to determine that Git status uses the SHA1 hash that's stored in the following file to know what the latest commit is for the dev branch of my PowerShell repository on the remote origin:

1Get-Content -Path .\.git\refs\remotes\origin\dev

git-outofdate23a.png

That file is only updated during a fetch or pull which explains why Git status has no idea that a more recent commit has been added to the remote origin.

My point about most git commands running locally has been made so I'll re-enable the network card on PC02:

1Get-NetAdapter -Name vEthernet* | Enable-NetAdapter -PassThru

git-outofdate29a.png

Now to leverage my existing PowerShell skills along with some Git commands to work a little magic. First, I'll return the SHA1 hash of the most recent dev branch commit for my local PowerShell repository and store it in a variable named localCommit. Then I'll return the same thing for the remote origin and store it in a variable named remoteCommit. I'll compare the two to see if they're equal. Next I'll run a command so if they're not equal, run git fetch. Lastly, I'll run Git status which now shows that my local repository is indeed out of date by one commit:

1($localCommit = git rev-list --all -n1)
2($remoteCommit = (git ls-remote origin dev) -replace '\s.*$')
3$localCommit -eq $remoteCommit
4if ($localCommit -ne $remoteCommit) {git fetch}
5git status

git-outofdate24a.png

The previous example could easily be turned into a function that would not only check the status, but fetch the changes if your local repository is indeed out of date:

 1function Update-MrGitRepository {
 2
 3    [CmdletBinding(SupportsShouldProcess,
 4                   ConfirmImpact='Medium')]
 5    param ()
 6
 7    $Location = Get-Location
 8
 9    if (Get-GitDirectory) {
10        $Repository = Split-Path -Path (git.exe rev-parse --show-toplevel) -Leaf
11    }
12    else {
13        throw "$Location is not part of a Git repsoitory."
14    }
15
16    if ((git.exe remote) -contains 'origin') {
17        $originURL = (git.exe remote -v) -match '^origin.*fetch\)$' -replace '^origin\s*|\s*\(fetch\)$'
18    }
19    else {
20        throw "Origin not setup for Git '$Repository' repository"
21    }
22
23    if ((Invoke-WebRequest -Uri $($originURL) -TimeoutSec 15).StatusCode -ne 200) {
24        Write-Warning -Message "Unable to communicate with remote origin '$originURL'"
25    }
26    else {
27        $currentBranch = git.exe symbolic-ref --short HEAD
28        $localCommit = git.exe rev-list --all -n1
29        $remoteCommit = (git.exe ls-remote origin $currentBranch) -replace '\s.*$'
30
31        if ($localCommit -ne $remoteCommit){
32
33            if ($PSCmdlet.ShouldProcess($currentBranch,'Fetch')) {
34                git.exe fetch
35            }
36
37        }
38        else {
39            "Local '$currentBranch' branch of the '$Repository' repository is already up-to-date with '$originURL'."
40        }
41
42    }
43
44}

The current version of the Update-MrGitRepository function can be found in my Git repository on GitHub.

Now you can see that the SHA1 hash stored in the file that I referenced earlier is the same as the latest commit for both the local and remote repositories:

1Get-Content -Path .\.git\refs\remotes\origin\dev
2($localCommit = git rev-list --all -n1)
3($remoteCommit = (git ls-remote origin dev) -replace '\s.*$')
4$localCommit -eq $remoteCommit

git-outofdate25a.png

You might be wondering why my local repository is still one commit behind? That's because although I fetched the changes, I didn't merge them. Now that I have the changes downloaded, you can see the commit that the local dev branch is pointing to along with the one that the remote one points to:

1git status
2git log --decorate --oneline --all -n6

git-outofdate26a.png

I can simply merge those changes in and now my local dev branch points to the same commit as the remote one:

1git merge
2git status

git-outofdate27a.png

Let's take one last look at the log to make sure that both the local and remote repositories point to the same commit:

1git log --decorate --oneline --all -n6

git-outofdate28a.png

As you can see, they do. I could have also used git pull to run both git fetch and git merge with one command.

µ