Git复制文件并保留历史记录

245

我有一个关于Git的有些混乱的问题。假设我已经提交了一个文件dir1/A.txt,并且git保留了一份提交历史记录。

现在我需要将该文件复制到dir2/A.txt(不是移动,而是复制)。 我知道有一个git mv命令,但我需要让dir2/A.txt有与dir1/A.txt相同的提交历史记录,而dir1/A.txt仍然存在。

我不打算在创建副本后更新A.txt,所有未来的工作都将在dir2/A.txt上进行。

我知道这听起来很混乱,我会补充说明,这种情况发生在基于Java的模块(Maven项目)上,我们需要创建一个新版本的代码,以便我们的客户在运行时有能力使用2个不同的版本,第一个版本最终将被删除,当对齐完成时。 当然,我们可以使用Maven版本控制,但我只是Git的新手,想知道Git在这里能提供什么。


2
我的2022年实验表明,git cp不起作用。 - Sridhar Sarnobat
7个回答

239

你需要做的是:

  1. 将文件移动到两个不同的位置,
  2. 合并进行上述操作的两个提交,然后
  3. 将一个副本移回原始位置。

使用 git blame 命令,您将能够查看历史归属信息,并使用 git log 命令查看两个文件的完整变更历史。

假设您想要创建文件 foo 的副本,名为 bar。那么您将按照以下步骤操作:

git mv foo bar
git commit

SAVED=`git rev-parse HEAD`
git reset --hard HEAD^
git mv foo copy
git commit

git merge $SAVED     # This will generate conflicts
git commit -a        # Trivially resolved like this

git mv copy foo
git commit

为什么这样做有效

执行上述命令后,您将得到一个类似于以下的版本历史记录:

( revision history )            ( files )

    ORIG_HEAD                      foo
     /     \                      /   \
SAVED       ALTERNATE          bar     copy
     \     /                      \   /
      MERGED                     bar,copy
        |                           |
     RESTORED                    bar,foo

当您询问Git有关foo的历史记录时,它会:

  1. 检测MERGED和RESTORED之间从copyfoo的重命名,
  2. 检测到copy来自MERGED的ALTERNATE父级,
  3. 检测ORIG_HEAD和ALTERNATE之间foo的重命名。

然后,它将深入研究foo的历史记录。

当您询问Git有关bar的历史记录时,它会:

  1. 注意到MERGED和RESTORED之间没有更改,
  2. 检测到bar来自MERGED的SAVED父级,
  3. 检测到ORIG_HEAD和SAVED之间foo的重命名。

然后,它将深入研究foo的历史记录。

就这么简单。 :)

您只需强制Git进入合并情况,以便您可以接受两个可跟踪的文件副本,并通过对原始文件的平移移动(很快就还原)来实现此操作。


11
在Git版本2.9下,似乎这种方法行不通。为了让Git追踪到bar的起源foo,我必须使用--follow-C标志。使用cp foo bar && git add bar && git commit可以得到相同的最终结果,但不会有奇怪的历史记录。我做错了什么吗? - stefanmaric
4
@peter-dillinger,好的解决方案!我在https://dev59.com/h3NA5IYBdhLWcg3wPLL-#46484848中将其改得更易读了。 - Robert Pollak
71
这是一个巧妙的解决方案,但用“简单”一词来描述这个十步曲折地绕过应该在任何旨在追踪合法可复制物品历史记录的系统中作为原子操作的缺失,确实很有趣。 - sdenham
27
如果你预计将来需要使用 git rebase 对这些提交进行操作,你需要注意这种方法。当我尝试使用这种保留历史记录的方法时,在进行 rebase 操作时,git 将这种方法生成的提交视为彼此冲突并需要手动合并。解决冲突的过程最终导致了我本来想保存的提交历史记录丢失。 - zwalker
4
我记得这在过去对我起过作用,但目前并没有。来自合并分支的文件从合并提交中获取了其历史的“起点”。尝试了几个GIT版本,包括Windows 7上的2.24.0版本。也尝试使用@LukasEder的脚本,结果相同。 - volvpavl
显示剩余13条评论

94
与Subversion不同,Git没有每个文件的历史记录。如果查看提交数据结构,它只指向先前的提交和此提交的新树对象。提交对象中没有显式存储哪些文件由提交更改或这些更改的性质的信息。
检查更改的工具可以基于启发式方法检测重命名。例如,`git diff` 命令有一个 `-M` 选项可以打开重命名检测。因此,在重命名的情况下,`git diff` 可能会显示一个文件已被删除并创建了另一个文件,而 `git diff -M` 实际上将检测到移动并相应地显示更改 (详情请参阅 `man git diff`)。
因此,在 Git 中,这不是关于如何提交更改,而是关于如何查看以后提交的更改。

8
我在http://pastebin.com/zEREyeaL上提供了一个可重现的示例,它显示`git blame`也知道重命名历史记录 - 而不使用任何选项。这难道不告诉我们历史记录以某种方式存储吗? - Daniel Alder
9
不行。就像 git diff -M 一样,这只是对树对象的智能分析。从 git blame 手册中可以看到:“整个文件重命名时,自动跟踪行的来源(当前没有关闭跟踪重命名的选项)。” - CliffordVienna
31
git mv 的存在意义是什么? - skirsch
4
@skirsch 方便 - CliffordVienna
13
不像Mercurial一样,Mercurial具有保留历史副本的功能。 - Omnifarious
@CliffordVienna:相比于仅仅移动文件,git mv有什么便利之处呢?是因为它可以将文件从旧位置中移除,并将其添加到新位置的版本控制中吗? - undefined

40

只需复制文件,添加并提交:

cp dir1/A.txt dir2/A.txt
git add dir2/A.txt
git commit -m "Duplicated file from dir1/ to dir2/"

下面的命令将显示完整的预复制历史记录:

git log --follow dir2/A.txt

要查看原始文件的继承逐行注释,请使用以下内容:

git blame -C -C -C dir2/A.txt

Git在提交时不跟踪副本,而是在使用例如git blamegit log查看历史记录时检测它们。

大部分信息来自于这里的回答:Record file copy operation with Git


2
这并不是很有用,因为 -C -C -C 搜索的是整个仓库,除非你的仓库很小,否则速度会非常慢。 - Timmmm
我可以执行 cp dir1/A.txt dir2/B.txt 并更改文件名吗? - alper

28

我已经稍微修改了Peter在这里的答案,创建了一个可重用的、非交互式的shell脚本,名为git-split.sh

#!/bin/sh

if [[ $# -ne 2 ]] ; then
  echo "Usage: git-split.sh original copy"
  exit 0
fi

git mv "$1" "$2"
git commit -n -m "Split history $1 to $2 - rename file to target-name"
REV=`git rev-parse HEAD`
git reset --hard HEAD^
git mv "$1" temp
git commit -n -m "Split history $1 to $2 - rename source-file to temp"
git merge $REV
git commit -a -n -m "Split history $1 to $2 - resolve conflict and keep both files"
git mv temp "$1"
git commit -n -m "Split history $1 to $2 - restore name of source-file"

3
很好的解决方案。我在使用含有空格文件时遇到了问题,所以我修改了你的代码来解决这个问题。 - Radon8472
嗯,使用git v2.17.1,这会让我有一个新提交的文件 $2 - 这种方法对你仍然有效吗? - frans
哦,即使使用 v2.1.4 版本,复制文件的日志文件也是空的。 - frans
@frans:当时它是有效的。如果您发现任何改进,请随意进行编辑... - Lukas Eder
@frans:这是一个Git问题还是一个Shell问题? - Lukas Eder
显示剩余5条评论

10

为了完整起见,我想补充一点,如果您想复制一个包含受控文件和非受控文件的整个目录,可以使用以下命令:

git mv old new
git checkout HEAD old

未经控制的文件将被复制,因此您应该清理它们:

git clean -fdx new

3
就我所知,第一条命令将不会复制未受控制的文件(而是移动它们),如果您之后使用“clean”命令删除它们,那么移动它们的意义何在? - hans_meine
@hans_meine 你说得对,最好先清理再移动。 - Hervé
15
当我这样做时,只有原始文件与历史记录保持连接,复制被视为具有新的文件和历史记录。这并没有回答问题:( - Griknok

2
在我的情况下,我在硬盘上进行了更改(将大约200个文件夹/文件从一个路径剪切/粘贴到工作副本中的另一个路径),并使用了SourceTree(2.0.20.1)来暂存检测到的更改(一个添加,一个删除)。只要我同时暂存了添加和删除,它就会自动合并为一个带有粉色R标志的单个更改(我认为是重命名)。
我注意到因为我一次性有如此多的更改,所以SourceTree稍微慢了一些来检测所有的更改,所以我的一些已经暂存的文件看起来像是只添加(绿色加号)或只删除(红色减号),但我不断刷新文件状态,并在新的更改最终出现时不断地暂存新的更改,几分钟后,整个列表就完美无缺,可以准备提交了。
我验证了历史记录的存在,只要我在查找历史记录时勾选“跟随重命名的文件”选项。

0

这个过程保留历史记录,但是有点折中的方法:

# make branchs to new files
$: git mv arquivos && git commit

# in original branch, remove original files
$: git rm arquivos && git commit

# do merge and fix conflicts
$: git merge branch-copia-arquivos

# back to original branch and revert commit removing files
$: git revert commit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接