Git的最佳实践 | 青训营笔记

170 阅读13分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第10篇笔记

Git简介

版本控制

image-20220527162308706

一开始的本地版本控制(RCS):本地代码的版本控制
集中式版本控制(SVN):提供一个远端服务器维护代码成本,本地不保存代码,解决多人协作问题
分布式版本控制(Git):每个仓库都能记录版本历史,解决只有一个服务器保存版本的问题

本地版本控制:
	通过本地复制文件,来完成版本控制,一般可以通过不同文件名区分版本;
	或者是使用开发出的本地版本控制软件,其中最流行的是RCS。RCS的原理就是本地保存所有变更的补丁集,可以理解成就是所有的Diff,通过这些补丁,我们可以计算出每个版本的实际的文件内容

集中版本控制:提供一个远端服务来保存文件,所有用户的提交都提交到该服务器中,增量保存所有Diff,如果提交的增量中和远端的文件存在冲突,则需要本地提前解决冲突
优点:
	学习简单,更容易操作,支持二进制文件,对大文件支持更友好
缺点:
	本地不存储版本管理的概念,所有提交只能联上服务器才能提交
	分支上的支持不够好,对于大型项目团队合作比较困难
	用户本地不保存所有版本的代码,如果服务器故障容易导致历史版本的丢失

分布式版本控制:
	每个库都有完整的提交历史,可以直接在本地进行代码提交;
	每次提交记录的都是完整的文件快照,而不是记录增量;
	通过Push等操作来完成和远端代码的同步,其提交不是在push阶段完成的,而是在本地阶段先完成提交的。
优点:
	分布式开发,每个库都是完整的提交历史,支持本地提交,强调个体,便于大型项目的开发维护
	分支管理功能强大,方便团队合作,多人协同开发
	校验和机制保证完整性,一般只添加数据,很少执行删除操作,不容易导致代码丢失
缺点:
	原理更复杂,学习成本高;并且对大文件的支持不够好,由于每次都是全量提交,所以大文件更新操作很占用空间(git-lfs工具可以弥补这个功能)


历史

image-20220527164739658

image-20220527164842480

Git基本使用方式

Git基本命令

image-20220527164948989

常见问题:
为什么明明配置了Git配置,但是依旧没有办法拉取代码
为什么Fetch了远端分支,但是看本地当前的分支历史还是没有变化

不能拉取代码:没有权限,比如没有配置密钥等等
fetch远端分支,本地历史没有变化:fetch只能更新origin的分支


Git目录介绍

image-20220527170719175

image-20220527171034432

项目初始化:
mkdir study
cd study
git init

其他参数:
--initial-branch 初始化的分支,一般创建的主分支都是master分支,但是如果需要改变初始分支的话可以通过这个参数执行创建的分支比如说main
--bare 创建一个裸仓库,一般在服务器上创建的都是一个裸仓库,而在本地创建的一般都是不加参数的仓库
--template 可以通过模板来创建预先构建好的自定义git目录

git的目录结构:
.git
├── branches
├── config
├── description
├── HEAD
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── prepare-commit-msg.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags
其中head包含了当前的分支,也就是指向的分支
config包含了git配置的信息
objects存储了文件信息
refs存储了分支信息
hooks中有很多.sample文件,这些文件都是hook的例子,如果后缀名有.sample的话这些hook是不会执行的,如果删除.sample的话这些hook就会执行了


Git Config:
不同级别的git配置:
--local:当前文件夹下的.git目录
--global:当前用户home文件夹中的.git目录
--system:/etc/gitconfig目录中的配置
每个级别的配置可能会重复,但是低级别的配置会覆盖高级别的配置


常见Git配置:
用户名配置:
	git config --global user.name "xxx"
	git config --global user.email xxx@qq.com
Instead of配置:(比如将http协议改为https协议)
	git config --global url.git@github.com:.insteadOf https://github.com
Git命令别名配置:(使用cin来代替这一长串命令)
	git config --global alias.cin "commit --amend --no-edit"


Git Remote

image-20220527202124702

Git Remote配置
 	git remote add origin_http https://github.com:git/git.git
	git remote add origin_ssh git@github.com:git/git.git
当同一个仓库我们使用不同的拉取和推送的源的时候可以通过
	git remote set-url --add --push origin_http git@github.com:my_repo/git.git



免密登录:
HTTP:(但是很少通过HTTP访问git,因为相对于ssh来说没有那么安全,并且存在硬盘或者内存也没那么方便)
	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:
SSH可以通过公私钥的机制,将生成的公钥存放在服务端,从而实现免密访问
目前的Key类型有四种,分别是dsa、rsa、ecdsa、ed25519,默认为rsa,但是由于安全问题不推荐使用dsa和rsa了,优先推荐使用ed25519
	ssh-keygen -t ed25519 -C "9@qq.com"
	密钥默认存在 ~/.ssh/id_ed25519.pub

Git Add

image-20220527221743171

image-20220527222023280

image-20220527222123483

image-20220527222216471

image-20220527222246737

image-20220527223059818

image-20220527223631433

objects:(普通的文件)
git add .
git commit -m "add readme"

每一个commit都会存储三个信息,分别是treeID、author(作者)、committer(提交信息),每一个commit都可以当做一个完整代码
每一个commit都会关联到一个tree上,tree有一个唯一的id,并且tree中存储着目录树的信息,目录树中存储着这次提交文件id,以及可能嵌套着别的目录树的信息,总之每一个tree都包含着全量的信息
每一个新提交的文件都是一个blob文件,包含着文件的完整信息



refs:(分支的概念)
refs中的内容就是对应的CommitID,因此把ref当做指针指向对应的Commit来表示当前Ref对应的版本
refs/heads前缀表示的是分支,同一个commit可以被多个分支引用,也就代表着从这个commit版本中去创建新的分支,分支的引用就是这个commit的版本
	git chectout test
除此之外还有其他种类的ref,比如refs/tags前缀表示的是标签,一般来说分支指向的commit版本是会经常改动的,而标签指向的版本一般来说不会变动,所以版本发布常使用标签来进行版本发布。在标签中还可以使用附注标签这种类型的标签,这种类型的标签与普通的标签不同的是,这种类型的标签会包含一些信息,比如提交的信息以及作者的信息。
	git tag v0.0.1
	git tag -a v0.0.2 -m "add feature 1"


追溯历史版本

image-20220528155256842

image-20220528155321316

image-20220528163622663

image-20220528163634417

image-20220528165443225

根据之前的介绍可以了解到通过ref指向的commit就能获取当前版本信息
历史版本信息存在于commit文件中的parent字段中,parent字段指向的就是上一个版本的信息,也就是历史版本信息由这个指针维护,历史版本信息通过这个指针串联起来
值得注意的是,新的commit文件指向的是一个新的tree文件,跟commit与一个tree项目树对应吻合

可以通过命令修改最近一次提交的信息,修改之后commitID会改变,也就是说这次修改会生成一个新的commit文件,并且当前分支的ref会指向这个新的commit,旧的commit会变成一个没有被任何文件指向的object,也就是悬空object

修改commit的massage
	git commit --amend
查找悬空object
	git fsck --lost-found


Git GC

image-20220530114228342

git gc命令可以产出一些不需要的object,以及会对object进行一些打包压缩来减少仓库的体积

reflog是用于记录操作日志,防止误操作后数据丢失,通过reflog来找到丢失的数据,手动将日志设置为过期
git log指令只能看到当前分支提交过的所有版本信息,不包括已经被删除的commit记录和reset操作。相比于git log指令,git reflog指令可以看到所有分支的所有操作记录信息,包括已经被删除的commit记录和reset的操作。但是如果存在大量的已被删除的commit,无疑对硬盘空间的占用是比较大的,这时就可以通过手动将不再被需要的commit设置为过期,再通过git gc指令将其删除就可以释放出一些空间

git gc prune=now 指定的是修建多久之前的对象,默认是两周前


完整的Git视图

image-20220530115201551

git本地仓库中,当前项目首先会有一个HEAD标签,指向的是当前分支

分支,也就是ref,会指向一个commitID,或者是指向Tag文件再指向commitID,代表着每一个分支都会指向当前最新的commit

commit,也就是版本信息,每一个commit都会维护着历史版本(通过父指针来维护)和自己的目录树(通过TreeID来维护)

Tree,也就是目录树,每一个目录树都代表着一个完整的项目文件,目录树中会通过指针来维护树结构以及文件信息

远端仓库的同步操作

image-20220530152750306

有两种同步方式,一种是远端与本地同步,另一种是本地上传到远端

远端到本地同步的方式有这几种:
git clone 拉取完整的仓库到本地目录,可以指定分支,深度
git fetch 将远端的某些分支最新代码拉取到本地,不会执行merge操作,会修改refs/remote内的分支信息,如果需要和本地代码合并需要手动操作
git pull 拉取远端分支,并和本地代码进行合并,操作等同于git fetch + git merge,也可以通过git pull --rebase完成git fetch + git rebase操作,但是这种操作不会解决冲突,如果存在冲突则需要自行解决

与本地代码合并的操作也有几种,分别是merge和rebase。
merge合并方式又会分为两种合并方式,分别是Fast-Forward与Three-Way Merge。Three-Way Merge就是下面的这种合并方式,会产生新的节点出来
	merge操作一般来说是对远端比较熟悉的时候才会使用的,merge操作在当前指向的分支进行操作,在操作完成后,HEAD会指向新的成功merge后的commit。比如说在master节点上对feature节点进行merge,在merge操作完成后,master会指向新的成功merge后的节点,并且留下一个merge的链
            master                															master
              |                                                    |
M1 --- M2 --- M3                              M1 --- M2 --- M3 --- M4
       \                                             \             /
        \--- F1                                       \--- F1 --- /
              |                                             |
           feature                                       feature
           

	rebase操作一般在对远端仓库不熟悉的时候才会使用,这个操作又被称为变基,比如在feature节点上进行rebase操作,也就是将feature节点checkout出来的分支转移到master的最新的分支上,操作完成后可以将feature看做是从master最新的commit上checkout出来的一样
            master  				             master  feature
              |    					  	            |       |
M1 --- M2 --- M3  						M1 --- M2 --- M3 --- F1
       \
        \--- F1 
              |
           feature


本地代码同步到远端的方式:
一般使用git push origin master命令即可完成,这个命令就是将本地代码的master分支推送到origin源上的master分支

但是有时会如果本地commit记录与远端的commit记录不一致的话,则会产生冲突,比如git commit --amend or git rebase都有可能导致这个问题
如果该分支就自己一个人使用,或者团队内缺点过可以修改历史,则可以通过git push origin master -f来完成强制推送,一般不推荐主干分支上进行该操作,正常都应该解决冲突后再进行推送

可以通过保护分支,来配置一些保护规则,防止误操作,或者一些不合规的操作出现,导致代码丢失

Git研发流程

集中式工作流

image-20220530155513049

集中式工作流
工作方式:
1. 获取远端master代码
2. 直接在master分支完成修改
3. 提交前拉取最新的master代码和本地代码进行合并(使用rebase),如果有冲突需要解决冲突
4. 提交本地代码到master

Gerrit:Gerrit是由Google开发的一款代码托管平台,主要的特点就是能够很好的进行代码评审。在aosp(android open source project)中使用的很广,Gerrit的开发流程就是一种集中式工作流
基本原理:
1. 依托于ChangeID概念,每个提交生成一个单独的代码评审
2. 提交上去的嗲啊不会存储在真正的refs/heads/下的分支中,而是存在一个refs/for/的引用下
3. 通过refs/meta/config下的文件存储代码的配置,包括权限、评审等配置,每个Change都必须要完成Review后才能合入
优点:
1. 提供强制的代码评审机制,保证代码的质量
2. 提供更丰富的权限功能,可以针对分支做细粒度的权限管控
3. 保证master的历史整洁性
4. Aosp多仓的场景支持更好
缺点:
1. 开发人员较多的情况下,更容易出现冲突
2. 对于多分支的支持较差,想要区分多个版本的线上代码时,更容易初夏难问题
3. 一般只有管理员才能创建仓库,比较难以在项目之间形成代码复用,比如类似的fork操作就不支持

分支管理工作流

image-20220530160403975image-20220530170709422

image-20220530171602667

分支管理工作流:
Git Flow、Github Flow、Gitlab Flow

Git Flow:分支类型丰富,规范严格
包含五种类型的分支:
	Master:主干分支
	Develop:开发分支
	Feature:特性分支
	Release:发布分支
	Hotfix:热修复分支
优点:吐过能按照定义的标准严格执行,代码会很清晰,并且很难出现混乱
缺点:流程过于复杂,研发容易不按照标准执行,从而导致代码出现混乱



Github Flow:Github的工作流,只有一个主干分支,基于Pull Request往主干分支中提交代码
团队合作方式:
1. owner创建好仓库后,其他用户通过Fork的方式来创建自己的仓库,并在fork的仓库上进行开发
2. owner创建好仓库后,统一给团队内成员分配权限,直接在同一个仓库内进行开发
Pull Request:
创建的pull请求都会在github界面中直接看到,pull请求可以在PullRequest页面执行CI、CA、CR等操作,执行合入,比如说添加一些意见,对代码进行code review等等操作之后,才能被合入master分支

并且github可以通过设置保护分支的选项,来限制合入的策略,以及限制直接的push操作。比如说在请求合入之前都需要进行pull请求、需要线性的合并、管理员也要遵守规则等等



Gitlab Flow:在主干分支和开发分支之上构建环境分支,版本分支,满足不同发布or环境的需要






合适的工作流

image-20220530171325234

常见问题

image-20220530171432254