Cerin
Cerin

Reputation: 64709

How to identify the last git merge into master?

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

Answers (2)

torek
torek

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.

Understanding real merges

"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.

Understanding fast-forward

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.)

What if you don't want a fast-forward?

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

oginski
oginski

Reputation: 364

See what happend using this command:

git reflog

And all history tree:

git log --graph

Upvotes: 0

Related Questions