作为一名开发人员,您一直在使用 Git。 你有没有到过这样的地步:“呃,哦,我刚刚做了什么?” 这篇文章将为您提供自信重写历史的工具。 开始前的注意事项 我还就这篇文章的内容进行了现场演讲。如果您更喜欢视频(或希望在阅读的同时观看)——您可以找到它 . 这里 我正在写一本关于 Git 的书!您是否有兴趣阅读初始版本并提供反馈?给我发电子邮件:gitting.things@gmail.com 在 Git 中记录更改 在了解如何在 Git 中 之前,您应该首先了解我们如何在 Git 中 更改。如果您已经了解所有术语,请随时跳过这一部分。 撤销 记录 将 Git 视为一个用于及时记录文件系统快照的系统非常有用。考虑一个 Git 存储库,它具有三个“状态”或“树”: 通常,当我们处理我们的源代码时,我们从一个 开始工作。 (或 )是我们的文件系统中具有与之关联的 的任何目录。 工作目录 工作目录(电子) 工作树 存储库 它包含我们项目的文件夹和文件以及一个名为 的目录。我在中更详细地描述了 文件夹的内容 . .git .git 以前的帖子 进行一些更改后,您可能希望将它们记录在您的 中。 (简称: )是 的集合,每个提交都是项目 在过去某个日期的样子的存档,无论是在您的机器上还是在其他人的机器上。 存储库 存储库 repo 提交 工作树 还包括我们的代码文件以外的东西,例如 、分支等。 存储库 HEAD 在两者之间,我们有 或 ;这两个术语可以互换。当我们 一个分支时,Git 会用最后 到我们 中的所有文件内容以及它们最初检出时的样子填充索引。 索引 暂存区 checkout 检出 工作目录 当我们使用 时, 是根据 的状态创建的。 git commit 提交 索引 因此, 或 是您下一次提交的游乐场。您可以使用 做任何您想做的事情,向其中添加文件,从中删除内容,然后仅当您准备就绪时,您才能继续并提交到存储库。 索引 暂存区 索引 是时候动手了🙌🏻 使用 来初始化一个新的仓库。将一些文本写入名为 的文件中: git init 1.txt 在上述三种树状态中, 现在在哪里? 1.txt 在工作树中,因为它还没有被引入到索引中。 为了 它,将它添加到索引中,使用 。 暂存 git add 1.txt 现在,我们可以使用 将我们的更改提交到存储库。 git commit 您创建了一个新的提交对象,其中包含一个指向描述整个工作树的树的指针。在这种情况下,它只会是根文件夹中的 。除了指向树的指针之外,提交对象还包括元数据,例如时间戳和作者信息。 1.txt 有关 Git 中的对象(例如提交和树)的更多信息, . 查看我之前的帖子 (是的,“退房”,双关语😇) Git 还告诉我们这个提交对象的 SHA-1 值。在我的例子中,它是 (这只是 SHA-1 值的前 7 个字符,以节省一些空间)。 c49f4ba 如果你在你的机器上运行这个命令,你会得到一个不同的 SHA-1 值,因为你是不同的作者;此外,您将在不同的时间戳上创建提交。 当我们初始化 repo 时,Git 会创建一个新分支(默认名为 )。和 .所以默认情况下,你只有 分支。如果你有多个分支会怎样? Git 如何知道哪个分支是活动分支? main Git 中的分支只是对提交的命名引用 main Git 有另一个指针称为 ,它(通常)指向一个分支,然后指向一个提交。顺便一提, 它包括带有一些前缀的分支名称。 HEAD 在引擎盖下, HEAD 只是一个文件。 是时候对 repo 进行更多更改了! 现在,我想创建另一个。因此,让我们创建一个新文件,并将其添加到索引中,就像以前一样: 现在,是时候使用 了。重要的是, 做了 件事: git commit git commit 两 首先,它创建一个提交对象,因此在 Git 的内部对象数据库中有一个对象具有相应的 SHA-1 值。这个新的提交对象也指向父提交。这是 在您编写 命令时指向的提交。 HEAD git commit 其次, 移动活动分支的指针——在我们的例子中是 ,指向新创建的提交对象。 git commit main 撤消更改 要重写历史,让我们从撤消引入提交的过程开始。为此,我们将了解命令 ,一个超级强大的工具。 git reset git reset --soft 所以你之前做的最后一步是 ,这实际上意味着两件事——Git 创建了一个提交对象并移动了 ,活动分支。要撤消此步骤,请使用命令 。 git commit main git reset --soft HEAD~1 语法 指的是 的第一个父级。如果我在提交图中有多个提交,则说“提交 3”指向“提交 2”,这反过来又指向“提交 1”。 HEAD~1 HEAD 并说 指向“Commit 3”。您可以使用 来引用“Commit 2”,而 将引用“Commit 1”。 HEAD HEAD~1 HEAD~2 所以,回到命令: git reset --soft HEAD~1 此命令要求 Git 更改 指向的任何内容。 (注意:在下图中,我使用 表示“无论 指向什么”)。在我们的示例中, 指向 。所以 Git 只会将 的指针更改为指向 。也就是说, 将指向“Commit 1”。 HEAD *HEAD HEAD HEAD main main HEAD~1 main 但是,此命令 影响索引或工作树的状态。因此,如果您使用 ,您将看到 已暂存,就像您运行 之前一样。 不会 git status 2.txt git commit 它将从 开始,转到 ,然后到“Commit 1”。请注意,这意味着“Commit 2”不再可从我们的历史记录中访问。 git log? HEAD main 这是否意味着“Commit 2”的提交对象被删除了? 🤔 不,它没有被删除。它仍然驻留在 Git 的内部对象数据库中。 如果您现在推送当前历史记录,通过使用 ,Git 不会将“Commit 2”推送到远程服务器,但提交对象仍然存在于您的本地存储库副本中。 git push 现在,再次提交——并使用“Commit 2.1”的提交信息来区分这个新对象和原来的“Commit 2”: 为什么“Commit 2”和“Commit 2.1”不同?即使我们使用相同的提交消息,即使它们指向 (由 和 组成的根文件夹),它们仍然具有不同的时间戳,因为它们是在不同时间创建的。 同一个树对象 1.txt 2.txt 在上图中,我保留了“Commit 2”来提醒你它仍然存在于 Git 的内部对象数据库中。 “Commit 2”和“Commit 2.1”现在都指向“Commit 1”,但只能从 访问“Commit 2.1”。 HEAD Git 重置——混合 是时候倒退并进一步撤消了。这一次,使用 (注意: 是 的默认开关)。 git reset --mixed HEAD~1 --mixed git reset 此命令与 相同。这意味着它获取 现在指向的任何指针,即 分支,并将其设置为 ,在我们的示例中 - “Commit 1”。 git reset --soft HEAD~1 HEAD main HEAD~1 接下来,Git 更进一步,有效地撤消了我们对索引所做的更改。也就是说,更改索引以使其与当前 匹配,即在第一步中设置后的新 。 HEAD HEAD 如果我们运行 ,这意味着 将被设置为 (“Commit 1”),然后 Git 会将索引与“Commit 1”的状态匹配——在这种情况下,它意味着 将不再是索引的一部分。 git reset --mixed HEAD~1 HEAD HEAD~1 2.txt 是时候用原始“提交 2”的状态创建一个新的提交了。这次我们需要在创建之前再次暂存 : 2.txt Git 重置 -- 硬 继续,撤消更多! 继续运行 git reset --hard HEAD~1 同样,Git 从 阶段开始,将 指向 ( ) 的任何内容设置为 (“Commit 1”)。 --soft HEAD main HEAD~1 到目前为止,一切都很好。 接下来,进入 阶段,将索引与 匹配。也就是说,Git 撤消了 的暂存。 --mixed HEAD 2.txt 现在是 步骤的时候了,Git 更进一步,将工作目录与索引的阶段相匹配。在这种情况下,这意味着也从工作目录中删除 。 --hard 2.txt (**注意:在这种特定情况下,文件 因此它不会从文件系统中删除;不过,为了理解 这并不是很重要)。 未被跟踪, git reset 因此,要对 Git 进行更改,您需要三个步骤。您更改工作目录、索引或临时区域,然后提交包含这些更改的新快照。要 这些更改: 撤消 如果我们使用 ,我们撤消了提交步骤。 git reset --soft 如果我们使用 ,我们也会撤消暂存步骤。 git reset --mixed 如果我们使用 ,我们撤销对工作目录的更改。 git reset --hard 真实场景! 场景#1 因此,在现实生活中,将“我爱 Git”写入文件 ( ),因为我们都爱 Git 😍。继续,暂存并提交: love.txt 哦,糟糕! 其实,我不想让你犯的。 我实际上希望你做的是在提交之前在此文件中多写一些情话。 你能做什么? 好吧,克服这个问题的一种方法是使用 ,有效地撤消您采取的提交和暂存操作: git reset --mixed HEAD~1 所以 再次指向“Commit 1”, 不再是索引的一部分。但是,该文件仍保留在工作目录中。您现在可以继续,并向其中添加更多内容: main love.txt 继续,暂存并提交您的文件: 干得好👏🏻 你得到了“Commit 2.4”指向“Commit 1”的清晰、漂亮的历史记录。 现在我们的工具箱里有了一个新工具, 💪🏻 git reset 这个工具超级好用,你几乎可以用它完成任何事情。它并不总是使用起来最方便的工具,但如果您小心使用它,它几乎可以解决任何重写历史的场景。 对于初学者,我建议几乎任何时候你想在 Git 中撤消时只使用 。一旦您对它感到满意,就可以继续使用其他工具了。 git reset 场景#2 让我们考虑另一种情况。 创建一个名为 的新文件;阶段和提交: new.txt 哎呀。其实,这是一个错误。你在 上,我希望你在功能分支上创建这个提交。我的坏😇 main 我希望您从这篇文章中获得两个最重要的工具。 是 。第一个也是最重要的一个是 当前状态与你想要进入的状态。 第二个 git reset 在白板上标出 对于这种情况,当前状态和期望状态如下所示: 你会注意到三个变化: 在当前状态下指向“Commit 3”(蓝色那个),但在期望状态下指向“Commit 2.4”。 main 分支在当前状态下不存在,但它存在并指向所需状态下的“Commit 3”。 feature 在当前状态下指向 ,在期望状态下指向 。 HEAD main feature 如果你能画出这个并且你知道如何使用 ,你绝对可以让自己摆脱这种情况。 git reset 因此,再次重申, 是深吸一口气,把它画出来。 最重要的 观察上图,我们如何从当前状态到想要的状态呢? 当然有几种不同的方法,但我只会为每种情况提供一个选项。也可以随意尝试其他选项。 您可以使用 开始。这会将 设置为指向之前的提交,“Commit 2.4”: git reset --soft HEAD~1 main 再次查看当前与期望图,您可以看到您需要一个新分支,对吧?您可以为它使用 或 (做同样的事情): git switch -c feature git checkout -b feature 此命令还会更新 以指向新分支。 HEAD 由于您使用了 ,因此您没有更改索引,因此它当前具有您想要提交的状态——多么方便!您可以简单地提交到 分支: git reset --soft feature 你达到了想要的状态 🎉 场景#3 准备好将您的知识应用于其他案例了吗? 对 添加一些更改,并创建一个名为 的新文件。暂存它们并提交: love.txt cool.txt 哦,糟糕,实际上我希望你创建两个单独的提交,每个更改一个 🤦🏻 想自己试试这个吗? 您可以撤消提交和暂存步骤: 执行此命令后,索引不再包含这两个更改,但它们仍在您的文件系统中。所以现在,如果你只暂存 ,你可以单独提交它,然后对 做同样的事情: love.txt cool.txt 不错😎 场景#4 创建一个包含一些文本的新文件 ( ),并将一些文本添加到 。暂存两个更改,并提交它们: new_file.txt love.txt 哎呀🙈🙈 所以这一次,我希望它在另一个分支上,但不是一个 分支,而是一个已经存在的分支。 新 所以,你可以做什么? 我会给你一个提示。答案非常简短而且非常简单。我们先做什么? 不,不 。我们画。这是要做的第一件事,因为它会让其他一切变得容易得多。所以这是当前状态: reset 和理想的状态? 您如何从当前状态进入所需状态,最简单的是什么? 因此,一种方法是像以前一样使用 ,但我希望您尝试另一种方法。 git reset 首先,移动 指向 分支: HEAD existing 直觉上,您要做的是采用蓝色提交中引入的更改,并将这些更改(“复制粘贴”)应用到 分支之上。而 Git 有一个专门用于此的工具。 existing 要让 Git 获取此提交与其父提交之间引入的更改,并将这些更改应用到活动分支,您可以使用 。此命令获取指定修订中引入的更改并将它们应用于活动提交。 git cherry-pick 它还创建一个新的提交对象,并更新活动分支以指向这个新对象。 在上面的示例中,我指定了创建的提交的 SHA-1 标识符,但您也可以使用 ,因为我们正在应用其更改的提交是 指向的提交。 git cherry-pick main main 但是我们不希望这些更改存在于 分支上。 仅将更改应用到 分支。你怎么能从 中删除它们? main git cherry-pick existing main 一种方法是 回 ,然后使用 : switch main git reset --hard HEAD~1 你做到了! 💪🏻 请注意, 实际上计算了指定提交与其父提交之间的差异,然后将它们应用于活动提交。这意味着有时 Git 将无法应用这些更改,因为您可能会遇到冲突,但这是另一篇文章的主题。 git cherry-pick 另外请注意,您可以要求 Git 任何提交中引入的更改,而不仅仅是分支引用的提交。 cherry-pick 我们获得了一个新工具,因此我们拥有 和 。 git reset git cherry-pick 场景#5 好的,改天,另一个回购协议,另一个问题。 创建一个提交: 并将其 送到远程服务器: push 嗯,哎呀😓… 我刚注意到一件事。那里有错字。我写了 而不是 。哎呀。那么现在最大的问题是什么?我 ed,这意味着其他人可能已经 了这些更改。 This is more tezt This is more text push pull 如果我使用 覆盖这些更改,正如我们目前所做的那样,我们将拥有不同的历史记录,一切都可能一团糟。您可以根据需要重写自己的回购副本,直到您 它为止。 git reset push 一旦你 更改,如果你要重写历史,你需要 确定没有其他人获取这些更改。 push 非常 或者,您可以使用另一个名为 的工具。此命令获取您提供给它的提交并从其父提交计算差异,就像 一样,但这次,它计算反向更改。 git revert git cherry-pick 因此,如果您在指定的提交中添加了一行,则反向将删除该行,反之亦然。 创建了一个新的提交对象,这意味着它是对历史记录的补充。通过使用 ,您没有重写历史。你承认了你过去的错误,这次提交是承认你犯了一个错误,现在你修正了它。 git revert git revert 有人会说这是更成熟的方式。有人会说,如果您使用 重写以前的提交,那么历史记录不会那么干净。但这是避免改写历史的一种方式。 git reset 您现在可以修复拼写错误并再次提交: 您的工具箱现在加载了一个新的闪亮工具, : revert 场景 #6 完成一些工作,编写一些代码,然后将其添加到 中。暂存此更改并提交: love.txt 我在我的机器上做了同样的事情,我使用键盘上的 箭头键滚动回到之前的命令,然后我按下 ,然后……哇。 Up Enter 哎呀。 我只是使用 吗? 😨 git reset --hard 发生了什么? Git 将指针移至 ,因此无法从当前历史记录中访问我所有 工作的最后一次提交。 Git 还取消暂存区中的所有更改,然后将工作目录与暂存区的状态相匹配。 到底 HEAD~1 宝贵 也就是说,一切都符合我的工作……消失的状态。 抓紧时间。吓坏了。 但是,真的,有理由惊慌失措吗?不是真的……我们是放松的人。我们做什么?好吧,直觉上,提交真的 消失了吗?不,为什么不呢?它仍然存在于 Git 的内部数据库中。 真的 如果我只知道它在哪里,我就会知道标识此提交的 SHA-1 值,我们就可以恢复它。我什至可以撤消撤消,然后 回此提交。 reset 所以我真正需要的是“已删除”提交的 SHA-1。 所以问题是,我如何找到它? 有用吗? git log 好吧,不是真的。 会转到 ,它指向 ,它指向我们正在寻找的提交的父提交。然后, 将通过父链追溯,其中不包括我宝贵工作的提交。 git log HEAD main git log 值得庆幸的是,创建 Git 的非常聪明的人还为我们创建了一个备份计划,称为 。 reflog 当您使用 Git 时,每当您更改 时,您可以使用 以及其他命令(如 或 ,Git 都会向 添加一个条目。 HEAD git reset git switch git checkout reflog 我们找到了我们的承诺!它是以 开头的。 0fb929e 我们还可以通过它的“昵称”—— 来联系它。因此,例如 Git 使用 来获取 的第一个父级,并 来引用 的第二个父级等等,Git 使用 来引用 的第一个 reflog 父级, 在上一步中指向的位置。 HEAD@{1} HEAD~1 HEAD HEAD~2 HEAD HEAD@{1} HEAD HEAD 我们还可以要求 向我们展示它的价值: git rev-parse 另一种查看 方法是使用 ,它要求 实际考虑 : reflog git log -g git log reflog 我们在上面看到 和 一样指向 ,后者指向“Commit 2”。但是 中该条目的父项指向“Commit 3”。 reflog HEAD main reflog 因此,要返回“Commit 3”,您只需使用 (或“Commit 3”的 SHA-1 值): git reset --hard HEAD@{1} 现在,如果我们 : git log 我们挽救了局面! 🎉👏🏻 如果我再次使用这个命令会发生什么?并运行 ? Git 会将 设置为 在上次 之前指向的位置,意思是“提交 2”。我们可以继续一整天: git commit --reset HEAD@{1} HEAD HEAD reset 现在看看我们的工具箱,里面装满了可以帮助您解决 Git 中出现问题的许多情况的工具: 使用这些工具,您现在可以更好地了解 Git 的工作原理。有更多的工具可以让你专门重写历史, ),但你已经在这篇文章中学到了很多东西。在以后的帖子中,我也会深入研究 。 git rebase git rebase 最重要的工具,甚至比此工具箱中列出的五个工具更重要的是,将当前情况与所需情况进行白板比较。相信我,这会让每一种情况看起来都不那么令人生畏,解决方案也会更加清晰。 了解更多关于 Git 的信息 我还就这篇文章的内容进行了现场演讲。如果您更喜欢视频(或希望在阅读的同时观看)——您可以找到它 . 这里 一般来说, 涵盖了 Git 及其内部的许多方面;欢迎你 (双关语😇) 我的 YouTube 频道 一探究竟 关于作者 是 CTO 和联合创始人 ,一种开发工具,可帮助开发人员及其团队使用最新的内部文档管理有关其代码库的知识。 Omer 是 Check Point 安全学院的创始人,并且是 ITC 的网络安全负责人,ITC 是一家培训有才华的专业人员以发展技术职业的教育机构。 奥马尔·罗森鲍姆 游泳 Omer 拥有特拉维夫大学的语言学硕士学位,并且是 . 简短的 YouTube 频道 首次发表 于此