[インデックス 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 の変更に追従するための複雑な条件分岐や、冗長なコード、そしてテストの不足が見られました。
具体的には、以下の問題に対処しています。
- Mercurial API の変更への対応: Mercurial のバージョンアップに伴い、
cmdutil.match
やcmdutil.revpair
といった一部の API がscmutil
モジュールに移動していました。このコミット以前のコードは、これらの変更に古い方法で対応しようとしており、コードが複雑化していました。 - コードの重複と複雑性: Mercurial のコマンド実行やファイル操作に関するヘルパー関数が、一貫性のない方法で実装されており、コードの重複や可読性の低下を招いていました。
- テストの不足:
codereview
拡張機能の動作を検証するための自動テストが不足しており、変更が既存の機能に与える影響を把握しにくい状況でした。 - Python バージョン要件の明確化:
json
パッケージの取り扱いに関して、Python 2.6 以降の標準ライブラリとしてのjson
の利用を明確にし、それ以前のバージョンでのsimplejson
やdjango.utils.simplejson
へのフォールバックロジックを削除することで、コードを簡素化しています。 - 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.status
をuiwrap
を介して呼び出し、その出力を解析してファイルリストを返します。これにより、ファイルステータスの取得ロジックが一元化され、より堅牢になっています。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 commit
や hg rollback
などが誤って使用されるのを防ぐために、これらのコマンドを無効化するフック (precommithook
) が導入されました。これにより、開発者は hg mail
, hg upload
, hg submit
といった codereview
拡張機能が提供するコマンドを使用するように強制されます。
コアとなるコードの変更箇所
lib/codereview/codereview.py
の変更点
-
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.commands
をhg_commands
として、mercurial.util
をhg_util
としてインポートするように変更。 - Python 2.6 以降の
json
モジュールを直接インポートするように変更し、古い Python バージョンとの互換性レイヤーを削除。 - Mercurial の最小バージョン要件を 1.4 に引き上げ、それ以前のバージョンではエラーで終了するように変更。
- Mercurial の古い API (
-
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
クラスと関連関数が追加。
- 長時間実行される処理中にステータスを表示するための
-
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 submit
、hg clpatch
、hg sync
などの主要なcodereview
コマンドの動作を検証。- レビュー担当者がいない場合の
hg submit
の失敗、複数のチェンジリストの線形性、およびhg ci
やhg 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
コマンドの出力を解析します。 - コードフォーマットチェック:
CheckGofmt
とCheckTabfmt
は、Go 言語のコードがgofmt
の規約に従っているか、および C/C++/Objective-C/Swift コードがタブインデントを使用しているかを自動的に検証します。これにより、プロジェクト全体のコードスタイルの一貫性が保たれます。 CONTRIBUTORS
ファイルの処理:ReadContributors
はCONTRIBUTORS
ファイルを読み込み、貢献者の情報をメモリにキャッシュします。CheckContributor
は、コミットを行うユーザーがこのファイルに登録されているかを確認し、登録されていない場合はエラーを発生させます。これは、プロジェクトの貢献ポリシーを強制するために使用されます。- Mercurial コマンドの制限:
precommithook
は、hg commit
やhg rollback
といった Mercurial の組み込みコマンドがcodereview
拡張機能が有効な場合に実行されるのを防ぎます。これにより、開発者はhg mail
,hg upload
,hg submit
といったcodereview
拡張機能が提供するワークフローに従うように強制されます。
lib/codereview/test.sh
このファイルは、codereview.py
拡張機能の動作を検証するためのシェルスクリプトベースのテストスイートです。
- テスト環境のセットアップ: 複数の Mercurial リポジトリ (
master
,clone1
,clone2
) を作成し、codereview
拡張機能を有効にするためのhgrc
設定を適用します。また、codereview.cfg
やCONTRIBUTORS
ファイルといった必要な設定ファイルも準備します。 - 機能テスト:
test_clpatch
: チェンジリストのパッチ適用 (hg clpatch
) と、その後のhg submit
およびhg sync
の動作を検証します。Rietveld サーバーとの連携もシミュレートし、チェンジリストの状態が正しく更新されることを確認します。test_reviewer
: レビュー担当者が指定されていない場合のhg submit
が失敗することを確認します。test_linearity
: 複数のチェンジリストが線形に適用されること、つまり、依存関係のあるチェンジリストが正しい順序でコミットされることを検証します。
- 制限テスト:
test_restrict
:codereview
拡張機能が有効な場合に、hg ci
(commit)、hg rollback
、hg backout
といった Mercurial の組み込みコマンドが正しく無効化されていることを確認します。これは、開発者がcodereview
拡張機能のワークフローに従うことを強制するための重要なテストです。
これらのテストは、codereview
拡張機能の主要な機能が期待通りに動作すること、および意図しないサイドエフェクトがないことを保証するのに役立ちます。
関連リンク
- Mercurial 公式サイト: https://www.mercurial-scm.org/
- Rietveld (Google Code Archive): https://code.google.com/archive/p/rietveld/ (Rietveld は現在メンテナンスされていませんが、歴史的な情報源として)
- Go 言語公式サイト: https://go.dev/
gofmt
ドキュメント: https://go.dev/blog/gofmt
参考にした情報源リンク
- Mercurial のドキュメントや API リファレンス (特に
mercurial.commands
,mercurial.util
,mercurial.scmutil
に関する情報) - Rietveld の設計に関する情報 (特にコードレビューのワークフローとチェンジリストの概念)
- Go 言語の
gofmt
の使用方法と目的に関する情報 - Python の
json
モジュールの歴史と互換性に関する情報 - 分散型バージョン管理システム (DVCS) の一般的な概念
- シェルスクリプトのテストフレームワークに関する一般的な知識