引子

说起 Git,正如标题所述,让我欢喜让我忧。回想起本科学材料写毕业论文的时候,根本没有版本控制的概念,改了之后不满意就 ctrl+z 或者再回来 ctrl+shift+z。

甚至大三的时候我连 github 都不知道怎么用,还问过别人怎么用,搜过 B 站的教程。现在看来,哈哈,成长的必经之路。

跨考后研一上学期我才学习了 Git,最开始接触的时候学的是廖雪峰的 Git,我还记了一个笔记叫《LT 学 Git》。

哈哈,结果发现自己最常用的场景居然是在我的 linux 和 macos 之间同步代码,无非就是下面几条指令。

git pull
git st
git add
git ci -m ""
git push origin

有时候老会忘记在执行操作前先进行 git pull 同步上游的代码变更,导致写着写着最后出现了冲突,然后终端出现诸多的信息自己也看不太懂。

给 chatgpt 进行处理也是云里雾里,并没有很好的理解其中的原理。

一转眼又到了另一个时间节点,我在尝试一生一芯时学到《计算机缺失的一课》这系列课程,从中学到不少好东西,例如 vim,tmux 还有 git。

但是从那里我只是比之前更懂 git 操作的底层原理罢了,例如 git 中的对象,树,引用等。

后来我去了熊厂实习,代码没有写几行,大多数的时间都是去偷文档和看代码,唯一写过的几行代码无非就是修改几个脚本罢了。

但是这个过程也需要进行 git,我已经记不清当时 git 出现了什么问题了,只记得出过两次代码不一致的问题,但都影响的只是自己。

好像是我自己在开发的过程中并没有及时去看主分支的代码更新,导致和另一位实习生对代码的过程中发现有的地方他有我没有,哈哈。还让他教了我几个操作。

Anyway,直到最近,我偶然间发现了朱双印的 git 博文,六七年前的老文章了,我抱着这次我一定要搞懂 git 的心态去看了看,嗯,果真收获颇丰。

虽然他只更新到了git pull命令,没有后面的 rebase,stash 等操作,但是仍然价值连城。直到最后我还是觉得想要真正掌握 git 操作,必须实践,就像找工作一样,实习的重要性不言而喻。(又开始后悔熊厂的实习了,But 只能向前看了)

简单梳理

我不想去阐述整个 git 的底层原理然后又去罗列出一堆操作(好吧,也许我还是会稍微列举),就拿一些例子来看吧。

首先,在上一篇文章中我们对 g 这个 golang 版本管理工具提出了 PR,那么整个过程中 git 操作是什么呢?

从源仓库 fork 出代码后,我将其 clone 到本地,接着对其进行操作。

创建新分支: 创建新分支是开发的重要一步,相当于在进行解耦操作,产生不同的逻辑线。

# 查看所有分支
git branch -a
* feature/add-riscv64-support
  master
  remotes/origin/HEAD -> origin/master
  remotes/origin/feature/add-riscv64-support
  remotes/origin/master
  • 可以看到一共有 5 个分支,前两个分支是本地分支;后两个分支其实是“代表”分支——Remoting-tracking,是本地仓库对远程仓库状态的一个快照
  • 现在 HEAD 指针意味着 origin 仓库默认的分支是 master
git branch -v  
* feature/add-riscv64-support e37e4db fix:Complete riscv64 support in pacakge.sh script
  master                      a82e89c chore: upgrade version number
  • 这回就只有前两个本地分支了,并且还有各自分支上的最新提交信息

编写代码

在这个过程中我之前一直有一个疑惑,什么时间点我该进行提交?是完成一个模块的开发?还是完成这个模块中某个函数的开发?

由于没有太多的实际经验,我询问了 ai,得到的回复大概是这样:

  • 完成一个函数
  • 修复一个 bug
  • 重构一部分代码
  • 一天工作结束时,提交当期进度

这些都算是一种小的频繁的提交,所以提交信息可以写的不那么规范。但是当我们想要提出一个 Pull Request 的时候,我们就需要进行rebase 操作了。

git rebase -i HEAD~2
  • rebase 操作可以将多个相同目的的零碎提交合并成一个,例如要完成一个登录功能,我们开发了相关的多个函数,每个都进行了提交,开发完成测试成功后,进行汇总。
  • 上面的rebase 操作指定了整理最近的 2 个提交;我们可以对其进行提交信息的重写和提交顺序的排列

通常,rebase 命令后呈现的页面类似这样:

pick a1b2c3d commit message one
pick e4f5g6h commit message two

# Rebase ...
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# ...
  • 有多种操作,每个操作的作用在注释中都有提示,这里我们想要将第二条提交信息合并到第一条中
  • 将第二条的 pick 改为 squash 即可

接下来会自动打开第二个编辑窗口,为合并后的提交编写信息,这会包含之前的提交信息,我们将其全部删除然后写一个全新的最终的提交信息。

提交信息

关于提交信息,昨天我才第一次看到这个规范

<类型>[可选 范围]: <描述>
  • fix 代表 bug 相关提交;这会引起 PATCH 补丁版本更新
  • feat 代表新增功能提交;这会引起 MINOR 版本更新
  • BREAKING CHANGE 代表包含了不兼容变更
  • docs,test 等等
  • 包含!提醒注意破坏性变更

更多具体的信息见上面的链接。

所以对于这次提交,我们 rebase 之后总结一个提交信息为 feat: Add support for linux/riscv64,in build and installation.

这里跑个题,略微比较一下 git mergegit rebase,二者都是用来合并分支的,但有所不同。

  • 主要的区别是 merge 操作会产生一个新的提交,解决完冲突(如果有冲突)后需要对其进行 add,commit;而 rebase 只是在修改,所以只需要 add
  • rebase 会让提交的历史记录变为直线,比较适用于私人分支;
  • 所以通常个人分支上对于很多提交信息我们用 rebase 来清理,到了 main 分支上我们使用 merge 来合并其他分支
  • 注意,当要进行合并操作时,先 checkout 到你的目的分支上,比如要把 A 往 main 上合并,就需要先 checkout 到 main 上

推送内容

当前我们已经将代码修改完成并暂存提交,同时添加了提交信息。下一步我们就要将这次修改推送到远程仓库去了。

git push -u origin feature/add-riscv64-support

平时最常用的是 git push origin,但是很明显这个指令不完整,完整的 push 命令是这样的

git push <远程仓库名> <本地分支名>:<远程上游分支名>

这里远程仓库名一般都是 origin

再回看刚开始的 push 命令,我们使用了 -u 参数,意思是将本地分支推送到远程仓库的同时为其创建上游分支,名称相同;这通常在第一次推送该分支时进行。

Btw,当本地分支与远程命名相同时,只要在此分支下可以直接执行git push,git pull不用指明后面的参数了。

通过git branch -vv更直观展现本地分支与上游分支的关系。

git branch -vv
* feature/add-riscv64-support e37e4db [origin/feature/add-riscv64-support] fix:Complete riscv64 support in pacakge.sh script
  master                      a82e89c [origin/master] chore: upgrade version number

既然说到 push,再聊聊对应的 pull 操作吧;pull 其实是 fetch 和 merge 操作的结合:

  • fetch将远程分支同步到本地的“代表”分支中
  • “代表”分支还需要与本地分支进行 merge
  • pull 默认将当前分支对应的远程分支进行拉取

所以之前我遇到的问题:自己开发过程中没有及时与 main 分支进行同步导致混乱,我当时应该先 git fetch origin 获取远程仓库所有最新的更改,然后切换到 main 分支进行git merge origin/main

在日常的开发中,推送都是从当期分支往其对应的上游分支推送,接着发起 Pull Request.

Pull Request

完成了上面的推送,来到 github 的 fork 页面,我们执行最后一步 PR.

在 PR 中写清楚这次推送是做什么的即可,向代码的审查人员描述清楚。如果有后续的修改,还是一样的套路,最后这次只需要git push即可。

如果审查通过了,我们的分支就会被 merge 到 main 分支中,最后我们切换到 main 分支并更新代码。

git checkout main
git pull origin main

没提到的

还有一些没有提到的操作,例如

git stash #保留暂时的更改
git stash pop
# 将工作区和暂存区全部回到上一次提交的状态
git reset --hard HEAD
git reset -mixed commit
git reset -hard commit
# 回滚提交但是保存提交的历史记录,回滚后会产生一个新的提交
git revert

最后还有分离 HEAD 操作,可以让 HEAD 指向某个提交,借此可以进行实验性的更改

  • 丢弃这个更改:回到某个分支
  • 根据这个更改创建新的分支

Anyway,还有很多没有提到的操作,但是我觉得 git 的学习绝对要通过真实的企业级的开发案例来锻炼,上面梳理中其实只是一个向开源项目提出 PR 的过程 :)

So,干中学!

这里是LTX,感谢您阅读这篇博客,人生海海,和自己对话,像只蝴蝶纵横四海。