git copy文件,而不是`git mv`

gjmwrych  于 2023-04-10  发布在  Git
关注(0)|答案(2)|浏览(145)

我意识到git是通过区分文件的内容来工作的。我有一些文件想要复制。为了绝对防止git混淆,有没有一些git命令可以用来将文件复制到不同的目录(不是mv,而是cp),并暂存文件?

cuxqih21

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 diff commit1 commit2

或间接地:

git show commit  # roughly, `git diff commit^@ commmit`
git log -p       # runs `git show commit`, more or less, on each commit

关于这一点有很多微妙之处-特别是,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>选项也会影响-Cgit 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所做的是,* 非常 * 粗略:

mv old new
git add new
git add old

第一步是显而易见的:我们需要重命名文件的工作树版本。第二步类似:我们需要把文件的索引版本放到合适的位置。第三个问题是 * 奇怪:* 为什么我们要“添加”一个刚刚删除的文件?好吧,git add并不总是添加一个文件:相反,在这种情况下,它会检测到文件 was 在索引中,现在已经不在了。
我们也可以将第三步拼写为:

git rm --cached old

我们所做的只是把旧名字从索引中删除。

但是这里有一个问题,这就是为什么我说“* 非常 * 粗略”。索引有每个文件的副本,这些文件将在下次运行git commit时提交。* 该副本可能与工作树中的副本不匹配。* 实际上,它甚至可能与HEAD中的副本不匹配,如果HEAD中有一个副本的话。
例如,在:

echo I am a foo > foo
git add foo

文件foo存在于工作树和索引中。工作树内容和索引内容匹配。但现在让我们更改工作树版本:

echo I am a bar > foo

现在索引和工作树不同了。假设我们想将底层文件从foo移动到bar,但是-出于某种奇怪的原因3-我们想 * 保持索引内容不变 *。如果我们运行:

mv foo bar
git add bar

我们将在新的索引文件中得到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 --aftergit cp的东西。

brqmpdu1

brqmpdu12#

这是 hackish,但它可以通过欺骗git本身来解决,方法是在单独的分支上重命名,并 * 强制 * git在合并时保留两个文件。

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 分支上完成的。

相关问题