git checkout -b rename-branch
git mv a.txt b.txt
git commit -m "Renaming file"
# if you did a git blame of b.txt, it would _follow_ a.txt history, right?
git checkout main
git merge --no-ff --no-commit rename-branch
git checkout HEAD -- a.txt # get the file back
git commit -m "Not really renaming file"
直接拷贝,你会得到这个:
$ git log --graph --oneline --name-status
* 70f03aa (HEAD -> master) COpying file straight
| A new_file.txt
* efc04f3 (first) First commit for file
A hello_world.txt
$ git blame -s new_file.txt
70f03aab 1) I am here
70f03aab 2)
70f03aab 3) Yes I am
$ git blame -s hello_world.txt
^efc04f3 1) I am here
^efc04f3 2)
^efc04f3 3) Yes I am
使用侧边的重命名并取回文件,您将获得:
$ git log --oneline --graph master2 --name-status
* 30b76ab (HEAD, master2) Not really renaming
|\
| * 652921f Renaming file
|/
| R100 hello_world.txt new_file.txt
* efc04f3 (first) First commit for file
A hello_world.txt
$ git blame -s new_file.txt
^efc04f3 hello_world.txt 1) I am here
^efc04f3 hello_world.txt 2)
^efc04f3 hello_world.txt 3) Yes I am
$ git blame -s hello_world.txt
^efc04f3 1) I am here
^efc04f3 2)
^efc04f3 3) Yes I am
基本原理是如果你想查看 original 文件的历史记录,git会毫无问题地做到这一点.......如果你想在 copy 上做到这一点,那么git将跟随重命名所在的单独分支,然后它将能够跳转到 copy 之后的原始文件,只是因为它是在 that 分支上完成的。
2条答案
按热度按时间cuxqih211#
简短的回答是“不”。但还有更多的东西需要了解;(正如JDB在评论中建议的那样,为了方便起见,我将提到
git mv
存在的原因。)稍长:你说Git会区分文件是对的,但是你可能在 * 什么时候 * Git会区分文件这一点上是错的。
Git的内部存储模型建议每个提交都是该提交中 * 所有 * 文件的独立快照。进入新提交的每个文件的版本,即快照中该路径的数据,是您运行
git commit
时该路径下索引中的任何内容。1实际的实现,在第一层,是每个快照文件以压缩的形式捕获为Git数据库中的一个 blob对象 。blob对象完全独立于该文件的每个先前和后续版本,除了一个特殊情况:如果你进行了一次新的提交,其中 * 没有 * 数据被更改,你将 * 重用旧的blob。所以当你连续进行两次提交,每次提交包含100个文件,并且只有一个文件被更改时,第二次提交重用了99个以前的blob,并且只需要将一个实际的文件快照到一个新的blob中。2
因此,Git会区分文件的事实根本不影响提交。没有提交依赖于前一次提交,除了存储前一次提交的哈希ID(也许可以重用精确匹配的blob,但这是它们精确匹配的副作用,而不是在运行
git commit
时进行花哨的计算)。现在,所有这些独立的blob对象最终会占用大量的空间。 此时 ,Git可以将对象“打包”到一个
.pack
文件中。它会将每个对象与一些选定的其他对象集进行比较-它们可能在历史中更早或更晚,并且具有相同的文件名或不同的文件名,理论上Git甚至可以压缩一个提交对象和一个blob对象,反之亦然(尽管实际上并不是这样)-并试图找到某种方法来使用更少的磁盘空间来表示许多blob。一系列独立的对象,使用它们的散列ID以原始形式完整地检索。因此,即使此时使用的磁盘空间量下降(我们希望!),所有对象都与之前完全相同。那么Git什么时候比较文件呢?答案是: 只有当你要求它。*“询问时间”是当你运行
git diff
时,直接:或间接地:
关于这一点有很多微妙之处-特别是,
git show
在合并提交时运行时会产生Git称之为 combined diffs 的差异,而git log -p
通常只是跳过合并提交的差异-但这些,沿着其他一些重要的情况,都是在Git运行git diff
时。当Git运行
git diff
时,您可以(有时)要求它查找或不查找副本。-C
标志,也拼写为--find-copies=<number>
,请求Git查找副本。--find-copies-harder
标志(Git文档称之为“计算开销大”)看起来比普通的-C
标志更难复制。(break inappropriate pairings)选项会影响-C
。-M
又名--find-renames=<number>
选项也会影响-C
。git merge
命令可以被告知调整重命名检测级别,但至少目前不能被告知查找副本,也不能中断不适当的配对。(One命令
git blame
执行的副本查找有些不同,上面的内容并不完全适用于它。)1如果你运行
git commit --include <paths>
或git commit --only <paths>
或git commit <paths>
或git commit -a
,可以把它们看作是在运行git commit
之前修改索引。在--only
的特殊情况下,Git使用临时索引,这有点复杂,但是它仍然从 an 索引提交-它只是使用特殊的临时索引而不是普通的索引。为了制作临时索引,Git从HEAD
提交中复制所有文件,然后用你列出的--only
文件覆盖它们。对于其他情况,Git只是将工作树文件复制到常规索引中,然后像往常一样从索引中提交。2实际上,实际的快照,将blob存储到存储库中,发生在
git add
期间。这秘密地使git commit
快得多,因为您通常不会注意到在启动git commit
之前运行git add
所花费的额外时间。为什么会有
git mv
git mv old new
所做的是,* 非常 * 粗略:第一步是显而易见的:我们需要重命名文件的工作树版本。第二步类似:我们需要把文件的索引版本放到合适的位置。第三个问题是 * 奇怪:* 为什么我们要“添加”一个刚刚删除的文件?好吧,
git add
并不总是添加一个文件:相反,在这种情况下,它会检测到文件 was 在索引中,现在已经不在了。我们也可以将第三步拼写为:
我们所做的只是把旧名字从索引中删除。
但是这里有一个问题,这就是为什么我说“* 非常 * 粗略”。索引有每个文件的副本,这些文件将在下次运行
git commit
时提交。* 该副本可能与工作树中的副本不匹配。* 实际上,它甚至可能与HEAD
中的副本不匹配,如果HEAD
中有一个副本的话。例如,在:
文件
foo
存在于工作树和索引中。工作树内容和索引内容匹配。但现在让我们更改工作树版本:现在索引和工作树不同了。假设我们想将底层文件从
foo
移动到bar
,但是-出于某种奇怪的原因3-我们想 * 保持索引内容不变 *。如果我们运行:我们将在新的索引文件中得到
I am a bar
。如果我们从索引中删除旧版本的foo
,我们将完全丢失I am a foo
版本。因此,
git mv foo bar
并不是真正的move-and-add-twice,或者move-add-and-remove。相反,它会重命名工作树文件 * 并 * 重命名索引内副本。如果原始文件的索引副本与工作树文件不同,重命名的索引副本仍然与重命名的工作树副本不同。如果没有像
git mv
这样的前端命令,很难做到这一点。4当然,如果你打算git add
一切,你不需要所有这些东西摆在首位。而且,值得注意的是,如果git cp
存在,它可能还应该复制索引版本,而不是工作树版本,所以git cp
确实应该存在。还应该有一个git mv --after
选项,就像Mercurial的hg mv --after
一样。这两个 * 都应该 * 存在,但目前还没有。(在我看来,对这两个选项的需求都比直接的git mv
少。)3对于这个例子来说,这有点愚蠢和无意义。但是如果你使用
git add -p
仔细地为一个中间提交准备了一个补丁,然后决定沿着这个补丁之外,你还想重命名这个文件,这绝对是方便的,能够做到这一点,而不会弄乱你精心修补的中间版本。4、不可能:
git ls-index --stage
将从索引中获取所需的信息,而git update-index
允许您对索引进行任意更改。您可以将这两者结合起来,并使用更好的语言编写一些复杂的shell脚本或编程,以构建实现git mv --after
和git cp
的东西。brqmpdu12#
这是 hackish,但它可以通过欺骗git本身来解决,方法是在单独的分支上重命名,并 * 强制 * git在合并时保留两个文件。
直接拷贝,你会得到这个:
使用侧边的重命名并取回文件,您将获得:
基本原理是如果你想查看 original 文件的历史记录,git会毫无问题地做到这一点.......如果你想在 copy 上做到这一点,那么git将跟随重命名所在的单独分支,然后它将能够跳转到 copy 之后的原始文件,只是因为它是在 that 分支上完成的。