使用Git记录文件复制操作

183
当我使用git-mv命令移动文件时,状态显示文件已重命名,即使我更改了一些部分,它仍然被认为是几乎相同的东西(这很好,因为它让我可以跟踪历史记录)。
当我复制一个文件时,原始文件有一些历史记录,我想将这些历史记录关联到新的副本上。
我试过移动文件,然后尝试在原始位置重新检出 - 一旦移动了,git就不允许我检出原始位置。
我试过进行文件系统复制,然后添加文件 - git将其列为新文件。
是否有任何方法使git记录文件复制操作,类似于它记录文件重命名/移动,其中可以追溯到原始文件的历史记录?

3
你应该考虑接受罗伯特的答案。它完美地解决了问题。 - EliadL
3个回答

130

如果由于某种原因您无法像Jakub Narębski的答案那样打开拷贝检测,您可以通过以下三个提交来强制Git检测已复制文件的历史记录:

  • 不要复制,切换到一个新分支并移动文件到该分支的新位置。
  • 在那里重新添加原文件。
  • 使用no-fast-forward选项--no-ff将新分支合并到原始分支。

感谢Raymond Chen。以下是他的过程。假设文件名为orig,您想将副本命名为apple

git checkout -b dup # create and switch to branch

git mv orig apple # make the duplicate
git commit -m "duplicate orig to apple"

git checkout HEAD~ orig # bring back the original
git commit -m "restore orig"

git checkout - # switch back to source branch
git merge --no-ff dup # merge dup into source branch

2020-05-19:上述方案的优点是不改变原始文件的日志,不创建合并冲突,并且更加简短。前一个方案有四个提交:

  • 不是复制,而是切换到新分支并将文件移动到新位置。
  • 切换回原始分支并重命名文件。
  • 将新分支合并到原始分支中,通过保留两个文件来解决琐碎的冲突。
  • 在单独的提交中恢复原始文件名。

(解决方案来源:https://dev59.com/questions/Z2Qn5IYBdhLWcg3wTloS#44036771.


10
简洁、简短、百分之百……这个答案是公共服务……在视野范围内点赞一切。 - ptim
5
我尝试按照这个(新的)食谱做,但不成功。如果你能展示实际的步骤,可能会有所帮助。 - Greg Lindahl
3
@RobertPollak 我尝试了几个版本但都没有成功。你所说的“移动文件”,是指 git mv orig new 吗?而“重新添加原始文件”,是指 cp new orig && git add orig 吗? - ՕլՁՅԿ
10
你需要创建一个分支,提交更改,然后合并回主分支的想法,而且最高评价是“简单和简洁”让人难以理解。在Mercurial中,你只需要执行 hg cp(或者如果已经自己复制过,则执行 hg cp -A)。很遗憾Git赢得了版本控制系统的普及竞赛。 - Arthur Tacca
2
完成这些步骤后,我遇到了与执行“cp oldFile newFile”相同的问题,它显示文件已直接添加,与旧文件没有任何关系。有什么想法是为什么会这样? - MichaelGofron
显示剩余24条评论

127
Git不会跟踪重命名或复制,这意味着它不会记录重命名或复制。相反,它进行重命名和复制检测。您可以使用-M选项在git diff(和git show)中请求重命名检测,您还可以使用-C选项请求更改文件中的附加复制检测,您可以使用-C -C在所有文件中请求更昂贵的复制检测。请参见git-diff手册。 -C -C意味着-C-C意味着-M-M--find-renames的快捷方式,-C表示--find-copies-C -C也可以拼写为--find-copies-harder
您还可以通过将diff.renames设置为布尔值true(例如true1)来配置git始终进行重命名检测,并且您还可以将其设置为copycopies来请求git进行复制检测。请参见git-config手册。

还要检查git diff-l选项和相关的配置变量diff.renameLimit


请注意,在Git中,git log <pathspec>的工作方式与其他命令不同:这里的<pathspec>是一组路径分隔符,其中路径可以是(子)目录名称。它在重命名和复制检测开始之前过滤和简化历史记录。如果您想要跟踪重命名和复制,请使用git log --follow <filename>(目前有点受限,并且仅适用于单个文件)。


2
@allyourcode:你有什么困惑吗?要默认开启复制检测,你需要将diff.renames设置为copies(例如:git config diff.renames copies)。我同意这有点违反直觉。 - Jakub Narębski
有一个部分我似乎无法解析,那就是“并且您可以默认请求进行重命名检测”。您的意思是diff.renames可以使用四个值(true、1、copy、copies),而且它们都是做同样的事情吗? - allyourcode
1
@allyourcode:抱歉,我没有注意到这个问题。现在已经修复了,谢谢。 - Jakub Narębski
4
Git使用基于内容寻址的对象数据库作为存储库存储。文件内容以SHA-1哈希值(类型+长度+内容)的“blob”形式存储在特定地址下。这意味着给定的内容仅存储一次。需要注意的是,这种自动去重是创建“bup”备份系统并使用git打包格式的原因。 - Jakub Narębski
3
与下面的解决方法不同,这种方法无法跟踪范围内的更改。Git日志允许使用范围参数(git log -L123,456:file.xyz),该参数正确地跟随重命名,但不包括复制,并且在这种情况下无法传递--follow参数;此外,据我所知,这种方法也无法在git blame中使用。 - Clément
显示剩余2条评论

1
这是基于Robert的答案。
对于我的用例,我需要将多个目录从一个实现移动到另一个实现(包括文件包含路径、单元测试等),我发现逐个移动每个文件非常具有挑战性和耗时。
我的解决方案包括提示输入源路径和目标路径。
我的解决方案还会在脚本成功执行到最后时删除为此目的创建的临时分支。
注意事项:
1. 脚本将尝试为您提供的第二个提示(新目标)创建一个新目录。 2. 这个解决方案和原始解决方案都将历史记录合并到当前分支中。我建议您从一个新分支开始,或者如果您有任何本地修改,请至少使用git stash save命令。
branchName=chore/temp/duplicate-file-history-by-script
currentBranchName="$(git branch --show-current)"

function copy_git_history() {
    targetToCopy=$1
    newDestination=$2

    echo "copying $targetToCopy to $newDestination and restoring it's history"

    git mv "$targetToCopy" "$newDestination"
    git commit -m "duplicating $targetToCopy to $newDestination to retain git history"

    git checkout HEAD~ "$targetToCopy"
    git commit -m "restoring moved file $targetToCopy to its original location"
}

### USER PROMPTS ###

echo "proceeding to copy files to current branch.  Please make sure you are prepared to have the current git branch modified: $currentBranchName"
# spacing to make things easier to read
printf "\n"

echo "Please enter the path to the file(s) you wish to duplicate, relative to $PWD"
read -r originalFileLoc

echo "Please enter the new path where you wish to copy the original file(s)"
read -r newFileLoc

### END: USER PROMPTS ###

# create the new branch to store the changes
git checkout -b $branchName

# create the duplicate file(s)
if [[ -d  "$originalFileLoc" ]]
then
    files="$originalFileLoc/*"
    echo "copying files from $originalFileLoc to $newFileLoc"
    mkdir -p "$newFileLoc"

    for file in $files
    do
      copy_git_history "$file" "$newFileLoc"
    done
else
  copy_git_history "$originalFileLoc" "$newFileLoc"
fi

# switch back to source branch
git checkout -
# merge the history back into the source branch to retain both copies
git merge --no-ff $branchName -m "Merging file history for copying $originalFileLoc to $newFileLoc"

# delete the branch we created for history tracking purposes
git branch -D $branchName

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