Git技术
分支
创建和切换分支
Git有许多用于处理分支的命令。创建分支的最常见方法是使用git checkout -b NAME_OF_NEW_BRANCH命令。这个命令会从当前分支上的最新提交开始,创建一个新分支,然后切换到该分支。
您也可以使用git checkout NAME_OF_EXISTING_BRANCH(不带-b选项)切换到已有分支。
还有许多其他分支命令:有关命令和选项的列表,请参见Git文档和类似Git分支备忘录的其他页面。
获取,推送和拉取分支
大多数Git网络操作命令都接受要与之交互的远程仓库的名称,但是如果您未指定远程名称,Git默认您是想与远程origin仓库进行交互。
git fetch命令告诉Git去获取一个仓库,并下载本地仓库未存储的所有提交的副本。这也适用获取远程仓库的分支。
下载远程分支的列表后,您可以使用git checkout NAME_OF_REMOTE_BRANCH命令根据远程分支名称创建本地分支。Git将创建一个与远程分支指向提交相同的新分支。它还会设置本地分支跟踪远程分支,这意味着来自本地分支的任何推送都将更新到远程分支。
稍后,您可以使用git push命令,将本地的新提交更新到远程分支。您还可以推送在远程仓库中尚未创建的分支到远程仓库。
如果远程分支具有本地分支中没有的提交,使用git pull则会将新的提交拉取到本地仓库中,并更新本地分支以包含这些提交。
如果您在本地分支上重写历史记录,以使其与远程分支不同,则git push命令将失败并显示错误。您可以强制推送,这将强制更新远程分支使用您此次推送的这些提交。 强制推送可能是危险的,具体取决于工作流程。 如果有人拉取了过去的历史,而您强制推送,那么现在他们将有冲突要处理。强制推送可以用于解决某些问题或反复更新PR的情况,但应谨慎使用。强制推送也仅仅是一种方法,如果你确实需要使用的时候尽管使用,不过使用的时候一定要格外小心。
合并分支
通过合并,您可以将分支B上存在的修改和历史记录合并到当前分支A中。合并的前提是两个分支先前都具有一组共同的提交,后来各自进行了不同的提交(对相同或不同文件的修改)。合并操作在当前分支上创建一个新的“merge commit”,该合并将所有修改集中到一次提交。这就允许开发人员通过分别编写代码然后将修改合并到一起的方式来进行代码协作。
合并是通过git merge OTHER_BRANCH_NAME命令完成的,它告诉Git从另一个分支合并到当前分支。
如果两个分支上的更改相互干扰,则存在合并冲突。Git会对文件中不匹配部分进行标记。如何解决冲突问题取决于您,之后保存更正后的文件,添加它并完成合并提交。我喜欢将SourceGear DiffMerge用作解决冲突的GUI工具,VS Code在突出显示文件中的冲突方面做得也很好,并且提供选择应用左侧或右侧修改版本的按钮。
功能分支策略
大多数团队使用“功能分支”策略进行开发。他们有一个主要的开发分支,例如main, master或develop。每当开发人员着手新任务时,他们都会基于主分支创建一个新分支,并且经常使用任务的名称和ID作为分支名称:git checkout -b feature/myapp-123-build-todos-list.
开发人员会在一段时间内使用其功能分支。工作完成后,他们将分支推送到团队的中央仓库,其他团队成员审查所做的修改,开发人员根据审查进行所需的代码修复(fix),最后将功能分支合并回主要的开发分支。
开发人员可能需要拉取已添加到主分支的更改,然后从主分支合并(merge down)到其功能分支。将功能分支合并回主分支称为merge up,从主分支合并到功能分支称为merge down.
Pull Requests
如果您使用过Git,那么您之前可能已经听说过术语“pull request”(简称“ PR”,有时也称为“合并请求”)。严格来说,“pull request”不是Git的概念,它是由仓库托管站点和工具(如Github,Gitlab和Bitbucket)在Git之上构建的合并工作流。
PR是一种在中央Git仓库/服务器级别进行代码审查和处理合并的方法。这通常与功能分支的使用相关联。开发人员将功能分支推送到仓库,并创建从some-feature分支合并到main主分支的PR。其他开发人员可以查看PR的页面,查看文件差异以及在特定行上留下建议更改的注释。功能开发人员根据这些建议进行更多提交,之后再次推送分支,PR会自动进行更新以跟进分支所做的修改。在其他团队成员批准之后,可以合并PR并删除功能分支。
在后台更新分支
通常,更新分支的本地副本的方法是先执行git checkout some-branch切换到分支,然后执行git pull命令。但是,如果此时我在功能分支上工作,我经常会有未保存的更改,而且我不想仅仅为了拉取而切换到主分支。
有一个非常有用的技巧,可以在不签出分支的情况下对分支进行拉取:
git fetch <remote> <remoteBranch>:<localBranch>
也就是说,当我在features/some-feature分支上工作时,我可以不切换分支来更新main分支。通常,本地分支和远程分支具有相同的名称。因此,我可以运行git fetch origin main:main命令,然后Git会从远程origin/main分支上下载所有新的提交,然后更新我的本地main分支。
重写Git历史记录
有多种方法可以更改Git仓库中的历史记录。不同方法在适用于不同情况,并且这些方法对于解决先前提交所造成的问题通常很有用。如前所述,Git提交是不可变的,因此您永远无法真正对其进行修改-您只能用新的提交替换。因此,当我们“重写历史记录”时,实际上是在创建“替代的历史记录”。
重要的是,您只应重写仍在您自己的仓库中并且从未推送到另一个仓库的历史记录! 只要没有推送提交,其他任何人都不会在乎它们,您可以按照您心所想重写它们。但是,一旦将其推送,其他人的仓库可能会依赖于旧的历史记录,而更改历史记录可能会给他们带来冲突。
Amending Commits
重写历史记录的最简单方法是“amend”最新提交。
这实际上意味着将其替换为稍有不同的提交。可以通过git commit --amend命令或GUI工具来完成:
从技术上讲,旧提交仍存在于Git的仓库中,但是当前分支引用将指向新创建的提交。
Resetting Branches
由于分支引用是指向给定提交的指针,因此我们可以通过更新引用以指向较早的提交来重置分支。通常用于回滚您所做的某些提交。
重置分支时,有三个选项允许您控制在磁盘和暂存区中的文件究竟作何变化:
● git reset:移动分支指针以指向不同的提交
● --soft:保留在磁盘和暂存区中的文件
● --mixed:保留在磁盘上的文件,但是清空暂存区
● --hard:清空暂存区,并且恢复到和指定提交相同的工作目录状态
使用git reset --soft命令是“安全的”,因为它不会更改磁盘上的任何文件。 而使用git reset --hard命令是“危险的”,因为它将清除已修改或尚未提交的所有文件,并将所有文件替换为指定提交中的文件。
git reset需要提交标识符作为参数。这可以是特定的提交哈希(git reset ABCD1234)或其他一些修订标识符。您甚至可以更新当前分支,使其指向其他分支上的提交(git reset --hard some-other-branch)
Rebasing Branches
“变基”是一种替代合并的方法,该方法会先用另一个分支的提交更新当前分支,然后在分支上重新创建提交。也就是说,变基不是直接合并两组更改,而是从较早的提交开始重写历史记录,使当前分支看起来是刚刚创建的。与合并相似,这是通过git rebase OTHER_BRANCH_NAME命令完成的。
想象一下,main分支刚开始有两个提交A <- B,我们从提交B开始创建一个功能分支。现在,其他人将更多提交合并到main分支中,main分支现在拥有四个提交A <- B <- C <- D。如果我们对功能分支执行变基,就像把我们的功能分支切断,然后将提交放到main分支末尾,变基之后我们的提交会在提交D而不是B之后。
Reverting Commits
重置(reset)分支实际上会丢弃较新的提交。如果我们想撤消较早提交中的更改,但保留此后的历史记录怎么办?
使用移除提交命令git revert会创建一个新的提交,该提交的修改是您指定提交的逆操作。它不会删除原始提交,因此历史记录实际上没有被修改。
Cherry-Picking
Cherry-picking允许您复制特定提交中的修改,并将这些修改作为新的提交应用于其他分支。例如,也许有一个紧急补丁必须直接在hotfix分支上创建并部署到生产中,但是您还需要确保在main分支上也具有补丁的提交。您可以从hotfix分支中挑选(cherry-pick)单个提交到main分支上。
git cherry-pick命令接受单个提交引用或提交范围。请注意,该范围不包括您列出的第一次提交。如果我运行git cherry-pick A..E命令,则它将提交B, C, D, E复制到该分支上。Git会使用新的哈希值创建提交(因为时间戳和父提交不同),但是会保留差异和提交元数据。
Interactive Rebasing
“变基”涉及重写整个分支的历史记录。此方法有一个变体,称为“交互式变基”,它使您可以有选择地修改分支上的较早提交。这是通过git rebase -i STARTING_COMMIT完成的。
交互式变基使您可以执行几种不同类型的修改。你可以:
● 编辑提交信息以进行提交
● 改变提交顺序
● 合并多个提交
● 删除提交
在对提交历史记录指定所需的更改之后,Git将执行您列出的修改,并在指定的起始提交之后更新所有提交。与其他历史记录重写操作一样,对于任何已更改的提交将生成一组对应的具有新的哈希的新提交,因为父提交修改,所以即使其余内容未修改也会生成新的提交。
从CLI运行交互式变基会在您的文本编辑器中显示起始提交之后所有提交的列表,以及一列奇怪的命令名称,例如“pick”和“squash”。通过修改编辑器中的文本,然后保存并退出,可以对提交进行重新处理。例如,如果您想交换几次提交的顺序,则可以剪切其中的一行文本并将其粘贴到其他位置。
我觉得这很麻烦,所以我强烈建议使用Git的图形用户界面的执行任何交互式变基操作。SourceTree和Fork具有用于执行交互式变基的相当不错的UI.
Reflog
彻底清除Git提交记录是非常困难的。即使您执行了git reset --hard,提交似乎已消失,但是Git仍然在内部保存了这些提交的副本。
如果您确实遇到无法从任何标签或分支引用获取提交的情况,则可以使用Git reflog进行回顾并再次找到它们。reflog显示所有提交,无论它们位于哪个分支上,或者是否仍然有指向该提交的有意义的指针。这样,您可以再次检出它们,创建指向这些提交的新标签或分支,或者只是查看差异。
高级历史重写
最后,Git支持一些非常高级的工具来重写整个仓库级别的历史记录。特别是git filter-branch命令,您可以用这条命令执行以下任务:
● 重写历史记录中的文件名和路径(例如:更改./src中的文件以使它们在仓库的root目录中显示)
● 创建一个新的仓库,使其只包含原始仓库中的某些文件夹,但包含所有历史记录
● 在许多历史提交中重写文件内容
git filter-branch命令执行起来非常慢,因此还有其他外部工具可以执行类似的任务。虽然我没有使用过它,但https://github.com/newren/git-filter-repo声称能够更快地运行相同类型的操作,这个工具甚至已经被Git官方文档推荐。
有时,仓库会因为一些大文件而变得臃肿不堪,如果您想要重写历史记录以假装那些文件根本不存在。名为BFG Repo Cleaner的工具可以很好地完成这一任务。
如果这些现有工具无法满足您的需求,则您也可以随时编写自己的工具。 我曾经写过一套基于Python的工具,以重写具有多年历史记录的整个JS代码仓库,包括对工具进行优化以使其在短短几个小时内即可运行完成。
这些工具非常强大,不应将它们用于日常工作。它们应该像灭火器,希望您永远都不需要使用它们,但是最好将其放在身边以防万一。
Git模式和最佳实践
既然我们已经涉及了许多命令和技术细节,那么如何实际地更好使用Git呢?下面是我发现最有帮助的一些事情:
编写好的提交信息
编写良好的提交消息至关重要。提交信息并不是为了需要编写而编写。您应当为该项目的未来开发者留下笔记,说明进行了哪些更改,或更重要的是为什么进行这些更改。任何人都可以查看提交中的一组差异并查看更改的行,但是如果没有良好的提交消息,在查看差异之前您就不知道进行这些更改的原因是什么。
有很多文章讨论了编写提交信息的规则,并提供了很多好的建议。我个人不太在意诸如“每行最多72个字符”或“在第一行中使用现在时,在其他行中使用过去时”之类的规则,尽管这样做有充分的理由。对我来说,关键规则是:
● 提交消息始终使用相关的任务ID开头。这样,您以后可以返回任务详情,以查看有关特定任务的详细信息。
● 第一行应该是关于修改的简短摘要。提交信息的第一行将在任何Git历史记录日志中显示,因此它既需要足够简短,又要清楚地描述整个提交。这一行描述修改的目的而不是做了那些修改。
● 如果您还有其他详细信息,请添加一个空白行,然后编写其他段落或要点。后面段落无论您想写多少都成!默认情况下,此部分通常会在Git UI中折叠,但可以展开以显示更多详细信息。我看到过一些优秀的提交信息,这些信息有很多个段落,它们为为什么进行修改提供了重要的上下文。
这种格式的典型示例如下所示:
MYAPP-123: Rewrite todos slice logic for clarity
- Added Redux Toolkit
- Replaced handwritten reducer with `createSlice`. This simplified the logic considerably,
because we can now use Immer and it auto-generates the action creators for us.
- Updated `TodosList` to use the selectors generated by the slice.
进行较小且独立的提交
这与编写良好提交信息的建议紧密相关。
从概念上讲,提交应该相对较小并且是独立的。一次提交可能会涉及多个文件,但是这些文件中的修改应该彼此紧密相关。如此提交的原因有很多:
● 可以容易地描述提交中的修改
● 方便查看提交中包含的修改
● 如果稍后需要还原(revert)提交,则受影响的其他修改会更少
● 当某人逐行查看历史记录时,将有与每行相关的更具体的注释(“Fixed bug X”,而不是“Made a bunch of changes”等等)
● 容易将提交历史一分为二并缩小可能导致错误的修改范围
例如,假设我要向项目添加新的JS库。我将提交一个仅包含package.json和yarn.lock修改的提交,然后将使用该库的代码修改放入另一个单独的提交中。您可以在我编写的“ Redux Fundamentals”示例程序的提交中看到这种提交方法的示例。
对我而言,提交历史应该像“讲述故事”一样说明如何完成给定任务。无论是在PR审查过程中还是在几年之后,任何人都应该能够通过浏览这一系列的提交信息,了解我所做的修改以及进行这种修改的思路。
推送前清理提交历史
在完成任务过程中,我经常会提交若干个“WIP”提交。也许我刚刚做了很多编辑工作,代码现在大部分都能正常工作了,我想在继续后续工作之前记录一个检查点;也许是我忘了提交一些特定的代码,后来又将其添加到另一个提交中。但是这些并不是我所说的“故事”的一部分。
在推送PR分支之前,我经常使用交互式变基清理提交历史。不能因为我的本地历史中有一些糟糕的提交就一定要让全世界都知道我的实际工作过程是这么糟糕。我在提交中所讲的“故事”是一种理想化的任务实现过程,“假装我完美地完成了此任务,并且一路上没有任何错误”。
仅重写未推送的历史记录
如前所述:只要分支仍然是本地分支并且没有被推送,您可以随意重写历史!但是,一旦被推送,就应该避免重写历史。
一个例外是,如果分支仍在进行PR,并且您重做了历史记录。在这一点上,很可能还没有人依赖它,因此您可以强制推送分支以更新PR。(React团队经常这样做)
保持分支短生命周期
对于分支中可以包含多少行代码或提交没有硬性规定。但是,功能分支通常不会长时间存在。所以,合并到PR中的修改会更少,并且您从其他分支拉取修改的机会也会减少。
有些人认为合并功能分支到主分支会更好,也有人认为使用变基以保持主分支干净整洁的方法更好。我个人喜欢合并提交,我更喜欢看到是什么时候合并。对于团队,最重要的是选定一个方法作为惯例并始终执行。
Code Archeology with Git
为什么好的提交实践很重要呢?
假设您在具有很多年历史的代码库上工作。有一天,您被分配了一项任务,需要变更代码库的某些部分。也许是要修复刚刚弹出的错误或者添加新功能。当您打开一个文件,里面有数百行代码,您通读了它,代码可能有点丑,逻辑中有很多特殊的条件判断,您不确定这些代码究竟会在什么时候运行结束。
现在通过代码文件您大概可以知道代码在做什么。但是除非文件有很多好的注释,否则没有太多信息说明为什么会有这样的代码,或者为什么代码最终会变成这个样子。我们自然地会假设“当前存在的任何代码都是正确的”,但实际上并不总是正确。
这就是拥有良好Git历史记录的关键所在。通过浏览文件的历史记录,您可以知道:
● 谁编写了这一行代码
● 何时更改了该代码,以及同时更改了哪些其他代码
● 这些修改是哪个任务的一部分
● 修改背后的目的是什么
● 作者当时在想什么
● 什么时候引入了Bug
当跟踪错误或编写功能代码时,这些都是非常有价值的信息。
显示文件历史修改
有多种方法可以查看文件修改的历史记录。
git log命令可以让您查看影响特定文件的提交。IDE和Git GUI也能够使您浏览文件的历史记录,显示文件差异,通常包括浏览文件的两个任意版本之间差异的能力。一些Git GUI还允许您浏览特定提交时的整个仓库的文件树。
Git还有一个git blame命令,该命令可以为每行打印提交ID,作者和时间戳。CLI输出很难阅读,但是每个好的IDE都可以在文件中的实际代码旁显示文件blame信息。IDE通常会增强blame信息,以向您显示更多有关作者,提交消息以及此提交之前和之后的提交的详细信息:
Github也提供了blame视图,并且可以轻松地跳转以查看文件的早期版本。Github还允许您浏览特定的文件版本和文件树。例如,https://github.com/reduxjs/react-redux/tree/v7.1.2显示了从tag为v7.1.2的React-Redux代码库,以及https://github.com/reduxjs/react-redux/blob/5f495b23bcf3f03e0bee85fa5f7b9c2afc193457 /src/components/connectAdvanced.js显示了指定文件版本的文件内容。(在Github上浏览文件时按y可将URL更改为指定文件哈希)
二分法排错
Git有一个非常简洁的命令,称为git bisect,您可以使用它来帮助查找引入错误的提交。当您执行git bisect时,您可以给它指定一个可能出错的提交范围。然后,Git将签出一次提交,然后您执行签出的代码以确定是否存在错误,然后执行git bisect good或者git bisect bad. 之后Git会签出另一个提交,您重复该过程直到找出引入错误的提交。它遵循的拆分模式使您仅需几个步骤就可以缩小引入错误的提交的范围。
最后的思考
作为软件开发人员,我们会使用许多工具。每个人对文本编辑器之类的工具都有自己的偏好,但是团队中的每个人应使用相同的版本控制系统。在当今世界,不可避免的是使用Git.
鉴于Git对现代发展的重要性,任何您为有效使用Git而做出的努力,最终将使您受益。任何阅读您提交信息的人都将感谢您为清楚描述正在发生的修改及其原因而付出的努力,他可能是阅读您的PR的团队成员,可能会是浏览代码库的将来的实习生,甚至可能是多年以后再次回顾代码的自己。
归根结底,良好的Git实践是保证代码库可维护性的关键。
{测试窝原创译文,译者:lukeaxu 如有问题欢迎评论区交流}