Reputation: 64709
I'm trying to identify the last merge into my master git branch, but the output of git log
makes no sense.
The very last commands I ran were git checkout master; git merge staging
, yet if I run git log
, the first entry reads:
Merge branch 'staging' into DEVBRANCH-123
This is quite bewildering. Why does the merge into my master branch not mention the master branch, but instead a completely unrelated branch? Searching all of my git logs, nothing actually mentions the master branch. Why is this?
Upvotes: 0
Views: 251
Reputation: 487755
If git merge
did a "fast forward" instead of actually doing a merge, this leaves no trace in the graph. It does leave traces in reflogs, and you can use these to discover them after the fact—but reflog entries eventually expire, wiping away the traces.
The git log
command uses the graph to show you each commit, so if there is no trace of the fast-forward-not-a-merge "merge", it does not show you the fast-forward-not-a-merge "merge". By default, git log
ignores reflogs entirely (for good reason: the reflog is mainly intended for mistake-recovery and showing it would normally make the view even worse).
You probably should have done git merge --no-ff
; see the last section below. You can still do that, by finding (via the reflog) where master
pointed before Git did its fast-forward operation.
"Fast forward" is one of the more confusing parts of Git. To understand when and why Git can do a fast-forward, we need to look at how Git implements branching and what branch names really are/do, and how merge works.
A branch name, like master
or staging
, is just a short, human-readable name that stands in for one single commit. This commit is the tip commit of a branch. When we make a new commit, Git adds the commit to the tip of the current branch (whose name is stored in HEAD
) and then makes that branch name point to the new tip. The new tip commit, in turn, points back to what was the tip just a moment ago, before we added the new commit:
... <-F <-G <-- branch
becomes:
... <-F <-G <-H <-- branch
Each commit "points back" to its immediate ancestor: its parent commit. This gives Git the ability to follow the history of commits, which is made up of those commits themselves. Commit H
contains the ID of commit G
; commit G
contains the ID of F
; and so on back to the very first commit (probably A
), which has no parent, because it was the very first commit.
To get started in this history-following, Git needs the branch name, which "points to" (contains the ID of) the current tip.
When you create a new branch and develop on it, the commits themselves create the branch-y part of the graph. Let's start with:
A--B--C <-- master
and then create a new branch:
A--B--C <-- master, branch (HEAD)
and then add a new commit:
A--B--C
A--B--C <-- master \ D <-- branch (HEAD)
Now that we have two branch names (master
and branch
) we need to know which one we're using, hence the (HEAD)
. That's the branch that changes, to point to the new commit, when we make the new commit.
Now, suppose we have a bunch of different commits on two different branches, starting from some common starting point:
...--C--G--H <-- master (HEAD)
\
D--E--F <-- branch
and we run git merge branch
. Git will find the common starting point C
. This is the merge base.
Git will now look to see "what we did" (git diff --find-renames <commit-C> <commit-H>
, and "what they did" (git diff --find-renames <commit-C> <commit-F>
. The merge action, to merge as a verb, combines these changes.
Whatever we did to some files from C
, Git tries to take our changes. Whatever they did to files from C
, Git tries to take their changes. If neither of us changed some files from C
, Git just carries the files forward. If we both touched the same files, Git has to combine those changes in those files (and we might get a merge conflict). If all goes well, though, Git now makes a new commit:
...--C--G--H---M <-- master (HEAD)
\ /
D--E--F <-- branch
The new commit goes on the current branch as usual, so that master
points to the new tip; but this new tip commit has a special property: it has two parents, not just the usual one. The first parent is the usual one, the previous tip of master
. The second parent is the tip of the other branch.
This commit is a merge commit. It's like any other commit—it has a log message, usually something kind of useless like merge branch 'branch'
, and an author name and email and time-stamp, and it records as its snapshot all the merged files. But it's a special kind of commit: a two-parent commit. This is a merge commit, using the word merge as an adjective. We often shorten it even more: it is a merge, merge-as-a-noun.
Now that we know how a normal or "real" merge works, let's look at when Git can and does do a fast-forward instead.
Suppose that, instead of:
...--C--G--H <-- master (HEAD)
\
D--E--F <-- branch
we just have:
...--C <-- master (HEAD)
\
D--E--F <-- branch
If we now run git merge branch
, Git needs to combine all the changes we made on master
since commit C
, with all the changes they made on branch
since commit C
. But wait, master
is commit C
. The diff from C
to C
is going to be empty! There's no need for merge-as-a-verb after all. We can just make the name master
point directly to commit F
!
...--C
\
D--E--F <-- master (HEAD), branch
This is a fast-forward operation. Git makes no new commit, it just moves the label master
forward, as fast as possible, across the chain of commits to the new branch tip. Now master
and branch
point to the same commit and we can toss out the kink in the graph:
...--C--D--E--F <-- master (HEAD), branch
Since git log
is going to look at the graph, starting from the current branch tip (the HEAD
), and we didn't make any new commit, we'll just view the commits starting from branch
, which after all points to the same commit.
(The reason you're seeing a merge at staging
is that the tip of staging
is itself a merge—a commit with two parents. This is the noun form of "merge", made because Git had to make one.)
You can prohibit git merge
from using a fast-forward non-merge operation: just give it --no-ff
as a command line option. Git will then see that it could do a fast forward, but won't do it: it will make a new merge commit, with author and timestamp and log message. The new commit's source code will come from the other branch tip, and the new commit will be a merge commit, with two parents:
...--C---------M <-- master (HEAD)
\ /
D--E--F <-- branch
In general, some merges should be done with --no-ff
. Merges from a staging branch into a master or release branch tend to be like this.
To undo a fast-forward, start by seeing if you can find out where the branch name pointed before the fast-forward. In this case:
git reflog master
will show you where master
pointed over time, as far back as your master
reflog entries go anyway. (Reflog entries stick around, by default, for at least 30 days, and at least 90 days for some cases, so you should have plenty of time here.)
Note that it's important that no new commits have been added to the branch since the fast-forward, since we're going to "rewind" things. Let's say, for instance, that the reflog shows that master
pointed to commit C
earlier.
You can now use git reset --hard <hash-id-of-C>
to move the current branch ("read HEAD
to get the branch name" => master
) so that it points to the same place it pointed-to before. The --hard
here means "wipe out the contents of the work-tree and staging-area, replacing them with the commit we moved to" (so don't do this unless those are all clean as well). The end result is that you're back to the situation as it was before you did the fast-forward merge in the first place. (Well, we can hope that staging
did not acquire new commits; or if it did, there are further workarounds.)
Now you can run git merge --no-ff staging
and force a real merge, and get the graph we drew for the --no-ff
case.
Upvotes: 2
Reputation: 364
See what happend using this command:
git reflog
And all history tree:
git log --graph
Upvotes: 0