在 Swift 中編寫腳本:Git Hooks
前言
這周,我決定完成因為工作而推遲了一周的TODO事項來改進我的Git工作流程。
為了在提交的時候盡可能多的攜帶上下文信息,我們讓提交信息包含了正在處理的JIRA編號。這樣,將來如果有人回到我們現在正在提交的源代碼,輸入??git blame?
?,就能很容易的找出JIRA的編號。
每次提交都包含這些信息可能會有點乏味(如果你使用了類似TDD[1]之類的方法,您會提交的更加頻繁),而且,盡管像Tower[2]這樣的git客戶端會讓此變得容易一些,但是您仍然需要手動將問題編號復制粘貼到提交消息中,并且記住這樣做,這是我最難以解決的問題??。
出于這個原因,我開始尋求了解git hooks,試圖自動化這項任務。我的想法是能夠從git分支獲取JIRA編號(我們有一個分支命名約定,形如:story/ISSUE-1234_branch-name),然后將提交消息更改為以JIRA編號為前綴,從而生成最終結果消息:ISSUE-1234-其他原本的提交信息。
用git hooks自動生成提交信息
?Git Hooks[3] 提供了一種在運行某些重要的git命令時觸發自定義操作的方法,例如在一次commit或者push之前執行一些操作。
在本例中,我使用了 commit-msg 鉤子,它能夠在當前提交信息生效前修改此信息。鉤子由一個參數調用,該參數是指向包含用戶輸入的提交消息的文件的路徑。這意味著,為了改變提交消息,我們只需要從文件中讀取、修改其內容,然后寫回調用掛鉤的文件。
要創建git鉤子,我們需要在 .git/hooks? 路經下提供一個可執行腳本。我的鉤子放在了 .git/hooks/commit-msg 路經之下。
為什么我使用Swift?
Git hooks可以使用任何你熟悉的,并且在主機上安裝了解釋器(通過shebang來指定)的腳本語言來編寫。
雖然有很多更受歡迎的選項,比如bash?、ruby等等,但我還是決定使用Swift。因為我對Swift更熟悉,因為我每天都在使用它,而且我真的非常喜歡它強大的類型語法以及低內存占用。
讓我們開始吧
你可以使用任何你喜歡的IDE編寫Swift腳本。但是如果你想要有適當的代碼補全以及調試能力,你可以為其創建一個Xcode項目。為此,在 macOS? 下選擇 Command Line Tool 創建一個新的項目。
在創建的文件頂部加上Swift shebang,引入Foundation庫。
#!/usr/bin/swift
import Foundation
這樣當git執行文件時,shebang將確保使用文件作為輸入數據調用/usr/bin/swift二進制文件。
編寫git鉤子
項目已經全部設置好,所以現在可以編寫git掛鉤了。讓我們走完所有的步驟。
檢索提交消息
要做的第一件事就是從腳本傳進來的參數檢索臨時提交文件的路徑然后讀取文件內容。
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?命令的能力開始。通過使用Process?api,腳本可以與git命令行界面交互。
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當前分支是什么,然后盡可能的從中提取出問題編號。
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編號和(-),以將其與提交信息的其余部分很好地分開。還必須確保檢查了提交信息字符串,僅在編號不存在時才添加編號:
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/路徑之下:
chmod +x main.swift
mv main.swift <path_to_your_repo>/.git/hooks/commit-msg
測試結果
現在repo已經全部設置好了,剩下的就是對部署的腳本進行測試。在下面的截屏中,創建了兩個分支,一個帶有問題編號,一個沒有,它們有著相同的提交信息。可以看出腳本運行正常,并且只在需要時才更改提交消息!
參考資料
[1] TDD: https://en.wikipedia.org/wiki/Test-driven_development。
[2] Tower: https://www.git-tower.com/mac。
[3] Git Hooks: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks。