0

There are 3 branches on my local machine while there are more on the remote. Some of co-workers are updating another branch so I need to keep my work up to date in order to get our codes run.

Let's say the branch I am working on called branch-1 and the other updated by others called branch-2. Now I tried git pull origin branch-2 after git checkout branch-1. It shows Everything is already up to date, which doesn't make sense to me because when I compared the codes between my IDE and the codes on GitLab.

Why did it happen and how should I solve it?

Memphis Meng
  • 1,267
  • 2
  • 13
  • 34
  • 1
    Potential duplicate of https://stackoverflow.com/questions/34344034/git-pull-on-a-different-branch – Robert Oct 18 '21 at 17:22
  • I tried the accepted answer but it was rejected because of "non-fast-forward". Can we have a solution for it? – Memphis Meng Oct 18 '21 at 17:40
  • Can you elaborate on this sentence "It shows Everything is already up to date, which doesn't make sense to me because when I compared the codes between my IDE and the codes on GitLab." Can you explain what branches you are comparing and why you feel it isn't up to date? – TTT Oct 18 '21 at 18:27
  • Good catch! I mean there is still difference between the local `branch-1` and the remote `branch-2`. Note that `branch-1` was created on my own and was never pushed onto remote. – Memphis Meng Oct 18 '21 at 18:38

1 Answers1

1

If, after:

git checkout branch-1
git pull origin branch-2

all of your work had been discarded, and you had only the contents of branch-2, would you not be upset? Should you not be happy that your work has been retained?

It shows Everything is already up to date ...

You need to be aware of the following items:

  1. git pull means run a git fetch command, then run a second Git command. (The choice of second command is configurable.)
  2. git fetch does not affect any of your branches (when used normally—the link to git pull on a different branch shows a different way to use it, that can affect your branches).
  3. The second Git command is therefore where all the big important action is.

In your case, the second command is git merge, and it is git merge that prints Everything up to date and does nothing.

I recommend that Git newbies avoid git pull, as its combination of git fetch plus a second command is "too magic", interfering with understanding what's going on in Git. Splitting it into the two separate commands won't cause instant enlightenment, but given a pair of separate but both-difficult mountain roads, it's probably wiser to walk or drive your route without a blindfold. If nothing else, you'll know which road you're on when you die. Besides, these mountain roads are often pretty.

Now that you know the first few things to know—that pull = fetch + (in this case) merge, and merge is where you're seeing the odd message—it's time to review the other things you should know before using Git:

  • Git is all about commits. It's not about files, although commits do hold files. It's not about branches either, although branch names allow us (and Git) to find the commits. It's the commits that are the key.

  • Each commit is numbered. But, these numbers are (a) huge, currently as big as 2160-1 or 1461501637330902918203684832716283019655932542975, and (b) seemingly-random. They're normally expressed in hexadecimal and humans really don't use them: they're just a bunch of garbage randomness to us. That's why we use branch names. But Git needs the numbers.

  • Each commit stores two things:

    • A commit has a full snapshot of every file, saved forever (or as long as the commit itself lasts). The files inside these snapshots are stored in a special, Git-only format in which they're compressed—sometimes very compressed—and, importantly, de-duplicated. In most commits, we mostly re-use old files, meaning that these new commits take no space for the re-used files.

    • Besides the snapshot, each commit holds some metadata, or information about this commit itself. That includes the name and email address of the author, for instance. It includes some date-and-time stamps. But it also includes—for Git's own use—the raw hash ID of a previous, or parent, commit. (In fact it's a list of parent hash IDs, but most commits just store one, and that's what we'll look at here.)

  • Once it is made, no part of any commit can ever be changed, not even by Git itself. (If there is a problem with a commit—if there is something wrong with it—we have to make a new and improved commit instead. The new commit gets a new number; the old commit, with its same old number, remains.)

  • A branch name stores one commit number.

Since you have three branches (branch-1, branch-2, and perhaps main?), you are having your Git store three commit numbers. It's possible for these branch names to all store the same number, or they could be all different numbers. What we will say about them is that they point to the commits whose numbers they store, rather like this:

... <-F <-G <-H   <--branch-1

Here the name branch-1 contains the commit number of—or, shorter, points to—commit H. Meanwhile, commit H itself contains the commit number of earlier commit G, as part of H's metadata. Commit G contains the commit number of some still-earlier commit, and so on: the whole process ends only when we get back to the very first commit ever, which can't point backwards to a parent, and therefore doesn't.

When we first make a new branch, the new name points to the same commits that make up the old branch:

...--F--G--H   <-- main

becomes:

...--F--G--H   <-- main, branch-1

All of the commits are now on both branches. Both names point to, or select, the last commit on the branch: currently that's commit H. But of course, we'll now make new commits. We need to add one more thing to this drawing, which will tell us which name we're using to find commit H. For that, we'll use the special name HEAD: written in all uppercase like this, this special name is how Git knows which branch name we're using. Git "attaches" HEAD to one branch name:

...--F--G--H   <-- main (HEAD), branch-1

Here, we're on branch main, as git status will say: we're using commit H via the name main. If we run:

git switch branch-1

to change branches, we stay on commit H, but now we're getting there through the name branch-1:

...--F--G--H   <-- main, branch-1 (HEAD)

As soon as we make the new commit, something very interesting happens. The git commit command:

  • gathers metadata, including your name and email address and the current date-and-time, but also including the current commit (hash ID);
  • packages up a snapshot of all files (de-duplicated, in Git's internal format);
  • writes all of this out as a new commit, which gets a new random-looking number, but we'll just call it I: the new commit points back to the existing commit H; and
  • last—crucially—writes I's actual hash ID, whatever that is, into the current branch name, i.e., branch-1.

The result looks like this:

...--F--G--H   <-- main
            \
             I   <-- branch-1 (HEAD)

The name branch-1 now locates commit I. All commits, up through and including I, are on branch-1. Commit H is the last commit on branch main. Commit H remains on both branches.

Now, suppose you use git clone to copy all the commits (though none of the branches) from some central repository, then create one name main in your copy. Your copy will also remember the original Git's main under the name origin/main, and your new clone will create your own main pointing to this same commit:

...--G--H   <-- main (HEAD), origin/main

(Your Git created your own main so that it would have somewhere to attach HEAD. The origin/main name is a remote-tracking name that your Git uses to remember the other Git repository's branch name, as of the last time you ran git fetch or otherwise had your Git update from theirs.)

You might create your own commits on your own branch at this point:

          I   <-- branch-1 (HEAD)
         /
...--G--H   <-- main, origin/main

Your co-workers also clone and begin working; the commits they make get unique hash IDs, so we'll make up unique one-letter names for their commits too.

Eventually they will run:

git push origin branch-2

or similar. This will send their new commits to the shared (centralized) repository copy, and create or update the name branch-2 there, so that the central repository now has:

...--G--H   <-- main
         \
          J   <-- branch2

If you now run git fetch origin, your Git will see that they have a new commit J and will obtain it from them. Your Git will see that they have a new name branch2, and will create your origin/branch2 to remember it. The result in your repository looks like this:

          I   <-- branch-1 (HEAD)
         /
...--G--H   <-- main, origin/main
         \
          J   <-- origin/branch2

This can go on for multiple commits of yours and/or theirs. Eventually, though, you may want to merge their work with your work. It is now time for git merge.

How git merge works

Let's say that at this point, you have, in your repository:

          I--K   <-- branch-1 (HEAD)
         /
...--G--H
         \
          J--L   <-- origin/branch2

I've taken the names main and origin/main out of the drawing as they're not required any more (though they may well still exist): the important parts are the commits, up through K and L, and the fact that there are names by which we can find these commits (branch-1 and origin/branch2 respectively). So we can now run:

git merge origin/branch-2

Your Git will locate two commits:

  • the current or HEAD commit, which is commit K; and
  • the commit found by origin/branch2, which is commit L.

Your Git will now use these commits, and their internal embedded backwards-pointing arrows, to find the best shared commit. In this case that's commit H. Git calls this the merge base.

Because both of your branch-tip commits are descended from this common starting point, it's easy now for Git to figure out what you changed, and to figure out what they changed. To find your changes, Git runs a git diff from the merge base commit to your branch-tip commit:

git diff --find-renames <hash-of-H> <hash-of-K>

This shows which files are different and, for each different file, gives a recipe for modifying the base (commit H) version to come up with the tip (commit K) version.

Repeating this with their branch tip:

git diff --find-renames <hash-of-H> <hash-of-L>

finds which files they changed, and produces a recipe for those changes.

The merge command now merely (?) needs to combine these two sets of changes. If all goes well with this combining, Git can apply the combined changes to the files from commit H—the merge base. This has the effect of keeping your changes, but also adding their changes.

If all doesn't go well, the merge stops in the middle, putting you in charge of fixing up the mess Git leaves behind. But we will just assume that it goes well here.

Having finished combining the changes and applying them to all the files from the merge base, Git now makes a new commit. This new commit, like every commit, has a snapshot: the snapshot is the set of files produced by applying the combined changes to the files in H. Like every commit, this merge commit also has metadata: you are the author and committer, "now" is when, and you can include a log message that's better than the default "merge branch branch-2".

There is in fact only one thing special about this new merge commit, and that is that instead of just one parent, like earlier commits we've seen, it has two: the new commit points back to both the current commit K and the to-be-merged (now actually merged) commit L, like this:

          I--K
         /    \
...--G--H      M   <-- branch-1 (HEAD)
         \    /
          J--L   <-- origin/branch2

As you make more commits, they simply build onto this structure:

          I--K
         /    \
...--G--H      M--N   <-- branch-1 (HEAD)
         \    /
          J--L   <-- origin/branch2

Your branch name branch-1 now points to commit N. N points back to M, which points backwards to both K and L simultaneously. Those two point back to I and J respectively, and those two point back to H, where history rejoins.

Sometimes there is nothing for git merge to do

If you now make new a new commit O, that too just adds on:

          I--K
         /    \
...--G--H      M--N--O   <-- branch-1 (HEAD)
         \    /
          J--L   <-- origin/branch2

Suppose at this point you were to run git merge origin/branch2. What will happen?

The rule for git merge begins with finding the two branch-tip commits. Those are now O and L. The next step for most merges1 is to find the merge base of these two branch-tip commits. The merge base is defined as the best shared commit (though more technically, it is the Lowest Common Ancestor of the two commits in the DAG). That means we need to find a good commit that can be found by:

  • starting at O and working backwards and
  • starting at L and working backwards.

So let's sit at L for a moment while we work backwards from O to N to M. The next commit, one more step backwards, is both K and L. Commit L is on both branches! Commit L is therefore the best such commit, and hence it is the merge base.

Now, the next part of a true merge would be to run two git diff commands, to compare the base's snapshot against each branch tip's snapshot. But the base is the other tip commit, so this diff would be empty.

Since the merge base of this merge attempt is the other commit, Git will do nothing at all. It will say: Already up to date.

Note that this does not mean that the snapshots in O and L are the same. It is the fact that the merge base L is the other commit that is important. There is literally nothing to merge. The git merge command says so and reports success: all is done.


1git merge -s ours is the exception here: there's no need to compute a merge base to run the rest of the merge strategy. Whether the command does so anyway, so as to detect degenerate cases, I have not tested.


Fast-forward merges

It's worth mentioning here one other special case, which is the fast-forward operation. Suppose that, instead of this degenerate case:

          o--O   <-- ours (HEAD)
         /
...--o--B   <-- theirs

for which git merge says up to date, we have:

          o--T   <-- theirs
         /
...--o--B   <-- ours (HEAD)

when we run git merge theirs? Now, just as last time, the merge base is commit B. A diff from B to B, to figure out what we changed, would be empty. But a diff from B to T (their commit) would provide a recipe-of-changes that would produce the snapshot in commit T from the snapshot in commit B.

It's therefore possible for Git to do a real merge here, and if you run git merge --no-ff, Git will do that:

          o--T   <-- theirs
         /    \
...--o--B------M   <-- ours (HEAD)

By default, though,2 git merge realizes that any merge commit M would automatically have the same snapshot as commit T (theirs), so it just moves the current branch name to point to commit T:

          o--T   <-- ours (HEAD), theirs
         /
...--o--B

(There's no longer any reason to bother with the kink in the drawing. I left it in to make it clearer that the name ours moved.)

(Fast-forwarding is technically a thing that happens to the name. When using git merge or git merge --ff-only to make it happen to the current branch, though, we get a "fast-forward merge", which is really just a git checkout or git switch to the other commit that drags the branch name with it. The git push and git fetch commands have the ability to move certain names in fast-forward fashion, though.)


2There is another special case that can force a true merge as well, involving annotated tags, but it's quite rare. It is documented: search for merging an annotated.


The bottom line

If you made it all the way here, (a) congratulations! and (b) what all this tells you is that your git pull is working just fine: it's just that you already have their commits in your branch. The pull ran a fetch, which found no new commits on their branch, and which therefore did not add any such commits. The the pull ran git merge, which found that there was nothing to merge: your branch already has their commit as an ancestor, through whatever parent chain finds their commit.

That, in turn, means that whatever you have that they don't—whatever shows up in a git diff you run from their branch tip commit to your branch tip commit—is what would get merged by git merge if you were to merge your branch-tip-commit into theirs. The diff you see if you run the diff the other way is "stuff you would have to remove, to revert to theirs".

torek
  • 448,244
  • 59
  • 642
  • 775