在 Swift 中编写脚本:Git Hooks

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

前言

这周,我决定完成因为工作而推迟了一周的TODO事项来改进我的Git工作流程。

为了在提交的时候尽可能多的携带上下文信息,我们让提交信息包含了正在处理的JIRA编号。这样,将来如果有人回到我们现在正在提交的源代码,输入git blame,就能很容易的找出JIRA的编号。

每次提交都包含这些信息可能会有点乏味(如果你使用了类似TDD之类的方法,您会提交的更加频繁),而且,尽管像Tower这样的git客户端会让此变得容易一些,但是您仍然需要手动将问题编号复制粘贴到提交消息中,并且记住这样做,这是我最难以解决的问题😅。

出于这个原因,我开始寻求了解git hooks,试图自动化这项任务。我的想法是能够从git分支获取JIRA编号(我们有一个分支命名约定,形如:story/ISSUE-1234_branch-name),然后将提交消息更改为以JIRA编号为前缀,从而生成最终结果消息:ISSUE-1234-其他原本的提交信息。

用git hooks自动生成提交信息

Git Hooks 提供了一种在运行某些重要的git命令时触发自定义操作的方法,例如在一次commit或者push之前执行一些操作。

在本例中,我使用了commit-msg钩子,它能够在当前提交信息生效前修改此信息。钩子由一个参数调用,该参数是指向包含用户输入的提交消息的文件的路径。这意味着,为了改变提交消息,我们只需要从文件中读取、修改其内容,然后写回调用挂钩的文件。

要创建git钩子,我们需要在.git/hooks路经下提供一个可执行脚本。我的钩子放在了.git/hooks/commit-msg路经之下。

为什么我使用Swift?

Git hooks可以使用任何你熟悉的,并且在主机上安装了解释器(通过shebang来指定)的脚本语言来编写。

虽然有很多更受欢迎的选项,比如bashruby等等,但我还是决定使用Swift。因为我对Swift更熟悉,因为我每天都在使用它,而且我真的非常喜欢它强大的类型语法以及低内存占用。

让我们开始吧

你可以使用任何你喜欢的IDE编写Swift脚本。但是如果你想要有适当的代码补全以及调试能力,你可以为其创建一个Xcode项目。为此,在macOS下选择Command Line Tool创建一个新的项目。

在创建的文件顶部加上Swift shebang,引入Foundation库。

  1. #!/usr/bin/swift
  2. import Foundation

这样当git执行文件时,shebang将确保使用文件作为输入数据调用/usr/bin/swift二进制文件。

编写git钩子

项目已经全部设置好,所以现在可以编写git挂钩了。让我们走完所有的步骤。

检索提交消息

要做的第一件事就是从脚本传进来的参数检索临时提交文件的路径然后读取文件内容。

  1. let commitMessageFile = CommandLine.arguments[1]
  2. guard let data = FileManager.default.contents(atPath: commitMessageFile),
  3. let commitMessage = String(data: data, encoding: .utf8) else {
  4. exit(1)
  5. }

在上面的代码片段中,我们首先拿到了提交文件的路径(git传递给脚本),然后通过FileManagerAPI读取了文件内容。如果因为一些原因检索失败了,我们退出(exit)脚本同时返回状态码1,这将告诉git终止此次提交。


注意:

根据git hooks文档,如果任何钩子脚本返回的状态码大于0,它都将终止即将要要发生的操作。这将在本文后面的部分中使用,以便在不需要做任何修改而优雅地退出。


检索问题编号

既然提交信息的字符串已经可用,接下来就需要找到当前分支并从中检索到问题编号。正如本文前面提到的,这只可能是因为团队对分支命名的严格格式,在其名称中始终包含JIRA编号(例如,story/ISSUE-1234_some-awesome-feature-work)。

为了实现这一点,我们必须检索当前的工作分支,然后用正则表达式从中检索问题编号。

让我们从添加脚本调用zsh shell命令的能力开始。通过使用Processapi,脚本可以与git命令行界面交互。

  1. func shell(_ command: String) -> String {
  2. let task = Process()
  3. let outputPipe = Pipe()
  4. let errorPipe = Pipe()
  5. task.standardOutput = outputPipe
  6. task.standardError = errorPipe
  7. task.arguments = ["-c", command]
  8. task.executableURL = URL(fileURLWithPath: "/bin/zsh")
  9. do {
  10. try task.run()
  11. task.waitUntilExit()
  12. } catch {
  13. print("There was an error running the command: \(command)")
  14. print(error.localizedDescription)
  15. exit(1)
  16. }
  17. guard let outputData = try? outputPipe.fileHandleForReading.readToEnd(),
  18. let outputString = String(data: outputData, encoding: .utf8) else {
  19. // Print error if needed
  20. if let errorData = try? errorPipe.fileHandleForReading.readToEnd(),
  21. let errorString = String(data: errorData, encoding: .utf8) {
  22. print("Encountered the following error running the command:")
  23. print(errorString)
  24. }
  25. exit(1)
  26. }
  27. return outputString
  28. }

现在实现了shell命令,那么就可以使用它询问git当前分支是什么,然后尽可能的从中提取出问题编号。

  1. let gitBranchName = shell("git rev-parse --abbrev-ref HEAD")
  2. .trimmingCharacters(in: .newlines)
  3. let stringRange = NSRange(location: 0, length: gitBranchName.utf16.count)
  4. guard let regex = try? NSRegularExpression(pattern: #"(\w*-\d*)"#, options: .anchorsMatchLines),
  5. let match = regex.firstMatch(in: gitBranchName, range: stringRange) else {
  6. exit(0)
  7. }
  8. let range = match.range(at: 1)
  9. let ticketNumber = (gitBranchName as NSString)
  10. .substring(with: range)
  11. .trimmingCharacters(in: .newlines)

请注意,如果没有匹配项(即分支名称中不包含JIRA问题编号),脚本将以0的状态退出,允许提交继续进行,而不进行任何更改。这是为了不破坏诸如main或其他测试/调查分支中的工作流。

修改提交信息

为了更改提交消息,必须将脚本开头读取的文件内容(包含提交消息)写回同一路径。

在这种情况下,只需要做一个更改,即在提交信息的前面加上JIRA编号和(-),以将其与提交信息的其余部分很好地分开。还必须确保检查了提交信息字符串,仅在编号不存在时才添加编号:

  1. if !commitMessage.contains(ticketNumber) {
  2. do {
  3. try "\(ticketNumber) - \(commitMessage.trimmingCharacters(in: .newlines))"
  4. .write(toFile: commitMessageFile, atomically: true, encoding: .utf8)
  5. } catch {
  6. print("Could not write to file \(commitMessageFile)")
  7. exit(1)
  8. }
  9. }

设置git钩子

现在脚本已经准备好了,是时候把它放在git可以找到它的位置了。Git钩子可以全局设置,也可以基于单个repo设置。

我个人对这类脚本的偏好是基于单个repo设置,因为这样可以在出现问题时为您提供更多的控制和可见性,并且如果钩子开始失败,它会在它设置的repo中失败,而不是全局都失败。

要设置它们,我们只需要使文件可执行,重命名并将其复制到所要设置repo的.git/hooks/路径之下:

  1. chmod +x main.swift
  2. mv main.swift <path_to_your_repo>/.git/hooks/commit-msg

测试结果

现在repo已经全部设置好了,剩下的就是对部署的脚本进行测试。在下面的截屏中,创建了两个分支,一个带有问题编号,一个没有,它们有着相同的提交信息。可以看出脚本运行正常,并且只在需要时才更改提交消息!

关于我们

我们是由 Swift 爱好者共同维护,我们会分享以 Swift 实战、SwiftUI、Swift 基础为核心的技术内容,也整理收集优秀的学习资料。


文章标签:

原文连接:https://juejin.cn/post/7115304738651897863

相关推荐

Taro框架完美使用Axios

看完这篇,你也可以搞定有趣的动态曲线绘制

Codable保姆级攻略

iOS CarPlay|使用 CarPlay 为你的 App 提速

零基础教你Unity集成IOS原生本地推送

StoreKit2 实际接入时候的踩坑与解决实录

iOS:runloop 运行循环

第四届青训营阅读打卡活动来啦,奖品、规则全面升级,快来学习吧

支持SwiftUI!Swift版图片&视频浏览器-JFHeroBrowser上线啦

用 JavaScript 复原何同学B站头图、对前端构建工具的一些理解、弹幕的常规设计与实现 丨酱酱的下午茶第31期

现今 Swift 包中的二进制目标

[Android开发学iOS系列] 语言篇: Swift vs Kotlin

LeetCode - #125 验证回文串

通过Vue自定义指令实现前端埋点,我不写单元测试,被批了,利用噪声构建美妙的 CS,Kotlin协程-CoroutineScope丨酱酱的下午茶第30期

iOS 数据存储

iOS怎么用代码实现这样奇怪的动画

利用 UIScrollView 实现六棱柱图片浏览效果

iOS中为什么会有这么多锁呢?

免费ios开发流程和步骤教程

[LD]iOS二进制组件化与Protocol的潜在风险