在 Swift 中編寫指令碼:Git Hooks

語言: CN / TW / HK

我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿

前言

這周,我決定完成因為工作而推遲了一週的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庫。

``` swift

!/usr/bin/swift

import Foundation ```

這樣當git執行檔案時,shebang將確保使用檔案作為輸入資料呼叫/usr/bin/swift二進位制檔案。

編寫git鉤子

專案已經全部設定好,所以現在可以編寫git掛鉤了。讓我們走完所有的步驟。

檢索提交訊息

要做的第一件事就是從指令碼傳進來的引數檢索臨時提交檔案的路徑然後讀取檔案內容。

```swift let commitMessageFile = CommandLine.arguments[1]

guard let data = FileManager.default.contents(atPath: commitMessageFile), let commitMessage = String(data: data, encoding: .utf8) else { exit(1) } ```

在上面的程式碼片段中,我們首先拿到了提交檔案的路徑(git傳遞給指令碼),然後通過FileManagerAPI讀取了檔案內容。如果因為一些原因檢索失敗了,我們退出(exit)指令碼同時返回狀態碼1,這將告訴git終止此次提交。


注意:

根據git hooks文件,如果任何鉤子指令碼返回的狀態碼大於0,它都將終止即將要要發生的操作。這將在本文後面的部分中使用,以便在不需要做任何修改而優雅地退出。


檢索問題編號

既然提交資訊的字串已經可用,接下來就需要找到當前分支並從中檢索到問題編號。正如本文前面提到的,這隻可能是因為團隊對分支命名的嚴格格式,在其名稱中始終包含JIRA編號(例如,story/ISSUE-1234_some-awesome-feature-work)。

為了實現這一點,我們必須檢索當前的工作分支,然後用正則表示式從中檢索問題編號。

讓我們從新增指令碼呼叫zsh shell命令的能力開始。通過使用Processapi,指令碼可以與git命令列介面互動。

```swift func shell(_ command: String) -> String { let task = Process() let outputPipe = Pipe() let errorPipe = Pipe()

task.standardOutput = outputPipe
task.standardError = errorPipe
task.arguments = ["-c", command]
task.executableURL = URL(fileURLWithPath: "/bin/zsh")

do {
    try task.run()
    task.waitUntilExit()
} catch {
    print("There was an error running the command: \(command)")
    print(error.localizedDescription)
    exit(1)
}

guard let outputData = try? outputPipe.fileHandleForReading.readToEnd(),
      let outputString = String(data: outputData, encoding: .utf8) else {
    // Print error if needed
    if let errorData = try? errorPipe.fileHandleForReading.readToEnd(),
       let errorString = String(data: errorData, encoding: .utf8) {
        print("Encountered the following error running the command:")
        print(errorString)
    }
    exit(1)
}

return outputString

} ```

現在實現了shell命令,那麼就可以使用它詢問git當前分支是什麼,然後儘可能的從中提取出問題編號。

```swift let gitBranchName = shell("git rev-parse --abbrev-ref HEAD") .trimmingCharacters(in: .newlines)

let stringRange = NSRange(location: 0, length: gitBranchName.utf16.count)

guard let regex = try? NSRegularExpression(pattern: #"(\w-\d)"#, options: .anchorsMatchLines), let match = regex.firstMatch(in: gitBranchName, range: stringRange) else { exit(0) }

let range = match.range(at: 1)

let ticketNumber = (gitBranchName as NSString) .substring(with: range) .trimmingCharacters(in: .newlines) ```

請注意,如果沒有匹配項(即分支名稱中不包含JIRA問題編號),指令碼將以0的狀態退出,允許提交繼續進行,而不進行任何更改。這是為了不破壞諸如main或其他測試/調查分支中的工作流。

修改提交資訊

為了更改提交訊息,必須將指令碼開頭讀取的檔案內容(包含提交訊息)寫回同一路徑。

在這種情況下,只需要做一個更改,即在提交資訊的前面加上JIRA編號和(-),以將其與提交資訊的其餘部分很好地分開。還必須確保檢查了提交資訊字串,僅在編號不存在時才新增編號:

swift if !commitMessage.contains(ticketNumber) { do { try "\(ticketNumber) - \(commitMessage.trimmingCharacters(in: .newlines))" .write(toFile: commitMessageFile, atomically: true, encoding: .utf8) } catch { print("Could not write to file \(commitMessageFile)") exit(1) } }

設定git鉤子

現在指令碼已經準備好了,是時候把它放在git可以找到它的位置了。Git鉤子可以全域性設定,也可以基於單個repo設定。

我個人對這類指令碼的偏好是基於單個repo設定,因為這樣可以在出現問題時為您提供更多的控制和可見性,並且如果鉤子開始失敗,它會在它設定的repo中失敗,而不是全域性都失敗。

要設定它們,我們只需要使檔案可執行,重新命名並將其複製到所要設定repo的.git/hooks/路徑之下:

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

測試結果

現在repo已經全部設定好了,剩下的就是對部署的指令碼進行測試。在下面的截圖中,建立了兩個分支,一個帶有問題編號,一個沒有,它們有著相同的提交資訊。可以看出指令碼執行正常,並且只在需要時才更改提交訊息!

關於我們

我們是由 Swift 愛好者共同維護,我們會分享以 Swift 實戰、SwiftUI、Swift 基礎為核心的技術內容,也整理收集優秀的學習資料。