避免混乱的 git 历史记录 避免混乱的 git 历史记录

2025-06-07

避免混乱的 git 历史记录

避免混乱的 git 历史记录

避免混乱的 git 历史记录

如果我们尝试说出明确定义现代软件开发的事物,源代码控制肯定会名列前茅,尤其是 git,它可能是当今使用最广泛的版本控制系统。

将代码版本存储在本地不同文件夹中、容易造成代码损坏的时代早已一去不复返。然而,许多开发人员仅仅将 Git 用作远程存储源文件的一种方式,而没有真正利用其一些更高级的功能,这些功能使我们能够拥有出色且易于阅读的 Git 历史记录。

本文将介绍 git-flow 的其中一种方式,它主要基于 git rebase,能够让你获得更流畅的 git 体验,尤其是在团队内部工作时。这是一种严格的方法,需要一段时间才能习惯。

这些经验源于我们在PROTOTYP内部实践的大型项目经验。我们非常重视代码审查,并确保代码库中所有变更的易读性。

该方法的主要目标是:

  • 更清晰的 git 历史记录

  • 更少的合并冲突

  • 强制代码审查

  • 增加分支稳定性

文章中的所有示例都是通过命令行完成的,但会有关于如何在 git 提供程序仪表板中实现某些部分流程的链接和参考。

初始化 git flow

旅程的第一步是明确初始化存储库上的 Git 流。

您可以在此处找到原作者对此事的更详细的文献资料

简而言之,这是一种分支模型,在过去对我们来说扩展性非常好,并且被广泛采用。

为了从命令行使用它,你很可能需要安装 git-flow。Tower 或 Sourcetree 等 GUI 解决方案通常已集成它。

您可以在此处查看安装说明

例子:

// initialises git on your repository
git init

// initialises git flow on your git repo
git flow init

在你的仓库中初始化 git flow 后,系统会询问你默认的分支名称。我们内部使用默认值,仅在标签前添加字母“v”,因此我们的版本号为 v1.0.0、v1.0.1 等等。

您可以随意使用最适合您团队和产品的版本控制系统进行发布。然而,语义版本控制(或称semver)一直以来都是我们的首选武器。虽然对于小型一次性项目来说,它可能有些过度,但事实证明,对于公司内部发布 SaaS 产品或移动应用的新功能来说,它非常有效。

git flow 引入的每个分支在生态系统中都有自己的位置,了解何时使用每个分支是必须的!

// New features
Feature branches? [feature/]

// Tags a version and merges develop to master. 
// A short lived branch. Versions bumps are ok inside it.
Release branches? [release/]

// A branch done from master directly, for fast hotfix push
// We use bugfix/ name for a bugfix branch that is branched from  
// develop
Hotfix branches? [hotfix/]

// Need to add some client specific code ? Use a support branch
Support branches? [support/]

// Tag for release branches
Version tag prefix? [v]

请记住,git 并不是某种魔杖,它不能自动解决您的所有问题,如果您不知道如何处理它们,那么拥有多个分支也没有任何意义。

锁定开发和主分支

对于不习惯这种方法的人来说,这可能有点争议,但我肯定会指出这是该过程中最重要的步骤之一。

保护**develop *和master分支将要求您的团队通过合并请求和代码审查流程将代码合并到它们中,这两种做法都是强烈鼓励的做法。

它还能避免很多潜在的“不稳定”问题,这些问题往往发生在人们直接将“小而无关紧要、不会造成任何影响的修复”推送到这些分支时。一旦出现问题,这迟早会让人非常沮丧。

如何做到这一点,因提供商而异,但以下是较受欢迎的提供商的概要:

Bitbucket: https://blog.bitbucket.org/2016/12/05/protect-your-master-branch-with-merge-checks/

GitHubhttps://help.github.com/articles/configuring-protected-branches/

GitLab: https://docs.gitlab.com/ee/user/project/protected_branches.html

我建议启用一些合并检查,例如在合并任何代码之前至少进行一次代码审查和来自另一个开发人员的批准。

通过功能分支添加新代码

考虑到没有代码可以直接推送到开发或主分支,因此需要创建一个新的功能分支来为您的应用添加新功能。

您可以从命令行执行此操作,也可以使用SourcetreeTower等工具中的集成功能。

源分支:开发
命名: feature/feature-name

例子:

git checkout develop
git branch feature/my-new-feature

成功创建功能分支后,请随意将代码推送到该分支,直到您准备好让团队中的另一个成员审核合并后的功能。

合并代码

为了通过这种方式合并代码,在完成功能之后,首先需要对其进行变基。这是一个多步骤的过程。

由开发人员重新定基

作为创建功能的开发人员,您通常希望从开发分支中提取同时发生的最新更改,并测试您的功能是否仍然有效。

git checkout develop
git pull
git checkout feature/your-feature-name

// This line will return a hash of commit where your branch diverged // from develop
git merge-base develop feature/your-feature-name

// {hash} is the result from the previous step
git rebase --onto develop {hash}

这将启动 rebase 过程,其中来自开发的更改将通过提交集成到您自己的功能分支中。

一旦你理解了 rebase 的步骤,还有一些更短的替代方案,例如:

git checkout feature/your-feature-name
git pull --rebase origin develop

您可以在此处查看完整的 rebase 文档

为什么不简单地使用合并?

观点很正确。当然,你可以将开发分支合并到你自己的功能分支,并获得类似的结果。但是,有一些关键的区别,这就是为什么在这种情况下我们更喜欢变基分支而不是合并。

重新定基:

  • 整合每次提交的变更。

  • 冲突也会在每次提交时出现,您可以轻松询问同事做出了哪些改变,以便更有效地共同解决冲突。

  • 如果您不确定是否在某个地方搞砸了,可以轻松中止并重新开始 Rebase,恢复您所做的所有更改。

    // After you stage files, you can continue to next commit
    git rebase --continue
    
    // Skips the current commit entirely
    git rebase --skip
    
    // Reverts all rebase changes that you did
    // Returns the branch to pre-rebase state
    git rebase --abort
    
  • 在进行变基时,您可以拥有更大的灵活性,因为如果您认为传入的提交是不必要的或者被您的更改所取代,您可以重命名它们或完全跳过它们。

  • 如果您经常将更改集成到功能分支,它不会创建不必要的合并提交。

  • 如果没有任何冲突,它将只传递所有提交,并且该过程将变得轻而易举,就像使用 git merge 一样。

合并混乱:

合并混乱

合并会将所有更改粘在一起,导致大量可能存在冲突的文件,解决这些差异通常非常繁琐。此外,如果发生冲突,还会使了解谁修改了文件以及修改原因变得更加困难。

但是,无论您选择什么,在创建 Pull Request 之前测试功能的负担肯定在开发人员身上,不应该被跳过。

创建拉取请求(某些提供商中的合并请求)

在您的 git 提供程序的界面内,从您的功能分支(或您分支出来的分支)创建一个拉取请求来进行开发。

Bitbucket: https://confluence.atlassian.com/bitbucket/create-a-pull-request-to-merge-your-change-774243413.html

GitHubhttps://help.github.com/en/articles/creating-a-pull-request

GitLab: https://docs.gitlab.com/ee/gitlab-basics/add-merge-request.html

Gitlab 在其界面中提供了自动 rebase 选项,这是我非常喜欢的一个功能,并且很高兴在 BitBucket 中看到它。

由审阅者重新定基

此步骤是获得可读且简化的 git 历史记录的主要技巧。

与用于解决潜在冲突并使用最新更改测试功能的开发人员 rebase 不同,这个 rebase 主要是为了可读性,尽管它也可以捕获确实发生的问题。

我们认为这是流程中的关键一步,因为在您创建 PR 后,您不知道有人需要多长时间才能审核和合并它。

通常情况下,在这种情况发生之前,会有新功能合并到您的开发分支,这意味着:

  • 新功能可能引入了需要解决的冲突

  • 从你第一次 rebase 的那一刻起,git 历史记录就已经发生了变化

如果审阅者进行另一次 rebase,它应该会改进功能的测试,最大限度地减少合并后功能无法正常工作的可能性,并为您提供非常干净和线性的 git 历史记录。

实际情况如何

这是来自 Sourcetree 的一个小截图,展示了通过这种方法合并的功能,以及developmaster其他分支之间的相互关系。

如你看到的:

  • 非常易读

  • 可以快速找到带有从develop到master标签的发布点

  • 您可以轻松查看功能或错误修复的合并顺序。
    如果您不确定哪个功能对项目造成了其他问题,这将非常有帮助。如果遇到这种情况,它甚至可以让使用*git bisect * 变得更加容易。

壁球还是不壁球

此处显示的示例使用了GitLab 自动提供的功能squash ,尽管您可以根据需要在本地压缩提交,如下所示:

git checkout feature/my-new-feature // If you are not on it
git rebase -i HEAD~1

这会将你的整个分支压缩为一个提交。建议你内部讨论一下你是否希望这样做。

从我们的角度来看,只要您遵循 rebase 流程,压缩就不会显著影响您的 git 图的可读性,除非您的功能有 100 多个提交。

在这种情况下,也许您在单个功能中捆绑了太多内容:)。

我更喜欢在提交 PR 之前先压缩分支中的提交,因为这样如果搞砸了某个分支,就必须切换到新的bugfix/hotfix/分支。而且,无论你的开发人员使用的是原子提交还是大量的提交,这都无关紧要。

但是,没有必要将所有提交都压缩到同一个提交中。如果是较大的功能,你可以通过交互式 rebase 手动将分支压缩到几个重要的提交中。

git checkout feature/your-feature-name
git merge-base develop feature/your-feature-name // returns {hash}
git rebase -i {hash}

您将看到如下屏幕:

pick be8606b Added localisation package and settings
pick 48e6aec Fixed an issue with payload not being propagated
squash 6085ce3 Added connected intl
squash 60ec657 Fixed connected intl
squash ba09d22 Modified tasks from package.json
pick 0bea497 Build android
pick 52c67b9 Updated packages
pick 21aa18c rm package lock

只需将pick改为squash即可将提交与之前的提交合并。

这通常用于压缩重复的提交或具有愚蠢名称的提交(例如删除 console.log),以维护相关且信息丰富的 git 历史记录。

不相信这是可行的方法吗?

如果你还没有用过 git,这很容易理解。至少尝试一下,用几天或者完成一个项目之后再做个评价。

它确实帮助我们改进了PROTOTYP的 git 使用方式,并显著减少了 git 冲突,提高了 git 的可读性。即使发生了冲突,也能快速解决,让我们能够更高效地专注于功能开发,而不会感到头疼。

所有项目切换到流程的过程并不容易,而且耗时较长。不过,一段时间后,每个人都对它给我们团队带来的成果感到满意。

以下是有关该过程的最后几点。

关于 Git rebase 的几句话

哦天哪。不是 rebase 吧?

对我们来说,git rebase是 git 最强大的功能之一。你可以把它想象成一个管理 git 历史记录的瑞士军刀。

然而,对于此功能的看法往往非常主观,这可能会导致一些误解。

以下是一些:

不要使用 rebase,这是一个破坏性操作!

这有一定的道理。Rebase 会重写 git 历史记录,如果使用不当,可能会导致灾难性的后果!

然而,git 的很多功能都具有潜在的破坏性。这并不意味着不应该使用它们,只是你需要了解如何以及何时使用它们,以及如果发生问题时如何缓解这些问题。

在公共代码库和开源项目中使用此方法时要谨慎,因为这些项目中有很多不同的人在推送代码,而且并非每个人都使用相同的原则。这需要整个团队都遵守纪律。

另外,如果多人同时开发同一个功能,请仅在拉取请求之前完全确定每个人都已提交更改的情况下进行 rebase。Rebase 会重写您自己分支的历史记录,因此可能会导致在分支上进行大量的拉取和推送操作,这本身就很麻烦。

这种流程只会让 git 图表变得更好看,而不会带来任何价值。

我强烈反对这一点,原因有很多。

首先,你可以写出漂亮的代码,也可以写出丑陋的意大利面条式代码,但两者都可以实现功能。但是,请问一下,哪种代码更容易理解和重构,而且不容易出现问题?

同样的逻辑也适用于此。简洁一些通常不是什么坏事。

此外,强制执行 rebase 的最终目的是尽量减少冲突问题,因为通过这种方法解决冲突要容易得多。漂亮的图表只是整个过程的一个附带效果。

如果使用 rebase 弄乱了我的分支和项目怎么办?

说实话,彻底搞砸一个项目或分支真的很难,尤其是多人同时开发,而且各自都有本地版本的情况下。他们随时可以强制推送更改来重置你的状态。

如果您使用合并并提交这些更改,但尚未正确解决问题,那么您也可能会弄乱分支。

然而,如果你确实设法做到了不可想象的事情,解决这些问题通常是一件非常容易的事情。

Git 引用日志

瞧,这支神奇的橡皮擦笔可以清除我们对存储库所做的所有坏事。

git reflog

执行该命令将显示分支所有状态的哈希值,最多 90 天。您可以简单地检出或重置分支到合并或变基之前的状态,然后通过查找执行任何破坏性操作之前的最后一个哈希值重新开始。

这里有点奇怪。有时候需要 rebase,有时候不需要。这是什么问题?

如果你的分支比开发分支的分支领先,则无需执行此操作。在以下情况下,请确保你已从开发分支拉取所有更改,如果仍然领先,则只需执行拉取请求即可。

如果你的分支位于develop分支的末端之后,则需要进行变基。注意,feature/other-feature并非连接到develop分支的最前端,而是连接到其末端之后的某个点。

结束语

我们希望本文根据我们的经验概述了该过程及其优缺点。

如果你正在寻找一种新的流程,能够提升 git 的可读性和稳定性,从而提高你和团队的效率,不妨试试看。它可能会大有裨益!

如果您的团队有类似或不同的工作流程,我们希望了解并比较一下。

文章来源:https://dev.to/prototyp/avoiding-the-messy-git-history-470d
PREV
构建自定义 React Hooks
NEXT
Tips and Tricks for Better JavaScript Conditionals and Match Criteria