笔者参加了今年字节跳动举办的后端青训营,在听了其中一节关于教导如何正确使用Git的课后,作下本篇笔记。
本篇笔记主要讲述了如何正确地使用Git进行代码版本管理。
前言
为什么要学习Git?
- 协同工作:业界绝大多数公司都是基于Git进行代码管理
- 开源社区:目前绝大多数的开源项目都是基于Git维护的
Git是什么
Git is a free and open source distributed version control system designed to handle everything from small to very large projects with speed and efficiency.
版本控制:一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。
分布式版本控制:
- 每个库都存有完整的提交历史,可以直接在本地进行代码提交
- 每次提交记录的都是完整的文件快照,而不是记录增量
- 通过Push等操作来完成和远端代码的同步
优点: - 分布式开发,每个库都是完整的提交历史,支持本地提交,强调个体
- 分支管理功能强大,方便团队合作,多人协同开发
- 校验和机制保证完整性,一般只添加数据,很少执行删除操作,不容易导致代码丢失
缺点: - 相对比较复杂,学习成本更高
- 对于大文件的支持不是特别好(git-lfs工具可以弥补这个功能)
Git的实践
初始化
Git仓库初始化:git init
其他参数:
--initial-branch
:git的默认初始化分支是master分支,也可以通过指定参数初始化为别的分支--bare
:创建一个裸仓库(纯Git目录,没有工作目录)---template
:可以通过模板来创建预先构建好的自定义git目录
配置
Git的配置级别从高到低分别有:
--global
:配置存在于~/.gitconfig--system
:配置存在于$(prefix)/etc/gitconfig---local
:配置存在于.git/config
每个级别的配置可能重复,但是低级别的配置会覆盖高级别的配置。
用户名配置:
git config --global user.name "username"
:其中username字符串为自定义用户名git config --global user.email xxx@xxx.com
:其中xxx@xxx.com为自定义用户邮箱
Instead of 配置
git config --global url.git@github.com:.insteadOf https://github.com/
:本例中将ssh协议换为http协议
Git命令别名配置
git config --global alias.cin "commit --ammend --no-edit
:本例中添加了cin命令表示commit --ammend --no-edit,用于简化操作
Git Remote
查看Remote
git remote -v
添加Remote
git remote add origin_ssh git@github.com:git/git.git
git remote add origin_http https://github.com/git/git.git
同一个Origin设置不同的Push和Fetch URL
git remote add origin git@github.com:git/git.git
git remote set-url -add -push origin git@github.com:my_repo/git.git
HTTP Remote 免密配置
URL:https://github.com/git/git.git
将密码存储在内存或硬盘,可以快速跳过认证阶段
- 内存:
git config --global credential.helper 'cache --timeout=3600'
- 硬盘:
git config --global credential.helper "store --file /path/to/credential-file"
(在不指定目录的情况下默认是 ~/.git-credentials) - 将密钥信息存在指定文件中:
${scheme}://${user}:${password}@github.com
SSH Remote
一般情况下不建议使用HTTP的方式访问Git,因为不够安全,使用SSH的方式会更常用。
URL:git@github.com:git/git.git。
SSH的免密配置方式通过公私钥的机制,将生成公钥存放在服务器,从而实现免密访问。
目前Key的类型分为四种,分别是dsa、rsa、ecdsa、ed25519
默认使用的是rsa,由于一些安全问题,现在已经不推荐使用dsa和rsa,优先推荐使用ed25519。
ssh-keygen -t ed25519 -C "your_email@example.com"
- 密钥默认存储在 ~/.ssh/id_ed25519.pub
执行以上语句后,控制台会输出密钥保存的目录,进入这个目录,打开对应的pub文件,将文件内容复制并设置到github上setting中Access - SSH and GPG keys中的对应位置即可
Git Add
git add .
:将变更的内容存储在暂存区
Git Commit
git commit -m "注释"
:将新增内容添加到git目录中
Objects
blob、tree、commit在git中都统一称为Object:
- Blob:存储文件的内容
- Tree:存储文件的目录信息
- Commit:存储提交信息,一个Commit可以对应唯一版本的代码
以上三个信息串联在一起的方式:
- 通过Commit寻找到Tree信息,每个Commit都会存储对应的Tree ID
- 通过Tree存储的信息,获取到对应的目录树信息
- 从tree中获得blob的ID,通过Blob ID获取对应的文件内容
Refs
refs的内容就是对应的Commit ID,因此把ref当做指针,指向对应的Commit来表示当前Ref对应的版本。
不同种类的ref:refs/heads前缀表示的是分支,除此之外还有其他种类的ref,比如refs/tags前缀表示的是标签。
Branch
git checkout -b
可以创建一个新分支,分支一般用于开发阶段,是可以不断添加Commit进行迭代的。
Tag
标签一般表示的是一个稳定版本,指向的Commit一般不会变更。通过git tag命令生成tag。
Annotation Tag
附注标签:一种特殊的Tag,可以给Tag提供一些额外的信息。
通过git tag -a
命令来完成附注标签的创建。
追溯历史版本
获取当前版本代码:通过Ref指向的Commit可以获取唯一的代码版本
获取历史版本代码:Commit里面会存有parent commit字段,通过commit的串联获取历史版本代码,操作步骤:
- 修改文件并提交,创建新的commit
- 查看最新的commit,新增了parent信息。
修改历史版本
commit --amend
:通过这个命令可以修改最近的一次commit信息,修改之后commit id会变- rebase:通过
git rebase -i HEAD~3
可以实现对最近三个commit的修改:- 合并commit
- 修改具体的commit message
- 删除某个commit
filter --branch
:该命令可以指定删除所有提交中的某个文件或者全局修改邮箱地址等操作
Git GC
悬空的Object:顾名思义就是没有ref指向的object。
GC:通过git gc
命令,可以删除一些不需要的object,以及会对object进行一些打包压缩来减少仓库的体积
Reflog:reflog是用于记录操作日志,防止误操作后数据丢失,通过reflog来找到丢失的数据,手动将日志设置为过期
指定时间:git gc prune=now
指定的是修剪多久之前的对象,默认是两周前
完整的Git视图
Git Clone、Pull、Fetch
- Clone:拉取完整的仓库到本地目录,可以指定分支、深度
- Fetch:将远端某些分支最新代码拉取到本地,不会执行merge操作,会修改refs/remote内的分支信息,如果需要和本地代码合并需要手动操作
- Pull:拉取远端分支,并和本地代码进行合并,操作等同于
git fetch
+git merge
,也可以通过git pull --rebase
完成git fetch
+git rebase
操作。可能存在冲突,需要解决冲突
Git Push
常用命令:一般使用git push origin master
命令即可完成
冲突问题:
- 如果本地的commit记录和远端的commit历史不一致,则会产生冲突,比如
git commit --amend or git rebase
都有可能导致这个问题 - 如果该分支就只有自己在使用,或者团队内确认过可以修改历史则可以通过
git push origin master --f
来完成强制推送,一般不推荐主干分支进行该操作,正常都应该解决冲突后再进行推送
推送规则限制:可以通过保护分支,来配置一些保守规则,防止误操作,或者一些不合规的操作出现,导致代码丢失
常见问题
- 为什么配置了Git配置,依然没有办法拉取代码?
- 免密认证没有配
- instead Of配置没有配,配的是SSH免密配置,但是使用的依然是HTTP协议访问
- 为什么Fetch了远端分支,但本地当前的分支历史仍然没有变化?
- Fetch会把代码拉取到本地的远端分支,但是不会合并到当前分支,所以当前分支历史没有变化
- Fetch会把代码拉取到本地的远端分支,但是不会合并到当前分支,所以当前分支历史没有变化
Git研发流程
不同的工作流
集中式工作流
集中式工作流只依托于master分支进行研发活动。
工作方式:
- 获取远端master代码
- 直接在master分支完成修改
- 提交前拉取最新的master代码和本地代码经合并(使用rebase),如果有冲突需要解决冲突
- 提交本地代码到master
集中式工作流-Gerrit
Gerrit是由Google开发的一款代码托管平台,主要的特点就是能够很好的进行代码评审。在aosp (android open source project) 中使用的很广,Gerrit的开发流程就是一种集中式工作流。
基本原理:
- 依托于Change ID概念,每个提交生成一个单独的代码评审。
- 提交上去的代码不会存储在真正的refs/heads/下的分支中,而是存在一个refs/for/的引用下。
- 通过refs/meta/config下的文件存储代码的配置,包括权限,评审等配置,每个Change都必须要完成Review后才能合入。
优点:
- 提供强制的代码评审机制,保证代码的质量
- 提供更丰富的权限功能,可以针对分支做细粒度的权限管控
- 保证master的历史整洁性
- Aosp多仓的场景支持更好
缺点:
- 开发人员较多的情况下,更容易出现冲突
- 对于多分支的支持较差,想要区分多个版本的线上代码时,更容易出现问题
- 一般只有管理员才能创建仓库,比较难以在项目之间形成代码复用,比如类似的fork操作就不支持。
分支管理工作流
分支管理工作流-GitFlow
Git Flow是比较早期出现的分支管理策略,包含五种类型的分支:
- Master: 主干分支
- Develop: 开发分支
- Feature: 特性分支
- Release: 发布分支
- Hotfix:热修复分支
优点: 如果能按照定义的标准严格执行,代码会很清晰,并且很难出现混乱。
缺点: 流程过于复杂,上线的节奏会比较慢。由手太复杂,研发容易不按照标准执行,从而导致代码出现混乱。
分支管理工作流-Github Flow
Github 的工作流,只有一个主干分支,基于Pull Request往主干分支中提交代码。
选择团队合作的方式:
- owner创建好仓库后,其他用户通过Fork 的方式来创建自己的仓库,并在 fork的仓库上进行开发
- owner创建好仓库后,统一给团队内成员分配权限,直接在同一个仓库内进行开发
分支管理工作流-Gitlab Flow
Gitlab推荐的工作流是在GitFlow和Gthub Flow上做出优化,既保持了单一分支的简便,又可以适应不同的开发环境。
原则:upstream first上游优先
只有在上游分支采纳的代码才可以进入到下游分支,一般上游分支就是master
代码合并
Fast-Forward:不会产生一个merge 节点,合并后保持一个线性历史,如果target分支有了更新,则需要通过rebase 操作更新source branch后才可以合入。
Three-Way Merge:三方合并,会产生一个新的merge节点。
如何选择合适的工作流
选择原则:没有最好的,只有最合适的
针对小型团队合作,推荐使用 Github 工作流即可
- 尽量保证少量多次,最好不要一次性提交上千行代码
- 提交Pull Request后最少需要保证有Code Review后再合入
- 主干分支尽量保持整洁,使用fast-forward合入方式,合入前进行rebase
大型团队合作,根据自己的需求指定不同的工作流,不需要局限在某种流程中。
常见问题
-
在Gerrit平台上使用Merge的方式合入代码。
- Gerrit是集中式工作流,不推荐使用Merge方式合入代码,应该是在主干分支开发后,直接Push。
-
不了解保护分支,Code Review,Cl等概念,研发流程不规范。
- 保护分支: 防止用户直接向主干分支提交代码,必须通过Pull Request来进行合入。
- Code Review,CI:都是在合入前的检查策略,Code Review是人工进行检查,CI则是通过一些定制化的脚本来进行一些校验。
-
代码历史混乱,代码合并方式不清晰。
- 不理解Fast Forward和Three Way Merge的区别,本地代码更新频繁的使用Three Way的方式,导致生成过多的Merge节点,使提交历史变得复杂不清晰。
- 不理解Fast Forward和Three Way Merge的区别,本地代码更新频繁的使用Three Way的方式,导致生成过多的Merge节点,使提交历史变得复杂不清晰。
写在最后
由于课程时间有限,课程中提到的大部分内容都是浅尝辄止,当做是对Git进行入门了解还是不错的。在日后的开发工作后,要注意使用Git的规范,进而提高开发效率。