How do I "git blame" a deleted line?
Asked Answered
V

6

625

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

Violate answered 10/12, 2010 at 0:5 Comment(4)
There's a followup with an answer clarifying that git log -S<string> /path/to/file wants a -c or -cc as well to show removals during merge (conflicts)Disavow
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 :)Disavow
I wish git blame would have an option to show deleted lines (with perhaps strikethrough or red text) with the revision in which they were deleted.Kickshaw
Would that be hard to write? I don't know much about Git internals.Violate
F
786

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

Ferocious answered 10/12, 2010 at 0:26 Comment(13)
Brilliant...just what i needed in this porting job I'm working on +1Vat
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!Burgoyne
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.)Demonstration
@Demonstration Merge commits generally have special diff handling, so yeah, that's probably what you ran into.Ferocious
@Demonstration To show the commits from merge, use the -c option additionally.Joycejoycelin
@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.Ferocious
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.2Delvecchio
@Aerovistae It's in the current version and it was there in 2010. 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.Ferocious
Keep in mind that -S<string> 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<regex>.Lowering
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.Tubing
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.phpHoneybunch
@Demonstration 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.Elinoreeliot
@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 lotGetraer
H
159

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
Hux answered 28/6, 2012 at 12:50 Comment(2)
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?Abstriction
@Abstriction , for that you can use blame --reverse --first-parent, it is slightly better.Flagstone
A
33

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.

Anastasia answered 20/12, 2018 at 14:3 Comment(0)
S
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

Shilling answered 10/3, 2017 at 0:15 Comment(2)
Can you explain the logic in the case where the following revision is a merge commit?Anora
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).Shilling
E
7

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"

Encrimson answered 23/9, 2019 at 22:27 Comment(1)
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.Neurovascular
S
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.

Simply answered 11/4, 2022 at 0:49 Comment(1)
Unfortunately, it doesn't seem to work when the line is modified by a merge commit compared to its second parent.Simply

© 2022 - 2024 — McMap. All rights reserved.