这是我参与「第三届青训营-后端场」笔记创作活动的第11篇笔记。
1.Git是什么
Git是一个能高效处理各种大小项目的,分布式的版本控制系统。
1.1 版本控制
版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。使用版本控制能够更好的关注变更,了解到每个版本的改动是什么;方便对改动的代码进行检验,预防事故发生;随时切换到不同的版本,回滚误删误改的代码问题。
| 版本控制类型 | 代表性工具 | 解决的问题 |
|---|---|---|
| 本地版本控制 | RCS | 本地代码的版本控制 |
| 集中式版本控制 | SVN | 提供一个远端服务器来维护代码版本,本地不保存代码版本,解决多人协作问题 |
| 分布式版本控制 | Git | 每个仓库都能记录版本历史,解决只有一个服务器保存版本的问题 |
1.1.1 本地版本控制
最初的方式:通过本地复制文件夹来完成版本控制,一般可以通过不同的文件名来区分版本。
解决方案:开发了一些本地的版本控制软件,其中最流行的是RCS。
基本原理:本地保存所有变更的补丁集,可以理解成记录每个版本的变化,通过这些补丁,我们可以计算出每个版本的实际的文件内容。
缺点:RCS这种本地版本控制存在最致命的缺陷就是只能在本地使用,无法进行团队协作,因此使用的场景非常有限,因此衍生出了集中式版本控制。
1.1.2 集中版本控制
代表性工具:SVN
基本原理:
- 提供一个远端服务来保存文件,所有用户都提交到该服务器中。
- 增量保存,如果提交的增量中和远端现存的文件有冲突,则需要本地提前解决冲突。
优点:
- 学习简单,更容易操作。
- 支持二进制文件,对大文件支持更友好。
缺点:
- 本地不存储版本管理的概念,所有提交都只能联上服务器后才可以提交。
- 分支上的支持不够好,对于大型项目团队的合作比较困难。
- 用户本地不保存所有版本的代码,如果服务端故障容易导致历史版本的丢失。
1.1.3 分布式版本控制
代表性工具:Git
基本原理:
- 每个库都存有完整的提交历史,可以直接在本地进行代码提交。
- 每次提交的都是完整的文件快照,而不是记录增量。
- 通过Push等操作来完成和远端代码的同步。
优点:
- 分布式开发,每个库都是完整的提交历史,支持本地提交,强调个体。
- 分支管理功能强大,方便团队合作,多人协同开发。
- 校验和机制保证完整性,一般只添加数据,很少执行删除操作,不容易导致代码丢失。
缺点:
- 相对于SVN更复杂,学习成本更高(命令很多)。
- 对大文件的支持不是特别好(git-lfs工具可以弥补这个功能)。
1.2 Git发展史
开发原因:市面上没有开源的分布式版本控制系统。
基于git衍生的平台:
- Github:全球最大的代码托管平台,大部分的开源项目都放在这个平台上。
- Gitlab:全球最大的开源代码托管平台,项目的所有代码都是开源的,便于在自己的服务器上完成Gitlab的搭建。相较与github可以有更多定制化的代码。
- Gerrit:由Google开发的一个代码托管平台,Android这个开源项目。
- ........
2. Git基本使用方式
git命令:{配置、提交、远端同步}
配置:git config、git remote。
提交代码:git add、git commit。
远端同步:{拉取代码、推送代码}
拉取代码:clone、pull、fetch。
推送代码:push。
常见问题
- 为什么配置了Git但仍然无法拉取代码?
没有配置密钥。配的SSH免密配置,但是使用的是HTTP协议访问。没有权限。
- 为什么Fetch了远端分支,但本地当前的分支历史没有变化?
fetch会把代码拉取到本地的远端分支,但是不会合并到当前分支,所以当前分支历史没有变化。
2.1 Git目录介绍
以>>开头的行表示操作步骤,其余的为介绍性文本。
>> 在命令行用mkdir demo创建文件夹test并用cd demo进入。
>> 执行git init初始化仓库。命令行输出已初始化。
init可以不加参数,也可以加其他参数。git版本不同可能指令也不同,输入git init -h或git help init可查看init能够加什么参数:
git init demo在当前文件夹下生成demo文件夹,并对demo进行git初始化(相当于省去了mkdir和cd的动作)。git init --bare直接将初始化的git文件放到当前目录下(默认是放到当前目录的.git文件夹下,加上--bare相当于没有.git文件夹了。一般用于服务器上)。--template=<template_directory>可以通过模板来创建预先定义好的自定义git目录。
>> tree .git查看目录(需要提前安装tree,sudo apt安装即可)。
根据git的不同版本,输出的目录树也不同。HEAD存储当前分支名。config存储当前仓库的配置。objects存储文件信息。refs存储分支信息。
git仓库分为工作区(Working Directory)和暂存区(Staging Area)。修改文件使用的是工作区(可以理解为当前文件夹demo的所有显示文件),使用git add可以将文件添加到暂存区(可以理解为当前文件夹下的.git文件)。commit可以将暂存区文件提交到仓库(远端)。
2.2 Git 配置
不同级别的Git配置:配置由高到低分为系统级别(--system,存于$(prefix)/etc/gitconfig文件)、全局级别(--global,存于~/.gitconfig文件)、本地级别(--local,存于.git/config文件)。每个级别的配置可能重复,但是低级别的配置会覆盖高级别的配置(从低到高查找配置)。
常见Git配置:
- 用户名配置:用户类有两个成员变量:邮箱地址和用户名。使用以下命令可以配置:
git config --global user.name "名字",git config --global user.email 邮箱 - Instead of配置:做url的替换。如
git config --global url.git@github.com:.insteadOf https://github.com/可以将ssh协议换成http协议。 - Git命令别名配置:用于简化命令。如
git config --global alias.cin "commit --amend --no-edit"可以用git cin代替git commit --amend --no-edit。
Git Remote配置:代表本地和远端仓库的关联信息。分为http和ssh两种。
>> git remote -v查看remote。此时没有输出,因为我们还没有加过remote命令。
>> 添加remote:使用git remote add origin_ssh git@github.com:git/git.git、git remote add origin_http https://github.com/git/git.git分别添加ssh源和http源。
相关帮助可以使用git remote -h查看相关帮助,例如如何修改已添加的remote。
>> 再次使用git remote -v可观察到配置改变。
可以通过配置两个不同的源连接到不同的平台。
同一个Origin设置不同的Push和Fetch URL的方法如下:
>> git remote add origin git@github.com:git/git.git添加一个新的源。
>> git remote set-url --add --push origin git@github.com:myRepo/git.git对新添加的源的push进行修改。
>> 使用git remote -v观察结果。
HTTP Remote的形式一般为URL:https://github.com/git/git.git,使用http协议做身份认证。
当依赖的库很多时,如果每一次clone或编译都要做身份认证是很麻烦的。因此需要配置免密的认证方式。
免密配置:
- 内存:
git config --global credential.helper 'cache --timeout=3600'将密码在内存中,存储3600秒。 - 硬盘:
git config --global credential.helper "store --file /path/to/credential-file"将密码存在磁盘里,不指定目录的情况下默认文件为~/.git-credentials。
将密钥信息存在指定文件中的具体格式为:${scheme}://${user}:${password}@github.com。
不推荐用http访问git因为相对ssh来说不安全,面密配置也不方便。
SSH Remote的形式一般为URL:git@github.com:git/git.git,使用ssh协议做身份认证。
免密配置:ssh可以通过公私钥的机制,将生成的公钥存放在服务端,从而实现免密访问。目前的key有4种类型,分别是:dsa、rsa、ecdsa、ed25519。默认使用的是rsa,由于一些安全问题,现在已经不推荐使用dsa和rsa了,优先推荐使用ed25519。
一些新版本的windows代码的ssh不再使用dsa和rsa。因此只配置dsa或rsa不能拉取这样的代码。
配置ed25519密钥方法:
>> 输入ssh-keygen -t ed25519 -C "your_email@example.com"生成密钥对。
此时系统会询问存储位置,直接摁回车即可,密钥默认存在~/.ssh/id_ed25519.pub。如果之前已经生成过密钥,系统会询问是否重写。可以选n。如果选y则会重写公私钥,需要重新将公钥放在远端。之后还会询问密码并输入两遍进行确认,不需要密码直接回车即可。
>> 使用cat 生成的公钥(公私钥目录会生成两个文件,.pub后缀的是公钥,无后缀的是私钥)查看公钥信息。
>> 复制公钥信息(包括邮箱)。点击github主页右上角头像,点击Setting。左侧选择SSH and GPG keys,右侧点击New SSH key。随便取一个title名称,将公钥信息粘贴到Key中。点击Add SSH key。此时会提醒输入github密码,输入即可。
可以配置多对公私钥,但SSH访问时需要指定使用哪一对。
2.2 Git 命令
>> 在demo的路径下输入touch readme.md创建readme文件。打开文件添加信息,保存退出。再输入tree .git可以看到.git目录还没有变化。输入git status查看当前状态。
>> 输入git add .将工作区内容添加到暂存区。再次输入tree .git和git status可以看到输出改变。
>> 输入git cat-file -p 文件ID即可查看之前添加的文件信息。
其中文件ID是从objects目录开始的路径。一般是两字母的文件夹名+文件名。
>> 使用git commit -m "add readme"将暂存区内容提交到本地仓库。
若只输入git commit则会自动弹出一个文本输入进程,让你输入一个消息名称。输入名称后使用ctrl-x离开,这时会提示是否保存,选择Y后会询问文件保存路径,使用回车确认路径即可。当然也可以先用ctrl-o写入,回车确认路径,再用ctrl-x离开,但这样可能在确认完路径之后又因为误触更改文件(可能需要额外一次操作),此时选择N即可。
>> 输入tree .git可以看到:objects目录下多了两个文件(一共三个,其中一个是之前add的readme.md);refs/heads目录下多了master文件。
>> 使用git cat-file -p 文件IDobjects目录下新增的文件的内容。
- 一个文件中包含
序号 blob 文件ID readme.md。这个文件是一个目录树类型的对象,存储了目录信息。可以用里面包含的文件ID寻找到文件名为readme.md的文件。 - 另一个文件包含多行,第一行为
tree Tree ID,紧接着的两行是作者信息和修改者信息,第5行是之前紧跟commit指令输入的message信息。
master存储的是提交信息的文件ID,使用cat即可查看。
>> 输入git log可以看到最新的提交日志。
2.3 Objects
commit/tree/blob在git里面都统一称为Object,除此之外还有个tag的object。
- Blob存储文件的内容。
- Tree存储文件的目录信息。
- 存储提交信息,一个commit可以对应唯一版本的代码。
如何把这些信息串联到一起呢?
- 通过commit寻找到tree的信息。每个commit都会存储对应的Tree ID。
- 通过tree存储的信息获取到对应的目录树信息。
- 通过文件ID获取文件内容。
>> 使用 git checkout -b test创建名为test的新分支(在refs/heads路径下)。
此时test文件和master文件内容是一样的,都存储了对应的提交信息的文件ID。
refs的内容就是对应的CommitID。因此把ref当做指针,指向对应的Commit来表示当前Ref对应的版本。
ref的种类并不相同,heads表示的是分支,其他种类的如tags表示的是标签。分支一般用于开发阶段,是可以不断添加Commit进行迭代的。标签一般表示的是一个稳定版本,指向的Commit一般不会变更。
>> 通过git tag v0.0.1命令可以生成名为v0.0.1的tag。
此时v0.0.1的内容和master和test都是一样的。
附注标签是一种特殊的tag,可以给tag提供一些额外的信息。
>> 使用git tag -a v0.0.2 -m "add feature 1"可以创建名为0.0.2的标签。
>> 此时再用cat .git/refs/tags/v0.0.2可以看到其信息和其他的ref信息不同。
v0.0.2指向了一个新的文件。
>> 使用git cat-file -p 文件ID查看指向的新文件。
可以看到除了提交信息的文件ID,还包含了标签的附注以及添加附注的用户的信息。
2.4 回溯历史版本
通过ref指向的commit可以获取唯一的代码版本,即当前版本代码。
commit里面存有的parent commit字段,通过commit的串联获取历史版本代码。
>> 更新readme.md内的信息,使用git add .和git commit -m "update readme"提交新版本。
>> 使用git log可以看到最新的提交信息。
>> 复制第一个commit的文件ID(test分支的),用git cat-file -p 文件ID查看内部信息。
(如果与master分支对比)除了tree文件的id变化以外(文件内容变化后会生成全新的blob文件,因此tree文件也发生变化),还多了一行parent,记录了它之前的版本。
>> 使用tree .git可以发现多了3个object,分别为新增的commit/tree/blob文件。
此时refs/heads/test的内容也会变化,指向新的commit。
2.5 修改历史版本
-
git commit --amend可以修改最后一次commit信息,修改后commit id会变。 -
git rebase -i HEAD~3可以实现对最近3个commit的修改,包括:- 合并commit
- 修改具体的commit message
- 删除某个 commit
-
filter --branch可以删除所有提交中的某个文件(比如某个大文件每个版本都会复制一份使得仓库容量不足)或者全局修改邮箱地址等操作。
>> 输入git commit --amend会进入文件编辑模式。可以改变message内容。
>> 使用git log可以看到test分支的commitID和message与上一次查看相比都发生变化。
>> 使用git cat-file -p 文件ID查看test分支的commit文件。
可以看到commit文件的message也已经发生变化,但tree信息和parent信息没有发生变化。
>> 使用tree .git就可以发现之前的操作新填了一个commit文件,而tree和blob都没有变化。
由于新增了object,而旧的object没有被删除,同时旧的object已经没有用了(没有指针指向他们)。我们把这些老的object称作悬空的object。
>> 使用git fsck --lost-found可以查看悬空的object(以git log中的commit为根节点做遍历,然后筛选出未被遍历到的节点)。
2.6 Git GC
gc:通过git gc命令删除不需要的object,以及会对object进行一些打包压缩来减少仓库的体积。
reflog:记录操作的日志,防止误操作后数据丢失。通过reflog来找到丢失的数据,手动将日志设置为过期。
指定时间:git gc prune=now指定的是修剪多久之前的对象,默认是两周前。
>> 使用git reflog expire --expire=now --all将过去的日志设置为过期。
>> 使用git gc --prune=now进行清理。
>> tree .git查看目录树,可以看到文件发生巨大变化。
此时再用git cat-file无法再找到之前悬空的文件,他们已经被删除。可以看到objects文件夹中仅有info和pack两个文件夹了。这是因为git已经将之前的非悬空的object打包成了pack。同时之前的refs也会被打包为packed-refs。
>> 使用cat .git/packed-refs可以看到文件中记录了之前的全部refs(包括heads以及tags)。
2.7 完整的Git试图
|refs| |refs| |refs| |refs|
| | | |
| v | |
|-----|tag| | |
v v v
|commit|---->|commit|---->|commit|
| | |
v v v
|tree| |tree| |tree|
| | |
v v v
|blob| |blob| |blob|
git中存放多个refs。指向分支或普通标签的refs记录commitID,指向附注标签的refs记录了tag,由tag再指向commitID。
每一个commit都会有自己的tree,以及blob(即每一个commit都是一个完整的仓库版本)。同时,有的commit会有自己的parent。
2.8 Git多人合作和远端仓库同步
- 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来完成强制推送,一般不推荐主干分支进行该操作,正常都应该解决冲突后再进行推送。
推送规则限制:通过保护分支来配置一些保护规则,防止误操作,或者一些不符合规矩的操作出现,导致代码丢失。
3.Git研发流程
常见问题
- 在Gerrit平台上使用Merge的方式合入代码
Gerrit是集中式工作流,不推荐使用Merge方式合入代码,应该是在主干分支开发后,直接Push。
- 不了解保护分支,Code Review, CI等概念,研发流程不规范
保护分支:防止用户直接向主干分支提交代码,必须通过PR来进行何如。
Code Review, CI:都是在合入前的检查策略,Code Review是人工进行检查,CI是通过一些定制化的脚本来进行一些校验。
- 代码历史混乱,代码合并方式不清晰
不理解Fast Forward 和 Three Way Merge 的区别,本地代码更新频繁的使用Three Way方式会导致生成过多的Merge节点,使提交历史变得复杂、不清晰。
3.1 不同的工作流
| 类型 | 代表平台 | 特点 | 合入方式 |
|---|---|---|---|
| 集中式工作流 | Gerrit/SVN | 只依托于主干分支进行开发,不存在其它分支 | Fast-forward |
| 分支管理工作流 | Github/Gitlab | 可以定义不同特性的开发分支,上线分支,在开发分支完成开发后再通过MR/PR合入主干分支 | 自定义,Fast-Forward,Three-Way Merge都可以 |
集中式工作流:只依托于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多仓的场景支持更好
缺点:
- 开发人员较多的情况下更容易出现冲突
- 对于多分支的支持较差,想要区分多个版本v的线上代码时,更容易出现问题
- 一般只有管理员才能创建仓库,比较难以在项目之间形成代码复用,比如类似的fork操作就不支持
分支管理工作流
| 分支管理工作流 | 特点 |
|---|---|
| Git Flow | 分支类型丰富,规范严格 |
| Github Flow | 只有主干分支和开发分支,规则简单 |
| Gitlab Flow | 在主干分支和开发分支之上构建环境分支,版本分支,满足不同发布或环境的需要 |
Git Flow是比较早期出现的分支管理策略,包含五种类型的分支:
- Master:主干分支
- Develop:开发分支
- Feature:特性分支
- Release:发布分支
- Hotfix:热修复分支
优点:如果能按照定义的标准严格执行,代码会很清晰,并且很难出现混乱。
缺点:流程过于复杂,上线的节奏会比较慢。由于太过复杂,研发容易不按规定标准执行,从而导致代码出现混乱。
Github工作流只有一个主干分支,基于Pull Request往主干分支中提交代码。
选择团队合作的方式:
- owner创建好仓库后,其他用户通过Fork的方式来创建自己的仓库,并在fork的仓库上进行开发。
- owner创建好仓库后,统一给团队内部成员分配权限,直接在同一个仓库内进行开发。
3.2 创建一个Pull Request
- 创建一个master主分支
>> 在github点击页面右上角头像,点击your repositories,点击绿色按钮New。
>> repository name中填写仓库名称,类型选择public,点击create repository创建。
>> 选择使用SSH,复制URL。
>> 终端命令行输入git clone URL。
此时会提示你clone了一个空仓库。
>> 使用cd进入clone的文件夹,执行tree .git可以看到已经初始化完成。
>> 创建readme文件,执行git add .,git commit -m "add readme",git push origin。
此时将代码提交到了master分支(默认分支也可能名为main,分支名可改)中。
>> 刷新github页面,可以看到readme文件已经被上传。
- 创建一个feature分支
>> 命令行输入git checkout -b feature切换到一个新的分支。
>> 修改readme,执行git add .,git commit -m "update readme",git push origin feature。
此时github页面会显示有新分支feature被push入仓库。而终端则会返回一个pull request链接。
>> 我们复制命令行中的# pull request链接,在浏览器打开。点击create pull request。或者在github中点击pull requests,new pull request,选择feature分支然后点击create pull request(两个)。
之后github会进行CI和冲突检查,检查结束后会显示绿色的对勾。我们可以在对话框中进行代码讨论。点击checks可以加入一些想要的代码检查。
>> github中点击files changed可以查看代码变更。
>> 确认代码可以合并以后点击Merge pull request,confirm merge进行合并。
此时页面分支处会显示紫色的Merged,切会Master分支也会发现文件已经改变。
>> 命令行输入git checkout master切换回master分支,git pull origin master拉取代码,git log查看日志。
日志中多了一个merge节点(merge、feature、原始master)。
还可以通过进行一些保护分支设置,来限制合入的策略,以及限制直接的push操作。
>> github中进入仓库,点击Setting,左侧点击Branches,选择要保护的分支(此处可以对分支改名)点击add rule。
- require a pull request before merging:必须通过pull request才能合并。
- require approvals:必须有人同意才能合并(一般多人需要勾选)
- require status checks to pass before merging:必须通过所有的check才能合并。
- require conversation resolution before merging:必须将评论的问题全部解决才能合并。
- require linear history:不想要merge节点,直接合并到主分支中。
- include administrators:配置对管理员也生效。
>> 输入保护名,可以起名为main,勾选需要pull request,取消勾选需要同意,勾选线形历史,勾选管理员生效。点击create。
此时就不能直接向仓库push代码了。
>> 修改readme,执行git add .,git commit -m "update readme",git push origin master。
push失败。
gitlab推荐的工作流是在gitflow和github flow上作出优化,既保持了单一主分支的简便,又可以适应不同的开发环境。
上游优先原则(upstream first):只有在上游分支采纳的代码才可以进入到下游分支,一般上游分支就是master,如果按环境来分下游就是预发布环境、线上环境,如果按版本来分下游就是后续版本。
3.3 代码合并
- Fast-Forward:不会产生一个merge节点,合并后保持一个线性历史,如果target分支有了更新,则需要使用rebase操作更新source branch后才可以合入。
- Three-Way Merge(三方合并):会产生一个新的Merge节点。
>> 命令行输入git checkout -b test,修改readme,git add ,git commit -m "test",git checkout master,git merge test --ff-only。
可以看到使用的是fast-forward方式。
>> 使用git log查看日志。
可以看到最新的commit和之前的用github进行merge的日志不同,最新的commit没有merge记录。
>> 命令行输入git checkout test,修改readme,git add ,git commit -m "test",git checkout master,git merge test --no-ff,git log。
可以看到最新的commit是merge节点。
3.4 如何选择工作流
选择原则:没有最好的,只有最合适的。
针对小型团队合作,推荐使用Github工作流即可。
- 尽量保证少量多次,最好不要一次性提交上前行代码
- 提交pull request后最少需要保证有CR后再合入
- 主干分支尽量保持整洁,使用fast-forward合入方式,合入前进行rebase
大型团队合作,根据自己的需要指定不同的工作流,不需要局限在某种流程中。