阅读视图

发现新文章,点击刷新页面。

优雅的处理Git多帐号与代理问题

  在工作中,常常会容易遇到一台电脑用多个 Git 账号的场景,比如账号 company 账号是工作用的,而账号 personal 是自己个人用的。 由于 Git 本身并没有多账号的机制,导致我们在默认设置下无法很好的区分哪个仓库使用哪个账号。 同时,在某些众所周知的场景下,我们无法直接访问到 Github 仓库,需要走一层 proxy 来加速我们的代码拉取与推送速度, 本文将使用 SSH config 相对优雅的解决这些问题。

用WinMerge进行文件夹比较时忽略git目录的方法

在选择比较目录目录的对话框上,点击左下方【Folder:filter/文件夹:过滤器】处的【Select…/选择】按钮,然后在【File Filters/文件过滤器】选项卡中选中【Ignore git】,确定,即可。
进入比较后,这一项也有办法解决。点击菜单【Tools/工具】->【Filters/过滤器】后,同样在【File Filters/文件过滤器】选项卡中选中【Ignore git】,确定,然后刷新重新比较,亦可。


  • (1):不像《侍魂》和《天外魔境》那样还需要把武器拾回来,而是每个人的武器都能飞去来。

Git 纪元

19世纪末,弗雷德里克·泰勒在美国一些工厂推广他的科学管理方法,这包括用科学的方法来代替人工经验判断、对工人进行专业训练以及按专业分工。亨利·福特在他的汽车工厂里也开始应用泰勒科学管理,并且在汽车生产上创新性的采用了流水线作业。1908年,第一辆福特T型车从流水线上面世,而经过优化的流水线在几年后甚至可以达到93分钟内生产一部汽车,强大的生产效率让福特超过了其他所有汽车厂商生产能力的总和。福特的流水线生产很快被其他厂商所借鉴,也迅速普及到了其他工业领域。可以说,福特T型车流水线作业的发明开启了大规模工业生产的时代,人类的生产力得到了巨大的提升。从英国移居到美国的阿道司·赫胥黎对此深受启发,在他的带有科幻色彩的反乌托邦小说《美丽新世界》中,描写未来的人类“文明社会”,人们不再用公元纪年,也不信仰上帝,改尊亨利·福特为唯一的神,以福特为纪元,并以第一辆福特T型车流水线生产的1908年为福特元年。

Git 纪元

19世纪末,弗雷德里克·泰勒在美国一些工厂推广他的科学管理方法,这包括用科学的方法来代替人工经验判断、对工人进行专业训练以及按专业分工。亨利·福特在他的汽车工厂里也开始应用泰勒科学管理,并且在汽车生产上创新性的采用了流水线作业。1908年,第一辆福特T型车从流水线上面世,而经过优化的流水线在几年后甚至可以达到93分钟内生产一部汽车,强大的生产效率让福特超过了其他所有汽车厂商生产能力的总和。福特的流水线生产很快被其他厂商所借鉴,也迅速普及到了其他工业领域。可以说,福特T型车流水线作业的发明开启了大规模工业生产的时代,人类的生产力得到了巨大的提升。从英国移居到美国的阿道司·赫胥黎对此深受启发,在他的带有科幻色彩的反乌托邦小说《美丽新世界》中,描写未来的人类“文明社会”,人们不再用公元纪年,也不信仰上帝,改尊亨利·福特为唯一的神,以福特为纪元,并以第一辆福特T型车流水线生产的1908年为福特元年。

Git tips

批量删除 git 分支

1
git branch -a | grep -e "fix/" | xargs git branch -D

批量添加匹配文件到暂存区

1
git status -s -uall | grep .vue | awk '{print $2}' | xargs git add

按最后提交日期排序所有远程分支

1
git branch -rv --sort=-committerdate

更多:How can I get a list of Git branches, ordered by most recent commit?

更好的 git log

1
git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"

列出另外一个分支没有的提交

1
git cherry -v branch-A branch-B

clone 特定分支

1
git clone -b develop git@github.com:user/myproject.git

递归移动文件夹

1
mv bar/{,.}* .

列出特定分支的记录

1
git log -p branch-name

查看特定文件的 git 记录

1
git log -p -- filename

修改 git 提交为任何人

1
git -c user.name="NEW NAME" -c user.email="new_email@gmail.com" commit --amend --date="Tue Nov 20 03:00 2018 +0100" --author="NEW NAME <new_email@gmail.com>"

查看当前所有子目录的 git 状态

1
find . -maxdepth 1 -mindepth 1 -type d -exec sh -c '(echo {} && cd {} && git status -s && echo)' \\;

创建 git 归档时忽略某些目录

1
tar cvfz app.tar.gz --exclude ".git/*" --exclude ".git" app/

列出未发布的 commit

1
git log @{u}..

在所有 commit 中寻找代码

1
git rev-list --all | xargs git grep

找到一个文件是何时被删掉的

1
git log --diff-filter=D -- path/to/file

删除远程分支

1
git push origin :branch

让 git 可以递归调用

1
git config --global alias.git '!git'

revert cherry-pick

1
git rebase -p --onto SHA^ SHA

永久删除文件

1
git filter-branch --tree-filter 'rm -rf my_folder/my_file' HEAD

永久链接

1
permalink = "!f() { echo "https://$(git config --get remote.origin.url | grep --color=never -o -E 'github.com[:/][^\\.]+' | sed s/\\:/\\\\//)/commit/$(git rev-parse @{u})"; }; open $(f)"

仅忽略本地副本的文件更改

1
2
3
git update-index --assume-unchanged <file>

git update-index --no-assume-unchanged <file>

忽略已跟踪的文件

1
git rm --cached <file>

获取当前分支名称

1
git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/'

查看某个版本的目录树

1
2
3
git show HEAD~5:hello.txt

git show awesome-feature:app/models

优雅的处理Git多帐号与代理问题

  在工作中,常常会容易遇到一台电脑用多个 Git 账号的场景,比如账号 company 账号是工作用的,而账号 personal 是自己个人用的。 由于 Git 本身并没有多账号的机制,导致我们在默认设置下无法很好的区分哪个仓库使用哪个账号。 同时,在某些众所周知的场景下,我们无法直接访问到 Github 仓库,需要走一层 proxy 来加速我们的代码拉取与推送速度, 本文将使用 SSH config 相对优雅的解决这些问题。

Hexo blog 的升级与同步方案

前一篇我们介绍了如何使用 Hexo 框架及 Next 主题搭建博客。这次来聊聊如何安全的更新博客与主题的版本。

next theme


早期写博客时笔者就有考虑过使用 git 来做版本控制,那时 github 私人仓库还没有免费开放,国内虽然有 coding 和码云这些平台有开放少量的私人仓库,但由于懒得折腾就选了最方便同步的 OneDrive(因为它只需将文件夹移入就可以实现跨设备共享)。

后来笔者因为工作的原因,需要在多设备中频繁切换,这种简单同步方式就会暴露出一些问题。比如说,在设备 A 想对博客做一些自定义的修改,其中可能会动到依赖,但此时设备 B 的文件正在同步,那这样可能会导致文件不一致的问题。可能会将旧的文件重新同步过来,这可能会导致程序报错,还不易于排查。

冲突文件合并失败会额外添加如 index-anran758's MacBook Pro.js 之类的同名文件,并且发生冲突时是隐式的,你甚至不知道发生了冲突,这种体验使用不太友好。

因此 OneDrive 的同步方式适用于改动不会太大的文件。


如果你对 git 版本控制比较熟悉的话,那可以通过 git 对 blog 进行版本控制。

使用源码托管平台的话就如上文所说主要有这么几种选择:

国内的 gitee(码云)coding 是一个不错的选择,代码的上传于下载速度也比较可观。国外可以使用 github,github 的私人仓库是今年才开放无限制免费创建仓库数量的,缺点由于众所周知的问题,有时可能拉代码速度较慢。

笔者使用的是 github 作为源码托管,下文将要介绍的方法对于 git 仓库是通用,因此根据自身的喜好选择对应的平台。

博客托管

托管 blog 源码的步骤如下:

  1. 找到对应的平台,创建私人仓库(注意是 Private,不要将自己的私人配置也开源咯)。

  2. 仓库创建完毕后,得到仓库的地址。打开命令行,进入 /blog 目录下并输入命令:

    1
    2
    3
    4
    5
    6
    # 初始化 git 项目
    git init

    # 添加一个名为 origin 的 remote
    # your_repo_path 是你创建仓库得到的仓库地址
    git remote add origin your_repo_path
  3. 由于 /theme/next 本身也是一个仓库,git 无法提交嵌套仓库的文件夹,因此需要在 .gitignore 添加配置,忽略该文件夹

    1
    2
    # 其他忽略规则...
    themes/next/
  4. 提交代码

    1
    2
    3
    4
    5
    6
    # 提交代码
    git add .
    git commit -m "new: blog 数据开始进行版本控制"

    # 设置上游(-u)并推送至远程的 master 分支
    git push -u origin master
  5. 这样我们就完成了博客的源码托管。

主题托管

Next theme 官网介绍的安装方式如下:

1
2
3
4
5
# 进入 blog 目录
cd blog

# 言下之意就是将该库克隆到 themes 目录下的 next 文件夹中
git clone https://github.com/theme-next/hexo-theme-next themes/next

Next theme 7.0+ 版本中,主题嵌入了检查版本更新的代码,每当运行本地服务器时,都会进行检查版本号的更新。当有新的版本发布时会在命令行输出警告:

1
2
3
WARN  Your theme NexT is outdated. Current version: v7.4.2, latest version: v7.5.0
WARN Visit https://github.com/theme-next/hexo-theme-next/releases for more information.

这时你想体验 Next 的新特性的话可能会有点麻烦,因为原先我们在旧版本上修改了配置,或添加了一些自定义的布局。这将会造成代码冲突。

因此我们需要独立开两条分支:

  • master 分支是官方发布的正式版本,我们不去修改 master 分支的中的任何文件。
  • 另一条是我们自己创建的新分支,笔者命名为 customize, 言下之意为该分支含有我们自定义的修改,包括私人配置等。

除此之外,由于主题配置文件(theme/next/_config.yml)中含有某些应用的 appid 或者 secret,这些配置不应该被其他人随意看到以防冒名滥用。因此我们应该将该项目额外添加一个 remote 来保存我们的私人配置。 具体操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 此时已经下载到了主题文件夹

# 创建并切换新分支
git checkout -B customize

# 进行主题配置或其他修改操作

# 提交改动(未推送)
git add .
git commit -m "chg: 修改为自定义配置"

# 添加一个名为 userRepo(名字可以自己定义,只要自己能搞清是哪个来源即可) 的新 remote,
git remote add userRepo git@github.com:anran758/hexo-xxx-next.git

# 设置上游(即以后使用 git pull/status 时默认拉取 userRepo 源的 customize 分支),并推送指定 remote
git push -u userRepo customize

如此就完成了代码的追踪,以后使用 next 主题就不是从 hexo-theme-next 中获取了,而是我们自己的私人仓库 hexo-xxx-next 中获取,安装方式是一样的。

版本升级

Next

前文说过我们将源码托管的需求之一就是为了解决代码合并的问题,为了体验新版本的特性,我们需要将新版本的代码合并进我们的分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 从 origin/master 获取最新版本的代码
# 理论上我们不修改 master 分支的代码不会发生冲突
git fetch origin
git pull --no-commit origin master

# 切换至 customize 分支
git checkout customize

# 检查本地是否有文件改动,有的话需要进行 commit 提交或者使用 git stash 藏起来
git status

# 合并代码
git merge master

我们最起码修改过 _config.yml,因此会发生冲突也不奇怪,有冲突咱们就解决冲突。

如果你使用 vscode 进行编码,侧边栏有一个源代码管理,打开它可以看到冲突的文件。

打开冲突的文件,判断冲突项确定要保留(删除)的代码,解决冲突后,提交到缓存区(git add .(file))。缓冲区有本次升级所涉及的代码,可以大致预览一下本次的更新都做了什么事

1
2
3
4
5
6
7
8
9
10
11
12
13
# 将缓冲区的文件提交至 commit
git commit -m "Merge release v(version) into customize branch"

# 提交代码
git push
# Counting objects: 99, done.
# Delta compression using up to 4 threads.
# Compressing objects: 100% (57/57), done.
# Writing objects: 100% (99/99), 12.86 KiB | 346.00 KiB/s, done.
# Total 99 (delta 71), reused 64 (delta 42)
# remote: Resolving deltas: 100% (71/71), completed with 41 local objects.
# To github.com:anran758/hexo-xxx-next.git
# 4a70c18..54805a2 customize -> customize

升级完后运行本地服务器最后会输出一条:

1
INFO  Congratulations! Your are using the latest version of theme NexT.

Hexo

若最新版本的 Hexo 引入了你想要的新功能,你想更新 Hexo 版本的话,首先确定版本号变动的是哪一位。

package.json 的版本号格式是数字由点分隔,如 主版本号.功能版本号.补丁版本号。若更新是主(大)版本号的话,则需要先修改 dependencies 依赖中 hexo 的主版本号,再输入 npm update

以下是 hexo@v3 更新为 hexo@v4 的示例:

1
2
3
4
5
6
7
{
// ...
"dependencies": {
+ "hexo": "^4.0.0",
- "hexo": "^3.9.0",
}
}

命令行输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
$ npx hexo -v
hexo: 3.9.0
hexo-cli: 2.0.0
os: Darwin 17.7.0 darwin x64
node: 12.13.1
v8: 7.7.299.13-node.16
uv: 1.33.1
zlib: 1.2.11
brotli: 1.0.7
ares: 1.15.0
modules: 72
nghttp2: 1.39.2
napi: 5
llhttp: 1.1.4
http_parser: 2.8.0
openssl: 1.1.1d
cldr: 35.1
icu: 64.2
tz: 2019c
unicode: 12.1

$ npm update
+ hexo@4.2.0
added 71 packages from 90 contributors, updated 14 packages and moved 5 packages in 12.513s

$ npx hexo -v
hexo: 4.2.0
hexo-cli: 3.1.0
os: Darwin 17.7.0 darwin x64
node: 12.13.1
v8: 7.7.299.13-node.16
uv: 1.33.1
zlib: 1.2.11
brotli: 1.0.7
ares: 1.15.0
modules: 72
nghttp2: 1.39.2
napi: 5
llhttp: 1.1.4
http_parser: 2.8.0
openssl: 1.1.1d
cldr: 35.1
icu: 64.2
tz: 2019c
unicode: 12.1

若只是后面两位版本号有变更的话,仅需输入 npm update 即可。

总结

单单从升级版本来合并代码的角度来看,实际上本地 commit 也可以做这种事,将 commit 储存在本地(.git)中不提交远端也是没有问题的,OneDrive也可以完成同步。

但从安全和可调试的角度来看,OneDrive的同步方式存在一定风险(懒的代价)。使用 git 版本控制可以清晰看到每一次提交的修改,不会多出奇奇怪怪的东西。必要的时候还可以进行回滚,相对来说更安全。但这种方案需要使用者了解一定的 git 知识。

从操作步骤来看,使用的 git 同步方案会产生多个仓库,这些仓库一般是拥有权限的人才能查看(修改)源码。比如完成了本文中两个仓库源码同步后,在另一台设备初次同步的步骤是:

  1. 通过 git clone 下载 blog 本体。
  2. 通过 git clone 下载私人仓库 next theme/theme 目录下。
  3. 进入两个仓库内安装对应的依赖

以上可以在 blog 项目下的 package.json 设置 scripts,通过一条命令来完成这些事。

由此我们可以看到,相比 OneDrive 的懒人方案,git 方案的操作步骤会更繁琐。更新方式也从自动更新变成手动更新。

两者种方案各有利弊,具体采用什么方案就看朋友们的习惯啦~


本文涉及到的 git 命令都是可以在 git 速查方案 查找相应的解释。

Git 分支管理与版本发布

Git 版本管理系统由 Linux 的作者 Linus Torvalds 于 2005 年创造,至今不到二十年。

起初,Git 用于 Linux Kernel 的协同开发,用于替代不再提供免费许可的 BitKeeper 软件。随后,这一提供轻量级分支的分布式版本管理系统得到了开源开发者的广泛喜爱,在大量开源项目中投入使用。如今,Git 几乎是版本管理系统的同义词。

Git 最大的创新就是轻量级的分支实现,鼓励分布式的开发者群体创建自己的分支。每个分支都是平等的,只是原始作者的分支或者实现最好的分支会被社群公认为上游,其他分支被认为是下游。下游分支的修改通过邮件列表发送补丁或 GitHub 发起 Pull Request 的方式向上游申请合并,最后大部分用户从上游分支取得源代码使用。

在这个模型下,如何协同不同分支的开发,当上游发布了多个版本,尤其是并行维护多个发布版本时,如何管理分支,就是一个亟需解决的问题。Git 自身的设计不解决这个问题,也不对此做建模。它只提供分支创建和合并等基本功能,而把具体的分支管理策略留给开源软件的开发者。

基础软件的分支策略

一路向前的 Curator

对于绝大部分的开源软件来说,既没有维护多个版本的需求,又没有重量级的发版检查,最适合自己的分支策略就是唯一上游分支,一路向前。

Apache Curator 采用了这种策略:主分支 master 是唯一的上游分支,版本号一路向前,没有在以前的功能版本发新的补丁版本的说法。

所有的下游修改,一般也是一个小修改一个分支,做完以后迅速提交到上游评审合并,几乎所有用户获取的版本都是从 master 分支上打 tag 得到的。

这种简单的策略被广泛使用,甚至可以做成自动化的流水线:

  • spotless 用一个专门的发布流水线来手动触发从 master 分支分析 changelog 并发布新版本的工作。
  • griseo 的流水线则是在推送新的 tag 的时候就触发把 tag 关联的新版本发布到 GitHub Release 页面和 PyPI 仓库上。这参考了 githubkit 的方案。
  • setup-zig 依赖 npm 生态的 semantic-release 工具集,实现了彻底的自动化:每次主分支合并代码后,自动分析出 changelog 并根据 changelog 的语义判断是否应该发布新版本,应该发布什么版本。

齐头并进的 Flink

Apache Flink 是一个典型的并行维护多个发布版本的开源软件。

Flink Release Management 提到,Flink 上游社群维护最近的两个特性版本,而其过往发布记录大致如下:

Flink 版本发布历史

值得注意的是,其版本发布时间并不随着语义版本号单调递增,例如 1.16.0 的发布日期(2022-10-28)就早于 1.15.4 的发布日期(2023-03-15)。

根据语义化版本的定义,patch releases 只包含必要的修复,而不应该包含新功能。如果仍然采取一路向前的分支策略,那么在发布了带有新功能的 1.16.0 版本后,再发布 1.15.4 版本,难道还能 revert 所有功能变更吗?这不现实。

所以 Flink 采取的是和并行维护发布版本线对应的分支策略:

  1. master 分支是不稳定的开发分支,对应 X.Y-SNAPSHOT 的快照版本,其中 X.Y 是下一个即将发布的功能版本。例如,现在最新的功能版本是 1.17 版本,那么 master 上的版本号就是 1.18-SNAPSHOT 了。
  2. release-X.Y 分支是 X.Y 系列版本的基础分支,该分支将接受 bug fix 类的提交。

分支是不稳定的 Git 引用,不同时间 check out 同一个分支可能得到不同的结果。Flink 在实际做版本发布的时候,选择的是 tag 的形式来发布不可变的版本:

  1. release-X.Y.Z-RCn 是一个标签版本,是一个静态的版本,对应 X.Y.Z 的第 n 次预发版本。由于 Flink 系统复杂,发布周期内需要进行大量测试,很可能有多次预发,为了避免强行覆盖 tag 导致破坏 tag 引用内容不可变的语义,Flink 用这一 tag 命名模式来给预发版本起名。
  2. release-X.Y.Z 是一个标签版本,是一个静态的版本,对应 X.Y.Z 的发布版本。实现上,它就是最后一个 release-X.Y.Z-RCn 标签,两者应该有相同的 commit hash 和完全一致的历史。

其他并行维护多个发布版本的开源软件也大多采用这种策略,Apache Pulsar 就是其中之一。除了把 release-X.Y 的分支名模式改成 branch-X.Y 和 release-X.Y.Z.RCn 的标签名换成 vX.Y.Z-candidate-n 以外,两者没有任何差别。

即使没有实际维护多个并行版本,使用这种分支策略仍有一个好处。那就是在发布周期较长的情况下,以切出发布分支的形式来完成 feature freeze 的工作。

例如 Apache Kvrocks 也维护了不同特性版本的分支,但是自进入 Apache 孵化器之后并没有发布 .0 以后的补丁版本。尽管如此,从 2.2 分支的历史可以看出,切出发布分支以后,主分支可以继续正常进功能代码,而 release manager 可以按需 cherry-pick 需要进入到本次发布的变更。这样就不会因为当前有个版本正在发布,而被迫推迟所有不适合进入正在发布的版本的 Pull Request 的评审和合并。

特性分支的 OpenJDK

从上面两种分支策略我们可以看到,大部分的开发分支都是一个轻量级的下游分支,其生命周期自开发功能始,终于合并到上游。上游实际要处理的分支策略是与自己的版本发布和管理策略对应的分支管理需求。

不过,对于大型项目来说,有一个变体需要专门介绍,那就是特性分支的分支策略。

特性分支之所以存在,是由于一个特定的功能提案涉及相对复杂的代码修改。为了加速分支内的开发迭代,同时避免把半成品的功能合入上游,提案实现团队拉出一个特性分支进行独立开发,定期合并上游的变更,在功能稳定后提交到上游进行发布。

可以看到,这个模式的特点主要有三个:

  1. 独立快速迭代,分支内部评审合并
  2. 定期合并上游的变更
  3. 稳定后提交到上游

OpenJDK 是特性分支的成熟实践者,他们甚至会为特性分支创建单独的代码仓库。

OpenJDK 的特性分支仓库

上图中,loom / amber / valhalla 都对应到一个或多个 JDK 功能提案,最终都是会以合入 JDK 主分支结束自己的生命周期。

Implementation of Foreign Function and Memory API (Third Preview) 是一个特性分支在稳定后提交到上游的例子。可以看到,上游 Reviewer 对特性分支进行评审,特性分支的开发者在分支上共同开发,并定期合并上游的变更。最终,整个 Thrid Preview 完成后一次性合入上游

+6,924 -8,006

从 feature branch 这种模式来看,实现这一模式的前提是软件代码的模块化。

上面强调的模式特点“定期合并上游的变更”,是特性分支在迭代过程中避免最终提交时和上游冲突,从而需要花大精力调整甚至重做的关键。如果代码的模块化很差,开发者都争先恐后地把自己的提交早点塞进上游以求不要过一会儿就 conflict 又要重新写,那么 feature branch 的模式是不能成功的。

另一方面,稳定后提交到上游其实可以用迭代的方式来看待。类似上面提到的 FFM 提案,其实也不是等到做出一个最终 GA 的版本才合入,而是分成 Incubator 阶段和 Preview 阶段。把通用的接口抽象和必要的重构做在上游,把预览的功能提供给用户试用,这也是 feature branch 能够持续和上游整合并得到用户反馈的关键手段,而不是闭门造车。

从 OpenJDK 的版本策略来看,在切换到三年一个 LTS 版本,其他版本后一个发布前一个即停止维护的策略以后,OpenJDK 上游实质上也只维护了一个 master 分支。对于 LTS 的 8 / 11 / 17 三个版本,OpenJDK 创建了三个单独的维护仓库来合并补丁:

这三个仓库的每一个都只维护一个分支,patch releases 一路向前。

今年新的 LTS 版本 21 发布后,应该会变成只维护 8 / 17 / 21 三个分支仓库。

Nightly 到 Stable 的 Rust

虽然实现形式略有出入,但是 OpenJDK 针对版本的分支策略其实还是 Flink 齐头并进策略的变种,只不过把 release-X.Y 分支变成了一个仓库,而且引入了 LTS 的概念定义了自己的长期维护分支策略。Python 则是完全符合齐头并进策略,只是分支名和标签名略有出入。

同样作为编程语言,Rust 选择的分支策略略有不同:从 Git branches 页面上看,它维护了 master / beta / stable 三个分支。

rust-branches

这一版本和分支策略的详细说明可以从 Rust Forge 页面上查到,或者从下游 rustup 的 Channels 说明做补充。

简单来说,master 对应 nightly 版本,每天定时构建后发布。beta 是 stable 前的缓冲。beta 版本发布后六周会发布对应的 stable 版本,然后从 master 切出新的 beta 版本。

Rust 开发者通常要么使用 nightly 版本,要么使用 stable 版本。beta 版本更像是 Rust 团队在发布 stable 前的一个六周的发布测试周期,也就是上面提到的其他软件的 release candidate 时间。

nightly 版本和 stable 版本的另一个重要不同是只有 nightly 版本可以开启 feature flag 启用没打上 stable 标签的功能,所以即使 stable 跟 nightly 版本在代码上仅差大致两三个月,但是很多不稳定的功能可能几年间都只能在 nightly 版本上使用。

Rust 除了直接对应到分支的 Nightly / Beta / Stable 版本以外,还有另外两个版本策略。

其一是语义化版本,目前最新的 Nightly 版本是 1.71.0 版本,最新的 Stable 版本是 1.69.0 版本。Rust 在版本号上其实是一路向前的。即使偶尔发布 >0 的补丁版本,那也是在没有新的功能版本需要发布的情况下,在当前的 X.Y.Z 版本的基础上发的 X.Y.Z+1 版本,而从未出现过版本号回退的现象。

其二是 Edition 策略。语义化版本的约束下,目前所有 1.x 版本的 Rust 都应该是后向兼容的,这种兼容性对于程序开发语言来说尤为重要。但是,保持大方向上的兼容如果变成教条,导致一些小的比如默认值的修改不能调整,那么反而可能是一件坏事。因此 Rust 通过 Edition 来指定一系列默认值和行为的集合,以在实际上保持后向兼容的情况下,优化软件开发的体验。

这个需求由于 Rust 解决的早,可能不这么做的恶性后果不太明显。Perl 7 的提案是一个很好的参考。在 Perl 5 稳定的二十几年后,为了保证后向兼容,即使你用的是今年新发布的 5.36.1 版本,为了用上很多“现代功能”,你需要手动调整一大堆默认行为:

1
2
3
4
5
6
7
use utf8;
use strict;
use warnings;
use open qw(:std :utf8);
no feature qw(indirect);
use feature qw(signatures);
no warnings qw(experimental::signatures);

不过,新版本的 Perl 可以用 use v5.36 一类的办法来实现类似的缩短样板代码的需求,所以 Perl 7 的提案也就被搁置了。

业务代码的分支策略

业务代码和基础软件最大的不同,就在于业务代码提交后是立即要运行在生产环境的,而基础软件发布后一般有比较长的采用周期,甚至业务代码可以锁定基础软件在某个稳定的早期版本上。

这一重要不同导致业务代码的分支策略是部署驱动的,其最新的技术探索应该是 GitOps 一类的方案。

不同于基础软件不同分支对应不同版本线,业务代码的不同分支对应的是不同的环境。一个典型的业务开发发布平台会有测试、预发和生产环境,业务代码仓库也分成对应的分支:

  1. master 分支或者 product 分支就是生产环境的代码,业务流量都会打到这个代码编译运行的应用上。
  2. staging 分支对应预发环境的代码。预发环境和生产环境形态一致,但是规模较小,有独立的域名,业务流量不会过来。
  3. 研发自己的开发分支可以部署到测试环境,测试环境大致仿照生产环境建立,但是数据库等资源是研发自己可以调整的,基本没有权限限制,所以可能和生产环境有出入。

某一服务/模块只有一个人负责的情况下,预发环境也可能没有一个专门的分支,而是发布一个开发分支的代码;测试环境有多人一起修改的情况下,也有可能临时拉出一个协同测试分支,保证共同开发的变更都在环境里。

可以看到,在业务自己就是代码的最终消费者的情况下,不同的生产版本或者不同的标签基本是不需要的。

不过,如果是微服务的情况,发布微服务接口定义的 contract 包的时候,还是有版本号的问题的。但是由于访问的流量都可以在企业内监控到,所以一般也不需要维护多个版本,而是在向后兼容的情况下一路狂奔,或者在需要做破坏性改动的情况下依据监控把所有下游服务的负责人都拉到一起讨论升级方案。

GitHub 的分支协同

最后,介绍一下不同分支策略在 Git / GitHub 上实际操作的一些协同技巧。

分支保护

第一个是 GitHub 支持分支保护,避免误删关键分支。

例如,我在 Zeronos 项目里就保护了 main 分支和归档以前尝试的 archive- 分支。

zeronos-protect-branches

ASF 项目的 committer 没有 GitHub 上 admin 的权限,不过 ASF INFRA 提供了一个 .asf.yaml 的配置文件支持指定保护分支。

Pulsar 保护了所有版本发布分支

Pulsar Site 保护了生产环境分支

合并策略

第二个是合并策略。GitHub 支持三种合并 Pull Request 的按钮:

  1. Create a merge commit
  2. Squash and merge
  3. Rebase and merge

我的个人倾向是参考 Flink 社群的经验,禁用 merge commit 的方式,大部分情况下采用 Squash and merge 的方式,少数情况下使用 Rebase and merge 合并。

GitHub 最佳配置推荐

Squash and merge 大于 Create a merge commit 是为了保持主分支相对简洁。很多 Pull Request 尤其是被 GitHub 合并展示 changeset 以后,很容易出现各种 “fix” “save” “tempsave” 的 commit 历史,这些内容极度干扰检索 Git 历史发现问题的效率。Squash and merge 能够把一个逻辑单元以单一的 commit 合到上游,避免了 commit 膨胀。

Rebase and merge 的价值就在确实一个事情需要几个逻辑步骤,但是又是同一个高内聚的主题的场合下,避免 Reviewer 来回 review 多个 Pull Request 和作者反复解决冲突,而是就在一个 Pull Request 里解决,但是把 commit 拆分清楚。最后合并的时候还是线性的历史,但是每一步的 commit 仍然是单独的,而不是 squash 以后的。

其实,在邮件列表时期,这些问题都会被自然解决。因为提交到邮件列表上的就是一个个补丁文件。那么为了能正常的 review 补丁,作者是习惯于自己整理好 commit 的。如果 commit 都是好好写和拆分的,其实 Create a merge commit 也没太大问题。GitHub 在降低了参与门槛的同时,也带来了一些额外的显性教育成本。

参考资料

本文其实起源于一位朋友凭借旧文《Git 分支整合与工作流》的印象,问我关于保护分支治理的问题。保护分支的动作好做,主要是分支管理的策略,推出哪些分支需要保护,以及推出分支策略背后的版本策略或业务部署策略。

关于 Git 的使用和协作,推荐两本必看好书:

  1. Pro Git
  2. Git 团队协作
❌