Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 10444] ファイルの概要

このコミットは、Mercurial バージョン管理システム用の codereview 拡張機能のクリーンアップと基本的なテストの追加に焦点を当てています。具体的には、lib/codereview/codereview.py という主要な Python スクリプトが大幅にリファクタリングされ、関連するテストスクリプト lib/codereview/test.sh が新規追加されています。

コミット

commit bbf952c3b4c5d7beb15c5c2a0ffb20f02323e6ea
Author: Russ Cox <rsc@golang.org>
Date:   Fri Nov 18 00:16:15 2011 -0500

    codereview: cleanup + basic tests
    
    R=adg, bradfitz
    CC=golang-dev
    https://golang.org/cl/5395044
---
 lib/codereview/codereview.py | 1240 +++++++++++++++++++++++-------------------
 lib/codereview/test.sh       |  198 +++++++
 2 files changed, 869 insertions(+), 569 deletions(-)

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/bbf952c3b4c5d7beb15c5c2a0ffb20f02323e6ea

元コミット内容

codereview: cleanup + basic tests

R=adg, bradfitz
CC=golang-dev
https://golang.org/cl/5395044

変更の背景

このコミットの主な背景は、Mercurial の codereview 拡張機能のコードベースの健全性を向上させることです。以前のバージョンでは、Mercurial の内部 API の変更に追従するための複雑な条件分岐や、冗長なコード、そしてテストの不足が見られました。

具体的には、以下の問題に対処しています。

  1. Mercurial API の変更への対応: Mercurial のバージョンアップに伴い、cmdutil.matchcmdutil.revpair といった一部の API が scmutil モジュールに移動していました。このコミット以前のコードは、これらの変更に古い方法で対応しようとしており、コードが複雑化していました。
  2. コードの重複と複雑性: Mercurial のコマンド実行やファイル操作に関するヘルパー関数が、一貫性のない方法で実装されており、コードの重複や可読性の低下を招いていました。
  3. テストの不足: codereview 拡張機能の動作を検証するための自動テストが不足しており、変更が既存の機能に与える影響を把握しにくい状況でした。
  4. Python バージョン要件の明確化: json パッケージの取り扱いに関して、Python 2.6 以降の標準ライブラリとしての json の利用を明確にし、それ以前のバージョンでの simplejsondjango.utils.simplejson へのフォールバックロジックを削除することで、コードを簡素化しています。
  5. Mercurial バージョン要件の引き上げ: 拡張機能が正しく動作するために必要な Mercurial の最小バージョンを 1.3 から 1.4 に引き上げ、古いバージョンでの利用を明示的に拒否するように変更しています。

これらの変更により、コードの保守性が向上し、将来的な機能追加やバグ修正が容易になることが期待されます。また、テストの追加により、拡張機能の信頼性が高まります。

前提知識の解説

このコミットを理解するためには、以下の技術的背景知識が必要です。

Mercurial (マーキュリアル)

Mercurial は、分散型バージョン管理システム (DVCS) の一つです。Git と同様に、各開発者がコードベースの完全な履歴を持つローカルリポジトリを持ち、変更を共有するためにリモートリポジトリと同期します。Mercurial は Python で書かれており、拡張機能を通じてその機能をカスタマイズ・拡張することができます。

  • リポジトリ (Repository): プロジェクトのすべてのファイルと変更履歴が保存されている場所。
  • チェンジセット (Changeset): 一連の変更をまとめた単位。Git のコミットに相当します。
  • 拡張機能 (Extensions): Mercurial の機能を拡張するための Python スクリプト。.hg/hgrc ファイルで設定することで有効化されます。
  • hgrc ファイル: Mercurial の設定ファイル。リポジトリ固有の設定は .hg/hgrc に、ユーザー固有の設定は ~/.hgrc に記述されます。

Rietveld (リートフェルト)

Rietveld は、Google のエンジニアである Guido van Rossum (Python の生みの親) によって開発された、Web ベースのコードレビューシステムです。Google App Engine 上で動作し、主に Subversion や Mercurial と連携して使用されます。開発者は変更を Rietveld にアップロードし、他の開発者が Web インターフェースを通じてコードをレビューし、コメントを残すことができます。

  • コードレビュー (Code Review): ソフトウェア開発プロセスにおいて、他の開発者が書いたコードを読み、改善点や潜在的なバグを指摘する作業。品質向上、知識共有、バグの早期発見に役立ちます。
  • チェンジリスト (Change List / CL): Rietveld におけるコードレビューの単位。通常、一つの論理的な変更のまとまりを表します。

gofmt

gofmt は、Go 言語のソースコードを自動的にフォーマットするツールです。Go 言語の標準ライブラリの一部として提供されており、Go コミュニティ全体で一貫したコードスタイルを強制するために広く使用されています。gofmt を実行することで、インデント、スペース、改行などが Go の標準的なスタイルガイドに従って自動的に修正されます。これにより、コードの可読性が向上し、スタイルに関する議論を減らすことができます。

CONTRIBUTORS ファイル

多くのオープンソースプロジェクト、特に Go 言語のプロジェクトでは、プロジェクトに貢献した人々のリストを CONTRIBUTORS という名前のファイルで管理することが一般的です。このファイルには、貢献者の氏名とメールアドレスが記載されており、プロジェクトのライセンスや貢献ポリシーの一部として扱われることがあります。コードレビューシステムでは、コミットを行うユーザーがこのファイルに登録されているかを確認する場合があります。

Python の json モジュール

Python 2.6 以降では、JSON (JavaScript Object Notation) データを扱うための標準ライブラリとして json モジュールが提供されています。それ以前のバージョンでは、simplejson という外部パッケージや、Django フレームワークにバンドルされていた django.utils.simplejson などが使われていました。このコミットでは、Python 2.6 以降を必須とすることで、これらの互換性レイヤーを削除し、コードを簡素化しています。

技術的詳細

このコミットは、codereview.py の内部構造と Mercurial との連携方法に大きな変更を加えています。

1. Mercurial API の抽象化と統一

以前の codereview.py は、Mercurial の内部 API (cmdutil, commands, hg, util, error, match, discovery, node) を直接インポートし、使用していました。しかし、Mercurial のバージョンアップに伴い、これらの API の一部が移動したり、振る舞いが変更されたりすることがありました。

このコミットでは、Mercurial の API 呼び出しをより安定した方法でラップするために、以下の変更が行われています。

  • mercurial.commands の利用: 以前は commands.incoming のように直接 commands モジュールから関数を呼び出していましたが、このコミットでは from mercurial import commands as hg_commands のようにエイリアスを付けて、hg_commands.incoming のように呼び出すことで、Mercurial のコマンド実行をより明確にしています。
  • mercurial.util の利用: 同様に、util.Abort などのユーティリティ関数も hg_util.Abort のようにエイリアス付きで利用するように変更されています。
  • uiwrap クラスの導入: Mercurial の ui (ユーザーインターフェース) オブジェクトは、コマンドの出力やユーザーとの対話に使用されます。uiwrap クラスは、ui.pushbuffer()ui.popbuffer() を使用して、Mercurial コマンドの出力をバッファリングし、後で取得できるようにするためのラッパーです。これにより、Mercurial コマンドの出力をプログラム的に処理しやすくなっています。
  • hg_matchPattern 関数の導入: ファイルのステータス(変更済み、追加済み、削除済み、不明など)を取得するために、以前は matchpats という関数が使われていましたが、このコミットでは hg_matchPattern という新しい関数が導入されています。この関数は hg_commands.statusuiwrap を介して呼び出し、その出力を解析してファイルリストを返します。これにより、ファイルステータスの取得ロジックが一元化され、より堅牢になっています。
  • hg_incoming, hg_outgoing, hg_log, hg_pull, hg_push, hg_commit などのラッパー関数の導入: Mercurial の主要なコマンド(incoming, outgoing, log, pull, push, commit)についても、uiwrap を使用したラッパー関数が導入されています。これにより、Mercurial コマンドの実行がより制御され、エラーハンドリングが改善されています。

2. StatusThread の導入

長時間実行されるコマンド(例: ネットワーク経由でのデータアップロード)中に、ユーザーに現在の状況を伝えるための StatusThread が導入されました。これは、別のスレッドで定期的にステータスメッセージを標準エラー出力に表示するものです。これにより、ユーザーはコマンドがフリーズしているのか、それとも単に時間がかかっているだけなのかを判断できるようになります。

3. test.sh による基本的なテストスイートの追加

lib/codereview/test.sh という新しいシェルスクリプトが追加されました。このスクリプトは、Mercurial のリポジトリをセットアップし、codereview 拡張機能を有効にし、基本的な hg submit, hg clpatch, hg sync などのコマンドの動作を検証します。また、CONTRIBUTORS ファイルの有無や、hg ci (commit) や hg rollback といった Mercurial の組み込みコマンドが codereview 拡張機能によって無効化されていることを確認するテストも含まれています。

4. CONTRIBUTORS ファイルの処理の改善

CONTRIBUTORS ファイルの読み込みと、ユーザーがこのファイルに登録されているかどうかのチェックに関するロジックが改善されました。ReadContributors 関数が導入され、CONTRIBUTORS ファイルを解析して貢献者情報をメモリにキャッシュするようになりました。また、CheckContributor および FindContributor 関数が、ユーザー名とメールアドレスを CONTRIBUTORS ファイルと照合するために使用されます。

5. gofmt およびタブフォーマットのチェック

CheckFormat, CheckGofmt, CheckTabfmt 関数が導入され、Go 言語のファイルが gofmt によって正しくフォーマットされているか、および C/C++/Objective-C/Swift ファイルがタブインデントを使用しているかをチェックするようになりました。これは、コードスタイルの統一を強制するための重要な機能です。

6. @clnumber パターンサポートの改善

Mercurial のファイルパターンマッチングにおいて、@clnumber という特殊な構文をサポートするための MatchAt 関数が導入されました。これは、特定のチェンジリスト (CL) に含まれるファイルを指定するためのものです。以前は ReplacementForCmdutilMatch という関数が使われていましたが、より明確な名前とロジックに変更されています。

7. Mercurial コマンドの無効化

codereview 拡張機能が有効な場合、Mercurial の組み込みコマンドである hg commithg rollback などが誤って使用されるのを防ぐために、これらのコマンドを無効化するフック (precommithook) が導入されました。これにより、開発者は hg mail, hg upload, hg submit といった codereview 拡張機能が提供するコマンドを使用するように強制されます。

コアとなるコードの変更箇所

lib/codereview/codereview.py の変更点

  1. Mercurial API のインポートとエイリアス化:

    --- a/lib/codereview/codereview.py
    +++ b/lib/codereview/codereview.py
    @@ -38,110 +38,60 @@ For example, if change 123456 contains the files x.go and y.go,
     "hg diff @123456" is equivalent to"hg diff x.go y.go".
     '''
     
    -from mercurial import cmdutil, commands, hg, util, error, match, discovery
    -from mercurial.node import nullrev, hex, nullid, short
    -import os, re, time
    -import stat
    -import subprocess
    -import threading
    -from HTMLParser import HTMLParser
    -
    -# The standard 'json' package is new in Python 2.6.
    -# Before that it was an external package named simplejson.
    -try:
    -# Standard location in 2.6 and beyond.
    -import json
    -except Exception, e:
    -try:
    -# Conventional name for earlier package.
    -import simplejson as json
    -except:
    -try:
    -# Was also bundled with django, which is commonly installed.
    -from django.utils import simplejson as json
    -except:
    -# We give up.
    -raise e
    -
    -try:
    -hgversion = util.version()
    -except:
    -from mercurial.version import version as v
    -hgversion = v.get_version()
    -
    -# in Mercurial 1.9 the cmdutil.match and cmdutil.revpair moved to scmutil
    -if hgversion >= '1.9':
    -    from mercurial import scmutil
    -else:
    -    scmutil = cmdutil
    -
    -oldMessage = """
    -The code review extension requires Mercurial 1.3 or newer.
    -
    -To install a new Mercurial,
    -
    -	sudo easy_install mercurial
    -
    -works on most systems.
    -"""
    -
    -linuxMessage = """
    -You may need to clear your current Mercurial installation by running:
    -
    -	sudo apt-get remove mercurial mercurial-common
    -	sudo rm -rf /etc/mercurial
    -"""
    +import sys
     
    -if hgversion < '1.3':
    -msg = oldMessage
    -if os.access("/etc/mercurial", 0):
    -msg += linuxMessage
    -raise util.Abort(msg)
    +if __name__ == "__main__":
    +	print >>sys.stderr, "This is a Mercurial extension and should not be invoked directly."
    +	sys.exit(2)
     
    -def promptyesno(ui, msg):
    -# Arguments to ui.prompt changed between 1.3 and 1.3.1.
    -# Even so, some 1.3.1 distributions seem to have the old prompt!?!
    -# What a terrible way to maintain software.
    -try:
    -return ui.promptchoice(msg, ["&yes", "&no"], 0) == 0
    -except AttributeError:
    -return ui.prompt(msg, ["&yes", "&no"], "y") != "n"
    +# We require Python 2.6 for the json package.
    +if sys.version < '2.6':
    +	print >>sys.stderr, "The codereview extension requires Python 2.6 or newer."
    +	print >>sys.stderr, "You are running Python " + sys.version
    +	sys.exit(2)
     
    -def incoming(repo, other):
    -fui = FakeMercurialUI()
    -ret = commands.incoming(fui, repo, *[other.path], **{'bundle': '', 'force': False})
    -if ret and ret != 1:
    -raise util.Abort(ret)
    -out = fui.output
    -return out
    +import json
    +import os
    +import re
    +import stat
    +import subprocess
    +import threading
    +import time
     
    -def outgoing(repo):
    -fui = FakeMercurialUI()
    -ret = commands.outgoing(fui, repo, *[], **{})
    -if ret and ret != 1:
    -raise util.Abort(ret)
    -out = fui.output
    -return out
    +from mercurial import commands as hg_commands
    +from mercurial import util as hg_util
     
    -# To experiment with Mercurial in the python interpreter:
    -#    >>> repo = hg.repository(ui.ui(), path = ".")
    +defaultcc = None
    +codereview_disabled = None
    +real_rollback = None
    +releaseBranch = None
    +server = "codereview.appspot.com"
    +server_url_base = None
    
    • Mercurial の古い API (cmdutil, hg, error, match, discovery) の直接インポートを削除し、mercurial.commandshg_commands として、mercurial.utilhg_util としてインポートするように変更。
    • Python 2.6 以降の json モジュールを直接インポートするように変更し、古い Python バージョンとの互換性レイヤーを削除。
    • Mercurial の最小バージョン要件を 1.4 に引き上げ、それ以前のバージョンではエラーで終了するように変更。
  2. StatusThread の追加:

    --- a/lib/codereview/codereview.py
    +++ b/lib/codereview/codereview.py
    @@ -199,6 +149,40 @@ def default_to_utf8():
     
     default_to_utf8()
     
    +#######################################################################
    +# Status printer for long-running commands
    +
    +global_status = None
    +
    +def set_status(s):
    +	# print >>sys.stderr, "\t", time.asctime(), s
    +	global global_status
    +	global_status = s
    +
    +class StatusThread(threading.Thread):
    +	def __init__(self):
    +		threading.Thread.__init__(self)
    +	def run(self):
    +		# pause a reasonable amount of time before
    +		# starting to display status messages, so that
    +		# most hg commands won't ever see them.
    +		time.sleep(30)
    +
    +		# now show status every 15 seconds
    +		while True:
    +			time.sleep(15 - time.time() % 15)
    +			s = global_status
    +			if s is None:
    +				continue
    +			if s == "":
    +				s = "(unknown status)"
    +			print >>sys.stderr, time.asctime(), s
    +
    +def start_status_thread():
    +	t = StatusThread()
    +	t.setDaemon(True)  # allowed to exit if t is still running
    +	t.start()
    +
     #######################################################################
     # Change list parsing.
     #
    
    • 長時間実行される処理中にステータスを表示するための StatusThread クラスと関連関数が追加。
  3. hg_matchPattern および Mercurial コマンドラッパーの追加:

    --- a/lib/codereview/codereview.py
    +++ b/lib/codereview/codereview.py
    @@ -362,7 +347,7 @@ class CL(object):
     		uploaded_diff_file = [("data", "data.diff", emptydiff)]
     		
     	if vcs and self.name != "new":
    -		form_fields.append(("subject", "diff -r " + vcs.base_rev + " " + getremote(ui, repo, {}).path))
    +		form_fields.append(("subject", "diff -r " + vcs.base_rev + " " + ui.expandpath("default")))
     	else:
     		# First upload sets the subject for the CL itself.
     		form_fields.append(("subject", self.Subject()))
    @@ -379,7 +364,7 @@ class CL(object):
     		ui.status(msg + "\n")
     	set_status("uploaded CL metadata + diffs")
     	if not response_body.startswith("Issue created.") and not response_body.startswith("Issue updated."):
    -		raise util.Abort("failed to update issue: " + response_body)
    +		raise hg_util.Abort("failed to update issue: " + response_body)
     	issue = msg[msg.rfind("/")+1:]
     	self.name = issue
     	if not self.url:
    @@ -404,7 +389,7 @@ class CL(object):
     		pmsg += " (cc: %s)" % (', '.join(self.cc),)
     	pmsg += ",\n"
     	pmsg += "\n"
    -	repourl = getremote(ui, repo, {}).path
    +	repourl = ui.expandpath("default")
     	if not self.mailed:
     		pmsg += "I'd like you to review this change to\n" + repourl + "\n"
     	else:
    @@ -735,101 +689,6 @@ _change_prolog = """# Change list.
     # Multi-line values should be indented.
     """
     
    -#######################################################################
    -# Mercurial helper functions
    -
    -# Get effective change nodes taking into account applied MQ patches
    -def effective_revpair(repo):
    -    try:
    -	return scmutil.revpair(repo, ['qparent'])
    -    except:
    -	return scmutil.revpair(repo, None)
    -
    -# Return list of changed files in repository that match pats.
    -# Warn about patterns that did not match.
    -def matchpats(ui, repo, pats, opts):
    -	matcher = scmutil.match(repo, pats, opts)
    -	node1, node2 = effective_revpair(repo)
    -	modified, added, removed, deleted, unknown, ignored, clean = repo.status(node1, node2, matcher, ignored=True, clean=True, unknown=True)
    -	return (modified, added, removed, deleted, unknown, ignored, clean)
    -
    -# Return list of changed files in repository that match pats.
    -# The patterns came from the command line, so we warn
    -# if they have no effect or cannot be understood.
    -def ChangedFiles(ui, repo, pats, opts, taken=None):
    -	taken = taken or {}
    -	# Run each pattern separately so that we can warn about
    -	# patterns that didn't do anything useful.
    -	for p in pats:
    -		modified, added, removed, deleted, unknown, ignored, clean = matchpats(ui, repo, [p], opts)
    -		redo = False
    -		for f in unknown:
    -			promptadd(ui, repo, f)
    -			redo = True
    -		for f in deleted:
    -			promptremove(ui, repo, f)
    -			redo = True
    -		if redo:
    -			modified, added, removed, deleted, unknown, ignored, clean = matchpats(ui, repo, [p], opts)
    -		for f in modified + added + removed:
    -			if f in taken:
    -				ui.warn("warning: %s already in CL %s\n" % (f, taken[f].name))
    -		if not modified and not added and not removed:
    -			ui.warn("warning: %s did not match any modified files\n" % (p,))
    -
    -	# Again, all at once (eliminates duplicates)
    -	modified, added, removed = matchpats(ui, repo, pats, opts)[:3]
    -	l = modified + added + removed
    -	l.sort()
    -	if taken:
    -		l = Sub(l, taken.keys())
    -	return l
    -
    -# Return list of changed files in repository that match pats and still exist.
    -def ChangedExistingFiles(ui, repo, pats, opts):
    -	modified, added = matchpats(ui, repo, pats, opts)[:2]
    -	l = modified + added
    -	l.sort()
    -	return l
    -
    -# Return list of files claimed by existing CLs
    -def Taken(ui, repo):
    -	all = LoadAllCL(ui, repo, web=False)
    -	taken = {}
    -	for _, cl in all.items():
    -		for f in cl.files:
    -			taken[f] = cl
    -	return taken
    -
    -# Return list of changed files that are not claimed by other CLs
    -def DefaultFiles(ui, repo, pats, opts):
    -	return ChangedFiles(ui, repo, pats, opts, taken=Taken(ui, repo))
    -
    -def Sub(l1, l2):
    -	return [l for l in l1 if l not in l2]
    -
    -def Add(l1, l2):
    -	l = l1 + Sub(l2, l1)
    -	l.sort()
    -	return l
    -
    -def Intersect(l1, l2):
    -	return [l for l in l1 if l in l2]
    -
    -def getremote(ui, repo, opts):
    -	# save $http_proxy; creating the HTTP repo object will
    -	# delete it in an attempt to "help"
    -	proxy = os.environ.get('http_proxy')
    -	source = hg.parseurl(ui.expandpath("default"), None)[0]
    -	try:
    -		remoteui = hg.remoteui # hg 1.6
    -	except:
    -		remoteui = cmdutil.remoteui
    -	other = hg.repository(remoteui(repo, opts), source)
    -	if proxy is not None:
    -		os.environ['http_proxy'] = proxy
    -	return other
    -
     desc_re = '^(.+: |(tag )?(release|weekly)\.|fix build|undo CL)'
     
     desc_msg = '''Your CL description appears not to use the standard form.
    @@ -851,15 +710,17 @@ Examples:
     
     '''
     
    +def promptyesno(ui, msg):
    +	return ui.promptchoice(msg, ["&yes", "&no"], 0) == 0
     
     def promptremove(ui, repo, f):
     	if promptyesno(ui, "hg remove %s (y/n)?" % (f,)):
    -		if commands.remove(ui, repo, 'path:'+f) != 0:
    +		if hg_commands.remove(ui, repo, 'path:'+f) != 0:
     			ui.warn("error removing %s" % (f,))
     
     def promptadd(ui, repo, f):
     	if promptyesno(ui, "hg add %s (y/n)?" % (f,)):
    -		if commands.add(ui, repo, 'path:'+f) != 0:
    +		if hg_commands.add(ui, repo, 'path:'+f) != 0:
     			ui.warn("error adding %s" % (f,))
     
     def EditCL(ui, repo, cl):
    @@ -899,10 +770,14 @@ def EditCL(ui, repo, cl):
     		# Check file list for files that need to be hg added or hg removed
     		# or simply aren't understood.
     		pats = ['path:'+f for f in clx.files]
    -		modified, added, removed, deleted, unknown, ignored, clean = matchpats(ui, repo, pats, {})
    +		changed = hg_matchPattern(ui, repo, *pats, modified=True, added=True, removed=True)
    +		deleted = hg_matchPattern(ui, repo, *pats, deleted=True)
    +		unknown = hg_matchPattern(ui, repo, *pats, unknown=True)
    +		ignored = hg_matchPattern(ui, repo, *pats, ignored=True)
    +		clean = hg_matchPattern(ui, repo, *pats, clean=True)
     		files = []
     		for f in clx.files:
    -			if f in modified or f in added or f in removed:
    +			if f in changed:
     				files.append(f)
     				continue
     			if f in deleted:
    @@ -954,7 +829,7 @@ def CommandLineCL(ui, repo, pats, opts, defaultcc=None):
     	else:
     		cl = CL("new")
     		cl.local = True
    -		cl.files = ChangedFiles(ui, repo, pats, opts, taken=Taken(ui, repo))
    +		cl.files = ChangedFiles(ui, repo, pats, taken=Taken(ui, repo))
     		if not cl.files:
     			return None, "no files changed"
     	if opts.get('reviewer'):
    @@ -972,13 +847,362 @@ def CommandLineCL(ui, repo, pats, opts, defaultcc=None):
     			return None, err
     	return cl, ""
     
    -# reposetup replaces cmdutil.match with this wrapper,
    -# which expands the syntax @clnumber to mean the files
    -# in that CL.
    -original_match = None
    -global_repo = None
    -global_ui = None
    -def ReplacementForCmdutilMatch(ctx, pats=None, opts=None, globbed=False, default='relpath'):
    +#######################################################################
    +# Change list file management
    +
    +# Return list of changed files in repository that match pats.
    +# The patterns came from the command line, so we warn
    +# if they have no effect or cannot be understood.
    +def ChangedFiles(ui, repo, pats, taken=None):
    +	taken = taken or {}
    +	# Run each pattern separately so that we can warn about
    +	# patterns that didn't do anything useful.
    +	for p in pats:
    +		for f in hg_matchPattern(ui, repo, p, unknown=True):
    +			promptadd(ui, repo, f)
    +		for f in hg_matchPattern(ui, repo, p, removed=True):
    +			promptremove(ui, repo, f)
    +		files = hg_matchPattern(ui, repo, p, modified=True, added=True, removed=True)
    +		for f in files:
    +			if f in taken:
    +				ui.warn("warning: %s already in CL %s\n" % (f, taken[f].name))
    +		if not files:
    +			ui.warn("warning: %s did not match any modified files\n" % (p,))
    +
    +	# Again, all at once (eliminates duplicates)
    +	l = hg_matchPattern(ui, repo, *pats, modified=True, added=True, removed=True)
    +	l.sort()
    +	if taken:
    +		l = Sub(l, taken.keys())
    +	return l
    +
    +# Return list of changed files in repository that match pats and still exist.
    +def ChangedExistingFiles(ui, repo, pats, opts):
    +	l = hg_matchPattern(ui, repo, *pats, modified=True, added=True)
    +	l.sort()
    +	return l
    +
    +# Return list of files claimed by existing CLs
    +def Taken(ui, repo):
    +	all = LoadAllCL(ui, repo, web=False)
    +	taken = {}
    +	for _, cl in all.items():
    +		for f in cl.files:
    +			taken[f] = cl
    +	return taken
    +
    +# Return list of changed files that are not claimed by other CLs
    +def DefaultFiles(ui, repo, pats):
    +	return ChangedFiles(ui, repo, pats, taken=Taken(ui, repo))
    +
    +#######################################################################
    +# File format checking.
    +
    +def CheckFormat(ui, repo, files, just_warn=False):
    +	set_status("running gofmt")
    +	CheckGofmt(ui, repo, files, just_warn)
    +	CheckTabfmt(ui, repo, files, just_warn)
    +
    +# Check that gofmt run on the list of files does not change them
    +def CheckGofmt(ui, repo, files, just_warn):
    +	files = [f for f in files if (f.startswith('src/') or f.startswith('test/bench/')) and f.endswith('.go')]
    +	if not files:
    +		return
    +	cwd = os.getcwd()
    +	files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
    +	files = [f for f in files if os.access(f, 0)]
    +	if not files:
    +		return
    +	try:
    +		cmd = subprocess.Popen(["gofmt", "-l"] + files, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=sys.platform != "win32")
    +		cmd.stdin.close()
    +	except:
    +		raise hg_util.Abort("gofmt: " + ExceptionDetail())
    +	data = cmd.stdout.read()
    +	errors = cmd.stderr.read()
    +	cmd.wait()
    +	set_status("done with gofmt")
    +	if len(errors) > 0:
    +		ui.warn("gofmt errors:\n" + errors.rstrip() + "\n")
    +		return
    +	if len(data) > 0:
    +		msg = "gofmt needs to format these files (run hg gofmt):\n" + Indent(data, "\t").rstrip()
    +		if just_warn:
    +			ui.warn("warning: " + msg + "\n")
    +		else:
    +			raise hg_util.Abort(msg)
    +	return
    +
    +# Check that *.[chys] files indent using tabs.
    +def CheckTabfmt(ui, repo, files, just_warn):
    +	files = [f for f in files if f.startswith('src/') and re.search(r"\.[chys]$", f)]
    +	if not files:
    +		return
    +	cwd = os.getcwd()
    +	files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
    +	files = [f for f in files if os.access(f, 0)]
    +	badfiles = []
    +	for f in files:
    +		try:
    +			for line in open(f, 'r'):
    +				# Four leading spaces is enough to complain about,
    +				# except that some Plan 9 code uses four spaces as the label indent,
    +				# so allow that.
    +				if line.startswith('    ') and not re.match('    [A-Za-z0-9_]+:', line):
    +					badfiles.append(f)
    +					break
    +		except:
    +			# ignore cannot open file, etc.
    +			pass
    +	if len(badfiles) > 0:
    +		msg = "these files use spaces for indentation (use tabs instead):\n\t" + "\n\t".join(badfiles)
    +		if just_warn:
    +			ui.warn("warning: " + msg + "\n")
    +		else:
    +			raise hg_util.Abort(msg)
    +	return
    +
    +#######################################################################
    +# CONTRIBUTORS file parsing
    +
    +contributors = {}
    +
    +def ReadContributors(ui, repo):
    +	global contributors
    +	try:
    +		f = open(repo.root + '/CONTRIBUTORS', 'r')
    +	except:
    +		ui.write("warning: cannot open %s: %s\n" % (repo.root+'/CONTRIBUTORS', ExceptionDetail()))
    +		return
    +
    +	for line in f:
    +		# CONTRIBUTORS is a list of lines like:
    +		#	Person <email>
    +		#	Person <email> <alt-email>
    +		# The first email address is the one used in commit logs.
    +		if line.startswith('#'):
    +			continue
    +		m = re.match(r"([^<>]+\S)\s+(<[^<>\s]+>)((\s+<[^<>\s]+>)*)\s*$", line)
    +		if m:
    +			name = m.group(1)
    +			email = m.group(2)[1:-1]
    +			contributors[email.lower()] = (name, email)
    +			for extra in m.group(3).split():
    +				contributors[extra[1:-1].lower()] = (name, email)
    +
    +def CheckContributor(ui, repo, user=None):
    +	set_status("checking CONTRIBUTORS file")
    +	user, userline = FindContributor(ui, repo, user, warn=False)
    +	if not userline:
    +		raise hg_util.Abort("cannot find %s in CONTRIBUTORS" % (user,))
    +	return userline
    +
    +def FindContributor(ui, repo, user=None, warn=True):
    +	if not user:
    +		user = ui.config("ui", "username")
    +		if not user:
    +			raise hg_util.Abort("[ui] username is not configured in .hgrc")
    +	user = user.lower()
    +	m = re.match(r".*<(.*)>", user)
    +	if m:
    +		user = m.group(1)
    +
    +	if user not in contributors:
    +		if warn:
    +			ui.warn("warning: cannot find %s in CONTRIBUTORS\n" % (user,))
    +		return user, None
    +	
    +	user, email = contributors[user]
    +	return email, "%s <%s>" % (user, email)
    +
    +#######################################################################
    +# Mercurial helper functions.
    +# Read http://mercurial.selenic.com/wiki/MercurialApi before writing any of these.
    +# We use the ui.pushbuffer/ui.popbuffer + hg_commands.xxx tricks for all interaction
    +# with Mercurial.  It has proved the most stable as they make changes.
    +
    +# We require Mercurial 1.4 for now.  The minimum required version can be pushed forward.
    +
    +oldMessage = """
    +The code review extension requires Mercurial 1.4 or newer.
    +
    +To install a new Mercurial,
    +
    +	sudo easy_install mercurial
    +
    +works on most systems.
    +"""
    +
    +linuxMessage = """
    +You may need to clear your current Mercurial installation by running:
    +
    +	sudo apt-get remove mercurial mercurial-common
    +	sudo rm -rf /etc/mercurial
    +"""
    +
    +hgversion = hg_util.version()
    +
    +if hgversion < '1.4':
    +	msg = oldMessage
    +	if os.access("/etc/mercurial", 0):
    +		msg += linuxMessage
    +	raise hg_util.Abort(msg)
    +
    +from mercurial.hg import clean as hg_clean
    +from mercurial import cmdutil as hg_cmdutil
    +from mercurial import error as hg_error
    +from mercurial import match as hg_match
    +from mercurial import node as hg_node
    +
    +class uiwrap(object):
    +	def __init__(self, ui):
    +		self.ui = ui
    +		ui.pushbuffer()
    +		self.oldQuiet = ui.quiet
    +		ui.quiet = True
    +		self.oldVerbose = ui.verbose
    +		ui.verbose = False
    +	def output(self):
    +		ui = self.ui
    +		ui.quiet = self.oldQuiet
    +		ui.verbose = self.oldVerbose
    +		return ui.popbuffer()
    +
    +def hg_matchPattern(ui, repo, *pats, **opts):
    +	w = uiwrap(ui)
    +	hg_commands.status(ui, repo, *pats, **opts)
    +	text = w.output()
    +	ret = []
    +	prefix = os.path.realpath(repo.root)+'/'
    +	for line in text.split('\n'):
    +		f = line.split()
    +		if len(f) > 1:
    +			if len(pats) > 0:
    +				# Given patterns, Mercurial shows relative to cwd
    +				p = os.path.realpath(f[1])
    +				if not p.startswith(prefix):
    +					print >>sys.stderr, "File %s not in repo root %s.\n" % (p, prefix)
    +				else:
    +					ret.append(p[len(prefix):])
    +			else:
    +				# Without patterns, Mercurial shows relative to root (what we want)
    +				ret.append(f[1])
    +	return ret
    +
    +def hg_heads(ui, repo):
    +	w = uiwrap(ui)
    +	ret = hg_commands.heads(ui, repo)
    +	if ret:
    +		raise hg_util.Abort(ret)
    +	return w.output()
    +
    +noise = [
    +	"",
    +	"resolving manifests",
    +	"searching for changes",
    +	"couldn't find merge tool hgmerge",
    +	"adding changesets",
    +	"adding manifests",
    +	"adding file changes",
    +	"all local heads known remotely",
    +]
    +
    +def isNoise(line):
    +	line = str(line)
    +	for x in noise:
    +		if line == x:
    +			return True
    +	return False
    +
    +def hg_incoming(ui, repo):
    +	w = uiwrap(ui)
    +	ret = hg_commands.incoming(ui, repo, force=False, bundle="")
    +	if ret and ret != 1:
    +		raise hg_util.Abort(ret)
    +	return w.output()
    +
    +def hg_log(ui, repo, **opts):
    +	for k in ['date', 'keyword', 'rev', 'user']:
    +		if not opts.has_key(k):
    +			opts[k] = ""
    +	w = uiwrap(ui)
    +	ret = hg_commands.log(ui, repo, **opts)
    +	if ret:
    +		raise hg_util.Abort(ret)
    +	return w.output()
    +
    +def hg_outgoing(ui, repo, **opts):
    +	w = uiwrap(ui)
    +	ret = hg_commands.outgoing(ui, repo, **opts)
    +	if ret and ret != 1:
    +		raise hg_util.Abort(ret)
    +	return w.output()
    +
    +def hg_pull(ui, repo, **opts):
    +	w = uiwrap(ui)
    +	ui.quiet = False
    +	ui.verbose = True  # for file list
    +	err = hg_commands.pull(ui, repo, **opts)
    +	for line in w.output().split('\n'):
    +		if isNoise(line):
    +			continue
    +		if line.startswith('moving '):
    +			line = 'mv ' + line[len('moving '):]
    +		if line.startswith('getting ') and line.find(' to ') >= 0:
    +			line = 'mv ' + line[len('getting '):]
    +		if line.startswith('getting '):
    +			line = '+ ' + line[len('getting '):]
    +		if line.startswith('removing '):
    +			line = '- ' + line[len('removing '):]
    +		ui.write(line + '\n')
    +	return err
    +
    +def hg_push(ui, repo, **opts):
    +	w = uiwrap(ui)
    +	ui.quiet = False
    +	ui.verbose = True
    +	err = hg_commands.push(ui, repo, **opts)
    +	for line in w.output().split('\n'):
    +		if not isNoise(line):
    +			ui.write(line + '\n')
    +	return err
    +
    +def hg_commit(ui, repo, *pats, **opts):
    +	return hg_commands.commit(ui, repo, *pats, **opts)
    +
    +#######################################################################
    +# Mercurial precommit hook to disable commit except through this interface.
    +
    +commit_okay = False
    +
    +def precommithook(ui, repo, **opts):
    +	if commit_okay:
    +		return False  # False means okay.
    +	ui.write("\ncodereview extension enabled; use mail, upload, or submit instead of commit\n\n")
    +	return True
    +
    +#######################################################################
    +# @clnumber file pattern support
    +
    +# We replace scmutil.match with the MatchAt wrapper to add the @clnumber pattern.
    +
    +match_repo = None
    +match_ui = None
    +match_orig = None
    +
    +def InstallMatch(ui, repo):
    +	global match_repo
    +	global match_ui
    +	global match_orig
    +
    +	match_ui = ui
    +	match_repo = repo
    +
    +	from mercurial import scmutil
    +	match_orig = scmutil.match
    +	scmutil.match = MatchAt
    +
    +def MatchAt(ctx, pats=None, opts=None, globbed=False, default='relpath'):
     	taken = []
     	files = []
     	pats = pats or []
    @@ -988,101 +1202,29 @@ def ReplacementForCmdutilMatch(ctx, pats=None, opts=None, globbed=False, default
     	if p.startswith('@'):
     		taken.append(p)
     		clname = p[1:]
    -		if not GoodCLName(clname):
    -			raise util.Abort("invalid CL name " + clname)
    -		cl, err = LoadCL(global_repo.ui, global_repo, clname, web=False)
    -		if err != '':
    -			raise util.Abort("loading CL " + clname + ": " + err)
    -		if not cl.files:
    -			raise util.Abort("no files in CL " + clname)
    -		files = Add(files, cl.files)
    +		if clname == "default":
    +			files = DefaultFiles(match_ui, match_repo, [])
    +		else:
    +			if not GoodCLName(clname):
    +				raise hg_util.Abort("invalid CL name " + clname)
    +			cl, err = LoadCL(match_ui, match_repo, clname, web=False)
    +			if err != '':
    +				raise hg_util.Abort("loading CL " + clname + ": " + err)
    +			if not cl.files:
    +				raise hg_util.Abort("no files in CL " + clname)
    +			files = Add(files, cl.files)
     	pats = Sub(pats, taken) + ['path:'+f for f in files]
     
     	# work-around for http://selenic.com/hg/rev/785bbc8634f8
    -	if hgversion >= '1.9' and not hasattr(ctx, 'match'):
    +	if not hasattr(ctx, 'match'):
     		ctx = ctx[None]
    -	return original_match(ctx, pats=pats, opts=opts, globbed=globbed, default=default)
    -
    -def RelativePath(path, cwd):
    -	n = len(cwd)
    -	if path.startswith(cwd) and path[n] == '/':
    -		return path[n+1:]
    -	return path
    -
    -def CheckFormat(ui, repo, files, just_warn=False):
    -	set_status("running gofmt")
    -	CheckGofmt(ui, repo, files, just_warn)
    -	CheckTabfmt(ui, repo, files, just_warn)
    -
    -# Check that gofmt run on the list of files does not change them
    -def CheckGofmt(ui, repo, files, just_warn):
    -	files = [f for f in files if (f.startswith('src/') or f.startswith('test/bench/')) and f.endswith('.go')]
    -	if not files:
    -		return
    -	cwd = os.getcwd()
    -	files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
    -	files = [f for f in files if os.access(f, 0)]
    -	if not files:
    -		return
    -	try:
    -		cmd = subprocess.Popen(["gofmt", "-l"] + files, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=sys.platform != "win32")
    -		cmd.stdin.close()
    -	except:
    -		raise util.Abort("gofmt: " + ExceptionDetail())
    -	data = cmd.stdout.read()
    -	errors = cmd.stderr.read()
    -	cmd.wait()
    -	set_status("done with gofmt")
    -	if len(errors) > 0:
    -		ui.warn("gofmt errors:\n" + errors.rstrip() + "\n")
    -		return
    -	if len(data) > 0:
    -		msg = "gofmt needs to format these files (run hg gofmt):\n" + Indent(data, "\t").rstrip()
    -		if just_warn:
    -			ui.warn("warning: " + msg + "\n")
    -		else:
    -			raise util.Abort(msg)
    -	return
    -
    -# Check that *.[chys] files indent using tabs.
    -def CheckTabfmt(ui, repo, files, just_warn):
    -	files = [f for f in files if f.startswith('src/') and re.search(r"\.[chys]$", f)]
    -	if not files:
    -		return
    -	cwd = os.getcwd()
    -	files = [RelativePath(repo.root + '/' + f, cwd) for f in files]
    -	files = [f for f in files if os.access(f, 0)]
    -	badfiles = []
    -	for f in files:
    -		try:
    -			for line in open(f, 'r'):
    -				# Four leading spaces is enough to complain about,
    -				# except that some Plan 9 code uses four spaces as the label indent,
    -				# so allow that.
    -				if line.startswith('    ') and not re.match('    [A-Za-z0-9_]+:', line):
    -					badfiles.append(f)
    -					break
    -		except:
    -			# ignore cannot open file, etc.
    -			pass
    -	if len(badfiles) > 0:
    -		msg = "these files use spaces for indentation (use tabs instead):\n\t" + "\n\t".join(badfiles)
    -		if just_warn:
    -			ui.warn("warning: " + msg + "\n")
    -		else:
    -			raise util.Abort(msg)
    -	return
    +	return match_orig(ctx, pats=pats, opts=opts, globbed=globbed, default=default)
    
    • ChangedFiles, ChangedExistingFiles, Taken, DefaultFiles といったファイル管理ヘルパー関数がリファクタリングされ、hg_matchPattern を利用するように変更。
    • CheckFormat, CheckGofmt, CheckTabfmt 関数が追加され、コードフォーマットのチェック機能が導入。
    • CONTRIBUTORS ファイルの読み込みとチェックに関する関数 (ReadContributors, CheckContributor, FindContributor) が追加。
    • Mercurial の scmutil.match をラップする MatchAt 関数が導入され、@clnumber パターンをサポート。

lib/codereview/test.sh の新規追加

#!/bin/bash
# Copyright 2011 The Go Authors.  All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.

# Test the code review plugin.
# Assumes a local Rietveld is running using the App Engine SDK
# at http://localhost:7777/
#
# dev_appserver.py -p 7777 $HOME/pub/rietveld

codereview_script=$(pwd)/codereview.py
server=localhost:7777
master=/tmp/go.test
clone1=/tmp/go.test1
clone2=/tmp/go.test2
export HGEDITOR=true

must() {
	if ! "$@"; then
		echo "$@" failed >&2
		exit 1
	fi
}

not() {
	if "$@"; then
		false
	else
		true
	fi
}

status() {
	echo '+++' "$@" >&2
}

firstcl() {
	hg pending | sed 1q | tr -d ':'
}

# Initial setup.
status Create repositories.
rm -rf $master $clone1 $clone2
mkdir $master
cd $master
must hg init .
echo Initial state >file
must hg add file
must hg ci -m 'first commit' file
must hg clone $master $clone1
must hg clone $master $clone2

echo "
[ui]
username=Grace R Emlin <gre@golang.org>
[extensions]
codereview=$codereview_script
[codereview]
server=$server
" >>$clone1/.hg/hgrc
cp $clone1/.hg/hgrc $clone2/.hg/hgrc

status Codereview should be disabled.
cd $clone1
must hg status
must not hg pending

status Enabling code review.
must mkdir lib lib/codereview
must touch lib/codereview/codereview.cfg

status Code review should work even without CONTRIBUTORS.
must hg pending

status Add CONTRIBUTORS.
echo 'Grace R Emlin <gre@golang.org>' >CONTRIBUTORS
must hg add lib/codereview/codereview.cfg CONTRIBUTORS

status First submit.
must hg submit -r gre@golang.org -m codereview \
	lib/codereview/codereview.cfg CONTRIBUTORS

status Should see change in other client.
cd $clone2
must hg pull -u
must test -f lib/codereview/codereview.cfg
must test -f CONTRIBUTORS

test_clpatch() {
	# The email address must be test@example.com to match
	# the test code review server's default user.
	# Clpatch will check.
	
	cd $clone1
	# Tried to use UTF-8 here to test that, but dev_appserver.py crashes.  Ha ha.
	if false; then
		status Using UTF-8.
		name="Grácè T Emlïn <test@example.com>"
	else
		status Using ASCII.
		name="Grace T Emlin <test@example.com>"
	fi
	echo "$name" >>CONTRIBUTORS
	cat .hg/hgrc | sed "s/Grace.*/$name/" >/tmp/x && mv /tmp/x .hg/hgrc
	echo '
Reviewer: gre@golang.org
Description:
	CONTRIBUTORS: add $name
Files:
	CONTRIBUTORS
'	| must hg change -i
	num=$(hg pending | sed 1q | tr -d :)
	
	status Patch CL.
	cd $clone2
	must hg clpatch $num
	must [ "$num" = "$(firstcl)" ]
	must hg submit $num
	
	status Issue should be open with no reviewers.
	must curl http://$server/api/$num >/tmp/x
	must not grep '"closed":true' /tmp/x
	must grep '"reviewers":\[\]' /tmp/x
	
	status Sync should close issue.
	cd $clone1
	must hg sync
	must curl http://$server/api/$num >/tmp/x
	must grep '"closed":true' /tmp/x
	must grep '"reviewers":\[\]' /tmp/x
	must [ "$(firstcl)" = "" ]
}

test_reviewer() {
	status Submit without reviewer should fail.
	cd $clone1
	echo dummy >dummy
	must hg add dummy
	echo '
Description:
	no reviewer
Files:
	dummy
'	| must hg change -i
	num=$(firstcl)
	must not hg submit $num
	must hg revert dummy
	must rm dummy
	must hg change -d $num
}

test_linearity() {
	status Linearity of changes.
	cd $clone1
	echo file1 >file1
	must hg add file1
	echo '
Reviewer: gre@golang.org
Description: file1
Files: file1
	' | must hg change -i
	must hg submit $(firstcl)
	
	cd $clone2
	echo file2 >file2
	must hg add file2
	echo '
Reviewer: gre@golang.org
Description: file2
Files: file2
	' | must hg change -i
	must not hg submit $(firstcl)
	must hg sync
	must hg submit $(firstcl)
}

test_restrict() {
	status Cannot use hg ci.
	cd $clone1
	echo file1a >file1a
	hg add file1a
	must not hg ci -m commit file1a
	must rm file1a
	must hg revert file1a
	
	status Cannot use hg rollback.
	must not hg rollback
	
	status Cannot use hg backout
	must not hg backout -r -1
}

test_reviewer
test_clpatch
test_linearity
test_restrict
status ALL TESTS PASSED.
  • Mercurial リポジトリの初期設定、codereview 拡張機能の有効化、CONTRIBUTORS ファイルの追加など、テスト環境の準備を行う。
  • hg submithg clpatchhg sync などの主要な codereview コマンドの動作を検証。
  • レビュー担当者がいない場合の hg submit の失敗、複数のチェンジリストの線形性、および hg cihg rollback といった組み込み Mercurial コマンドが codereview 拡張機能によって制限されていることのテストが含まれる。

コアとなるコードの解説

lib/codereview/codereview.py

このファイルは、Mercurial の codereview 拡張機能の主要なロジックを含んでいます。

  • Mercurial API のラッパー: 以前は Mercurial の内部 API を直接呼び出していましたが、このコミットでは hg_commands, hg_util, hg_node といったエイリアスを導入し、uiwrap クラスを介してこれらの API を呼び出すことで、Mercurial のバージョンアップに対する堅牢性を高めています。これにより、Mercurial の内部実装が変更されても、この拡張機能のコードを大幅に修正する必要が少なくなります。
  • StatusThread: 長時間かかる操作(例: Rietveld サーバーとの通信)中に、ユーザーに進行状況を伝えるためのバックグラウンドスレッドです。これにより、CLI アプリケーションのユーザーエクスペリエンスが向上します。
  • ファイル管理ヘルパー関数: ChangedFiles, Taken, DefaultFiles などの関数は、リポジトリ内の変更されたファイルを効率的に特定し、既存のチェンジリストとの関連を管理するためのものです。これらは hg_matchPattern を利用して、Mercurial の status コマンドの出力を解析します。
  • コードフォーマットチェック: CheckGofmtCheckTabfmt は、Go 言語のコードが gofmt の規約に従っているか、および C/C++/Objective-C/Swift コードがタブインデントを使用しているかを自動的に検証します。これにより、プロジェクト全体のコードスタイルの一貫性が保たれます。
  • CONTRIBUTORS ファイルの処理: ReadContributorsCONTRIBUTORS ファイルを読み込み、貢献者の情報をメモリにキャッシュします。CheckContributor は、コミットを行うユーザーがこのファイルに登録されているかを確認し、登録されていない場合はエラーを発生させます。これは、プロジェクトの貢献ポリシーを強制するために使用されます。
  • Mercurial コマンドの制限: precommithook は、hg commithg rollback といった Mercurial の組み込みコマンドが codereview 拡張機能が有効な場合に実行されるのを防ぎます。これにより、開発者は hg mail, hg upload, hg submit といった codereview 拡張機能が提供するワークフローに従うように強制されます。

lib/codereview/test.sh

このファイルは、codereview.py 拡張機能の動作を検証するためのシェルスクリプトベースのテストスイートです。

  • テスト環境のセットアップ: 複数の Mercurial リポジトリ (master, clone1, clone2) を作成し、codereview 拡張機能を有効にするための hgrc 設定を適用します。また、codereview.cfgCONTRIBUTORS ファイルといった必要な設定ファイルも準備します。
  • 機能テスト:
    • test_clpatch: チェンジリストのパッチ適用 (hg clpatch) と、その後の hg submit および hg sync の動作を検証します。Rietveld サーバーとの連携もシミュレートし、チェンジリストの状態が正しく更新されることを確認します。
    • test_reviewer: レビュー担当者が指定されていない場合の hg submit が失敗することを確認します。
    • test_linearity: 複数のチェンジリストが線形に適用されること、つまり、依存関係のあるチェンジリストが正しい順序でコミットされることを検証します。
  • 制限テスト:
    • test_restrict: codereview 拡張機能が有効な場合に、hg ci (commit)、hg rollbackhg backout といった Mercurial の組み込みコマンドが正しく無効化されていることを確認します。これは、開発者が codereview 拡張機能のワークフローに従うことを強制するための重要なテストです。

これらのテストは、codereview 拡張機能の主要な機能が期待通りに動作すること、および意図しないサイドエフェクトがないことを保証するのに役立ちます。

関連リンク

参考にした情報源リンク

  • Mercurial のドキュメントや API リファレンス (特に mercurial.commands, mercurial.util, mercurial.scmutil に関する情報)
  • Rietveld の設計に関する情報 (特にコードレビューのワークフローとチェンジリストの概念)
  • Go 言語の gofmt の使用方法と目的に関する情報
  • Python の json モジュールの歴史と互換性に関する情報
  • 分散型バージョン管理システム (DVCS) の一般的な概念
  • シェルスクリプトのテストフレームワークに関する一般的な知識