版本控制手册
本文中出现的 [ ] 为根据需求自行修改的变量。
基本命令
-
git init
:将当前目录配置成git
仓库,信息记录在隐藏的.git
文件夹中。 -
git config --global user.name [xxx]
:设置全局用户名,信息记录在~/.gitconfig
文件中。 -
git config --global user.email [xxx@xxx.com]
:设置全局邮箱地址,信息记录在~/.gitconfig
文件中。 -
git clone [project route]
克隆项目。项目路径有 ssh 和 https 两种方式,前者需要上传本机用户密钥,后者需要每次输入远端用户账户密码。
Ubuntu生成密钥命令:
ssh-keygen -t rsa -b 2048
查看密钥:
cat ~/.ssh/id_rsa.pub
-
git add [file]
提到暂存区。 -
git rm --cached [file]
取消追踪文件(add 的逆操作).gitignore
忽略的文件如果处于已追踪状态则忽略失败,需要进行取消追踪操作 -
git commit -m "[message]"
提交本地库。 -
git commit -a --amend -m "[message]"
修改最新提交。 -
git commit -a --amend --no-edit
修改最新提交内容,但不修改提交信息。 -
git status
查看当前Git仓库的状态。 -
git log
查看版本记录。 -
tig
查看版本记录(推荐),第三方应用需手动安装。 -
git push
将本地提交推送到远端。 -
git pull
拉取远端分支合并到当前分支(注意拉取前,如已对文件进行改动,应stash save)。
暂存区存在的意义
Q:那么为什么要在“工作目录”和“版本库”之间添加“暂存区”呢?直接从“工作目录”到“版本库”不香吗?
A:保证提交的原子性,即每一个提交都是由多个文件的修改组成,而且这个提交是原子性的,要么这些修改全部成功,要么全部失败。原子性提交使得把项目整体还原到某个阶段或者时间点变得极为简便。
分支命令
git branch
查看当前分支。git checkout -b [dev-user]
在本地创建并切换到dev-user分支。git branch -D [dev-user]
删除本地dev-user分支。git reflog
查看引用日志(指针的移动过程)。
本地库合并
适用于管理个人项目(仅需开发机)。
测试场景(vscode插件Git Graph):
-
git merge [dev-user]
将特性分支合并到当前分支(基础分支)。会生成新节点,两条分支的提交历史保持不变。通常在基础分支上进行
git merge 特性分支
。 -
git rebase [master]
将当前分支(特性分支)的更改更新到基础分支上,会修改提交历史成为一条线性结构。通常在特性分支上进行
git rebase 基础分支
。具体操作:
- 创建临时分支:以基础分支的最新提交为底座创建临时分支
- 处理第一个commit:Git会计算第一个commit与临时分支当前状态之间的差异,并将这些差异以补丁的形式应用到临时分支上,然后创建一个新的提交来记录这些更改。
- 处理第二个commit:接着,Git会对第二个commit执行相同的操作。它会计算第二个commit与第一个新创建的提交之间的差异,并将这些差异应用到临时分支上,然后再次创建一个新的提交。
- 以此类推…
远端库合并
适用于多人协作项目(需借助共享服务器)。
-
git push --set-upstream origin [dev-user]
在本地开发分支上的工作完成后,直接将分支推送到远端,并在远端服务器建立MR/PR。将本地的
dev-user
分支推送到名为origin
的远程仓库,并在那里创建一个名为dev-user
的分支(如果尚不存在的话),同时设置这个远程分支为本地dev-user
分支的上游分支。这样,在未来可以简单地使用git pull
来从该远程分支拉取更新,或使用git push
将本地的更改推送到该远程分支,而无需每次都指定远程仓库和分支名称。 -
git push origin --delete [dev-user]
删除远端dev-user分支。
测试场景(vscode插件Git Graph):
merge 方式: 为了保证push的代码不与主线分支产生冲突,所以需要在本地进行一次merge,注意这里是在开发分支merge主线分支。之后push开发分支,在web端提MR\PR,等待审核合并。(特点:历史记录保持完整,会产生多余无意义节点)
rebase 方式: 本地rebase后,push提MR,合并后依旧会多一个merge节点,原因是项目合并方式是Merge。
项目代码合并方式设置
-
使用Squash + fast-forward合并方式,效果如下:
Squash 使得两个提交版本合并为最终的一个版本
fast-forward 使得不创建新的节点,并且强制要求rebase(基于目标分支的最新版本开发)
删除本地和远端的dev分支后,开发痕迹即消失。
stash
-
git stash
将当前工作区的所有更改(包括暂存的和非暂存的)保存到一个临时的存储区域(称为“储藏”),同时恢复工作区到最近一次提交的状态。 -
git stash save [message]
储藏更改(同时添加备注信息)。 -
git stash apply/pop
恢复储藏,stash实际上使用了一个栈(stack)结构来管理保存的更改,apply只读栈顶元素并不弹栈,pop 读取栈顶元素并弹栈。 -
git stash list
查看储藏列表。 -
git stash drop [id/name]
根据索引或名称删除指定的储藏。 -
git stash clear
清空整个储藏列表。
stash的应用场景:
同一分支pull冲突时:当在一个分支上进行开发,并需要更新代码以确认最新更改时,如果直接pull可能会导致大量冲突。这时,可以使用git stash将当前未提交的更改保存起来,然后完整地pull下最新代码并检查。检查完毕后,可以使用git stash pop将之前的更改取出来并解决可能的冲突。
切换分支解决问题时:当在一个分支上开发新功能时,可能需要切换到另一个分支去修改一个紧急的bug。这时,可以使用git stash将当前未提交的更改保存起来,然后切换到另一个分支进行修改。修改完毕后,可以切换回原来的分支并使用git stash pop恢复之前的更改。
版本回滚
git reset --hard [hash]
将当前分支的指针移动到指定的版本。
有可能对远端的影响:
-
git push -f
强制推送,当本地通过reset版本回退时,可以通过此命令强制同步远端指针。场景:比如远端和本地都是(V3 V2 V1) 三个版本,此时发现(V3 V2)是错误版本,需要改为( V f i n a l V_{final} Vfinal V1) 。
方案:可以在本地reset到V1,commit 一个 V f i n a l V_{final} Vfinal ,之后
git push -f
,即可同步到远端分支。解释: reset + commit 后相当于本地分支指针不再指向V3那个分叉,指向了一个新的分叉。push同步后远端指针也指向新分叉,相当于本地和远端的V3节点所在分叉都没有了指针进行管理。
PR and MR
公司中的场景:团队中的 Committer 也就是帮同事们检视代码 (Code Review) 和合入代码的人,经常听到有同事在群里喊“大佬,帮我合个 PR”,“大佬,我刚提交了一个 MR,帮忙合一下,急着出补丁”。
我有点懵了,PR 和 MR 到底哪个才是正确的,这两个到底有什么区别,我决定先搞清楚这两个概念。
Pull Request 到底是个啥?
Pull Request 实在是一个令人难以理解的词,尽管学会 PR 很长时间了,自己也知道是什么意思了,但自己语言天赋实在是菜,不能白话出来,直到今天看到知乎上的一个回答,才恍然: 我改了你们的代码,请拉回去看看吧。
PR 的直译是: 拉取请求。即使会使用 PR 的人还是会有疑惑,原因是拉取的主体理解有误,PR 的意思是: 一个请对方拉取自己的代码的请求。
经常用 Github 的同学对这个肯定很熟悉了。Github 聚集了 4000 万开发者,过亿的开源项目,如果想给别人的开源仓库贡献代码,通常是先 fork 别人的项目,然后本地修改完成提交到自己的个人 fork 仓库,最后提交 PR 等待别人合入你的代码。
Github-fork机制工作流:
我们重点看一下第 6 步,小明写完代码了想合入到原作者的仓库,新建了一个"pull request",拉请求? 这明明是推啊,小明将自己的修改推到原作者的仓,感觉叫"push request"比较合适吧。
既然 Github 坚持叫"pull request",我们试着理解一下它的思路,小明写完代码了心里肯定是在想: 原作者大神,我改了点东西,你快把我的修改拉回去吧。站在原作者的角度思考,叫 pull request 好像也说得过去,每天有大量的人从我这里 fork 代码走,我只会拉取我感兴趣的代码回来。
我好像把自己说服了。
什么是 Merge Request?
MR 的全称是 Merge Request,相信玩过Gitlab 的同学都知道这个。
插播一下,Github 这么好用了为什么还有人玩 Gitlab,这就要几年前说起了。在微软没有收购 Github 之前,Github 上面所有的项目必须是公开的,也就是说自己很渣的代码也必须要公开,不能藏着噎着。但是在一些小的公司或者创业团队,代码这种核心资产是不希望被公开,他们迫切需要私密仓这种需求,所以很多人都选择了 Gitlab。当然后面 Github 也放开了私有仓库,这是后话了。
Gitlab-merge 机制工作流:
团队中每个人都从远程仓库 develop 分支拉取代码,本地基于 develop 分支新建特性分支,修改完代码将特性分支推到远程仓,紧接着新建 Merge Request 期望将自己的特性分支合入 develop 分支。
从上面这个流程来看 Merge Request 就是将自己的特性分支合入到主干分支。
Pull Request VS Merge Request
总结一下上面两个例子。
-
Github 是玩 fork 模式的,开发者提交自己的代码新建 Pull Request,请求原作者: “把我的代码拉回去吧”。
-
Gitlab 是玩分支模式的,开发者提交自己的代码新建 Merge Request,想将自己的特性分支合并到主干。
上面总结的好像很有道理,但是不要忘了,Github 也可以玩分支模式,Gitlab 也可以玩 fork 模式,更令人无语的是:
Github 上合并分支还是叫 Pull Request; Gitlab 上 fork 模式也是叫 Merge Request;
不行,这种答案我没法接受,去 stackoverflow 上搜一些大家是怎么理解的。果然有一个帖子很火:
Pull request vs Merge request
有一个回答摘取了 Gitlab 的官方解释:
Merge or pull requests are created in a git management application and ask an assigned person to merge two branches. Tools such as GitHub and Bitbucket choose the name pull request since the first manual action would be to pull the feature branch. Tools such as GitLab and Gitorious choose the name merge request since that is the final action that is requested of the assignee. In this article we’ll refer to them as merge requests.
翻译过来简单理解就是:这两个没有本质区别,站在不同立场说法不一样而已。
好了,官方已经盖棺定论了,这两个就是一个东西,不要纠结啦~
开发人员提交PR/MR流程
基于Squash + fast-forward机制
开发环节:
未克隆项目:
git clone
将项目从远端克隆到本地。
已克隆项目:
git checkout [master]
处于基分支下。分支名根据实际情况选择,此样例表示基于master
开发。git pull
保证基版本较新,从而减少后续冲突概率。
切换分支开发:
git checkout -b [dev_xxx]
切换到新分支。- Add/Modify/… code 进行开发。
git add .
更改添加到暂存区。git commit -m "xxxxx"
提交到本地仓库。
提交环节:
-
git checkout [master]
先回到基分支。 -
git pull
这次操作依旧是保证基版本较新,之所以操作两次的原因是有可能开发过程中有别人的代码合入到了主分支,在提交之前要保证和最新代码没有冲突。 -
git checkout [dev_xxx]
回到开发分支。 -
git rebase [master]
以rebase的方式合到本地的主分支。 -
此时通过
git status
查看是否有冲突。如果有冲突,查看代码时会有以下提示 <<<<< HEAD 别人改的 ===== 你改的 >>>>> branch message 修改完之后删除三行提示即可
-
git add .
-
git rebase --continue
还有冲突则继续解决冲突。 -
git push
推到远端仓库。 -
在web端创建新的MR,将开发分支申请合入主分支。
commit/MR/PR 规范
目的: git commit 规范的主要目的是为了规范化 commit 格式,使每次的 commit 清晰指明本次提交的目的、备注信息以及影响范围。提MR/PR也相当于一个commit。
commit message格式 :<type>: <subject>
type: 用于说明git commit的类别,只允许使用下面的标识。
-
feat:新功能(feature)。
-
fix:修复bug,可以是QA发现的BUG,也可以是研发自己发现的BUG。
-
docs:文档(documentation)。
-
style:格式(不影响代码运行的变动)。
-
refactor:重构(即不是新增功能,也不是修改bug的代码变动)。
-
perf:优化相关,比如提升性能、体验。
-
test:增加测试。
-
chore:构建过程或辅助工具的变动。
-
revert:回滚到上一个版本。
-
merge:代码合并。
subject: commit目的的简短描述。
-
不超过50个字符。
-
中英文皆可。
-
开头有一个空格。
-
结尾不加句号或其他标点符号。
例:
-
feat: 温度历史记录查询
-
fix: 温度数值精度错误
-
refactor: 视频流读取模块
TODO
- fetch
- diff
- restore
- revert (对于已合并的MR想撤销,野路子可以取消保护分支,然后本地reset+push -f)
辅助工具的变动。