一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情。
引子
在GitHub上发布新版本时,我们通常会填写版本名称以及内容。如下图:
有些时候更新的内容比较散乱,没有办法概括。下面这种Release notes就会出现:
作为发布给用户的日志也许问题不大,但是将来同事或者自己需要参考的时候就一头包。必须钻进长长的git history或者PR列表里去找。既然为了发布新版本做了那么多工作,为何不把描述写详细一点呢?
土方法
先来一个笨办法,把新功能或者修复的bug内容复制粘贴过来,如下:
- fixed bug A
- turned off xxx
- fixed bug B
很麻烦不说,看了也没法联系到具体的代码。那么我们把PR链接或者commit hash也抄过来好了。
- fixed bug A (github.com/project/pull/14)
- turned off xxx(github.com/project/pull/13)
- fixed bug B (github.com/project/pull/11)
将来需要回溯检查的时候是方便了,但全部手动复制过来很花时间,而且也是重复劳动。能不能让这个过程更自动化呢?
自动化
来写一个script:
#!/usr/bin/python
import argparse, datetime, getpass, re, sys
from subprocess import Popen, PIPE
from shlex import split
def git_fetch():
process = Popen(split("git fetch"), stdout=PIPE, stderr=PIPE)
stdout1 = process.stdout
return
def has_module_changes(tag1, tag2, module):
process = Popen(split("git --no-pager diff --name-only {startTag} {endTag}".format(startTag=tag1, endTag=tag2)), stdout=PIPE, stderr=PIPE)
stdout1 = process.stdout
pattern = re.compile("^{0}\/.*".format(module))
for line in stdout1:
if pattern.match(line):
return True
return False
def get_merge_tuples(baseTag, targetTag):
pattern = re.compile(".*<parents>(\w*)\s(\w*)</parents>.*<message>(.*\s#(\d*).*\sfrom\s(.*))</message>.*")
process = Popen(split("git --no-pager log {startTag}..{endTag} --merges --oneline --pretty=format:\"<parents>%P</parents> <message>%s</message>\"".format(startTag=baseTag, endTag=targetTag)), stdout=PIPE, stderr=PIPE)
stdout1 = process.stdout
lines = []
for line in stdout1:
matchedPattern = pattern.findall(line)
if not matchedPattern:
continue
mergeDict = {}
mergeDict["parents"] = "{0} {1}".format(matchedPattern[0][0], matchedPattern[0][1])
mergeDict["parent1"] = matchedPattern[0][0]
mergeDict["parent2"] = matchedPattern[0][1]
mergeDict["message"] = matchedPattern[0][2]
mergeDict["prId"] = matchedPattern[0][3]
mergeDict["prAnchor"] = "[PR#{0}]: https://github.com/projectA/pull/{0}".format(matchedPattern[0][3])
mergeDict["prLink"] = "[#{0}][PR#{0}]".format(matchedPattern[0][3])
mergeDict["branch"] = matchedPattern[0][4]
process2 = Popen(split("git merge-base --octopus {0}".format(mergeDict["parents"])), stdout=PIPE)
stdout2 = process2.stdout
for line2 in stdout2:
mergeDict["mergeBase"] = line2
lines.append(mergeDict)
return lines
def createReleaseNotes(params):
anchors = []
commits = []
anchors.append("[Release_v{version}]: https://github.com/projectA/tree/{tag}\n".format(version=params["releaseTargetVersion"],tag=params["releaseTargetTag"]))
mergeTuples = get_merge_tuples(params["releaseBaseTag"], params["releaseTargetTag"])
for mergeTuple in mergeTuples:
if not has_module_changes(mergeTuple["mergeBase"], mergeTuple["parent2"], params["releaseModule"]) and mergeTuple["prId"] not in params["includePrIds"]:
continue
anchors.append("{0}\n".format(mergeTuple["prAnchor"]))
words = mergeTuple["message"].split()
words[3] = mergeTuple["prLink"]
commits.append("* {0}\n".format(" ".join(words)))
sys.stdout.write("\n\n-----\n")
sys.stdout.write("| [v{version}](#anchor_v{version}) | `{author}` | {dateOfRelease} |\n\n".format(version=params["releaseTargetVersion"],author=params["releaseAuthor"],dateOfRelease=params["releaseDate"]))
sys.stdout.write("## <a name=\"anchor_v{version}\">[v{version}][Release_v{version}]</a>\n\n".format(version=params["releaseTargetVersion"]))
for anchor in anchors:
sys.stdout.write(anchor)
sys.stdout.write("\n### Description\n\n")
sys.stdout.write(params["releaseAbstract"])
sys.stdout.write("\n### Commits/PR's\n\n")
for commit in commits:
sys.stdout.write(commit)
sys.stdout.write("\n### Published By\n\n")
sys.stdout.write("* Author: `{author}`\n".format(author=params["releaseAuthor"]))
sys.stdout.write("* Date: {date}".format(date=params["releaseDate"]))
sys.stdout.write("\n-----\n\n")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='This is a script the generates Release Notes.')
parser.add_argument('-v2','--targetVersion', help='Target version of the Release Notes (without the \'v\' prefix; e.g., 3.0.0). The program will infer the target tag if this parameter is provided.',required=False)
parser.add_argument('-v1','--baseVersion', help='Base version of the Release Notes (without the \'v\' prefix; e.g., 3.0.0). The program will infer the base tag if this parameter is provided.',required=False)
parser.add_argument('-w','--author', help='Author of the Release Notes.',required=False)
parser.add_argument('-d','--date',help='Date of release in MM/DD/YYYY format', required=False)
parser.add_argument('-a','--abstract',help='Abstract/Summary for the Release Notes for this release. Enclose text in double quotes (e.g., \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\").', required=False)
parser.add_argument('-t2','--targetTag',help='Override the release target tag with argument provided.', required=False)
parser.add_argument('-t1','--baseTag',help='Override the release base tag with the argument provided.', required=False)
parser.add_argument('-m','--module',help='Module upon which the release notes will be created.', required=False)
parser.add_argument('-i','--include',help='Include the following PRs (comma-separated PR numbers)', required=False)
parser.add_argument('-s','--spec',help='Format: <module name>:<target version>:<base version> (e.g., paypal-goals:3.1.0:3.0.0)', required=False)
args = parser.parse_args()
params = {}
if args.author is not None:
params["releaseAuthor"] = args.author
else:
params["releaseAuthor"] = getpass.getuser()
if args.date is not None:
params["releaseDate"] = args.date
else:
params["releaseDate"] = datetime.datetime.today().strftime('%m/%d/%Y')
if args.spec is not None:
specSplits = filter(None, [x.strip() for x in args.spec.split(":")])
if not specSplits or len(specSplits) < 3:
sys.stderr.write("The spec argument provided is not valid. Format should be: <module name>:<target version>:<base version>\n")
sys.exit(1)
params["releaseModule"] = specSplits[0]
params["releaseTargetVersion"] = specSplits[1]
params["releaseBaseVersion"] = specSplits[2]
params["releaseTargetTag"] = "{module}-v{version}".format(module=params["releaseModule"],version=params["releaseTargetVersion"])
params["releaseBaseTag"] = "{module}-v{version}".format(module=params["releaseModule"],version=params["releaseBaseVersion"])
if args.module is not None:
params["releaseModule"] = args.module
elif "releaseModule" not in params:
sys.stderr.write("Please provide a module name parameter (use -m or --module flag).\n")
sys.exit(1)
if args.targetVersion is not None:
if args.targetVersion.startswith('v'):
params["releaseTargetVersion"] = args.targetVersion[1:]
else:
params["releaseTargetVersion"] = args.targetVersion
params["releaseTargetTag"] = "{module}-v{version}".format(module=params["releaseModule"],version=params["releaseTargetVersion"])
elif "releaseTargetVersion" not in params:
sys.stderr.write("Please provide a target version parameter (use -v2 or --targetVersion flag).\n")
sys.exit(1)
if args.targetTag is not None:
params["releaseTargetTag"] = args.targetTag
elif "releaseTargetTag" not in params:
sys.stderr.write("Please provide a release tag for the Release Notes (use -t2 or --targetTag flag).\n")
sys.exit(1)
if args.baseVersion is not None:
if args.baseVersion.startswith('v'):
params["releaseBaseVersion"] = args.baseVersion[1:]
else:
params["releaseBaseVersion"] = args.baseVersion
params["releaseBaseTag"] = "{module}-v{version}".format(module=params["releaseModule"],version=params["releaseBaseVersion"])
if args.baseTag is not None:
params["releaseBaseTag"] = args.baseTag
if "releaseBaseTag" not in params and "releaseBaseVersion" not in params:
sys.stderr.write("Please provide a either a base tag (use -t1 or --baseTag flag) or a base version (use -v1 or --baseVersion) parameter.\n")
sys.exit(1)
if args.abstract is not None:
params["releaseAbstract"] = "{0}\n".format(args.abstract)
else:
params["releaseAbstract"] = "For `{moduleName}` Module Release `v{version}`\n".format(moduleName=params["releaseModule"], version=params["releaseTargetVersion"])
if args.include is not None:
params["includePrIds"] = filter(None, [x.strip() for x in args.include.split(",")])
else:
params["includePrIds"] = []
git_fetch()
createReleaseNotes(params)
主要原理就是提取git tag,作比较然后提取中间的内容。
把它放进项目的/scripts文件夹,运行:
./scripts/releaseNotes.py -m module_name -v1 1.2.0 -v2 1.3.0
将会在console中打印出如下完整的版本日志,简单地复制粘贴去GitHub就好啦。
-----
| [v1.3.0](#anchor_v1.3.0) | `user_p` | 04/20/2022 |
## <a name="anchor_v1.3.0">[v1.3.0][Release_v1.3.0]</a>
[Release_v1.3.0]: https://github.com/projectA/tree/fun-v1.3.0
[PR#14]: https://github.com/projectA/pull/14
[PR#13]: https://github.com/projectA/pull/13
[PR#11]: https://github.com/projectA/pull/11
### Description
For `fun` Module Release `v1.3.0`
### Commits/PR's
* Merge pull request [#14][PR#14] from user_p/bugfix/fix-bug-A
* Merge pull request [#13][PR#13] from user_a/feature/turn-off-xxx
* Merge pull request [#11][PR#11] from user_p/bugfix/fix-bug-A
### Published By
* Author: `user_p`
* Date: 04/20/2022
-----
复制到GitHub上将会如下图:
根据具体的需求,可以修改上面的script进行功能的加强。例如加入更多的commit message。
总结
有了这样详细的版本日志,以后无论是别人还是自己回溯代码的改动都会方便一些。甚至当别的团队询问随机的两个版本之间有哪些改动的时候(例如一个季度的更新,从版本0.9.0到1.2.0),你也可以飞速地把答案丢给他们,无需再花时间收集整理去写一份总结。