重写代码存储库历史记录,或者为什么有时可以使用git push -f





一位年轻的Padawan可以使用git存储库收到的第一条训诫之一是:“千万不要吃黄雪,要这样做git push -f。”由于这是新手软件工程师需要学习的数百个格言之一,因此没有人会花时间阐明为什么不应该这样做。就像婴儿和火:“火柴不是给孩子的玩具”,仅此而已。但是我们作为人和专业人士而成长和发展,有一天,“为什么,实际上呢?”这个问题出现了。全面增长。本文是根据我们的内部聚会而写的,主题为:“何时以及应该重写提交的历史记录。”







我听说有些公司在面试中回答这个问题的能力是高级职位面试的标准。但是,要更好地理解其答案,您需要了解为什么重写历史根本不好?



为此,我们需要快速浏览git存储库的物理结构。如果您确定对回购设备一无所知,则可以跳过此部分,但是即使在查找过程中,我也为自己学到了很多新东西,而有些旧东西却不那么相关。



在最低级别上,git repo是对象和指向它们的指针的集合。每个对象都有自己唯一的40位哈希(20个十六进制字节),该哈希是根据对象的内容计算得出的。







摘自The Git Community Book



插图主要对象类型是Blob(只是文件的内容),树(指向Blob和其他树的指针的集合)和提交。类型为commit的对象仅是指向树,先前的提交和服务信息的指针:日期/时间,作者和注释。



我们过去使用过的分支和标签在哪里?它们不是对象,它们只是指针:一个分支指向其中的最后一个提交,标记指向存储库中的任意提交。也就是说,当我们在IDE或GUI客户端中看到精美绘制的分支上带有提交圈时,它们是动态构建的,沿着提交链从分支的末端一直到“根”。存储库中的第一个提交没有上一个提交,而不是指针为空。



需要了解的重要一点:同一提交可以同时出现在多个分支中。创建新分支时,不会复制提交,它只是从发出命令时HEAD所在的位置开始“增长” git checkout -b <branch-name>



那么,为什么重写存储库的历史记录有害吗?







首先,这很明显,当您将新的故事上载到工程团队正在使用的存储库时,其他人可能会丢失他们的更改。该命令git push -f 从服务器上的分支中删除所有不在本地版本中的提交,并写入新的提交。



由于某些原因,很少有人知道该团队git push拥有“安全”钥匙--force-with-lease如果其他用户向远程存储库添加了提交,则导致命令失败。我总是建议改用它-f/--force



该命令git push -f被视为有害的第二个原因是,当尝试将一个具有重写历史记录的分支与保留了该分支的分支合并(更确切地说,是保留了从重写历史记录中删除的提交)时,我们将遇到许多冲突(按数字)实际上)。有一个简单的答案:如果您仔细地遵循GitflowGitlab Flow,那么这种情况极有可能甚至不会出现。



最后,重写历史还有令人不快的一面:实际上,从分支中删除的那些提交实际上并不会消失在任何地方,而只会永远挂在回购中。琐事,但不愉快。幸运的是,git开发人员也使用垃圾收集命令解决了这个问题git gc --prune。大多数git主机,至少是GitHub和GitLab,有时会在后台执行此操作。



因此,在消除了更改存储库历史记录的担心之后,我们终于可以继续讨论主要问题:为什么需要它,何时才是合理的?



实际上,我可以确定几乎每一个或多或少活跃的git用户都至少更改过一次历史记录,这时突然发现上一次提交中出了点问题:一个烦人的错字输入了代码,而不是从那个代码中进行了一次提交用户(通过个人电子邮件,而不是工作电子邮件,反之亦然),忘记添加新文件(如果您像我一样喜欢使用git commit -a)。即使更改提交的描述,也需要重写它,因为哈希也是从描述中计算的!



但这是一个微不足道的情况。让我们看一些更有趣的。



假设您做了几天就看到的一项重要功能,每天将工作结果发送到服务器上的存储库(4-5次提交),然后将所做的更改发送给审核。两个或三个不懈的审阅者向您推荐了各种大小的编辑建议,甚至发现了门槛(还有4-5次提交)。然后,质量检查人员发现了一些也需要修复的极端情况(需要再提交2-3次)。最后,在集成过程中,发现了一些不兼容性或引入了自动测试,这也需要修复。



如果现在不看就按“合并”按钮,那么诸如“我的功能,第1天”,“第2天”,“修复测试”,“修复检查”之类的十二个提交将添加到主分支(对于许多人来说,这是老式的母版)等等当然,这对squash模式有所帮助,现在GitHub和GitLab中都使用了squash模式,但是您需要谨慎:首先,它可以用不可预测的内容替换提交描述,其次,替换功能的作者按下“合并”按钮的那个按钮(在我们国家/地区,这通常是一个机器人,可以帮助发布工程师构建今天的部署)。因此,最简单的方法是,在最终集成到发行版之前,使用折叠分支的所有提交git rebase



但是也有可能您已经通过回购历史来回顾代码审查,这回想起了奥利维尔·沙拉。如果某个功能由于分解不佳而已经进行了数周的切割,或者尽管为此而败坏了体面的团队,但是在开发过程中需求发生了变化,就会发生这种情况。例如,这是一个真正的合并请求,两周前才提交给我进行审查:







我的手自动伸到了“报告滥用”按钮,因为您还能如何表征50条提交并更改近2000行的请求?并想知道如何进行审查?



老实说,我花了两天时间才强迫自己开始这篇评论。对于工程师来说,这是正常的反应。处于类似情况的某人只是不看就按了“批准”,意识到在合理的时间内他们仍然无法以足够的质量来审查此更改。



但是有一种方法可以使朋友的生活更轻松。除了为更好地分解问题所做的初步工作之外,在编写完主代码后,您还可以将其编写历史转变为更具逻辑性的形式,并通过绿色测试将其分解为原子提交:“为其创建了一个新服务和一个传输层”,“构建了模型并编写了代码”检查不变式”,“添加验证和异常处理”,“编写测试”。

这些提交中的每一个都可以分别进行审查(GitHub和GitLab都可以这样做),并且可以在任务之间进行切换或休息时在团队中进行。带钥匙



的同一个将帮助我们完成所有这一切。作为参数,您需要向其传递提交的哈希值,您将需要从中重写历史记录。如果我们正在谈论最后的50次提交,如图中的示例,则可以写(用数字“ 50”代替)。 顺便说一句,如果您在执行任务的过程中将master分支添加到自己,那么首先需要重新设置该分支的基础,以使合并提交和来自master的提交不会在您的脚下混乱。git rebase--interactivegit rebase --interactive HEAD~50







有了git仓库内部知识,应该很容易理解rebase如何在master上工作。该命令获取分支中的所有提交,并将第一个提交的父级更改为master分支中的最后一个提交。参见图:









插图取自Pro Git书。



如果C4和C3的更改发生冲突,则在冲突解决后,提交C4将更改其内容,因此在第二张图中将其重命名为C4'。



这样,您将最终得到一个分支,该分支仅包含您的更改,并且从母版的顶部开始增长。当然,大师必须是最新的。您可以只使用服务器上的版本:(git pull --rebase origin/master如您所知,它是git pull等效的git fetch && git merge,并且密钥--rebase将强制git重新设置基准,而不是合并)。



让我们最后回到git rebase --interactive...它是由程序员为程序员制作的,并且意识到人们在此过程中会承受的压力,我们试图尽可能地节省用户的神经,并减轻了用户过度紧张的需要。这是您将在屏幕上看到的内容:





这是流行的Guzzle软件包的存储库。看来他可以使用变基...



生成的文件在文本编辑器中打开。您将在下面找到有关在此做什么的详细信息。接下来,在轻松编辑模式下,您决定如何处理分支中的提交。一切都像棍子一样简单:选取-保留原样,重新编写字词-更改提交描述,压扁-与上一个合并(该过程从下往上进行,也就是说,上一个是下面的行),删除-完全删除,编辑-这就是有趣的是停止并冻结。 git遇到edit命令后,它将采用已将提交中的更改添加到暂存模式的位置。您可以更改此提交中的任何内容,在此提交上添加更多内容,然后命令git rebase --continue继续进行变基过程。



哦,而且,您可以交换提交。这可能会产生冲突,但是通常,重新设置过程很少完全没有冲突。就像他们说的那样,他们脱下头后就不会为自己的头发哭泣。



如果您感到困惑,并且似乎一切都消失了,则可以使用紧急弹出按钮git rebase --abort,该按钮将立即将所有内容恢复为原来的状态。



您可以重新设置多个基准,仅触摸故事的一部分,而其余部分则保持选中状态,从而使您的故事像陶壶一样变得越来越美观。就像我在上面写的那样,这是个好习惯,要确保每个提交中的测试都是绿色的(为此,编辑非常有帮助,并且在下一次通过时-压入)。



另一种特技飞行,在需要将同一文件中的多个更改分解为不同的提交时很有用- git add --patch。它本身可以很有用,但是与edit指令结合使用时,它将允许您将一个提交拆分为多个提交,并在单个行的级别上完成它,如果我没记错的话,它没有GUI客户端,也没有IDE不允许。



再次确保一切都井井有条,您终于可以放心地做点什么了,这是本教程的开始git push --force。哦,就是这样--force-with-lease







首先,您很可能会在此过程上花费一个小时(包括基于master的初始重新设置),或者如果该功能真的在扩展,则甚至花费两个小时。但这甚至比等两天让审阅者强迫自己最终接受您的请求要好得多,再等两天直到审阅该请求,这要好得多。将来,您很可能会在30-40分钟内适应。带有内置冲突解决工具的IntelliJ产品(全面披露:FunCorp向员工支付这些产品的费用)在此方面特别有用。



我要警告您的最后一件事是在代码检查期间不要重写分支历史记录。请记住,认真的审阅者可能会在本地克隆您的代码,以便能够通过IDE查看它并运行测试。



谢谢所有读到最后的人!我希望这篇文章不仅对您有用,而且对收到您的代码进行审查的同事也很有用。如果您有一些很棒的git hack-在评论中分享它们!



All Articles