Reputation: 7334
I do a git status
and it says
Your branch is up-to-date with 'origin/master'.
But I do a git pull
anyway and suddenly it says
14 files changed, ...
And I feel like I've been lied to. I suspect git isn't broken. Which must mean I don't understand something.
I do a bit of reading here and here and learn of two non destructive ways to ask if I'm up to date
git status -uno
git fetch --dry-run
Since fetch
is part of pull
I assume these two will disagree the same way the last two did.
What's fundamentally confusing me is I think of 'up-to-date' as meaning: "hey we've compared two copies of this repository (the master branch) and they're the same"
Ok fine, but if I can ask the question two different ways and get two different answers, when each question is about comparing two different copies then doesn't that mean there have to be at least three copies of the repository?
As in:
A == B != C
I know there is a remote copy of master
I know there is my local copy of master
What the heck is this third thing?
Upvotes: 6
Views: 7285
Reputation: 488143
To add a bit1 to Tim Biegeleisen's answer, git status
works by performing two diffs plus also comparing your current HEAD
to its upstream.
Here's the complete(...ish) picture.
Given a remote repository R, git fetch
copies from every branch it sees on R
—you can see what it sees by running git ls-remote R
—and renames them in the process. For branches B1, B2, and B3, your Git creates or updates remote-tracking branches R/B1, R/B2, and R/B3. (More precisely, these are references whose name starts with refs/remotes/
and then continues on to name the same remote R, e.g., origin/
, and then the branch name. This guarantees that these references never collide with your own local branches, which start with refs/heads/
: your master is refs/heads/master
while that copied from remote origin
is refs/remotes/origin/master
.
(Your Git may also bring over tags, depending on flags you give to git fetch
. The default is slightly complicated: it brings over tags for any commits it brings over while bringing over branches. With --no-tags
it skips tags entirely, and with --tags
it brings over all tags. Tags, unlike branches, do not have special per-remote name spaces: your tag T1
is really refs/tags/T1
, and if your Git brings over a tag T2
from remote R, it just names it refs/tags/T2
. If two tags collide, your Git defaults to ignoring the extra one, i.e., if you already have a T2
, your Git drops their T2
on the floor.2)
In order to bring over these branches (and maybe tags), your Git must bring over the commits (and any other objects) they point-to, as identified by the SHA-1 hashes you will see in that same git ls-remote
output. To get a commit, your Git has to get any trees and blobs to which that commit object points. Your Git and their Git therefore have a conversation, leading to the object counting and compressing and so on that you see: your Git already has some set of objects and yours and theirs simply work to see what you have in common, to determine how best to get you the ones you don't have yet.
All of these objects get inserted into your repository. At this point, they are pointed-to by your remote-tracking branches, such as origin/master
. If you now run git status
, it can—and does—work entirely locally.
Let's say you're on your own master
. In this case, your HEAD
reference simply contains the string ref: refs/heads/master
.3 This is in fact how Git knows that you are on branch master
. Meanwhile, Git stores, under .git/config
, some extra data to record that your local master
has origin/master
(really refs/remotes/origin/master
; Git just abbreviates a lot) as its upstream.
So, git status
discovers that you are on master
and also looks up origin/master
. These two names—refs/heads/master
and refs/remotes/origin/master
—point to two commit IDs. Those commit IDs may be the same, or may be different. If they are the same, the two branches are in sync. If they differ, the two branches differ. One may contain more commits than the other—so that one is strictly ahead and the other strictly behind—or they may have some commits that are different on both branches, and some commits that are common to both.
(This is where Git's terminology breaks down: does "branch" mean "branch name", like master
? Or does it mean "the set of all commits reachable by starting at the branch's tip-most commit and working back through history"? The answer is that it means both, and we are supposed to figure out which meaning to use.)
To get the ahead 3
and/or behind 5
count, git status
uses git rev-list --count
:
git rev-list --count origin/master..master
git rev-list --count master..origin/master
This two-dot syntax means "find the set of all commits reachable from the identifier on the right, and subtract away the set of all commits reachable from the identifier on the left". Suppose, for instance, master
is strictly ahead of origin/master
. We can draw the commit chain like this:
... <- o <- o <- o <-- origin/master
\
o <-- master
Here there is one commit on master
that is not on origin/master
. All commits on origin/master
are on both branches: both the local branch and the remote-tracking branch. But there is one commit on master
that is not on origin/master
.
When git fetch
obtains new commits, those new commits normally point back to existing commits. So if git fetch
picks up one new commit on origin/master
, the picture changes:
... <- o <- o <- o <- o <-- origin/master
\
o <-- master
Now neither branch is strictly behind, and you will probably want to merge or rebase your work.
Meanwhile, git status
also compares:
HEAD
commitThe (single, distinguished) index contains the set of all files that will go into the next commit you can make. When you git add
new contents for existing files, this replaces the existing file in the staging area. (Strictly speaking, the index contains only the hash, plus necessary stuff like the file's path, plus a bunch of cache information to speed up git status
and git commit
. The add
step actually copies the file into the repository, computing its hash, at add
time, and stores the new hash into the index.) Adding a totally new file adds a new entry, and removing an existing file with git rm
adds a special "erase" or "white-out" entry so that Git knows not to put that file into the next commit.
When git status
shows you what is staged for commit, it does so by diff-ing the index against HEAD
.
When git status
shows you what is not staged for commit, but could be, it does so by diffing the work-tree against the index.
1OK, a lot.
2In a bug in pre-1.8.4 or so versions of Git, tags could change as long as they moved in a fast-forward manner, which is the same rule applied by default during git push
. I think this did not happen on fetch
though.
3HEAD
is a reference, just like refs/heads/master
and refs/remotes/origin/master
, but with some special handling. In particular, HEAD
is normally an indirect reference, with that ref:
prefix stuff. While any reference can be indirect, HEAD
is the only useful indirect, at least currently (Git version 2.8.x). Furthermore the only useful indirect values are to regular local branches. When HEAD
contains the name of a local branch, git status
says that you are "on" that branch.
Checking out a commit by its SHA-1 hash ID, or using --detach
, puts the raw ID into HEAD
. In this case git status
claims that you are not on any branch. In fact, you are on the (single) anonymous branch: new commits you make go into the repository as usual, but are known only by the special name HEAD
, and if you check out some other branch, the IDs of those commits become somewhat difficult to retrieve. (They're still stored in the reflog for HEAD
, until those reflog entries expire; after that point, they are eligible for being garbage-collected by git gc
.)
Upvotes: 3
Reputation: 521178
When Git says Your branch is up-to-date with 'origin/master'
it is comparing your local master
branch against the remote tracking branch called origin/master
. This tracking branch also exists locally and is what is actually used when you update your local branch. And when you do a git fetch
, all the remote tracking branches get updated.
Your local master
branch was current with the tracking branch, but this does not mean that the local branch is current with the actual master
branch on the repository. Had you done a git fetch
after your initial call to git status
, you would have a seen a message saying that your local master
branch was "behind" origin/master
. Doing a git pull
means updating the tracking branch origin/master
, and then merging that into your local branch to sync everything.
Here is a simplistic diagram showing the flow of information from the remote to your local branch:
remote master (repo) --> origin/master (local tracking branch) --> master (local branch)
Upvotes: 6