178

I'm relatively new to Git. I used Subversion (SVN) before.

I noticed that most of the graphical Git front-ends and IDE plugins don't seem to be able to display the history of a file if the file has been renamed. When I use

git log --follow

on the command line, I can see the whole log across renames.

According to Linus Torvalds (alternative link) the --follow switch is a "SVN noob" pleaser; serious Git users don't use it:

--follow is a total hack, meant to just satisfy ex-SVN users who never knew anything about things like parenthood or nice revision graphs anyway.

It's not totally fundamental, but the current implementation of "--follow" is really a quick preprocessing thing bolted onto the revision walking logic, rather than being anything really integral.

It literally was designed as a "SVN noob" pleaser, not as a "real git functionality" thing. The idea was that you'd get away from the (broken) mindset of thinking that renames matter in the big picture.

How do the hardcore Git users get the history of a file when it was renamed? What is the 'real' way to do this?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Mike
  • 1,931
  • 2
  • 12
  • 7
  • Are you looking for something more hardcore than using git mv oldfile newfile which maintains the history for the renamed file? – David Hall Apr 21 '11 at 12:05
  • 20
    @David Hall: `git mv oldfile newfile` doesn't cause the rename to be recorded *at all* - it's just the same as deleting one file and adding another. git only works out renames and copies from the state of the tree at each commit after the fact. – Mark Longair Apr 21 '11 at 12:20
  • @Mark thanks - didn't know that. But am I correct that using the mv command gives git enough of a helping hand that the history will be intact, whereas renaming in other ways (e.g. outside of git) might break the history? – David Hall Apr 21 '11 at 12:23
  • 21
    @David Hall: If you rename the file with another tool outside git (e.g. `/bin/mv oldfile newfile`), but then do `git add newfile; git rm oldfile`, the result is indistinguishable from that of `git mv oldfile newfile`. – Mark Longair Apr 21 '11 at 12:28
  • 1
    This ideology falls apart if you ever move a file to a new repository, in which case the inability to move its *entire* history may be a major issue. Although of course there are limits to how much true history can really come with a file in a complex project. – Roman Starkov Jun 17 '14 at 22:38
  • 1
    Note: `git log --follow` improves a bit with git 2.9 (June 2016): see [my answer below](http://stackoverflow.com/a/36615639/6309) – VonC Apr 14 '16 at 06:48
  • 2
    As of v2.15, you may want to experiment with `--color-moved` when you `diff`. – Michael Jan 18 '18 at 18:01
  • the arrogance of that comment is outstanding... git might be good for some thing, but it's a complete clusterfck in many other cases, as in keeping track of history when you re-organize files – Rafa Jul 23 '19 at 08:27
  • The link to Linus' email is dead. You can see it in http://git.661346.n2.nabble.com/Git-log-can-not-show-history-before-rename-td2244341.html#a2246019 – PickBoy Sep 05 '19 at 12:32
  • Torvalds built a simple-minded snapshot-based version control program with poor support for renaming and then arrogantly dismissed the valuable ideas and expectations of people who have not only worked but even implemented systems with good rename support, insinuating that they are inexperienced people who have not thought everything through like he has. – Kaz May 28 '23 at 16:45
  • For instance, he claimed that, aha, you idiots who want renaming have not thought about the case when two branches introduce different objects by the same name and then have to merge. But I solved the problem in the Meta-CVS version control system: there would be a conflict on the directory structure that you would resolve by giving the files different names, or possibly deprecating one of the object (so it's not in the directory structure). You could combine the files into one and choose one of the two objects to have that name and content, removing the other, and such. – Kaz May 28 '23 at 16:47

6 Answers6

83

I think that the general drive behind Linus' point is that—and take this with a pinch of salt—hardcore Git users don't ever care about the history of a "file". You put content in a Git repository because the content as a whole has a meaningful history.

A file rename is a small special case of "content" moving between paths. You might have a function that moves between files which a Git user might track down with the "pickaxe" functionality (e.g., log -S).

Other "path" changes include combining and splitting files; Git doesn't really care which file you consider renamed and which one you consider copied (or renamed and deleted). It just tracks the complete content of your tree.

Git encourages "whole tree" thinking whereas many version control systems are very file-centric. This is why Git refers to "paths" more often than it refers to "filenames".

Michael
  • 8,362
  • 6
  • 61
  • 88
CB Bailey
  • 755,051
  • 104
  • 632
  • 656
  • Hi Charles, Thanks for your answer. It seems that I use git very much the same way I used SVN. Although I understand that git is very different than other version control systems, many concepts in git appear strange to me yet... I should probably finish that git book I recently bought. – Mike Apr 21 '11 at 13:26
  • 34
    Linus' point was that a "proper" gui would be able to track chunks of code across files, and he was hoping we would have such tools by now. Unfortunately, we still don't have that luxury, and --follow is still useful. – Michael Parker Oct 21 '14 at 16:54
  • 12
    Does git actually give you a solution other than `--follow` for this? – Griwes Jul 09 '15 at 10:27
  • 2
    @Griwes not currently. I suppose the "proper" way Linus is thinking of is to `log `, `show` the oldest commit in the log, manually identify where the content came from, then repeat from `log `. This would assume your commits are small for it to be practical - but then again, tiny commits is already a huge boost to quality of life. – Emil Lundberg Jun 22 '16 at 09:07
  • 24
    I would argue that "whole tree" thinking is enhanced by `--follow` being the default. What I mean is when I want to see the history of the code within a file, I really don't usually care whether the file was renamed or not, I just want to see the history of the code, regardless of renames. So in my opinion it makes sense for `--follow` to be the default because I don't care about individual files; `--follow` helps me to ignore individual file renames, which are usually pretty inconsequential. – Eddified Jul 12 '17 at 19:41
  • 3
    So... if I decide to care about the "content" instead of the file, how do I print the commits relevant to the content that's currently in this file? I would be delighted if git tracked it all across different files for me and reported a log of all the changes -- I just don't see how to get it. – Ed Avis Oct 08 '18 at 11:47
  • 3
    The problem is when the filename actually has relevance. Such as a user wondering what happened to a file that existed in a project that spans 20 years of history. When such a user attempts to upgrade to a newer version and a file they had modified years ago is not there at all, how do they find out where their local changes now need to be applied? – Dread Quixadhal Aug 22 '20 at 20:35
  • @DreadQuixadhal this is the situation I'm currently experiencing. Did you find a solution for it? – kreuzerkrieg Sep 02 '20 at 07:02
41

I have exactly the same issue that you are facing. Even though I can give you no answer, I believe you can read this email Linus wrote back in 2005, it is very pertinent and might give you a hint about how to handle the problem:

…I'm claiming that any SCM that tries to track renames is fundamentally broken unless it does so for internal reasons (ie to allow efficient deltas), exactly because renames do not matter. They don't help you, and they aren't what you were interested in anyway.

What matters is finding "where did this come from", and the git architecture does that very well indeed - much better than anything else out there. …

I found it referenced by this blog post, which could also be useful for you to find a viable solution:

In the message, Linus outlined how an ideal content tracking system may let you find how a block of code came into the current shape. You'd start from the current block of code in a file, go back in the history to find the commit that changed the file. Then you inspect the change of the commit to see if the block of code you are interested in is modified by it, as a commit that changes the file may not touch the block of code you are interested in, but only some other parts of the file.

When you find that before the commit the block of code did not exist in the file, you inspect the commit deeper. You may find that it is one of the many possible situations, including:

  1. The commit truly introduced the block of code. The author of the commit was the inventor of that cool feature you were hunting its origin for (or the guilty party who introduced the bug); or
  2. The block of code did not exist in the file, but five identical copies of it existed in different files, all of which disappeared after the commit. The author of the commit refactored duplicated code by introducing a single helper function; or
  3. (as a special case) Before the commit, the file that currently contains the block of the code you are interested in itself did not exist, but another file with nearly identical contents did exist, and the block of the code you are interested in, together with all the other contents in the file existed back then, did exist in that other file. It went away after the commit. The author of the commit renamed the file while giving it a minor modification.

In git, Linus's ultimate content tracking tool does not yet exist in a fully automated fashion. But most of the important ingredients are available already.

Please, keep us posted about your progress on this.

Michael
  • 8,362
  • 6
  • 61
  • 88
Alberto Bacchelli
  • 1,029
  • 1
  • 9
  • 9
  • Thanks for posting those articles. It wasn't until I read them that I fully grasped the idea of content history!.I have been thinking about this the wrong way! – DavidGamba Apr 13 '13 at 16:55
  • That email from Linus is great, thanks for posting this. – mik01aj Apr 12 '16 at 15:19
  • Fun fact, [Git v2.15 adds `--color-moved`](https://stackoverflow.com/a/47192896/241211) a move toward that "ideal tracking system." I was playing with it to see it track moved lines within a file, but realized accidentally that it tracks moved lines in the _entire diff._ – Michael Jan 18 '18 at 17:52
  • 2
    Linus explains a complex situation. But here we have a simple situation: a file was just renamed (or moved to another directory). Thus there should be a simple solution. I think that the issue comes from the fact than contrary to Subversion, the user cannot instruct Git at commit time where a file comes from, and that `--follow` can be wrong (e.g., if 2 files have the same contents, or if there is a modification in addition to the file move). – vinc17 Feb 14 '19 at 17:41
16

I noticed that most of the graphical git front-ends and IDE plugins don't seem to be able to display the history of a file if the file has been renamed

You'll be happy to know that some popular Git UI tools now support this. There are dozens of Git UI tools available, so I won't list them all, but for example:

  • Sourcetree, when viewing a file log, has a checkbox "Follow renamed files" in the bottom left
  • TortoiseGit has a "follow renames" checkbox on the log window in the bottom left.

More information on Git UI tools:

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Michael Parker
  • 7,180
  • 7
  • 30
  • 39
  • Source works great for when renaming once, when renaming twice, change details is not available for commits before renaming. I reported the bug here: https://jira.atlassian.com/browse/SRCTREE-5715 – Intel May 17 '18 at 08:31
  • gitk works great even when a file is renamed twice in history. The command looks like this "gitk --follow path/to/file" – Intel May 17 '18 at 08:32
  • Nice!!! Would never have spotted "Follow renamed files” in SourceTree but for your answer. – Max MacLeod Nov 13 '20 at 10:11
8

Note: git 2.9 (June2016) will improve quite a bit the "buggy" nature of git log --follow:

See commit ca4e3ca (30 Mar 2016) by SZEDER Gábor (szeder).
(Merged by Junio C Hamano -- gitster -- in commit 26effb8, 13 Apr 2016)

diffcore: fix iteration order of identical files during rename detection

If the two paths 'dir/A/file' and 'dir/B/file' have identical content and the parent directory is renamed, e.g. 'git mv dir other-dir', then diffcore reports the following exact renames:

renamed:    dir/B/file -> other-dir/A/file
renamed:    dir/A/file -> other-dir/B/file

(note the inversion here: B/file -> A/file, and A/file -> B/file)

While technically not wrong, this is confusing not only for the user, but also for git commands that make decisions based on rename information, e.g. 'git log --follow other-dir/A/file' follows 'dir/B/file' past the rename.

This behavior is a side effect of commit v2.0.0-rc4~8^2~14 (diffcore-rename.c: simplify finding exact renames, 2013-11-14): the hashmap storing sources returns entries from the same bucket, i.e. sources matching the current destination, in LIFO order.
Thus the iteration first examines 'other-dir/A/file' and 'dir/B/file' and, upon finding identical content and basename, reports an exact rename.


With Git 2.31 (Q1 2021), the file-level rename detection has been improved for diffcore.

See commit 350410f (29 Dec 2020), and commit 9db2ac5, commit b970b4e, commit ac14de1, commit 5c72261, commit 81c4bf0, commit ad8a1be, commit 00b8ccc, commit 26a66a6 (11 Dec 2020) by Elijah Newren (newren).
(Merged by Junio C Hamano -- gitster -- in commit a5ac31b, 25 Jan 2021)

diffcore-rename: accelerate rename_dst setup

Signed-off-by: Elijah Newren

register_rename_src() simply references the passed pair inside rename_src.

In contrast, add_rename_dst() did something entirely different for rename_dst.
Instead of copying the passed pair, it made a copy of the second diff_filespec from the passed pair, referenced it, and then set the diff_rename_dst.pair field to NULL.
Later, when a pairing is found, record_rename_pair() allocated a full diff_filepair via diff_queue() and pointed its src and dst fields at the appropriate diff_filespecs.

This contrast between register_rename_src() for the rename_src data structure and add_rename_dst() for the rename_dst data structure is oddly inconsistent and requires more memory and work than necessary.
[...] This patch accelerated the setup time by about 65%, and final write back to the output queue time by about 50%, resulting in an overall drop of 3.5% on the execution time of rebasing a few dozen patches.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
4

Here's how you do it:

git log -M --summary | grep rename | grep BASENAME

Just put the basename in there.

If too many results then you can also serially grep each intermediate directory name.

William Entriken
  • 37,208
  • 23
  • 149
  • 195
  • Thanks for this! For future readers: I got `warning: exhaustive rename detection was skipped due to too many files. \n warning: you may want to set your diff.renameLimit variable to at least 1800 and retry the command.` so I did `git config --global diff.renameLimit 1800` and ran the command again. – gawkface Mar 02 '22 at 06:17
  • 1
    Also, this command does not tell the commit id so I added a `--before=20` to each grep to find that in the grep context. P.S: also found that `--line-buffered` option for grep helps with not waiting for the full results in the pipe so that speeds up feedback too! – gawkface Mar 02 '22 at 06:26
2

On Linux, I have verified that SmartGit and GitEye is able to follow renames when following the history of a particular file. However, unlike gitk and GitEye, SmartGit shows a separate file view and repository view (which contains the directory structure, but not the list of files contained within).

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
prusswan
  • 6,853
  • 4
  • 40
  • 61