导读
在写完 # Shell 难学?那是因为你没找到场景(一) 后,反响不错。而且后续笔者又进一步完善了 git-workflow 的功能,让其脱离 nodejs
的依赖,正式成为所有语言通用的脚本工具。
这其中最关键的两个功能就是版本自动升级和自动生成 CHANGELOG。完成这两个功能的过程中,笔者又提炼出了一些场景,来作为学习 shell 的载体。另外,还会顺带介绍一些不太常用但是很好玩的 git 命令。
正文
版本自动升级
git-workflow 的描述中可以看到,版本是用打 tag 的方式实现的。所以我们需要至少实现获取、更新、校验版本号的功能。具体如下:
获取远程最新版 tag
_tagVersion=$(git ls-remote --tags origin | awk "{print \$2}" | sort -t "." -k1,1n -k2,2n -k3,3n | awk "END{print}" | sed "s/.*\/\([0-9\.]*\).*/\1/g")
这个命令很长,我们一句句的解析。第一句 git ls-remote --tags origin
是列出远程的 tag 列表,效果如下:
3351d8e78b9d1cbacc9ec2cff468bb46c6c58c2e refs/tags/1.1.0
cfa7224d274c4f0ace474267b2f16cb7bacbd9e5 refs/tags/1.1.1
76e49432ad7cbcac7e6bf920c5458ffca46d163b refs/tags/1.1.1^{}
526a151cd2e558df416d9a65141812aad987ac36 refs/tags/1.2.0
6aeff0887811c998b77ff0308864ee312102c8ba refs/tags/1.2.1
b3599b5c2f7d23ba70944db9b128b8a9bdc0daab refs/tags/1.2.2
接下来 awk "{print \$2}"
的作用是获取第二列,也就是带版本号的这一列。
然后用 sort -t "." -k1,1n -k2,2n -k3,3n
进行排序,如果不这样排序,1.12.0
会排在 1.2.0
的前面,所以要用这个命令排一下,具体语法就不展开了,有兴趣的同学可以自己查阅一下。
再然后用 awk "END{print}"
获取最后一行数据,即 refs/tags/1.2.2
。
最后使用 sed "s/.*\/\([0-9\.]*\).*/\1/g"
利用正则匹配,获取最后的版本号,即 1.2.2
。
到这里,我们简单总结一下。
awk
命令主要用来处理多列文本,获取最后一行数据;sort
命令用来排序;sed
命令用来进行正则提取,但是其正则能力有限,可以配合grep
的正则能力做更多事情。 我们只要知道大概的命令能够实现什么效果就可以了,具体怎么用,可以现用现查。
version 自动升级
获取到 version 后,接下来就是按照 semver 的规范来更新版本了。代码如下:
# SIDE EFFECTS
# Usage:
# _updateVersion 1.2.3 patch
# echo $_tagVersion # 1.2.4
_updateVersion() {
ver=$1
IFS=. read -r major minor patch <<EOF
$ver
EOF
case "$2" in
patch) tag="$major.$minor.$((patch + 1))" ;;
major) tag="$((major + 1)).0.0" ;;
*) tag="$major.$((minor + 1)).0" ;;
esac
_tagVersion=$tag
}
就像注释中展现的,该函数接收 2 个入参,第一个参数是基础版本号,第二个参数是升级模式(major、minor、patch)。
这个函数是参考了这个问答中的回答。其中 IFS
代表的是分隔符,这行代码大概的功能就是以 .
为分隔符,把分割的结果分别赋予 major、minor、patch 三个变量。case 的逻辑就不多展开了。
另外,值得一提的是,shell 函数的返回值只能是数字。所以要想改变 version 就不得不引入副作用,即在该函数内部修改全局变量,这实在是不得已的方法。所以笔者也特意在变量和方法前面加上了 _
作为特别标识。
版本号校验功能会在后文提到。
自动生成 CHANGELOG
解决了 version 的问题,接下来是更复杂的 CHANGELOG。这里高度借鉴了 conventional-changelog,从符合 约定式提交 的 commit message 中提取信息,生成 CHANGELOG。细节先不展开了,可以简单的理解为要实现下面的转换:
commit b3599b5c2f7d23ba70944db9b128b8a9bdc0daab (HEAD -> develop, tag: 1.2.2, origin/develop)
Author: zhaolandelong <landelong.zhao@tusimple.ai>
Date: Tue Feb 8 12:42:33 2022 +0800
feat: add fileheader to ensure version
commit 6aeff0887811c998b77ff0308864ee312102c8ba
Author: zhaolandelong <landelong.zhao@tusimple.ai>
Date: Tue Feb 8 10:49:06 2022 +0800
fix: fix deploy tag
commit 1e868519053913704c5fe96f0c0582143ba9cda7
Author: zhaolandelong <landelong.zhao@tusimple.ai>
Date: Tue Feb 8 10:45:32 2022 +0800
feat: update README
转换为
## [1.2.2](https://github.com/zhaolandelong/git-workflow/compare/1.2.0...1.2.2) (2022-02-08)
### Bug Fixes
* fix deploy tag ([6aeff08](https://github.com/zhaolandelong/git-workflow/commits/6aeff0887811c998b77ff0308864ee312102c8ba))
### Features
* add fileheader to ensure version ([b3599b5](https://github.com/zhaolandelong/git-workflow/commits/b3599b5c2f7d23ba70944db9b128b8a9bdc0daab))
* update README ([1e86851](https://github.com/zhaolandelong/git-workflow/commits/1e868519053913704c5fe96f0c0582143ba9cda7))
这种可读性良好的 md 文本。
获取两个版本之间的 log 并格式化
先来个简单的,获取仓库的 url 前缀。
# 从 .git/config 文件查找 "url = xxx" 的内容,然后获取第三列。
gitOriginUrl=$(grep -E "^\s+url = .+\.git$" .git/config | awk "{print \$3}")
# 结果:https://github.com/zhaolandelong/git-workflow.git
# 截断最后 4 个字符
gitUrl=${gitOriginUrl:0:-4}
# 结果:https://github.com/zhaolandelong/git-workflow
然后就是如何获取到 message,hash 等内容了,这里介绍一下 git 的一些命令,不常用,但是很有趣。
logs=$(git log --pretty=format:"%s ([%h]($gitUrl/commits/%H))" $preCommit...$curCommit)
git log $preCommit...$curCommit
这个命令是获取两个 commit 之间的 log。注意,那三个点 ...
不是任何省略,就是固定语法,就是这样写的。这两个变量很灵活,可以是 commit,branch,tag。这样就可以应对不同的场景。比如正式发版时,会采用 tag;提测时会采用 HEAD 加 tag;甚至每次提 PR 时都可以用 HEAD 加 branch。这里就不展开了,有兴趣的同学可以自己试一试。
git log --pretty=format:"xxxx"
可以将 log 处理成你想要的格式,%x
表示占位符,比如 %s
表示内容,%h
表示短 hash,%H
表示长 hash 等,具体可以查看 git log 文档。
经过上述命令生成的 $logs
长这样:
feat: add fileheader to ensure version ([b3599b5](https://github.com/zhaolandelong/git-workflow/commits/b3599b5c2f7d23ba70944db9b128b8a9bdc0daab))
fix: fix deploy tag ([6aeff08](https://github.com/zhaolandelong/git-workflow/commits/6aeff0887811c998b77ff0308864ee312102c8ba))
feat: update README ([1e86851](https://github.com/zhaolandelong/git-workflow/commits/1e868519053913704c5fe96f0c0582143ba9cda7))
已经跟想要的结果很像了,接下来就是按照 Fetures 和 Bug Fixes 分类。如果是其他高级语言还是很轻松的,用 shell 稍微麻烦一点,主要考验正则水平。
if [ $(echo "$logs" | grep -Ec "^fix(\(.+\))?: ") -gt 0 ]; then
echo -e "\n### Bug fixes\n" >>CHANGELOG.md
echo "$logs" | grep -E "^fix: " | sed "s/fix:/*/" >>CHANGELOG.md
echo "$logs" | grep -E "^fix\(.+\): " | sort | sed "s/fix(\(.*\)):/* **\1:**/" >>CHANGELOG.md
fi
首先,利用 grep -Ec
来判断是否有 fix 开头的内容(注意还处理了 scope 的情况),并且计算出其数量。
然后,如果数量大于 0,则在 CHANGELOG.md
文件中写入 Bug Fixes 小标题。
最后,利用 grep + sed
分别处理不带 scope 和带 scope 的记录。注意,带 scope 的记录做了一下排序。处理后就是最终想要的效果。feat、perf、revert 同理,只需要替换 fix 即可。
获取某个 commit 的日期并格式化
这个需求初看起来好像不难。实际上还是有一些坑的,主要是 git 方面。
git show 1.2.2 -s --pretty=format:"%ad" --date=format:"%Y-%m-%d" | awk "END{print}"
# 2022-02-08
首先,git show xxx
是列出某个 commit 的具体信息,参数同样可以是 commit、tag、branch。这里有个坑,就是采用不同参数,打印的结果是不一样的,所以需要用 awk
获取一下最后一行内容,保证输出效果。至于会有什么不一样,这里就卖个关子,大家动手试一下印象会比较深刻。
然后,-s
可以过滤到 diff 信息,这是我们不需要的。光想方设法去掉这些大量的 diff 信息就花了笔者很多时间,主要是有一些惯性思维了,想用 shell 去解决,结果 git 自带这样的命令。所以说选择比努力更重要……
最后,--pretty=format
上文已经提到过了,%cd
代表提交日期。值得注意的是 --date=format:"xxx"
参数,是对日期进行格式化,比较好看懂,就不展开了。
检测分支和 tag 是否符合规范
git-workflow 已经说过,要切好功能分支并且打好版本 tag,脚本才能正常运行。锦上添花,我们再来实现一下校验功能。
检测远程分支是否存在并且红色警告
# branch check
if [ $(git branch -r | grep -v "HEAD" | grep -Ewc " origin/($deployBR|$releaseBR|$developBR)") -ne 3 ]; then
echo -e "\n\033[0;31mPlease create [$deployBR, $releaseBR, $developBR] branches and push them to origin first.\033[0m\n"
exit
fi
首先,列出远程分支,过滤掉 HEAD 记录,用正则查找完全符合分支名的数据个数是否正确。注意 grep -w
是完全匹配整个单词的意思,可以省了我们在正则中加入 ^$
。
然后,如果发现数据不对,则进行红色打印,并退出。echo -e "\033[0;31m-----\033[0m"
,这个命令就是彩打,注意,-e
不能省,只需修改 31
这个数就可以改变颜色,具体参考:How to change the output color of echo in Linux。
检测是否存在符合 semver 的远程 tag
# tag check
if [ $(git ls-remote --tags origin | awk '{print $2}' | grep -Ec "^refs/tags/[0-9]+\.[0-9]+\.[0-9]+(\^\{\})?$") -eq 0 ]; then
echo -e "\n\033[0;31mPlease create a SEMVER tag and push it to origin first.\neg: 1.0.0, 0.0.0\033[0m\n"
exit
fi
有了上文的铺垫,这个命令就很好理解了。值得一提的是远程 tag 有可能会带有 ^{}
的结尾(具体什么意思笔者还没有完全搞懂,可以看下这个问答),所以正则里面会有一部分兼容表达式。这里就可以看出 grep
对于正则的支持是很充分的。
结语
这个纯 shell 全语言可用的版本出来之后,项目终于算告一段落了。整个过程前后大概持续了两周,实际上一周基本就可以了,主要是还隔了个春节假期。整个项目下来,笔者切实感觉自己 shell
和 git
的功力有了一定的提高。起码再出现类似的需求,心里更有谱了,也更具有判断力了,对此还是很欣喜的。
下一步就是在部门内部推这个脚本的落地,如果能够顺利推广到全部门,那么这个项目就有了更多价值。毕竟作为工程师,落地并解决实际问题才是我们的本职工作。最近发现了一篇汇总张一鸣博客的文章,终于又有新内容放到文章结尾了,与大家共勉。
有感:人生的差距就是在自我感觉良好中拉开的。 —— 张一鸣微博