首页 > 解决方案 > 仅还原分支上的更改

问题描述

我仍在尝试理解一些 git 概念。我对分支的理解是,每个分支都可以有自己的更改,这些更改只会在该分支中,然后您可以将更改推送并合并到master. 我马虎并在我的master分支上进行更改(未提交),所以所有这些更改都传递到我不想要更改的新分支。当我尝试将更改还原到最后一个推送的master分支时,它会在我的所有分支中还原这些更改。有没有办法可以恢复本地master分支中的所有内容,并有选择地恢复对我已经创建的分支中特定文件的更改?

例如,假设我的仓库中有 file1 和 file2。我对最新版本的 repo 很满意。然后我对 file1 和 file2 进行了一些更改,master但没有提交这些更改。然后我决定为这些文件更改中的每一个创建一个分支,以便我可以单独处理它们,所以我创建了新的分支file1_updatefile2_updatemaster. 由于master发生了变化,这些一直延续到file1_updatefile2_update。我想恢复 file2 infile1_update和 file1 in file2_update,然后将所有内容恢复master到最新版本而不做任何更改。有没有办法做到这一点?

标签: gitversion-control

解决方案


注意:在阅读下面的文本之前或之后(我建议在之后),您可能还想查看Checkout another branch when there are uncommitted changes on the current branch

Git 真正做的是保存快照。这几乎就是它的全部内容:

$ git init          # create empty repository: no commits exist yet

然后,反复:

... do some work ...
$ git add <files>   # copy the work into the index
$ git commit        # turn everything that is in the index, into a snapshot

每个现在都git commit打包索引中的任何内容(又名暂存区,又名缓存),并将其转换为快照,这是永久的——嗯,大部分是永久的——并且完全只读。

我们稍后会回到这一切。

提交、哈希 ID 和分支名称

除了第一次提交之外,您总是在现有快照上制作快照。新快照获得一个提交哈希 ID——一些明显随机的十六进制数字字符串,例如. 这是提交的真实名称:它是 Git 如何使用提交来获取快照的方式。这可以让您在未来的某个时间找出您现在保存的内容。b7bd9486b055c3f967a870311e704e3bb0654e4f

每个提交记录当时作为现有快照的提交的哈希 ID。如果我们使用单个大写字母,作为人类我们可以理解的,而不是大而丑陋的哈希 ID,我们可以称之为第一个快照A。因此,第二个快照B保存了其中的实际哈希 ID A。我们说这B 指向 A

A  <--B

当我们制作第三张快照C时,我们会坐在上面B,所以C指向B

A <-B <-C

那么,我们和 Git 需要知道的是,最新的快照是什么? 这就是分支名称的真正含义:分支名称,如master,记录最后一个快照。如果最新的是C,我们有:

A--B--C   <-- master

如果我们进行新的提交Dmaster现在需要记住名称DD将指向C; master不需要再记住C了,因为D会:

A--B--C--D   <-- master

提交中的箭头总是向后指向,从子到父,因为没有任何东西——不是 Git 本身——可以改变任何现有提交中的任何内容,我们真的不需要绘制它们。但是分支名称箭头确实会随着时间而改变,所以我们应该继续绘制它们。

现在,假设我们在此时创建一​​个新的分支名称dev。该名称dev将记录一些提交 ID。它可以记录这四个中的任何一个,但默认设置是使用当前的提交 ID,这是一个master持有者,给我们这个:

A--B--C--D   <-- dev, master

现在我们有两个分支名称,我们需要知道:我们使用的是哪个分支名称? 这就是HEAD进来的地方:我们将 HEAD 这个词附加到这些名称之一。那就是我们当前的分支,它的提交 ID 存储在分支名称中,所以如果我们在 on dev,图片真的是:

A--B--C--D   <-- dev (HEAD), master

现在,如果我们进行的提交EE将指向D,Git 将更新当前名称( dev) 以指向E

A--B--C--D   <-- master
          \
           E   <-- dev (HEAD)

如果我们现在运行git checkout master并进行新的提交FF将返回,而不是指向E,而是指向D——这就是master指向——并且 Git 将更新master为指向F

A--B--C--D--F   <-- master (HEAD)
          \
           E   <-- dev

就是这样:这就是分支名称的全部意义!它只记录最新的提交,Git 称之为提示提交。好东西都在提交中:每个提交都是索引中所有内容的完整快照。

索引和工作树

提交中的所有文件都采用特殊的、仅限 Git 的压缩形式(通常是高度压缩的,至少对于源文本文件而言)。Git 几乎是唯一可以读取它们或对它们进行任何操作的程序。1 因此,Git 需要一种您和您的计算机可以读写普通格式文件的方法。这些文件进入你的工作树,所谓的因为在这里,你可以使用它们。

然而,Git 有所有文件的中间形式。它获取那些压缩的、仅限 Git 的只读文件,并将它们——嗯,关于它们的东西,真的——复制到 Git 称之为index的东西中。在这里,文件仍然以仅 Git 的形式压缩,但在这里,它们可以被覆盖。它还使用这个索引来跟踪——索引缓存,因此这些名称——关于工作树文件的信息。这是 Git 获得最大速度的地方。有类似的没有索引的 VCS,证明从理论上讲它是不必要的,但它们比 Git 慢(有时慢得多)。

提供此索引后,Git 会强制您使用该索引,即使您并不真的想要。它不是直接从提交复制文件到工作树,而是首先将文件从提交复制到索引中,然后才它们展开为工作树中的正常形式。这就是 Git 让您git add每次都运行的原因:git add所做的是将文件工作树复制索引中(在此过程中将其压缩为 Git 格式)。

与其他 VCS 相比,这就是它git commit如此快速的原因:Git 可以立即获取索引中的任何内容,将其打包到提交中,然后完成。压缩文件的所有艰苦工作已经完成!Git 甚至不必查看工作树。

这也意味着在 之后git commit,您刚刚提交的提交与索引匹配。因此,在 之后,索引与 的提示提交匹配,因为 Git 在更新工作树时将提交复制到索引。在更改为有新的提示提交后,索引与 的(新)提示提交匹配,因为 Git 复制了索引(将其冻结到快照中)以进行提交。git checkout branchbranchgit commitbranchbranch


1没有什么可以改变它们:这是一个设计特点;所有内容的实际内容都存储在密码校验和哈希 ID 下。(这就是哈希 ID 的实际来源。哈希 ID 对每一位都非常敏感,所以如果你要更改某些东西——偶然地,比如磁盘错误,或者故意覆盖它——Git 会检测到对象的校验和不再匹配用于检索对象的校验和键。这就是为什么一旦提交,所有内容都是只读的。

可以故意忘记提交。这样做有时很棘手,而且它们很容易恢复: Git 的主要设计目的是添加东西,而不是删除它们,并且更愿意添加新东西而不是忘记旧东西。我们不会在这里详细介绍这一点。


“但提交看起来像差异!”

如果你运行:

git show <commit>

或者:

git log -p

您将看到每个提交显示为一个补丁。Git 可以这样做,因为每个提交都将其先前的提交(其父提交)存储在提交中。Git 只是简单地提取两个快照并比较它们。无论有什么不同,都会显示出来。

(合并提交有一个复杂的地方,但我们也将忽略它。)

恢复

现在可以非常简单地描述 revert 所做的事情:2 Git 将提交转换为补丁,然后将补丁反向应用到其他提交。

也就是说,如果 commit-as-patch 说“向文件 A 添加一行”,Git会从该文件中删除该行。如果 commit-as-patch 说“从文件 B 中删除一行”,Git将该行添加到该文件中。

将提交反向应用到当前提交(通过工作树并使用与当前提交匹配的索引)后,Git 将更新的文件复制到索引中,就像 by 一样git add,然后进行新的提交,自动提供提交日志信息。您可以使用各种标志覆盖其中的一些,并且当补丁无法正确应用时会出现并发症(参见脚注 2)。但这主要是它。


2这其实太简单了。Revert 确实调用了 Git 的三向合并机制(就像 一样git cherry-pick)。然而,在简单、无冲突的情​​况下,“应用补丁并提交”(cherry-pick)或“反向应用补丁并提交”(revert)就足以描述该过程。


恢复是这个过程的一个糟糕的名字

Mercurial(在其他方面很像 Git,只是速度较慢且对用户更友好)调用 thishg backout而不是hg revert,因为它取消了提交的更改。动词revert,通常带有助词torevert to,意思是——至少对某些人来说——将整个内容改回来。也就是说,而不是说:

“提交 a123456 更改了文件 README.txt 的一行,我希望将那一改回”

人们有时的意思是:

“自提交 a123456 以来,README.txt 发生了很大变化,我想要 a123456 中的版本,所以这意味着我想要 _____”

他们用“将 README.txt 恢复为 a123456”填写空白,因此他们达到了git revert.

不是git revert这样。为此需要README.txt commit中提取文件a123456git checkout令人困惑的是,执行此操作的主要 Git 命令是. (它应该是一个单独的命令,而在 Mercurial 中它是:它是!)如果你想要在 Git 中这样做,你可以这样写:git checkout branchhg revert

git checkout a123456 -- README.txt

README.txt它从 commit复制a123456到索引中(像往常一样),然后将其扩展为正常的、非 Git-only 的格式到您的工作树中作为 file README.txt

请注意,在所有现代版本的 Git 中,您还可以使用:

git show a123456:README.txt

它会在您的屏幕上显示该文件的内容(截至该提交),并且通常与重定向一起使用,以便您可以将其保存到工作树内部或外部的文件中:

git show a123456:README.txt > restored-readme

例如。这不会影响索引。


推荐阅读