1. 修改 fileA -> 提交 fileA 2. 修改 fileB -> 提交 fileB
现在假设我已经满意于在文件 B 中所做的更改,但实际上我发现我不需要更改 A,希望将其恢复为以前的版本。Git 似乎会对整个工作目录进行快照,所以 "git reset --hard hexid_of_step1" 命令会将两个文件都还原到原始状态。有没有办法只舍弃文件 A 的更改,同时保留文件 B 的更改呢?
使用:
git restore --source=<hash-id> --worktree --staged -- fileA
(假设使用Git 2.23或更高版本)。
在你的第二点中,当你说“Git似乎会对整个工作目录进行快照”时,你已经走在了正确的轨道上。Git确实会拍摄快照。但这里并不是指工作树。
首先,让我们注意一下:Git不是关于文件的,而是关于提交的。Git也不是关于分支的:它是关于提交的。提交包含文件,所以我们确实可以保存文件。像main
或master
这样的分支名称帮助我们找到提交。但是真正重要的是提交。因此,我们需要知道提交是什么,它有什么作用,以及如何找到它。
正如你也看到的那样,每个提交都有一个唯一的哈希ID,表示为一个巨大、丑陋、难以记忆的十六进制数字。这个数字是Git找到提交的最低级别方法。如果你有某个特定的提交,它就有那个数字,无论你是如何得到它的,将该数字提供给Git将使你访问该提交。(如果你没有那个提交,Git会说它不认识这个数字;你将不得不将你的Git连接到其他拥有该提交的Git上,并从他们那里获取它。)
大多数情况下,我们不能真正使用这些数字,但我们会记住它们是Git查找提交的底层方法。现在让我们看看提交中有什么。每个提交都包含两个东西:
提交保存了Git知道的每个文件的完整快照,形式为您(或其他人)进行提交时的形式。我们稍后会再次讨论这个问题。
另外,提交还保存了一些元数据,或者关于该提交的信息。元数据包括制作提交的人的姓名和电子邮件地址。有两个这样的元数据(作者和提交者),以及两个日期和时间戳。有一个日志消息的空间,在其中解释您为什么做出了提交。对于Git自己的操作至关重要,每个提交都存储了先前提交哈希ID的列表。通常,这个列表只有一个条目:这个提交的(单一)父。
每当我们有一个现有提交的哈希ID时,我们就说我们正在“指向”该特定提交。因此,每个提交通常都向后指向一个先前的提交。这形成了一个向后看的链:
... <-F <-G <-H
其中,H
代表链中最后一次提交的哈希 ID。提交 H
包含一个快照和一些元数据——制作者、时间、原因等等——而这些元数据包括较早提交 G
的哈希 ID。Git 可以使用 G
的哈希 ID 从该先前提交检索相同的内容。
比较两个快照可以告诉我们——或者告诉 Git——发生了什么变化。如果我们查看提交记录,通常会看到这一点,例如使用 git log -p
或 git show
。实际上,每个提交都包含自己的完整快照,但我们只会看到发生了变化的文件。当然,在通过比较提交 H
和提交 G
来展示提交 H
后,git log
现在可以向后跳一步到提交 G
本身。Git 现在可以从 F
检索快照,并因此为我们展示提交 G
的更改。从这里,Git 可以向后跳一步到提交 F
,以此类推。
所有这些都需要一件事:Git 需要知道链中最后一个提交 H
的哈希 ID。这就是分支名称发挥作用的地方:像 main
或 master
这样的名称仅仅保存链中最后一个或者说是“tip”提交的哈希 ID。即使有更新的提交,这也是正确的,这就是我们看到“分支”的地方:
...--G--H <-- main
\
I--J <-- feature
feature
让Git定位提交J
。提交J
向后指向提交I
,后者向后指向H
,以此类推。fileA
的更改而保留对fileB
的更改?”git restore
。在早期的Git版本中,执行此操作的命令是git checkout
。在Git 2.23及更高版本中,git checkout
仍然有效;只是git checkout
用于许多事情,而git restore
用于较少的事情,因此git restore
是一种更专注的工具。git checkout
,因为我经常遇到这种情况:-))来提取我们关心的一个文件。git checkout <hash-id> -- fileA
或者:
git restore --source=<hash-id> --worktree --staged -- fileA
关于Git提交,或者说任何Git对象(提交是由Git的三个内部对象组成的)还有一件重要的事情要知道:它们完全、彻底、100%只读。即使是Git本身也不能改变一次提交。因为物理上不可能改变提交,所以你实际上并不直接处理或使用提交。存储在提交中的文件以特殊的、仅供Git使用的压缩和去重形式保留。这对Git有很多好处:例如,如果它们相同,它使得很容易看到哪些文件在任意一对提交之间是相同的,因为它们被去重了。这也意味着,无论有多少提交保持某个特定版本的100兆字节文件,现实中只有一个存储版本。
无论如何,为了使用提交,Git必须将该提交提取到您的工作树中。这是git checkout
(或在Git 2.23或更高版本中的git switch
)的主要功能之一:它将从某个提交中取出所有文件,将它们扩展为有用的形式,以填充您的工作树。
同时,这种类型的git checkout
(或git switch
)还执行了两个操作:它将特殊名称HEAD
附加到某个分支名称上,并从该提交中填充Git的索引。当创建新提交时,附加HEAD
是很重要的。Git的索引也是如此。
索引(也称为暂存区,有时称为缓存(现在很少用,主要用于像git rm --cached
这样的标志中))是Git中的一个关键数据结构。索引所持有的内容在一些角落和扩展情况下变得复杂,但在大多数情况下,可以简单地描述为保存您的建议的下一个提交,或者至少是其快照。(所有下一个提交的元数据都是在您进行下一个提交时实时生成的。)
换句话说,这些快照是从Git的索引中获取的,而不是从您的工作树中获取的。这就是Git让你反复运行git add
的原因。每次git add
实际上都是对Git的指令:使用我在这个文件/这些文件中拥有的任何内容,并更新你的索引以匹配。
git checkout xyzzy
可能意味着以下两种情况之一:
git checkout xyzzy -- # check out the branch
或者:
git checkout -- xyzzy # restore the file
有时候不清楚哪一个命令会被执行,或者在模糊的情况下你想要哪个命令。从2.23版本开始,git checkout
现在注意到这种情况并要求澄清;git switch
和git restore
是单独的命令,已经明确了你需要的命令。
这里还有一件事要轻轻地提一下,那就是.gitignore
文件。这个文件的名称是错误的:它并不意味着“忽略这些文件”。相反,它的意思是:“如果这些文件未被跟踪,请不要抱怨它们未被跟踪,并且不要自动将它们添加到Git的索引中,以便它们变成已跟踪文件。”但是,一旦某个文件被跟踪,将该文件列在.gitignore
中就没有任何效果。
正如你无疑所见,通过哈希ID来命名提交很麻烦。运行git log
并使用剪切和粘贴功能可以正常工作(我自己也经常这样做),但是有许多有用的命名提交的方法。这些在gitrevisions手册页中有记录。它相当复杂,值得随着时间的推移经常重新阅读,以逐渐习惯Git的约定和风格。
这里要考虑的主要是相对的命名方式。假设你知道你想要从上一个提交中获取某个文件的版本。你可以运行git log
获取其哈希ID,然后使用git checkout
或git restore
来获取它。但是由于HEAD
始终命名当前提交,如果我们只是告诉Git:“从HEAD
向后退一次提交”,我们就会得到正确(前一个)的提交。我们可以使用帽子^
或波浪线~
后缀来实现:
git checkout HEAD^ -- fileA
git checkout HEAD~2 -- fileA
使用我们的示例:
...--G--H <-- main
\
I--J <-- feature (HEAD)
git checkout main -- fileA
G
,我们可以将其命名为feature~3
(向后数三步:J-1=I,-1=H,-1=G),或者main~1
,或者main^
,或者main~
。这两个符号——帽子和波浪线——可以帮助您走得更远。之后,还有一些搜索提交的方法,但有时会变得非常棘手(一些搜索从所有分支开始!)。commit C: your changes to fileB
commit B: your changes to fileA
commit A: where you started before you made any changes
现在你的分支看起来像是A-B-C
,但是听起来你希望得到A-C'
,其中C'只包含对fileB的更改,但有一个新的哈希(因为当您更改提交中的任何内容时,包括父提交时,必须具有新的哈希)。 通过交互式变基,一种简单的方法是从本地分支历史记录中删除提交B:
git rebase -i A # where A is the commit ID (hashcode) of A
git rebase -i HEAD~2 # the second parent of your current head is A in this case
附注2:如果您不想重写历史记录,并且您不能通过还原提交来简单地恢复文件,那么您可以使用torek's answer在新提交中将文件恢复到先前的版本。该答案还有一个额外的好处,即如果您希望这样做,您可以恢复文件的特定版本,而不管自那时以来该文件发生了什么。