Git 使用指南

Git 是目前世界上最先进的 分布式 版本控制系统,本文详细介绍了 Git 在单人模式和多人协作下的基本概念、使用方式和常用指令,最后对比了几种最流行的 Git 协同模型,可以更高效地进行团队开发。

版本控制系统

SVN 集中式版本控制系统

  • 版本库放在中央服务器
  • 必须联网才能工作
  • 服务器磁盘故障存在数据丢失风险

Git 分布式版本控制系统

  • 没有中央服务器,每台个人PC都有一个完整的版本库
  • 工作时无需联网
  • 通过把各自的修改推给对方进行协作
  • 服务器数据丢失可用任意一个代码仓库恢复,宕机期间也可以提交代码到本地仓库
  • 安全性强,Git管理的每一个文件、目录、提交等都拥有一个SHA-1哈希值

Git 独奏

基本概念

暂存区

  1. 暂存区一般称为stage或index,只有添加到暂存区的文件才可以被提交。
  2. 暂存区保存在./img/Git使用指南/index文件中,是一个包含文件索引的目录树,有类似工作区的目录结构,其中记录了文件名、文件的状态信息等。
  3. 具体的文件内容保存在git对象库./img/Git使用指南/objects目录中,文件索引建立了文件和对象库中对象实体之间的对应关系。

git-stage

  1. 可以把HEAD理解成一个指针,HEAD指针通常指向当前所在分支的分支指针,分支指针总是指向当前分支的最新提交,即通常情况下HEAD指针总是指向了当前分支的最新提交(通过分支指针间接指向)。

    从master检出test分支

  2. HEAD指针和分支指针的当前指向,分别保存在./img/Git使用指南/HEAD文件和./img/Git使用指南/refs/heads/<branch>文件中。在git中,可以在./img/Git使用指南/refs目录下找到一系列含有SHA-1值的文件,这类文件也被称为“引用” (refs或references)。

    $ cat ./img/Git使用指南/HEAD
    ref: refs/heads/test

    $ cat ./img/Git使用指南/refs/heads/test
    6f396a68315694be320949fba3f6a57d5e1c318f

  3. 分离头指针(detached HEAD),即HEAD指针没有指向分支指针,而是直接指向了某个具体的提交。一般用来快速地基于某个提交进行一些实验或者测试,如果对结果满意就保留,否则就丢弃。

    检出某个提交时HEAD指针的变化

  4. 在写法上,通常用HEAD表示当前版本,HEAD^是上一个版本,HEAD^^是上上一个版本,上100个版本可以写作HEAD~100

创建仓库

  • git init:初始化仓库
  • git clone:克隆远程仓库
1
git clone <repo> [<dir>] --depth=1 -b <branch> --single-branch  # 克隆指定分支最近的一次提交
  • git config:修改配置文件(优先级:local > global > system)

初始化结果:生成.git文件(本地版本库),用户名和邮件地址配置成功(不做校验)

文件变更

  • git add:添加文件到暂存区
1
2
3
git add [file1] [file2] ... / [dir]
git add ./-A # 添加所有已跟踪和未跟踪文件的变更
git add -u # 更新已跟踪的文件变更(修改、删除)
  • git commit:提交暂存区文件到本地仓库
1
2
git commit -m "message"
git commit -m "message" --amend # 追加提交
  • git status:查看工作区和暂存区之间的文件变更状态
1
2
3
4
git status  # 标准显示
git status -s # 简洁显示
git status -v # 详细显示(文件内容改动)
git status --ignored # 显示被忽略的文件
  • git diff:比较文件内容差异

git-diff

1
2
3
4
5
6
git diff  # 比较工作区和暂存区
git diff HEAD/<branch>/<commit> # 比较工作区和HEAD/branch/commit
git diff --cached # 比较暂存区和HEAD
git diff -- <file>/<path> # 比较文件/目录差异
git diff <commit1> <commit2> -- <paths> # 比较两个commit之间的具体文件差异
git diff --stat # 摘要显示

当执行git statusgit diff命令扫描工作区改动的时候,git会先依据./img/Git使用指南/index文件中记录的(已跟踪文件的)时间戳、长度等信息判断工作区文件是否改变。如果文件时间戳发生改变,则文件内容可能被修改了,此时再打开文件,对比文件内容,判断文件是否真正被更改。这也是Git高效的因素之一。

  • git rm:删除文件,并自动变更暂存区
1
2
git rm [<options>] [--] <file>...
git rm --cached --force -r <file> # 移除不需要跟踪的文件(不删除工作区文件)
  • git ls-files:查看工作区和暂存区的文件信息
1
2
git ls-files -c  # 显示已缓存到暂存区的文件(默认)
git ls-files -s # 显示暂存区的文件对象信息(模式位,文件哈希值,暂存区编号,文件名)

版本切换

  • git log:查看提交历史(显示当前HEAD之前的提交历史)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
git log  # 从近到远列出所有提交记录
# 显示参数
--stat # 显示文件差异(行数统计)
-p # 显示文件差异(详细的具体改动)
--oneline # 简洁显示
--graph # 以ASCII图形模式显示
–-pretty # 格式化输出 %h-简短哈希字符串 %s-提交说明
# 筛选参数
-n # 限定数量
--since, --after # 限定指定时间之后的提交
--until, --before # 限定指定时间之前的提交
--author # 限定作者
--grep # 搜索commit关键字
--no-merges # 过滤掉merge commit
git log --/commit/branch/tag # 限定路径/提交/分支/标签
# 示例
git log --pretty="%h - %s" --author='Junio C Hamano' --since="2008-10-01" --before="2008-11-01" --no-merges -- test/
  • git reflog:查看命令历史
1
git reflog  # 可以查看已被删除的commit记录和reset操作(用来恢复本地错误操作)
  • git reset:重置

1
2
3
4
5
git reset [<commit>] [--] <paths>...  # 不改变引用(值),替换暂存区中的文件,默认值为HEAD
git reset [--mixed|--soft|--hard] [<commit>] # 改变引用,有不同的选项
git reset HEAD # 撤销对暂存区的修改(将暂存区恢复到最近提交的状态,默认使用--mixed选项)
git reset --hard HEAD # 将工作区、暂存区都恢复到最近提交的状态
git reset --soft HEAD^ # 只修改引用,不修改工作区、暂存区文件

reset实质:重置引用(改变HEAD指针所指向的分支指针指向,HEAD指针始终指向当前分支指针)

  • git revert:版本回滚
1
git revert <commit>  # 回滚到指定的版本,回滚也会作为一次提交进行保存(适用于多人协同)

reset和revert的区别:git reset是回到某次commit,在本次commit之后的修改都会被删除;git revert是在最近一次commit之后生成一个新的commit,之前的所有commit都会被保留。

Git 和声

分支管理

  • git branch:分支管理
1
2
3
4
5
6
7
8
9
git branch  # 查看本地分支
git branch <branch> # 创建本地分支
git branch -r # 查看远程分支
git branch -a # 查看全部分支
git branch -d <branch> # 删除分支
git branch -vv # 查看本地分支和远程分支的关联关系
git branch -u/--set-upstream-to origin/<远程分支名> [<本地分支名>]
# 建立本地分支(默认为当前分支)与远程分支的关联
git branch --unset-upstream # 取消当前本地分支与远程分支的映射关系
  • git checkout:检出

git-checkout

1
2
3
4
5
6
7
8
9
git checkout [<commit>] [--] <paths>...  # 不改变HEAD指针,替换工作区中的文件,默认值是暂存区
# 省略<commit>时用暂存区文件覆盖工作区,不省略<commit>时用指定提交中的文件覆盖暂存区和工作区中对应的文件
# 用法:git checkout -- filename // 放弃单个文件修改
# git checkout –- . 或 git checkout . // 撤销一切本地修改
git checkout <commit> # 检出某个提交,改变HEAD指针并进入“分离头指针”状态
git checkout <branch> # 改变HEAD指针,检出branch分支(完成图中的三个步骤)
git checkout -b <new_branch> # 改变HEAD指针,创建并检出到新分支
git checkout -b <本地分支名> origin/<远程分支名> # 在本地创建和远程feature分支的对应分支
# 例:git checkout -b dev origin/feature 在本地创建和远程feature分支的对应dev分支

checkout实质:重置HEAD

远程同步

  • git remote:操作远程仓库
1
2
3
4
git remote  # 查看关联的远程仓库名称
git remote -v # 查看关联的远程仓库的详细信息
git remote add <shortname> <url> # 关联远程仓库,使用<shortname>指定一个仓库别名(通常为origin)
git remote rm/remove <shortname> # 删除远程仓库关联

远程分支通常写作远程主机名/分支名,如origin/master,保存在./img/Git使用指南/refs/remotes/origin目录下

  • git fetch:获取远程仓库中的最新版本到本地
1
2
3
git fetch <远程主机名>  # 取回远程更新(所有分支),不改变本地仓库文件
git fetch <远程主机名> <分支名> # 指定分支
git fetch origin tag <tagname> # 拉取远程分支的指定版本
  • git pull:获取远程仓库中的最新版本并merge到本地
1
2
3
4
5
6
# 分支拉取/推送顺序的写法是<来源地>:<目的地>
git pull <远程主机名> <远程分支名>:<本地分支名>
git pull origin feature:dev # 从远程库拉取分支,进行合并
git pull # 快速拉取(等同于git fetch + git merge)
git pull -p # 拉取时在本地删除远程已经删除的分支
git pull --rebase <远程主机名> <远程分支名>:<本地分支名> # 采用rebase模式

git pull = git fetch + git merge

  • git push:推送本地修改到远程仓库
1
2
3
4
5
6
7
8
9
git push <远程主机名> <本地分支名>:<远程分支名>
git push origin dev:feature # 向远程库推送自己的修改(如果远程分支不存在,则新建)
git push origin feature # 当前分支与上游分支同名时可省略
git push # 关联后可直接使用快速推送(推送当前分支,并且当前分支与上游分支同名)
git push -u/--set-upstream origin <本地分支名> # 推送时添加远程关联
git push origin --delete <远程分支名> # 删除远程分支
# 撤销远程提交:
git reset --hard <commit>
git push origin <本地分支名> --force # 强制覆盖远程版本

合并分支

  • git merge:合并分支

1
2
3
git merge <branch> -m "message"  # 合并指定分支到当前分支
git merge <branch> --no-ff -m "message" # 不使用Fast-forward模式进行合并
git merge --abort # 放弃合并(发生冲突时)
  • git rebase:变基(衍合分支)

1
2
3
4
git rebase <branch> -m "message"  # 衍合指定分支到当前分支
git rebase <branch> --no-ff -m "message" # 不使用Fast-forward模式进行合并
git rebase --continue # 解决代码冲突后,继续进行rebase
git rebase --abort # 放弃衍合(发生冲突时)

merge和rebase的区别:

git merge会将两个分支的最新一次提交进行合并,发生冲突并解决冲突后,执行git addgit commit,最终会产生一个新的commit。合并的结果为非线性,但只用解决一次冲突。(快进式合并的合并结果为线性,且不产生任何新的commit)

git rebase会把当前分支的commit放到公共分支的最后面(变基),发生冲突并解决冲突后,执行git addgit rebase --continue,不会产生额外的commit,会改变最近一次commit的hash值。合并的结果呈现为线性,但看不到完整的历史脉络(不要在公共分支使用rebase,否则容易破坏其他人的commit记录),同时rebase的冲突需要一个个解决(把commit打散成patch),一次rebase可能要解决多次冲突。

  • git cherry-pick:择优挑选
1
2
3
4
5
git cherry-pick <commit1> <commit2>  # 将指定的提交转移到当前分支,并在当前分支产生一个新的提交
git cherry-pick <commit1>..<commit2> # 将1~2之间的提交都转移到当前分支
git cherry-pick <branch> # 将指定分支的最近一次提交,转移到当前分支
git cherry-pick --continue # 解决代码冲突后,继续进行cherry-pick
git cherry-pick --abort # 放弃合并(发生冲突时)

使用merge合并另一个分支的所有变动,使用cherry-pick合并任意分支中的部分提交。

冲突处理

Git 进阶

其他指令

  • git tag:标签管理
1
2
3
4
5
6
7
8
git tag  # 查看所有标签
git tag <tagname> [<commit>] # 创建标签,默认值是HEAD
git tag -a <tagname> -m "message" # 添加标签信息
git tag -d <tagname> # 删除标签
# 推送标签(tag):
git push origin <tagname> # 推送一个本地标签
git push origin --tags # 推送全部未推送过的本地标签
git push origin :refs/tags/<tagname> # 删除远程标签

tag实际是一个指向commit的引用,保存在./img/Git使用指南/refs/tags/目录下,可以使用git checkout <tagname>检出

  • git stage:暂存
1
2
3
4
5
6
7
git stash  # 保存当前工作区和暂存区的状态到git栈
git stash list # 查看git栈中的文件列表
git stash show # 查看git栈中的文件改动
git stash pop [stash@{$num}] # 从git栈中恢复一个stash(默认为最近的)
git stash apply [stash@{$num}] # 从git栈中恢复一个stash(不删除栈中的内容)
git stash branch <branch> [stash@{$num}] # 基于stash创建分支
git stash clear # 清空git栈中所有内容
  • git blame:文件追溯
1
2
git blame <filename>  # 查看文件每一行的内容作者
git blame <filename> -L a,b # 查看文件第a行到第b行之间的内容作者
1
2
3
4
5
git bisect start <lastCommit> <firstCommit> # 指定差错范围(自动切换回之前的版本)
# 例:git bisect start HEAD 4d83cf
git bisect good # 正常
git bisect bad # 异常
git bisect reset # 退出查错,回到最近一次的提交

文件忽略

  1. 使用.gitignore忽略不希望添加到版本库的文件。
  2. .gitignore文件可以放在任何目录。
  3. .gitignore文件模板
  4. 忽略语法:
    1. .gitignore文件中的空行或者以井号(#)开始的行会被忽略。
    2. 可以使用通配符:星号*代表任意多字符,问号?代表一个字符,方括号[abc]代表可选字符。
    3. 如果名称的最前面是一个路径分隔符/,表明要忽略的文件在此目录下,而非子目录的文件。
    4. 如果名称的最后面是一个路径分隔符/,表明要忽略的是整个目录,不忽略同名文件。
    5. 通过在名称的最前面添加一个感叹号!,代表不忽略。
1
2
3
4
5
6
7
8
# 示例:
# 这是注释行 —— 被忽略
*.a # 忽略所有以 .a 为扩展名的文件
!lib.a # 但是 lib.a 文件或者目录不要忽略(即使前面设置了对 *.a 的忽略)
cache # 忽略所有名称为 cache 的目录和文件
/TODO # 只忽略当前目录下的 TODO 文件,子目录的 TODO 文件不忽略
build/ # 忽略所有 build/ 目录下的文件
doc/*.txt # 忽略文件如 doc/notes.txt,但是文件如 doc/server/arch.txt 不被忽略

常用指令速查

Git 协同模型

Atlassian:什么是成功的Git Workflow?

在评估团队的工作流程时,最重要的是要考虑团队的文化。工作流程应该能够提高团队工作的效率,而不是成为限制生产力的负担。评估Git Workflow时应考虑以下几点:

  • 这个Workflow是否与团队规模相匹配?
  • 使用这个Workflow是否容易撤销失误和修复错误?
  • 使用这个Workflow是否会给团队带来新的不必要的认知开销?

Centralized Workflow

工作模式

  1. 管理员初始化中央存储库(裸存储库) — git init --bare
  2. 开发人员从远程仓库克隆工程到本地仓库 — git clone
  3. 在本地仓库编辑文件和提交更新 — git add git commit
  4. Fetch远程仓库已更新的commit到本地仓库,并rebase到已更新的commit的上面 — git fetchgit rebasegit pull --rebase
  5. Push本地master分支到远程master分支 — git push

处理冲突

  • 开发人员在执行第四步时,本地提交与远程提交可能会发生冲突,git会暂停rebase过程,需要使用 git statusgit add 来手动解决冲突。

优点

  1. 适合从SVN过渡到分布式版本控制系统,无需改变已有的工作流程和团队协作方式。
  2. 简单、直接,完美的线性提交记录。
  3. 适合小型团队。

Feature Branch Workflow

Feature Branch Workflow是对Centralized Workflow的逻辑扩展,其主要思想就是在开发每个功能时都应该创建一个独立的分支而不在master分支中进行。由于每个分支是独立且互不影响,这就意味着master分支中不会包含broken code,对持续集成环境很有帮助。

工作模式

  1. 仍然使用远程仓库和master分支记录官方工程
  2. 开发者为每个功能或问题创建一个单独的feature分支 — git checkout -b
  3. 在本地仓库编辑文件和提交更新 — git add git commit
  4. 将feature分支推送到远程仓库 — git push
  5. 发送pull request来请求管理员是否将feature分支(远程)合并到master分支(远程)

Pull Request

  • 开发者当完成一项功能后,不会立即将其合并到master分支,而是发起一个pull request请求。
  • 其他的团队成员接收到请求,可以审查、讨论和修改代码(Code Review)。
  • 项目管理员合并新增的feature分支到master分支,关闭pull request。

优点

  1. 每个功能都创建一个独立的分支,易于持续集成环境。
  2. Pull Request机制。
  3. 是一种非常灵活的开发方式。

TrunkBased Workflow

Trunk based development是Paul Hammant在2013年提出的模型,是SVN的基本开发模式。TBD模型由单个master分支(trunk)和许多release分支组成,所有开发都在trunk上进行,使用衍生出的release分支交付。

工作模式

  • 在trunk分支开发,开发完成或hotfix后衍生出release分支发布。
  • release分支不可修改,每一次更新,都有对应的版本号标签。

小规模团队:直接在trunk上进行开发。

Simple TrunkBased

大规模团队:从trunk衍生出短期的feature分支进行开发,开发完成后合并回trunk。

Complex TrunkBased

优点

  1. 模型简单

  2. 易于持续集成

GitFlow Workflow

GitFlow Workflow由Vincent Driessen在2010年首次提出并广受欢迎,它定义了围绕项目生命周期设计的严格分支模型。核心思想与Feature Branch Workflow类似,但是给特定的分支赋予了非常具体的角色,并定义它们应如何以及何时进行交互。

具体而言有四种分支:

  • Main Branches
  • Feature Branches
  • Release Branches
  • Hotfix Branches

工作模式

Main Branches

  • master分支:用来保存正式的、可在生产环境中部署的代码,每一次更新,都有对应的版本号标签。
  • develop分支:从master分支派生,是每次迭代版本时的共有开发分支。
Feature Branches

  • feature分支:用来开发一项新的软件功能。
  • develop分支可以同时衍生出多个feature分支。
  • 当一个feature分支开发完成后,将这个分支上的代码变更合并至develop分支。
  • feature分支应该永远不与master分支直接交互。
Release Branches

  • release分支:用来存放准备发布的代码。
  • 当develop分支开发完成后,可以从develop衍生出一个release分支。
  • release分支中不应该添加任何新功能,应仅包含测试、错误修复、文档生成及其他面向发布的任务。
  • 测试中出现的bug,统一在release分支下进行修改,并推送至远程分支。
  • 修改内容必须合并回develop分支,上线时从release分支合并到master分支。
  • 合并后的master应该被标记一个新的版本号。
Hotfix Branches

  • Hotfix分支:用来快速给已发布产品修复bug或微调功能。
  • 从master分支直接衍生出来。
  • 完成bug修复后,合并至master分支以及develop分支。
  • 合并后的master应该被标记一个新的版本号。

完整流程

优点

  1. 为管理大型项目提供了一个强大的框架,可用于大规模的团队协作场景。
  2. 非常适合有计划发布周期的项目,可帮助实施持续交付下的DevOps最佳实践。

使用场景

企业级开发,大型团队,敏捷要求相对较低(分支存活时间长短不一、合并繁琐、依赖管理)。

AoneFlow Workflow

AoneFlow只使用三种分支类型:master分支、feature分支、release分支,以及三条基本规则。

工作模式

规则一:开始工作前,从master分支创建feature分支。

AoneFlow的feature分支基本借鉴GitFlow,每当开始一项新的工作时,从master分支衍生出一条feature分支(通常以feature/前缀命名),然后在这个分支上提交代码修改。每个工作项对应一个feature分支,所有的修改都不允许直接提交到master分支。

  • master分支长期存在(项目的整个生命周期),由项目主要负责人管理。
  • feature分支作为临时分支,用于开发的具体功能特性或修复bug,在功能完成后删除。

AoneFlow1

规则二:通过合并feature分支,形成release分支。

从master分支上衍生出一条release分支(通常以release/前缀命名),将所有本次要集成或发布的feature分支依次合并过去。

  • GitFlow:feature -> develop -> release
  • TrunkBased:master -> release

AoneFlow2

release分支的用途可以很灵活,通常将每条release分支与具体的环境相对应,比如release/test对应测试环境,release/prod对应线上正式环境等等。

  • release分支既可以为长期分支也可以为短期分支(存在于一个或者多个版本之间),由测试负责人管理。

规则三:发布到线上正式环境后,合并相应的release分支到master分支,在master分支上添加标签,同时删除该release分支关联的feature分支。

当一条release分支完成线上正式环境的部署后,为了避免在代码仓库里堆积大量feature分支,还应该清理掉已经上线部分的feature分支。与GitFlow相似,master分支上的最新版本始终与线上版本一致,如果要回溯历史版本,只需在master分支上找到相应的版本标签即可。

AoneFlow3

其他:对于hotfix,可以创建一条新的release分支,对应线上环境。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
开发人员A从Master拉取代码生成feature_20210101_A_login
开发人员B从Master拉取代码生成feature_20210101_B_register

测试负责人Y从Master拉取release/test
开发人员A提交Pull Request PR1
开发人员B提交Pull Request PR2

开发负责人F和开发人员C评审PR1 PR2,评审通过

测试负责人Y合并代码到release/test(如果遇到合并冲突,由开发人员处理)
测试人员X对release/test中的代码进行测试
完成测试后对比Master是否比release/test时有更新,如果没有则直接使用release/test构建上线申请
否则从最新Master拉取分支release/prod,合并相关PR并进行回归测试

优点

  1. 每个功能都创建一个独立的分支,易于持续集成环境。
  2. release分支的feature组成是动态的,易于调整需求或增删功能。
  3. release分支之间是松耦合的,可以有多个集成环境分别进行不同的feature组合的集成测试。

Forking Workflow

Forking Workflow与以上讨论的工作流程不同,它不是多个开发者共享一个远程仓库,而是每个开发者都拥有一个独立的服务端存储库。也就是说每个contributor都有两个仓库:本地私有的仓库和远程共享的仓库。

Forking Workflow这种工作流主要好处就是每个开发者都拥有自己的远程仓库,可以将提交的commits推送到自己的远程仓库,但只有工程维护者才有权限push提交的commits到官方仓库,其他开发者在没有授权的情况下不能push。Github很多开源项目都是采用Forking Workflow工作流。

工作模式

  1. 项目创建者在服务器上有一个官方的存储仓库。
  2. 开发者fork官方仓库来创建它的拷贝,然后存放在自己的服务器上。
  3. 当开发者准备好发布本地的commit时,他们push commit到他们自己的公共仓库。
  4. 在自己的公共仓库发送一个pull request到官方仓库。
  5. 维护者pull贡献者的commit到他自己的本地仓库。
  6. 审查代码确保它不会破坏工程,合并它到本地仓库的master分支。
  7. push master分支到服务器上的官方仓库。
  8. 其他开发者应同步官方仓库。

优点

  1. 便于多人协作和独立开发。
  2. 是开源项目的理想工作流程。

参考资料