14

I made a fix to a file, and committed it to my git repository.

Some time later, I found that it had regressed. I wanted to know in which commit the fix had been taken out, so I tried git bisect and git log --all -S TERM, where "TERM" was a string added by my fix and which appeared nowhere else in the project.

I found that git bisect blamed an unrelated commit, and I found that git log --all -S TERM only listed the commit in which I added TERM, and didn't list any commits as having removed it, even though it was no longer in the file at "master".

After some manual searching, I found that there had been a merge commit between two branches. One branch had my fix and one did not. The merge author had picked the branch without my fix (strange, as there was no conflict in the file).

My questions are:

  • Why can git bisect not find merge changes? I don't see anything in the manpage about this limitation. Is there a different way to use it that will find the bad merge in the above scenario?
  • Why does git log --all -S TERM not list the merge commit? The manpage says it lists commits which "introduce or remove an instance of ". Does that merge commit not remove my string? Is there a different way to use it that will find the bad merge in the above scenario?
  • If git bisect and git log -S are useless in the above scenario, what is an efficient way to find the bad merge? It look me quite a long time to track down the change in question without these tools.
Rich
  • 15,048
  • 2
  • 66
  • 119
  • 1
    git bisect _can_ find merge commits but if there's an older commit (e.g. ancestor on the far side) in the range which you are bisecting that has the "broken" version of the file then obviously that will be preferred. – CB Bailey Jun 26 '14 at 12:54
  • Yes, that's probably what happened to me. Are there any workarounds to get `bisect` to find the real culprit in that case? – Rich Jun 26 '14 at 12:58
  • That's a good question, you really need to limit your bisect range to only those commits that have your fix as an ancestor... not sure – CB Bailey Jun 26 '14 at 13:07
  • You really need a combination of `--ancestry-path` from `rev-list` and `bisect`. I'm not sure if you can use both together, though. If you could, the first merge that wiped your change ought to get fingered. – CB Bailey Jun 26 '14 at 13:12
  • git reflog and git reflog --all will give you the commit hashes of the mis-placed commits. try to find using that. – StaticVariable Aug 05 '18 at 15:16

2 Answers2

5

Why does git log --all -S TERM not list the merge commit?

You must add the -m option to it:

git log -m --all -S TERM

In order to understand why a special option is needed in that case, let's first look at the operation of git log -p which must include the changes in the listed history. As you can easily check, by default for merge commits changes are NOT shown. The reason is that a change is the difference between two revisions whereas for a merge commit we have three (or more) revisions. The -m option to git log addresses that problem:

Diff Formatting

...

-m

This flag makes the merge commits show the full diff like regular commits; for each merge parent, a separate log entry and diff is generated. An exception is that only diff against the first parent is shown when --first-parent option is given; in that case, the output represents the changes the merge brought into the then-current branch.

Then, assuming that git log -S works as if by processing the diff returned by git log -p you should admit that by default the merge commits would be excluded from the results. However, fortunately you can combine the -S and -m options in git log and it works as expected.

Leon
  • 31,443
  • 4
  • 72
  • 97
  • 1
    Unfortunately this doesn't seem to work correctly for me on git 2.15.1.windows.2. The output includes merge commits that don't appear to change the given text - and the same is true of `-G`. – Robin Green Aug 04 '18 at 07:08
  • @RobinGreen Did you look at the output of `git log -m -p`? Does it make sense? Are the results of `git log -m -S` consistent with the output of `git log -m -p`? – Leon Aug 04 '18 at 08:13
  • Thanks for your answer, this is very useful! I haven't accepted it yet, as it doesn't cover `git bisect`. Also, you might want to put the command `git log -m --all -S TERM` at the top of your answer, as it is currently buried right at the bottom and not fully spelled out. – Rich Aug 13 '18 at 15:57
  • @Rich Agree. Updated. – Leon Aug 13 '18 at 16:10
  • Note that this solution doesn't work when we specify a specific file to git log (`git log -m --all -S TERM path/to/file/toSearch`): the normal commits are correctly searched, but the merge commit are not shown. I don't know why. – Jidehem Oct 10 '18 at 19:21
  • @Jidehem Specifying the `path` argument to git log makes it list only those commits in which the file `path` was modified. Are you sure that the merge commit that is not shown modifies your file? – Leon Oct 11 '18 at 08:36
  • @Leon: yes, the file is modified. Or rather: the file is kept unchanged in respect to merge parent A. However, it is modified in respect to parent B. Merge handling is tricky. – Jidehem Oct 11 '18 at 08:43
3

Solution using git bisect

git bisect start $badCommit $goodCommit
git bisect run bash -c "! git merge-base --is-ancestor $goodCommit HEAD || git grep -q $TERM"
bit bisect reset

Note: this solution use git bisect run for simplicity. You can use git bisect in "interactive" mode too. See Explanation section below.

Main idea

The idea is to restrict the commits analyzed by the bisect algorithm to the ones in the ancestry path.

The idea was suggested by CB Bailey in this comment, although I didn't find a way using git rev-list.

Explanation

For the part about git bisect, the explanation is a bit tricky. It is based on how the bisect algorithm works. The algorithm makes the assumption that all ancestors of a good commit are good. Thus, when testing a specific commit in git bisect, you must mark commits that are not descendants of the good commit as good too. This typically not the case when using only a simple grep function. You can tell if commits are descendant of another commit using git merge-base --is-ancestor $goodCommit HEAD (more details on How can I tell if one commit is an ancestor of another commit (or vice-versa)? or checking if a commit is an ancestor of another)

Jidehem
  • 1,066
  • 11
  • 18