“git reset”和“git checkout”的区别是什么?

520

我一直认为git resetgit checkout是相同的,因为它们都将项目恢复到特定的提交状态。然而,它们不可能完全相同,因为那样就会重复了。这两者之间的实际区别是什么?我有点困惑,因为svn只有svn co来还原提交。

新增

VonC和Charles很好地解释了git resetgit checkout之间的区别。我现在的理解是,git reset将所有更改都还原到特定的提交状态,而git checkout则更多地准备了一个分支。我发现以下两个图表对我的理解非常有用:

http://a.imageshack.us/img651/1559/86421927.png http://a.imageshack.us/img801/1986/resetr.png

新增 3

http://think-like-a-git.net/sections/rebase-from-the-ground-up/using-git-cherry-pick-to-simulate-git-rebase.html 中可以了解到,checkout 和 reset 可以模拟 rebase 操作。

enter image description here

git checkout bar 
git reset --hard newbar 
git branch -d newbar 

enter image description here


请查看https://git-scm.com/blog/2011/07/11/reset.html。 - pedrorijo91
1
关于“是否错误或过于简化”,是的,第一个图表误导了检出和重置之间的区别。(对于 -- files 变体可能是可以的;我不确定。)那个图表让人觉得主要区别在于它们是否影响索引或者工作目录。请见我的回答。第二和第三个图表非常有帮助,可以看到真正的差异。第四和第五个图表有助于检查你是否理解这些命令的含义,但不会真正帮助你达成目标。 - LarsH
1
我发现“Git工具重置解密”一书中的“查看它”部分(https://git-scm.com/book/en/v2/Git-Tools-Reset-Demystified#_check_it_out)提供了最有用的摘要。 - Josiah Yoder
1
prosseek:如果您同意@LarsH的观点,认为第一张图是误导性的,请将其删除,谢谢。 - Josiah Yoder
请注意,checkout和reset仅模拟rebase的第二部分,并且需要执行其他步骤(在链接的think-like-a-git.net文章中提供),以防止数据丢失。 - cowlinator
ADDED 2 在哪里? - Marcus Mangelsdorf
8个回答

232
  • git reset 是专门用来更新索引,移动HEAD。
  • git checkout 是用来更新工作树(到索引或指定的树)。仅在检出分支时才会更新HEAD(否则,你会得到一个分离的HEAD)。
    (实际上,从Git 2.23 Q3 2019开始,这将是git restore,而不一定是git checkout

相比之下,由于svn没有索引,只有工作树,svn checkout 将复制给定版本到一个单独的目录中。
git checkout更接近的等效命令是:

  • svn update(如果您在同一个分支中,意味着相同的SVN URL)
  • svn switch(如果您例如从另一个SVN存储库URL检出相同的分支)

所有这三个工作树修改(svn checkoutupdateswitch)在git中只有一个命令:git checkout
但是由于git还具有索引的概念(即“暂存区”位于存储库和工作树之间),因此您还需要使用git reset


Thinkeye 在评论中提到了文章“重置解密

例如,如果我们有两个分支'master'和'develop'指向不同的提交,并且我们目前在'develop'上(所以HEAD指向它),并运行git reset master,'develop'本身现在将指向与'master'相同的提交。
另一方面,如果我们运行git checkout master,'develop'将不会移动,HEAD本身将移动。 HEAD现在将指向'master'。
因此,在这两种情况下,我们都将HEAD移动到指向提交A,但我们如何移动是非常不同的。reset将移动分支HEAD指向,checkout将移动HEAD本身指向另一个分支。

http://git-scm.com/images/reset/reset-checkout.png

关于这些观点:

LarsH在评论中补充道

The first paragraph of this answer, though, is misleading: "git checkout ... will update the HEAD only if you checkout a branch (if not, you end up with a detached HEAD)".
Not true: git checkout will update the HEAD even if you checkout a commit that's not a branch (and yes, you end up with a detached HEAD, but it still got updated).

git checkout a839e8f updates HEAD to point to commit a839e8f.

De Novo评论区表示同意:

@LarsH 是正确的。
第二个要点存在一个关于 HEAD 的误解,只有当你切换到一个分支时,它才会更新 HEAD。
HEAD 就像影子一样跟随你的位置变化。
检出某些非分支引用(例如标签)或直接提交将移动 HEAD。分离的 head 并不意味着你已经脱离了 HEAD,而是指 head 与分支引用分离,你可以从 git log --pretty=format:"%d" -1 等命令中看到这一点。

  • 附加的 head 状态将以 (HEAD -> 开始,
  • 分离的状态仍将显示 (HEAD,但不会有箭头指向分支引用。

12
我认为 git reset 是关于修改分支“标签”,可以选择性地更新索引或工作树作为副作用。而 git checkout 则是关于更新工作树并切换当前“选定”的分支(即 HEAD)。 - Mikko Rantalainen
2
@MikkoRantalainen 不行。git reset 百分之百跟 HEAD 有关。它甚至可以在分离 HEAD 模式下工作(https://dev59.com/CG865IYBdhLWcg3wIrFg#3965714),这意味着*没有*分支!`git checkout` 也可以在分离 HEAD 模式下工作,或者用于在分离 HEAD 模式下检出 SHA1:在这种情况下同样没有涉及分支。 - VonC
3
所有被搜索引擎带到这里的迷失灵魂,以下是进一步阅读材料,我认为它非常有价值:http://git-scm.com/blog/2011/07/11/reset.html。 - Thinkeye
3
《Reset Demystified》的解释非常好。然而,这个回答的第一段有误导性:“git checkout ... 只有在你检出一个分支时才会更新HEAD(否则,你将得到一个分离头指针)”。这是不正确的……即使你检出的是不是分支的提交,git checkout仍会更新HEAD(是的,你最终得到了一个分离的HEAD,但它仍然被更新了)。也许我误解了你所说的“更新”的含义?git checkout a839e8f会将HEAD更新为指向提交a839e8f - LarsH
2
@LarsH 是正确的。第二个要点对于“仅在检出分支时更新HEAD”中的HEAD有一个误解。HEAD就像影子一样跟随你所在的位置。检出某些非分支引用(例如标签)或直接提交将移动HEAD。分离头并不意味着您已经与HEAD分离,而是表示头已从分支引用中分离出来,您可以从git log --pretty=format:"%d" -1中看到。附加头状态将以(HEAD ->开头,分离的头仍将显示(HEAD,但不会有指向分支引用的箭头。 - De Novo
显示剩余9条评论

75

简单来说,reset 重置索引而不影响工作树,而 checkout 更改工作树而不影响索引。

将索引重置为与 HEAD 匹配,工作树保持不变:

git reset

从概念上讲,这会检查工作树中的索引。要实际执行任何操作,您必须使用-f来强制覆盖任何本地更改。这是一个安全功能,以确保“无参数”形式不具有破坏性:

git checkout

一旦您开始添加参数,确实会存在一些重叠。

checkout通常与分支、标签或提交一起使用。在这种情况下,它将重置HEAD和索引为给定的提交,并执行索引到工作树的检出。

此外,如果您向reset提供--hard,您可以要求reset覆盖工作树并重置索引。

如果您当前已经检出了一个分支,则在提供替代分支或提交时,resetcheckout之间存在一个重要区别。reset将更改当前分支以指向所选提交,而checkout将保持当前分支不变,但将检出提供的分支或提交。

其他形式的resetcommit涉及到提供路径。

如果您向reset提供路径,则无法提供--hard,而reset仅会将提供的路径的索引版本更改为所提供提交的版本(或者如果您没有指定提交,则为HEAD)。

如果您向checkout提供路径,就像reset一样,它将更新提供的路径的索引版本以匹配提供的提交(或HEAD),但它将始终检出提供的路径的索引版本到工作树中。


2
说“checkout”不改变索引是不正确的:当用于从一个分支切换到另一个分支时,它会改变索引。 - wiki1000
1
在它们最简单的形式中,reset 重置索引而不影响工作树,而 checkout 更改工作树而不影响索引。这有多令人困惑啊 :| - YetAnotherBot

63

还原更改的一个简单用例:
1. 如果您想要撤销修改文件的暂存,使用reset。
2. 如果您想要放弃对未暂存文件的更改,使用checkout。


1
完美的答案。谢谢。 - user358591
是的,除了关于索引和工作树的精确但过长的注释之外,重置(重置分支上的索引=提交)也可以用类似简单的方式来表达,而checkout基本上是更改分支(无论它是否可以从提交中创建临时分支=分离头)。 - FantomX1

19
简而言之,reset移动当前分支引用,而 checkout 则不会(它移动 HEAD)。根据 Pro Git 书籍在 Reset Demystified 下的解释,“reset” 首先会移动 HEAD 指向的内容。这与更改 HEAD 本身(这是 “checkout” 所做的)不同;“reset”将移动 HEAD 指向的分支。这意味着,如果 HEAD 设置为“master”分支(即您当前在“master”分支上),运行“git reset 9e5e6a4”将使“master”指向“9e5e6a4”。当然,关于两个命令对索引和工作树产生的影响,取决于使用的参数,还有很多细节。两个命令之间可能存在许多相似之处和差异。但在我看来,最关键的区别是它们是否移动了当前分支的末端。参见 VonC 的答案,其中包含了来自同一文章的非常有帮助的文字和图表摘录,这里不再重复。

2
良好的反馈,除了我的旧回答。+1 - VonC
1
精准而且切中要害! - OuttaSpaceTime

6

简要记忆技巧:

git reset HEAD           :             index = HEAD
git checkout             : file_tree = index
git reset --hard HEAD    : file_tree = index = HEAD

2

这两个命令(reset和checkout)是完全不同的。

checkout X 不等于 reset --hard X

如果X是一个分支名称,checkout X会更改当前分支,而reset --hard X不会。


4
但如果 X 是一个文件或文件夹,那么它们是相同的。 - Ted Bigham
1
我认为你错了。使用“checkout X”只是跳转到X分支。使用“reset --hard X”会丢弃当前分支在X之后的所有更改。因此,在后一种情况下,您的当前分支将被更改,而不是前一种情况。如果我错了,请纠正我。 - Dániel Sándor

2

git reset -> 移除所有文件从暂存区,即撤消git add <files>

git reset <commit_ID> -> 撤消指定提交后的所有文件和暂存。

如果使用reset命令添加了--hard,则它将删除目录中的文件并移除暂存区中的文件。

git checkout <commit_ID> -> 您将返回到指定的提交状态,但不在任何分支中。

如果您键入git branch -a,您将看到您处于

(HEAD detached at <commit_ID>)

.

根据控制台:

您处于“分离的 HEAD”状态。您可以四处查看,进行实验性更改并提交它们,您可以通过切换回分支而不影响任何分支来放弃在此状态下进行的任何提交。

然后您将获得两个选项:

  1. git switch -c <new-branch-name>:将从该提交创建一个新分支,并将其变为当前分支。
  2. git switch -c -> 它基本上是撤消检出命令,使您返回到旧分支。

1
这里是对含糊不清的澄清:
- git checkout会将HEAD移动到另一个提交(也可以使用分支名称进行更改),但是: - 在您所在的任何分支上,指向该分支末尾的指针(例如,“main”)将保持不变(因此您可能会处于“分离头”状态)。 - 另外,暂存区和工作目录将保持不变(与checkout之前相似的状态)。
示例:
git checkout 3ad2bcf <--- checkout to another commit
git checkout another-branch <--- checkout to another commit using a branchname

  • git reset也会移动HEAD,但有两个不同之处

    1. 它还会移动指向当前分支末尾的提交的指针。例如,假设当前分支的指针名为“main”,然后执行git-reset,现在,主要指针将指向另一个提交,并且HEAD也将指向该提交(基本上,HEAD间接地指向该提交,通过指向主指针,它仍然是一个附加头(!),但在这里没有任何区别)。

    2. Git-reset不一定会使暂存区和工作目录保持与重置之前相同的状态。正如您所知,有三种类型的重置:soft、mixed(默认值)和hard:

      • 使用soft重置,暂存区和工作目录都保持在重置之前的状态(在这方面类似于checkout,但不要忘记差异#1)。
      • 使用混合重置(默认重置类型),除了差异#1之外,暂存区的建议下一次提交(您已经添加的内容)也将设置为新指向的HEAD提交。但是,在工作目录中,所有文件仍将具有您的最新编辑(这就是为什么这种类型的重置是默认的,以便您不会丢失工作)。
      • 使用hard重置,除了差异#1之外,所有三个树HEAD、暂存区和工作目录也将更改为新指向的HEAD提交。

例子:

git reset --soft 3ad2bcf
git reset da3b47

“2. 此外,暂存区和工作目录将保持不变(与checkout之前的状态相似)。”这是不正确的,根据git checkout <branch>文档:‘为了准备在<branch>上工作,通过更新索引和工作树中的文件,并将HEAD指向该分支来切换到它。工作树中文件的本地修改被保留,以便可以提交到<branch>。’ - Géry Ogam

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接