Git 使用基础指南

0. 参考文档

本文中绝大部分内容来自以下两个文档,想要深入了解某一个主题的,建议仔细查看原文档。

1. 基本概念

1.1 文件状态

  • 已修改(modified):对应工作区(Working directory),包括新增和修改的文件;
  • 已暂存(staged):对应暂存区(Staging area);
  • 已提交(committed):对应提交历史(Git directory)。

简单理解其相互关系:

已修改 - add -> 已暂存 - commit -> 已提交 - checkout -> 已修改

1.2 提交与分支

提交(Commit)和分支(Branch)的关系,可以类比链表指针。Git 中的分支,本质上是个指向 commit 对象的可变指针。

如下图所示,提交历史共有 3 次提交记录,每一次提交可以类比为链表中的一个节点,节点包含指向下一个节点(上一次提交)的指针信息。

另外还有三个特殊的指针,mastertesting是两个分支,可以类比为两个指向链表头的指针;HEAD是一个特殊的指针,用于指示当前工作区所在的位置。

如下图所示,是在testing分支上工作后,进行了一次提交。master仍然指向原来的提交,testing指向最新的提交,而由于当前在testing分支上工作,HEAD随着 testing 一起向前移动了一步。

如下图所示,如果切换到master分支上,并且又进行了一次提交,则masterHEAD一起向前移动,指向87ab2这次提交。

综上所属,分支是一个指针,而不是一个容器

在 Git 的概念里,分支是一个很轻量化的概念,新建、修改和移除分支的代价都不大。因此也推荐保持分支的短期性,一个分支专注于一项特定的任务,即来即走。

1.3 合并(merge)

合并操作的对象都是分支,包括:

  • 源分支:包含新增内容的分支;
  • 目标分支:需要并入新增内容,会发生改变的分支。

合并有两种形式:

  • 快速向前合并(Fast-forward merge)
  • 三方合并(Three-way merge)

1.3.1 快速向前合并

当目标分支是源分支的直接上游时,合并时会采用快速向前的方式,并出现“Fast forward”提示。

操作实质:把目标分支的指针直接向前移动到源分支的指针所指向的位置。

如下图所示,包含masterhotfixiss53三个分支,由于当前masterhotfix的直接上游,将采用 Fast-forward 的合并方式。

合并结果如下图所示,合并之后masterhotfix分支指向同一个提交。此时hotfix分支已经完成历史使命,可以删除了。

1.3.2 三方合并

然后回到iss53分支进行开发工作,又进行了一次提交后,需要将改动内容合并入master,而此时,这两个分支已经分岔了。Git 会用两个分支的末端(C4 和 C5)以及它们的共同祖先(C2)进行一次三方合并计算。

Git 对三方合并后的结果重新做一个新的快照,并自动创建一个指向它的提交对象(C6),如下图所示。此时iss53分支已经完成历史使命,也可以删除了。

1.3.3 冲突解决

并不是每一次合并操作都非常顺利,有时可能会产生冲突。

<重要原则>:快速向前合并从原理上就不会产生冲突,冲突只会发生在三方合并的时候。

冲突产生的原因:简单理解,可以认为是 C4 的提交,和 C3-C5 的提交,修改了文件的同一个部分。Git 无法自动合并,必须由人来裁决。

冲突解决的步骤:解决冲突的步骤与进行一次新的提交的步骤几乎一样。(1)修改冲突的代码,(2)将修改过的文件加入缓冲区(add 操作),(3)进行一次合并提交(commit 操作)。

1.4 衍合(rebase)

1.4.1 基本原理

衍合是除了合并操作以外,另一种把一个分支中的修改整合到另一个分支中的办法。

回到两个分支进行了各自的提交而导致分岔的情况下,如下图所示。

现象这样的场景:

当你在experiment分支上工作时,有其他人向master分支上推送了新的内容。

你想要把master中的更新同步到experiment分支中,如果使用 merge 操作会产生一个额外的合并提交。

而 rebase 的操作逻辑是:把在 C3 里产生的变化补丁在 C4 的基础上重新打一遍。操作结果如下图所示。

此时,你的experiment分支包含了master分支上的最新内容,可以以此为基础继续开发。(默认master分支上的内容都是稳定的,需要所有人向其兼容的)。

如果需要把experiment分支上的内容也并入master分支,可以执行安全的快速向前合并。结果如下图所示。

1.4.2 重要守则

git rebase是 Git 操作中的黑魔法,用好了可以化腐朽为神奇,用不好会带来灾难性后果。

<重要原则>:绝不要在公共的分支上使用它!!!

用更白话一点的说法:从分岔点开始往后的提交,如果已经 push 过,那就已经是公共的提交了,这个分支就是公共分支,必须假设其他人的工作会依赖于这些公共提交,也就不能再用 rebase 操作了。

因为衍合的过程改变了分支的历史,原来的 C3 变成了 C3',如果之前 C3 已经发布到了远程,则在本地变更为 C3'后,远程分支与本地分支不一致了,会导致后续的 push 操作无法进行。

因为在进行 push 操作的时候,将本地分支推送到关联的远程分支时,本质上也是一个 Fast-forward 模式的合并操作。

1.5 远程服务器

虽然只在本地工作也能使用 Git,但为了协作,一般会有一个代码托管的远程服务器,运行一个 Git 服务。

1.5.1 远程分支和本地分支

本质上,每一个 Git 库的副本,都有自己的一套分支,而不同副本之间的分支可以进行关联追踪。

在一个本地库上执行git branch -a,典型会有有如下显示(部分删节):

  develop
* feature_xxx
  master
  remotes/origin/HEAD -> origin/master
  remotes/origin/develop
  remotes/origin/feature_xxx
  remotes/origin/master

前三个是本地分支,带remotes前缀的是远程分支,origin代表服务器名。

执行git branch -vv查看分支的追踪关系,有如下显示(部分删节):

  develop                  1bf6d01 [origin/develop]
* feature_xxx              7f3d5c9 [origin/feature_xxx]
  master                   40f9f56 [origin/master]

一些有效但不那么准确的理解:

  • 远程分支和本地分支是不同的分支;
  • 具有追踪关系的远程/本地分支之间,通过 pull/push 操作进行同步,合并方式限定为 Fast-forward。

2. Git 工作流

使用一个简化的Git Flow工作流。

2.1 Git 分支类型

将会使用masterdevelopfeature三种分支,暂时不使用hotfixrelease分支。

  • master分支:
    • 仓库的主分支,包含最近发布的可稳定使用的版本;
    • 一般由仓库管理员从develop分支进行合并,不能直接向master进行推送;
    • master分支始终存在,不可删除。
    • master的每一个 commit 都应该打上标签,作为对外发布的版本号。
  • develop分支:
    • 主要开发分支,基于master创建,始终包含最新完成功能的代码以及 bug 修复后的代码;
    • 接受从feature发起的合并请求,不能直接向develop进行推送;
    • develop分支始终存在,不可删除。
  • feature分支:
    • 基于develop创建,用于某一个特定的新功能/新特性开发;
    • feature分支可同时存在很多个,用于多个功能同时开发;
    • feature分支属于临时分支,当合并入develop后,建议选择删除,如有继续开发的需要,重新基于develop创建新的feature分支。
  • develop基于master创建,并且在develop向前演进的过程中,master不会接受其他来源的提交和合并。因此,每一次developmaster分支的合并,都应该是 Fast-forward 模式的。
  • feature基于develop创建,并最终合并入develop,在此过程中,develop可能会合并入其他feature分支的内容,如何保证提交 PR 时不产生合并冲突,在下一节详细讨论。
  • 以文档内容为主的 WIKI 库不设develop分支,围绕master分支进行协作。

2.2 基本原则

几项 Git 协作的基本原则:

  • 重要:在远程服务器上,只会进行 Fast-forward 模式的合并,并且是通过 Pull-Request 进行。在提交 PR 之前,需要确认目标分支(一般是develop)是源分支(一般是feature)的直接上游。可以通过提交图进行确认。

  • 重要:所有的冲突都在本地解决,在远程没有解决冲突的途径。

  • 保持每一次 Commit 有明确的意义,必要时通过交互式的 rebase 操作,对 Commit 进行整理。

    整理方法参见说明

  • 保证分支命名规范且有意义,保证 commit-message 包含简明且充分的信息。

    branch-name 和 commit-message 的规范参见协作规范文档

  • 保持合适的推送频率(Push 操作,非 Commit 操作)

    针对正在执行的任务,如果 3 天以上都没有 Push,无法让其他人同步进度,则说明提交频率过低;如果在一个时段内连续提交,疯狂刷屏,则说明推送频率过高。

    建议针对某一个具体的开发任务,一天推送 1 至 2 次是一个合适的频率;可以在每天工作结束前,整理当天的 Commit,并执行一次 Push 操作。

2.3 典型工作流程

2.3.1 仓库初始化

从 Server 端通过git clone获取仓库一个完整的副本,并在本地新建feature_0分支。

2.3.2 合并流程

本地feature_0分支有了两个新的提交(C3&C4),在此同时,远程的origin/develop接受了feature_1分支的 PR,向前推进到了 C5。如下图所示:

不合理的做法

此时,如果直接将本地的feature_0分支推送到远程,则origin/developorigin/feature_0处于分岔的状态。如下图所示:

按照一般 PR 的规则,无法将origin/feature_0合并入origin/develop,因为目标分支并不是源分支的直接上游。如果强行合并,若刚好两个分支没有修改过同一个文件,则可以侥幸合并成功。

但并不推荐这么做,因为违反了 PR 只做 Fast-forward 操作的原则。且很多时候会产生冲突,在网页上无法处理。

如下图所示:

合理的做法

在推送feature_0分支之前,先将本地的develop分支和远程的origin/develop进行同步(通过git pull操作)。如下图所示:

在本地进行衍合操作,将feature_0分支的提交,更新到develop分支之前。

# 在 feature_0 分支下
git rebase develop

如果发生冲突,则按照命令行的提示手动解决冲突。正确 rebase 后的结果,如下图所示:

在此基础上,本地分支继续开发,又向前推进了一次提交,然后同步到远程的origin/feature_0分支上。此时,origin/developorigin/feature_0的直接上游,可以实现 Fast-forward 模式的合并。(假设在此期间origin/develop没有再接受新的 PR,如有,则重复上面的流程)。结果如下图所示:

2.3.3 衍合的注意事项

回到最初分岔的阶段,如下图所示:

假设已经将feature_0分支推送到远程,与origin/feature_0同步过一次。

若此时想基于origin/develop最新提交的基础,进行继续开发,先将更新拉取到本地,如下图所示:

在本地执行之前一样的衍合操作,将feature_0分支添加到develop分支的头部,会发现本地的feature_0和远程的origin/feature_0不一致了,此时无法再执行 Push 操作,因为本地和远程发生了冲突。如下图所示:

如果已经将提交推送至远程,有两种后续的解决方案:

(1)通过 merge 而非 rebase 进行合并,在本地执行三方合并

同时要求在本地解决可能的合并冲突。结果如下图所示:

此时将feature_0推送到远程,与origin/feature_0进行同步。这时,origin/developorigin/feature_0的直接上游,符合 PR 的规则,可以正常提交合并请求。结果如下图所示:

(2)如果本地已经使用了 rebase 操作进行合并,可以强制推送进行同步

强制推送git push -f是另一个比较危险的操作。

<重要原则>:一定只在自己的工作分支上使用!!!一定不能向其他人的工作分支执行强制推送!!!

除非万不得已,不要使用强制推送。在推送前,需要反复确认本地分支包含了所有需要的工作内容。

推送后,远程分支和本地分支已经一致,并且origin/developorigin/feature_0的直接上游。结果如下图所示:

2.3.4 再谈合并与衍合

  • git rebasegit merge 做的事其实是一样的,它们都被设计来将一个分支的更改并入另一个分支,只是实现方式不同,最终的文件结果是一致的。

  • git merge的好处是,不会对历史进行任何更改,是安全的;缺点是,每次合并都会引入一个额外的提交(比如上图的 C6),如果上游分支非常活跃,一定程度会污染本地的开发分支,形成一个非常复杂的开发历史。

  • git rebase的好处是,开发历史会非常整洁和线性,没有不必要的合并提交;缺点是,这个操作包含一定的风险性,会变更开发历史,所以必须严格遵守衍合的操作守则——绝不要在公共的分支上使用它!!!任何衍合操作都不应该更改已经同步到远程服务器的提交。

  • git rebase的一个额外的好处是,可以用来清理提交历史,让历史中的每一次提交都包含明确的开发意义,便于回顾和追溯。

    这意味着,在开发过程中,本地可以进行相对随意的 Commit,在整体 Push 之前,通过git rebase -i对开发历史进行整理,合并部分 Commit,修改 commit-message。

2.3.5 推荐的操作方法

综上所述,一个推荐的基于 git 协作的开发流程:

  1. 将本地的develop分支与远程的origin/develop进行同步,基于最新的develop,新建本地的开发分支feature
  2. feature分支上工作,正常进行git commit操作;
  3. (可选)开发过程中关注origin/develop的更新,如果更新的内容与feature开发的内容相关,则拉取到本地,通过git rebase操作,合并入feature
  4. 在执行git push前,如有必要,通过git rebase -i对提交历史进行整理,注意只清理还未推送到远程的提交;
  5. 在执行git push前,必须确认origin/develop的状态,执行一次git pull操作,如果有更新,则拉取到本地,通过git rebase操作,合并入feature
  6. 确认feature在合并origin/develop的最新更新后,依然能够按照预想的方式正常运行,如果有问题则修正;
  7. 执行git push操作,将feature推送到远程,与origin/feature进行同步。

3. Git 配置

在使用 Git 前,对其进行有效的配置,可以达到事半功倍的效果。

3.1 配置层级

Git 的配置包含三个层级,每一级别的配置会覆盖上层的相同配置:

  • 系统配置/etc/gitconfig文件,设置时配合--system选项;
  • 用户配置~/.gitconfig文件,设置时配合--global选项;
  • 项目配置./git/config文件。

3.2 必须配置

设置名字和邮箱,用于识别用户,会记录到每一次 commit 的信息中。

git config --global user.name "John Doe"
git config --global user.email johndoe@example.com

3.3 建议配置

3.3.1 文本编辑器

git config --global core.editor vim

建议用 vim,如果不习惯可以用 gedit、code 之类代替。

3.3.2 commit-message 模板

使用 commit-message 模板可以减少每次 commit 操作时需要固定输入的内容。

设置模板文件
$HOME/.gitmessage.txt中或新建任意文本文件,写入如下内容:

<feat>(XXX):
<>

启用模板文件

git config --global commit.template $HOME/.gitmessage.txt

3.3.3 自动换行符

Windows 使用回车和换行两个字符来结束一行(CRLF),而 Mac 和 Linux 只使用换行一个字符(LF)。

# 只在window开发,linux/windows运行
$ git config --global core.autocrlf true
# linux开发和运行
$ git config --global core.autocrlf input
# 只在windows开发和运行
$ git config --global core.autocrlf false

4. 建议

使用命令行操作,减少对 IDE 内置工具的依赖。

  • 掌握命令行操作,能够让你更好地理解 Git 的工作原理;
  • IDE 简化了操作,但也增加了误操作的可能。
posted @ 2021-12-18 15:18  LyleChen  阅读(120)  评论(0编辑  收藏  举报