git - 仅还原分支上的更改
问题描述
我仍在尝试理解一些 git 概念。我对分支的理解是,每个分支都可以有自己的更改,这些更改只会在该分支中,然后您可以将更改推送并合并到master
. 我马虎并在我的master
分支上进行更改(未提交),所以所有这些更改都传递到我不想要更改的新分支。当我尝试将更改还原到最后一个推送的master
分支时,它会在我的所有分支中还原这些更改。有没有办法可以恢复本地master
分支中的所有内容,并有选择地恢复对我已经创建的分支中特定文件的更改?
例如,假设我的仓库中有 file1 和 file2。我对最新版本的 repo 很满意。然后我对 file1 和 file2 进行了一些更改,master
但没有提交这些更改。然后我决定为这些文件更改中的每一个创建一个分支,以便我可以单独处理它们,所以我创建了新的分支file1_update
并file2_update
从master
. 由于master
发生了变化,这些一直延续到file1_update
和file2_update
。我想恢复 file2 infile1_update
和 file1 in file2_update
,然后将所有内容恢复master
到最新版本而不做任何更改。有没有办法做到这一点?
解决方案
注意:在阅读下面的文本之前或之后(我建议在之后),您可能还想查看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
如果我们进行新的提交D
,master
现在需要记住名称D
。 D
将指向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
现在,如果我们进行新的提交E
,E
将指向D
,Git 将更新当前名称( dev
) 以指向E
:
A--B--C--D <-- master
\
E <-- dev (HEAD)
如果我们现在运行git checkout master
并进行新的提交F
,F
将返回,而不是指向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 branch
branch
git commit
branch
branch
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,通常带有助词to和revert to,意思是——至少对某些人来说——将整个内容改回来。也就是说,而不是说:
“提交 a123456 更改了文件 README.txt 的一行,我希望将那一行改回”
人们有时的意思是:
“自提交 a123456 以来,README.txt 发生了很大变化,我想要 a123456 中的版本,所以这意味着我想要 _____”
他们用“将 README.txt 恢复为 a123456”填写空白,因此他们达到了git revert
.
不是git revert
这样。为此,需要README.txt
从commit中提取文件a123456
。git checkout
令人困惑的是,执行此操作的主要 Git 命令是. (它应该是一个单独的命令,而在 Mercurial 中它是:它是!)如果你想要在 Git 中这样做,你可以这样写:git checkout branch
hg 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
例如。这不会影响索引。
推荐阅读
- protractor - 自动化 webrtc 屏幕共享
- firebase - 仅在满足条件时使用 Firebase 函数发送通知
- android - android - adb 多个设备/模拟器
- asp.net - 如何将Bootstrap(网格系统)应用于div?
- javascript - JS - 找出隐藏的 DOM 元素的可见百分比
- matlab - 如何生成具有不同温度的磁盘的热图?
- amazon-web-services - 如何创建具有 50 GB 存储空间的窗口平台的新 AMI?
- keystore - 无法验证 fiware-idm 用户帐户
- angular - 如何规范化ngrx/store中的深层嵌套数据?
- vue.js - 如何按字母顺序对 Vue.js / Buefy 形式的选项列表进行排序?