Is it possible to move/rename files in Git and maintain their history?

前端 未结 14 2170
梦如初夏
梦如初夏 2020-11-22 04:42

I would like to rename/move a project subtree in Git moving it from

/project/xyz

to

/components/xyz

If I

相关标签:
14条回答
  • 2020-11-22 05:35

    No.

    The short answer is NO. It is not possible to rename a file in Git and remember the history. And it is a pain.

    Rumor has it that git log --follow --find-copies-harder will work, but it does not work for me, even if there are zero changes to the file contents, and the moves have been made with git mv.

    (Initially I used Eclipse to rename and update packages in one operation, which may have confused Git. But that is a very common thing to do. --follow does seem to work if only a mv is performed and then a commit and the mv is not too far.)

    Linus says that you are supposed to understand the entire contents of a software project holistically, not needing to track individual files. Well, sadly, my small brain cannot do that.

    It is really annoying that so many people have mindlessly repeated the statement that Git automatically tracks moves. They have wasted my time. Git does no such thing. By design(!) Git does not track moves at all.

    My solution is to rename the files back to their original locations. Change the software to fit the source control. With Git you just seem to need to "git" it right the first time.

    Unfortunately, that breaks Eclipse, which seems to use --follow. git log --follow sometimes does not show the full history of files with complicated rename histories even though git log does. (I do not know why.)

    (There are some too clever hacks that go back and recommit old work, but they are rather frightening. See GitHub-Gist: emiller/git-mv-with-history.)

    0 讨论(0)
  • 2020-11-22 05:37

    I would like to rename/move a project subtree in Git moving it from

    /project/xyz
    

    to

    /components/xyz

    If I use a plain git mv project components, then all the commit history for the xyz project gets lost.

    No (8 years later, Git 2.19, Q3 2018), because Git will detect the directory rename, and this is now better documented.

    See commit b00bf1c, commit 1634688, commit 0661e49, commit 4d34dff, commit 983f464, commit c840e1a, commit 9929430 (27 Jun 2018), and commit d4e8062, commit 5dacd4a (25 Jun 2018) by Elijah Newren (newren).
    (Merged by Junio C Hamano -- gitster -- in commit 0ce5a69, 24 Jul 2018)

    That is now explained in Documentation/technical/directory-rename-detection.txt:

    Example:

    When all of x/a, x/b and x/c have moved to z/a, z/b and z/c, it is likely that x/d added in the meantime would also want to move to z/d by taking the hint that the entire directory 'x' moved to 'z'.

    But they are many other cases, like:

    one side of history renames x -> z, and the other renames some file to x/e, causing the need for the merge to do a transitive rename.

    To simplify directory rename detection, those rules are enforced by Git:

    a couple basic rules limit when directory rename detection applies:

    1. If a given directory still exists on both sides of a merge, we do not consider it to have been renamed.
    2. If a subset of to-be-renamed files have a file or directory in the way (or would be in the way of each other), "turn off" the directory rename for those specific sub-paths and report the conflict to the user.
    3. If the other side of history did a directory rename to a path that your side of history renamed away, then ignore that particular rename from the other side of history for any implicit directory renames (but warn the user).

    You can see a lot of tests in t/t6043-merge-rename-directories.sh, which also point out that:

    • a) If renames split a directory into two or more others, the directory with the most renames, "wins".
    • b) Avoid directory-rename-detection for a path, if that path is the source of a rename on either side of a merge.
    • c) Only apply implicit directory renames to directories if the other side of history is the one doing the renaming.
    0 讨论(0)
  • 2020-11-22 05:38

    To rename a directory or file (I don't know much about complex cases, so there might be some caveats):

    git filter-repo --path-rename OLD_NAME:NEW_NAME
    

    To rename a directory in files that mention it (it's possible to use callbacks, but I don't know how):

    git filter-repo --replace-text expressions.txt
    

    expressions.txt is a file filled with lines like literal:OLD_NAME==>NEW_NAME (it's possible to use Python's RE with regex: or glob with glob:).

    To rename a directory in messages of commits:

    git-filter-repo --message-callback 'return message.replace(b"OLD_NAME", b"NEW_NAME")'
    

    Python's regular expressions are also supported, but they must be written in Python, manually.

    If the repository is original, without remote, you will have to add --force to force a rewrite. (You may want to create a backup of your repository before doing this.)

    If you do not want to preserve refs (they will be displayed in the branch history of Git GUI), you will have to add --replace-refs delete-no-add.

    0 讨论(0)
  • 2020-11-22 05:40

    Git detects renames rather than persisting the operation with the commit, so whether you use git mv or mv doesn't matter.

    The log command takes a --follow argument that continues history before a rename operation, i.e., it searches for similar content using the heuristics:

    http://git-scm.com/docs/git-log

    To lookup the full history, use the following command:

    git log --follow ./path/to/file
    
    0 讨论(0)
  • 2020-11-22 05:44

    It is possible to rename a file and keep the history intact, although it causes the file to be renamed throughout the entire history of the repository. This is probably only for the obsessive git-log-lovers, and has some serious implications, including these:

    • You could be rewriting a shared history, which is the most important DON'T while using Git. If someone else has cloned the repository, you'll break it doing this. They will have to re-clone to avoid headaches. This might be OK if the rename is important enough, but you'll need to consider this carefully -- you might end up upsetting an entire opensource community!
    • If you've referenced the file using it's old name earlier in the repository history, you're effectively breaking earlier versions. To remedy this, you'll have to do a bit more hoop jumping. It's not impossible, just tedious and possibly not worth it.

    Now, since you're still with me, you're a probably solo developer renaming a completely isolated file. Let's move a file using filter-tree!

    Assume you're going to move a file old into a folder dir and give it the name new

    This could be done with git mv old dir/new && git add -u dir/new, but that breaks history.

    Instead:

    git filter-branch --tree-filter 'if [ -f old ]; then mkdir dir && mv old dir/new; fi' HEAD
    

    will redo every commit in the branch, executing the command in the ticks for each iteration. Plenty of stuff can go wrong when you do this. I normally test to see if the file is present (otherwise it's not there yet to move) and then perform the necessary steps to shoehorn the tree to my liking. Here you might sed through files to alter references to the file and so on. Knock yourself out! :)

    When completed, the file is moved and the log is intact. You feel like a ninja pirate.

    Also; The mkdir dir is only necessary if you move the file to a new folder, of course. The if will avoid the creation of this folder earlier in history than your file exists.

    0 讨论(0)
  • 2020-11-22 05:44
    git log --follow [file]
    

    will show you the history through renames.

    0 讨论(0)
提交回复
热议问题