fbpx

Git Hook 整合 Redmine API 自動提交版本變更記錄至 Issue

redmine-git軟體開發過程中常需要追蹤 Bug 或工作狀態,我們也常會用到 TracRedmine 之類的專案管理系統,此類軟體專案管理系統除了 Task 安排之外,最重要的就是可以結合版本控制系統進行程式碼的修訂整合。有些情況我們在開發過程中不時要上去系統回報與更新狀態,某些研發團隊甚至還被要求必須記錄那該死的工作時數(當然這麼做是有特別原因...)。但是對於開發者而言,這些繁瑣的事務性工作都是打擾開發的因子,我們最喜歡的還是「自動」、「自動」和「自動」。相對於硬體的自動化,軟體在實現自動化顯得方便多了。工作流程自動化是很重要的,如果能讓開發管理工作減少到只要提交程式碼,那就是最高的境界囉。

Redmine Rest API

在還沒有達到最高境界之前,我先來介紹如何用 GitHook 整合 Commit Message 到 Redmine 工作單中。Redmine 這套優秀的專案管理系統採用 Ruby on Rails 開發,預設就內建了基本的 Rest API,讓我們可以很輕易地直接透過 RESTful Web Service 與其進行整合 (對 REST 感到陌生的朋友可以參考這篇文章)。Redmine API 在呼叫時必須傳入 API Key,透過 API Key 呼叫 Web Service 就如同已經登入的使用者,包含存取權限都是一樣的。每一位 Redmine 使用這都可以取得自己的 API Key,再登入 Redmine 後進入「我的帳戶」就可以取得,畫面如下:

redmine-api-key

上圖這字串就是 API Key,之後就可以拿來呼叫 Web Service 囉。

Git Hook 機制

其實從以前到現在的版本控制系統都擁有 Hook 機制與介面,好讓我們加入自己的功能來擴充版本控制系統。Git Hook 目前已經提供非常完善的 Hook 機制,我們預計想在開發者 Push 程式碼時自動解析 message 並且透過上述的 Redmine API 回應的對應的 Issue 中,因此今天會介紹到 pre-receive Hook。Hook 其實都是一些 Script,我們可以在 Repository 下的 hooks 目錄看到很多 sample 程式碼,大部分種類的 Git Hook 皆是透過命令結束回傳值 (Return Value / Exit Code)」來確定是否中斷 Git 程序 (非零即中斷),Script 的 STD OUT 就等同於 Git 輸出訊息,用來進行各種檢查與系統整合相當方便。

先修改 /hooks/pre-receive 檔案,預設不存在,請自行建立即可。

vim your_repos/hooks/pre-receive

加入以下內容,這裡將每一個 Push Rev 送給 redmine-add-change-log.php 這支程式來處理,稍等我們會在這支檔案實做整合 Redmine 的程式碼 (GitHub)。

 

#!/bin/bash

# ignore tag push
if [[ "$3" == "refs/tags/"* ]]; then
  exit 0
fi

while read oldrev newrev refname
do
    for rev in $(git rev-list ${oldrev}..${newrev})
    do
        log=`git show -s --format="%s" $rev`
        author=`git show -s --format="%an" $rev`
        ./hooks/redmine-add-change-log.php "${log}" "${author}" "${rev}"
        if [ $? != 0 ]
        then
            exit $?
        fi
    done
done

exit 0

接著我們來編輯 redmine-add-change-log.php 這支檔案 (GitHub)

vim your_repos/hooks/redmine-add-change-log.php

#!/usr/bin/env /usr/bin/php
<?php

// defined
$hostname = 'http://127.0.0.1:8000';
$key = 'YOUR_API_KEY';
$updateMode = 'XML'; // XML | JSON

error_reporting(E_ERROR);
ini_set('display_errors', 'On');

// check argv
if (count($argv) !== 4) {
    echo 'Parameter miss.';
    exit(1);
}
$message = $argv[1];
$author = $argv[2];
$rev = $argv[3];

// check log format
preg_match('/^#(\d+)\s(.+)$/is', $message, $matches);
if (count($matches) !== 3) {
    echo "Log format check fail. Example as follows:\n";
    echo "#ISSUE_NO CHANGE_LOG\n";
    exit(1);
}
$issueNo = $matches[1];
$log = $matches[2];
$url = sprintf('%s/issues/%d.json?key=%s', $hostname, $issueNo, $key);

// check issue no
$issueInfo = file_get_contents($url);
if ($issueInfo === false) {
    echo "Redmine issue '#{$issueNo}' not found.\n";
} else {
    // add commit log to redmine
    $issueObj = json_decode($issueInfo, true);
    if ($issueObj['issue']['done_ratio'] < 90) {
        $doneRatio = $issueObj['issue']['done_ratio'] + ((90 - $issueObj['issue']['done_ratio']) / 2);
    } else {
        $doneRatio = $issueObj['issue']['done_ratio'];
    }
    $notes = sprintf('%s: %s (commit:%s)', $author, $matches[2], substr($rev, 0, 8));
    if ($updateMode === 'JSON') {
        $data = json_encode(array(
            'issue' => array(
                'notes' => $notes,
                'done_ratio' => $doneRatio
            )
        ));
        $contentType = 'application/json; charset=utf-8';
    } else {
        $data = '<?xml version="1.0"?>';
        $data .= '<issue>';
        $data .= '<notes>' . $notes . '</notes>';
        $data .= '<done_ratio>' . $doneRatio . '</done_ratio>';
        $data .= '</issue>';
        $url = sprintf('%s/issues/%d.xml?key=%s', $hostname, $issueNo, $key);
        $contentType = 'application/xml; charset=utf-8';
    }

    $curl = curl_init($url);
    curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PUT');
    curl_setopt($curl, CURLOPT_HEADER, false);
    curl_setopt($curl, CURLOPT_HTTPHEADER, array(
        'Content-Type: ' . $contentType,
        'X-Redmine-API-Key: ' . $key
    ));
    curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
    $response = curl_exec($curl);
    $httpStatus = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    if ($httpStatus !== 200) {
        echo "Update redmine issue fail! (#${issueNo})\n" . print_r($data, true) . "\n";
    } else {
        echo "Update change log to redmine issue #${issueNo}.\n";
    }
}

上述的程式會透過使用者輸入的 message 自動解析,並且建立回應內容在 Redmine Issue 中,此外還會更新 Issue 的進度,讓它慢慢接近 100%,這個功能只是測試,其實這樣設計沒什意義,功能是永遠做不完的...哈

後來發現透過 JSON Format Call Redmine API 傳送資料,碰到 utf-8 中文訊息會失效 (Server 回應 500),所以後還改用 XML Format 就沒有這個問題了。

建立好 Hook 之後我們來測試一下,假設我們在 Redmine 有一個 #4 Issue 如下:

redmine-issue

接著新增一支檔案並且 Push 到 Git 上,message 必須輸入「#4 ...」好讓 Git Hook 解析完自動更新所屬的 Redmine Issue,範例如下:

cd your_workcopy

echo 'I am login!' > login.html

git add login.html

git commit -m "#4 add login html page"

git push

Push 之後如果沒有錯誤,我們就可以看到 Redmine Issue 已經被更新囉,如下:

redmine-issue-update

由於我們 Redmine 也設定了檔案「儲存機制」與 Git 掛勾,因此版本號會變成可以點選的連結(如果沒有自動變成連結只是因為 Redmine 還沒有同步到最新的程式碼,這時候只需要點一下「儲存機制」功能即可進行刷新),如下:

redmine-src-diff

如果想在 Git 加入其他功能,可以自行修改 /hooks 中檔案即可。

關於工作流程自動化

透過 Git Hook 其實可以完成很多輔助軟體品質提昇的工作,像是 Coding Style Check 與 Unit Test 等等。若是盡可能在程式碼進行整合測試之前,就先針對品質進行控管工作,對於整個開發流程的節奏掌握相當有幫助。人性都是懶惰的,自動化在軟體開發領域更應該徹底實現,我們每天設計軟體為了幫助人們快速完成工作,同樣地設計軟體幫自己快速完成工作。雖然軟體開發速度無法在幾年內就能夠得到一個數量級的提升,但隨著自動化輔助開發工作,我們花在附屬性工作 (註1) 的時間比例上慢慢降低,自動化能讓我們更專注於本質性工作,何樂不為呢?

註 1:人月神話這本書中作者 Frederick P. Brooks 提及,將軟體設計工作區分為本質性與附屬性兩類工作。本質性工作指的是軟體抽象概念的形成與設計,附屬性工作指的是將抽象的邏輯轉譯為機器可以實現的過程或程式碼。

 

  4 comments for “Git Hook 整合 Redmine API 自動提交版本變更記錄至 Issue

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料