miauのブログ

はてなダイアリー「miauの避難所」をはてなブログに移行しました

Redmine がらみの Subversion hook 設定

Subversion の設定もろもろについては、以前 書きましたけど、ところ変われば運用方法も違っていて、今回は

  • ITS に Trac ではなく Redmine 使う
  • メンバが多いからコミット時のメールは飛ばさないほうがいい

とのことで。要するにふつうに Redmine と連携する設定ができればいいみたいなので、その設定をまとめておきます。

コミット時にリポジトリビューを最新にする(trac-post-commit-hook 相当?)

Redmine はコミットメッセージに含まれる「refs #xxx」のようなメッセージを検出して、勝手にチケットとの紐付けを行ってくれますので、Subversion 側で trac-post-commit-hook のような hook を仕込まなくても連携自体は行えます。

ただし Redmine は「リポジトリ」画面を開いたタイミングでリポジトリの最新情報を取得する形なので、Subversion 側で更新が行われてもチケット等には即時反映されません。これをコミット時に反映するための設定がこちら。

今回は RedmineSubversion リポジトリが同サーバなので Redmine 0.8 までの対策でもよかったんですが、0.9 以降の方法で設定しました。

まず Redmine 側で API キーを生成。(「管理」→「設定」→「リポジトリ」の「リポジトリ管理用のWebサービスを有効にする」にチェックを入れて「キーの生成」→「保存」)

Subversion リポジトリ側では hooks/post-commit に実行権限を付加した後、

/usr/bin/wget -q -O /dev/null http://localhost/redmine/sys/fetch_changesets?key=********************&id=project1 &

の一行を追加すれば OK と。「********************」の部分には Redmine 側で設定した API キーを、id 部分にはプロジェクトのコードを指定します。あと http://localhost/redmine/ の部分は Redmine のルートに置き換えてください。

余談

ちなみに id にプロジェクトの id(projects.id)ではなくコード(projects.identifier)を指定してもうまく動作している理由がよくわからなかったんですが、Project クラスで

  def self.find(*args)
    if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
      project = find_by_identifier(*args)
      raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
      project
    else
      super
    end
  end

こんな感じで find() が再定義されていて納得。今まで id(数字)を渡さないとダメだと思ってわざわざ調べてました・・・。

リビジョンコメントの更新時に Redmine に反映する(trac-admin resync 相当)

pre-revprop-change を設定すれば Subversion のリビジョンコメント(コミットメッセージ)を変更できるのは 以前 書いた通りなんですが、この変更を ITS 側にも反映させたいですね。

Trac を使っている場合は post-revprop-change で trac-admin resync を呼んだりするわけですが、Redmine ではどうするんだろう?と調べてみると、ちゃんと作っている方がいました。すばらしい。

スクリプトを叩く形なので RedmineSubversion が別サーバだと使えませんが、今回は同一サーバに収まっているのでこのまま使うことにします。(もし別サーバの場合は app/controllersys_controller.rb の fetch_changesets をコピーしてそれっぽいメソッドを作ればいけると思います。)

このスクリプトを script/resync として配置しておいて、post-revprop-change で

env RAILS_ENV=production /opt/redmine-1.1.2-1/ruby/bin/ruby /opt/redmine-1.1.2-1/apps/redmine/script/resync project1 "$REV" &

みたいにやってやれば OK と。

  • hook の実行ユーザ(apache)で RAILS_ENV が設定されていなかったので設定
  • #!/usr/bin/env ruby の形では ruby が見つからなかったので起動時に ruby のフルパス指定

とやってます。

この辺りを気軽に更新できるのは、Trac と違ってデータで保持してる強みですね。(Trac はチケットに対するコメントを追加する形なので、こう簡単には書き換えられない・・・と思います。)

コミットメッセージ中にチケット番号を必須にする

チケット駆動で進めるための習慣付けとして、チケット番号がメッセージに含まれないコミットは弾いてしまうことにします。

下調べ

大雑把にチェックするだけなら grep なんかで対応できるんですが、「コミットはうまく通ったけどチケットに反映されていなかった」となると悲しいので、なるべく Redmine 側の処理に合わせてやりたいところです。

Redmine のソースを見ると、コミットメッセージからチケット番号を探す処理は Changeset モデルで

  TIMELOG_RE = /
    (
    ((\d+)(h|hours?))((\d+)(m|min)?)?
    |
    ((\d+)(h|hours?|m|min))
    |
    (\d+):(\d+)
    |
    (\d+([\.,]\d+)?)h?
    )
    /x

  def scan_comment_for_issue_ids
    return if comments.blank?
    # keywords used to reference issues
    ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
    ref_keywords_any = ref_keywords.delete('*')
    # keywords used to fix issues
    fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)

    kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")

    referenced_issues = []

    comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|

こんな感じで判定されているみたいです。

pre-commit

上記の処理を参考に pre-commit にチェック処理を入れると・・・

# Make sure that the log message contains ticket number.
SVNLOOK=/usr/bin/svnlook
RUBY=/opt/redmine-1.1.2-1/ruby/bin/ruby
$SVNLOOK log -t "$TXN" "$REPOS" | \
    $RUBY -e 'exit !!$stdin.read.match(/([\s\(\[,-]|^)((refs|references|IssueID|fixes|closes)[\s:]+)(#\d+)/i)' || \
    { echo 'Please input ticket number(e.g. "refs #xxx").' 1>&2 && exit 1; }

こうなりました。

「refs|references|IssueID|fixes|closes」の部分は Redmine の管理→設定→リポジトリの「参照用キーワード」「修正用キーワード」にあわせてください。「*」になっているならこの部分の正規表現は「?」をつけてオプションにしてしまえばいいかと。(あるいはもっと簡単に /#\d+/ にしてしまうとか。)

(2011-09-12 追記)

上記の正規表現だとコミットは通るけど Redmine に認識されないケースがありましたので修正しました。具体的には、Redmine 側の正規表現は、直後に「(?=[[:punct:]]|\s|<|$)」があることをチェックしているのですが、これを削っていました。

具体的な修正方法ですが、ここまでくると Ruby を独立したスクリプトにしたほうがよさそうなので、

この redmine_valid_comment.rb を hooks ディレクトリに置いて、pre-commit は以下のように変更しました。

# Make sure that the log message contains ticket number.
SVNLOOK=/usr/bin/svnlook
RUBY=/opt/redmine-1.1.2-1/ruby/bin/ruby
$SVNLOOK log -t "$TXN" "$REPOS" | \
    $RUBY $REPOS/hooks/redmine_valid_comment.rb || \
    { echo 'Please input ticket number(e.g. "refs #xxx").' 1>&2 && exit 1; }

動作確認してたらなぜかチケット番号直後に全角文字があっても通るのでおかしいなーと思っていたんですが、svnlook log で取得される値は「refs #731?\227?\128?\128?\227?\131?\129?\227?\130?\177?\227?\131?」みたいにエスケープされてて、[[:punct:]] が「?」にマッチするから通ってしまっていたみたいで。上記のスクリプトにはこのデコード処理を入れています。UTF-8 になるから /u 修飾子をつけたほうがいい気もするんですが、今のところ Redmine 本体に合わせて入れてません。

commit_ref_keywords、commit_fix_keywords は Redmine の管理→設定→リポジトリの「参照用キーワード」「修正用キーワード」にあわせてください。

(2011-09-12 追記 ここまで)

pre-revprop-change→mod_dav_svn 経由だと設定できず

pre-revprop-change も同様に

# svn diff
Index: pre-revprop-change
===================================================================
--- pre-revprop-change  (リビジョン 117)
+++ pre-revprop-change  (作業コピー)
@@ -60,7 +60,13 @@
 PROPNAME="$4"
 ACTION="$5"

-if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
+if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then
+    # Make sure that the log message contains ticket number.
+    RUBY=/opt/redmine-1.1.2-1/ruby/bin/ruby
+    $RUBY -e 'exit !!$stdin.read.match(/([\s\(\[,-]|^)((refs|references|IssueID|fixes|closes)[\s:]+)(#\d+)/i)' || \
+        { echo 'Please input ticket number(e.g. "refs #xxx").' 1>&2 && exit 1; }
+    exit 0
+fi

 echo "Changing revision properties other than svn:log is prohibited" >&2
 exit 1

こんな感じで設定すればいいだろう、と思っていたんですが・・・なぜかうまく動作せず。

いろいろ調べてみると、標準入力に変更後のプロパティ(ここではコミットメッセージ)が入ってくるはずなのに、変更前のプロパティが入力されている模様。

バグっぽいので調べてみると、

という感じで今月 ML で話題になったけど「再現しない」とか言われてるみたい。

今回 Apache 経由でアクセスしてるから mod_dav_svn の不具合かな?と調べてみると、今月になって

このあたりの(3 年以上前の)チケットがクローズされてました。ML で話題になって発掘されたんでしょうね。

そしてこの変更は

Version 1.7.0
(?? ??? 2011, from /branches/1.7.x)
(中略)
    * fixed: mod_dav_svn runs pre-revprop-change hook twice (issue #3085)
    * fixed: mod_dav_svn doesn't return stderr to user on failure (issue #3112)

とのことで 1.7.0 で修正予定みたいです。とりあえず pre-revpro-pchange のほうは 1.7.0 のリリースを待って設定しようかと。

(追記)Redmine Libsvn Plugin

Redmine 側の話なので今回のテーマからは外れるんですけど、RedmineSubversion の連携がらみなのでついでに。libsvn を使って Redmine から Subversion リポジトリへのアクセスを高速化するプラグインだそうです。

今のプロジェクトでは Redmine 環境を構築した人が導入してくれてたけど、社内の Redmine にも入れたいのでメモ。

(2016-08-25 追記)「リビジョンコメントの更新時に Redmine に反映する」SVN が別サーバにある場合


まず対比として「コミット時にリポジトリビューを最新にする(trac-post-commit-hook 相当?)」で行っていることを説明すると、以下のような感じです。

  • Redmine 側ではあらかじめ /sys/fetch_changesets というアクションが用意されている
    • このアクションでは id(プロジェクトのコード)を受け取って、そのプロジェクトのリポジトリから最新の情報を取得する
  • SVN 側の post-commit hook では Redmine の /sys/fetch_changesets を呼び出す。この際 id パラメータも渡す。

これを参考にすると、「リビジョンコメントの更新時に Redmine に反映する(trac-admin resync 相当)」を実現するためには、例えば以下のような仕組みがあればよさそうです。

  • Redmine 側に新たに /sys/update_changeset_comment というアクションを用意する
    • このアクションでは id(プロジェクトのコード)、rev(コメントを変更したいリビジョン)を受け取って、そのプロジェクトのリポジトリから、指定されたリビジョンの最新のコメントを取得する
  • SVN 側の post-revprop-change hook では Redmine の /sys/update_changeset_comment を呼び出す。この際 id、rev パラメータも渡す。

これを実現したい場合、以下のようにすれば実装できそうです。

(2016-08-29 追記)上記の実装例

2016-08-25 追記部分の Redmine 側の実装例を

に上げました。Redmine 2.5.3 での試しに実装した際の diff になっています。

Redmine 1.4 から 1 つのプロジェクトに複数のリポジトリが持てるようになっていますが、そこは考慮していません(複数のリポジトリがある場合にはメインリポジトリが対象になります)ので、必要でしたらリポジトリの識別子も受け取るように変更してご利用ください。