613

git blame is great for modified and added lines, but how can I find when a line that existed in a specific previous commit was eventually deleted. I'm thinking bisect, but I was hoping for something handier.

(Before you ask: in this case, I just did a git log -p and searched through for the code line and (a) some idiot had just deleted the vital line in the previous commit and (b) I was that idiot.)

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Michael Lorton
  • 43,060
  • 26
  • 103
  • 144
  • 6
    There's a [followup](http://stackoverflow.com/q/12591247/923794) with an answer clarifying that `git log -S /path/to/file` wants a `-c` or `-cc` as well to show removals during merge (conflicts) – cfi Jul 04 '13 at 12:16
  • 3
    It should be `-c` and `--cc`. @Steen: Correct, thanks for pointing out! Stupid oversight. Wish I could edit the comment. Adding a new one, then deleting mine, then you delete yours is all too cumbersome I guess :) – cfi Sep 15 '15 at 07:06
  • 5
    I wish `git blame` would have an option to show deleted lines (with perhaps [strikethrough](http://stackoverflow.com/q/8357203/60075) or red text) with the revision in which they were deleted. – Craig McQueen Jul 06 '16 at 00:46
  • Would that be hard to write? I don't know much about Git internals. – Michael Lorton Jul 06 '16 at 06:28

6 Answers6

769

If you know the contents of the line, this is an ideal use case for:

git log -S <string> path/to/file

which shows you commits which introduce or remove an instance of that string. There's also the -G<regex> which does the same thing with regular expressions! See man git-log and search for the -G and -S options, or pickaxe (the friendly name for these features) for more information.

The -S option is actually mentioned in the header of the git-blame manpage too, in the description section, where it gives an example using git log -S....

Randall Ma
  • 10,486
  • 9
  • 37
  • 45
Cascabel
  • 479,068
  • 72
  • 370
  • 318
  • Brilliant...just what i needed in this porting job I'm working on +1 – jkp Jan 30 '11 at 11:56
  • 57
    After using Git for 1+ year, it still amazes me to see that Git *always* have an command/option somewhere to address almost any usage scenario I have. Thanks for sharing this one, it is exactly what I need now! – Pascal Bourque Jun 09 '11 at 17:56
  • 26
    This method has worked for me before, but just now I saw a case where it did not find the commit where the line was deleted. It turned out the line in question was deleted in a merge commit -- would that explain the failure? (The `git blame --reverse` method found it though.) – antinome Jun 19 '13 at 21:59
  • @antinome Merge commits generally have special diff handling, so yeah, that's probably what you ran into. – Cascabel Jun 19 '13 at 22:42
  • 11
    @antinome To show the commits from merge, use the `-c` option additionally. – yunzen Mar 25 '14 at 13:48
  • @Aerovistae Which manpage? There's a brief mention in the `git-blame` manpage, pointing you to pickaxe + git-log (the last two paragraphs in the description section). Then the manpages for `git-log` and `git-diff` both have full descriptions. – Cascabel May 19 '14 at 17:33
  • 2
    I did a ctrl+f on "-s" on the manpage and found nothing. Where on the page do you see it?? I'm using git 1.8.5.2 – temporary_user_name May 19 '14 at 19:14
  • @Aerovistae It's in the current version and it was there in 2010. http://git-scm.com/docs/git-blame Like I said, last two paragraphs of the description section (the -S is in the block quote example). But the full docs are in git log. This is just because the OP asked about blame, and I wanted to point out how you might have found it from there. – Cascabel May 19 '14 at 19:23
  • Keep in mind that `-S` does not track in-file moves, because it only compares the number of occurrences. If your code was removed by placing it in a comment, then rather use `-G`. – Peet Brits Apr 12 '16 at 09:26
  • 1
    Just a quick note that you don't have to run the command against a single file. `git log -S ... path/to/directory` works fine. – Florian Brucker Jun 27 '18 at 12:17
  • We just had a PR merged that had many other merge commits in it, and it reintroduced an old line of code. the last change the pickaxe would show was when we removed the line of code, until I finally found out that the --first-parent option was needed to show the diff compared to the then HEAD. git log -m --first-parent -p -Gfoobar baz.php – David Mann Feb 04 '19 at 19:59
  • @antinome It didn't find the commit for me as well, so I used your approach of reverse blame, to find the commit in which it last existed and searched for its next commit in the `git log` output. – haridsv Apr 22 '19 at 05:27
  • @Cascabel, what if the String after ' -S' is too common code like '}' or 'i =1;' and so on? I'm trying to retreive commits that only have delete lines with other ones, I tried use diff to get delete lines `git -C $repo_path diff $commit_range -- $file_path | egrep '^[-][^-]'` and log -S `git -C $repo_path log $commit_range --oneline --pretty=tformat:%H -S'$delete_lines' $file_path` to get the commit id, the idle result is get one commit id for each change lines, but some 'common code' return a lot – butter Nov 05 '21 at 08:27
154

I think what you really want is

git blame --reverse START..END filename

From the manpage:

Walk history forward instead of backward. Instead of showing the revision in which a line appeared, this shows the last revision in which a line has existed. This requires a range of revisions like START..END where the path to blame exists in START.

With git blame reverse, you can find the last commit the line appeared in. You still need to get the commit that comes after.

You can use the following command to show a reversed git log. The first commit shown will be the last time that line appears, and the next commit will be when it is changed or removed.

git log --reverse --ancestry-path COMMIT^..master
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Chronial
  • 66,706
  • 14
  • 93
  • 99
  • 14
    If there are multiple merges from the branch where the line was added into the branch where the line is missing (or any other case where there are multiple paths in the lineage from START to END), `git blame --reverse` will show the revision before the merge that was chronologically last, not the revision before the initial merge where the decision was made to not take the line. Is there some way to find the earliest revision where the line stopped existing rather than the most recent one? – rakslice Oct 17 '13 at 20:46
  • 2
    @rakslice , for that you can use blame --reverse --first-parent, it is slightly better. – max630 Jul 17 '17 at 11:10
30

Just to complete Cascabel's answer:

git log --full-history -S <string> path/to/file

I had the same problem as mentioned here, but it turned out that the line was missing, because a merge commit from a branch got reverted and then merged back into it, effectively removing the line in question. The --full-history flag prevents skipping those commits.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
estani
  • 24,254
  • 2
  • 93
  • 76
11

git blame --reverse can get you close to where the line is deleted. But it actually doesn't point to the revision where the line is deleted. It points to the last revision where the line was present. Then if the following revision is a plain commit, you are lucky and you got the deleting revision. OTOH, if the following revision is a merge commit, then things can get a little wild.

As part of the effort to create difflame I tackled this very problem so if you already have Python installed on your box and you are willing to give it a try, then don't wait any longer and let me know how it goes.

https://github.com/eantoranz/difflame

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
eftshift0
  • 26,375
  • 3
  • 36
  • 60
  • Can you explain the logic in the case where the following revision is a merge commit? – Nasif Imtiaz Ohi Dec 28 '21 at 22:32
  • 1
    Can't remember the details right now *(and I even think that I need to correct some corner cases in difflame) but if the following revision is a merge commit, then you need to track down the other parents of the merge revision to see where the line was deleted (and also consider that it might have been deleted in the merge revision itself, independently of what the parents had). – eftshift0 Dec 29 '21 at 00:35
6

For changes hidden in merge commits

Merge commits automatically have their changes hidden from the Git log output. Both pickaxe and reverse-blame did not find the change. So the line I wanted had been added and later removed and I wanted to find the merge which removed it. The file git log -p -- path/file history only showed it being added. Here is the best way I found to find it:

git log -p -U9999 -- path/file

Search for the change, then search backwards for "^commit" - the first "^commit" is the commit where the file last had that line. The second "^commit" is after it disappeared. The second commit might be the one that removed it. The -U9999 is meant to show the entire file contents (after each time the file was changed), assuming your files are all max 9999 lines.

Finds any related merges via brute force (diff each possible merge commit with its first parent, run against tons of commits)

git log --merges --pretty=format:"git diff %h^...%h | grep target_text" HEAD ^$(git merge-base A B) | sh -v 2>&1 | less

(I tried restricting the revision filter more, but I ran into problems and don't recommend this. The add/removal changes I was looking for were on different branches which were merged in at different times and A...B did not include when the changes actually got merged into the mainline.)

Show a Git tree with these two commits (and a lot of the complex Git history removed):

git log --graph --oneline A B ^$(git merge-base A B) (A is the first commit above, B is the second commit above)

Show history of A and history of B minus history of both A and B.

Alternate version (seems to show the path more linearly rather than the regular Git history tree - however I prefer the regular git history tree):

git log --graph --oneline A...B

Three, not two dots - three dots means "r1 r2 --not $(git merge-base --all r1 r2). It is the set of commits that are reachable from either one of r1 (left side) or r2 (right side), but not from both." - source: "man gitrevisions"

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Curtis Yallop
  • 6,696
  • 3
  • 46
  • 36
  • This ended up being the one that helped me track down a change. One caveat to keep in mind: the line I was looking for was removed when the file had a different name, so I had to figure out what that was and use that when searching. – CTS_AE Aug 17 '22 at 23:00
2

If you prefer a GUI, the freeware DeepGit is good for this. While in the Blame view for the old revision of the file, select the lines of interest by dragging over the line numbers in the left margin. The file's log at the top is filtered to only show commits relevant to those lines. The top commit would be their deletion.

Or, if viewing a later revision of the file where the lines are already missing, select the lines that surrounded the deleted lines by dragging over the left margin. The commit log above is filtered to those lines and will include the commit that deleted the lines in between those selected. Click the Diff button and click through the filtered revisions to find the deletion.

RyanCu
  • 516
  • 4
  • 12
  • Unfortunately, it doesn't seem to work when the line is modified by a merge commit compared to its second parent. – RyanCu May 23 '23 at 04:16