[インデックス 12994] ファイルの概要
このコミットは、Go言語のコードレビューダッシュボード(misc/dashboard/codereview
)における機能改善を目的としています。具体的には、コードレビューにアサインされたレビュアーがまだレビュープロセスに参加していない場合に、自動的にメール通知を送る機能を追加しています。これにより、レビュアーが変更リスト(CL: Change List)を見落とすことなく、迅速にレビューを開始できるよう支援します。
コミット
- コミットハッシュ:
4335ec9eeba145032bbe24c53bd130cb7ca49394
- Author: David Symonds dsymonds@golang.org
- Date: Mon Apr 30 22:03:56 2012 +1000
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4335ec9eeba145032bbe24c53bd130cb7ca49394
元コミット内容
misc/dashboard/codereview: send mail to assigned reviewers if they aren't already looped in.
R=golang-dev, bradfitz, r
CC=golang-dev
https://golang.org/cl/6128054
変更の背景
Go言語のプロジェクトでは、Rietveldというコードレビューツールが使用されていました。このツールは、変更リスト(CL)の提出、レビュー、承認のプロセスを管理します。しかし、レビュアーがCLにアサインされた際に、そのレビュアーがRietveld上でまだCLのレビューに参加していない場合、通知が不足し、レビューの開始が遅れる可能性がありました。
このコミットの背景には、以下のような課題があったと考えられます。
- 通知の不足: レビュアーがCLにアサインされても、Rietveldの通知システムだけでは見落とす可能性があった。
- レビュープロセスの遅延: レビュアーがCLの存在に気づかないことで、レビュー開始が遅れ、結果として変更がマージされるまでの時間が長くなる。
- 手動でのフォローアップの負担: アサインした側がレビュアーに手動で連絡を取る必要があり、手間がかかる。
このコミットは、これらの課題を解決し、コードレビュープロセスをよりスムーズかつ効率的に進めることを目的としています。特に、ダッシュボード側でレビュアーのアサインを検知し、必要に応じて自動的にメールを送信することで、レビュアーの早期関与を促します。
前提知識の解説
このコミットを理解するためには、以下の技術や概念に関する知識が必要です。
- Rietveld: Googleが開発したオープンソースのコードレビューツール。Goプロジェクトの初期のコードレビューに利用されていました。変更リスト(CL)の管理、コメントの追加、承認(LGTM: Looks Good To Me)などの機能を提供します。
- Google App Engine (GAE): Googleが提供するPaaS(Platform as a Service)。Webアプリケーションやモバイルバックエンドを構築・ホストするためのプラットフォームです。このダッシュボードアプリケーションはGAE上で動作しています。
appengine/datastore
: GAEが提供するNoSQLデータベースサービス。アプリケーションのデータを永続化するために使用されます。このコミットでは、CLの情報をDatastoreに保存・取得しています。appengine/mail
: GAEが提供するメール送信サービス。アプリケーションからメールを送信するために使用されます。appengine/delay
: GAEの機能で、関数呼び出しを遅延実行(非同期実行)させるためのもの。タスクキュー(Task Queue)を利用して、時間のかかる処理やバックグラウンド処理を非同期で行う際に便利です。delay.Func
は、指定した関数を後で実行するためのラッパーを生成します。appengine/taskqueue
: GAEのタスクキューサービス。非同期処理や定期的な処理を実行するためのキューを提供します。appengine/delay
はこのタスクキューを利用して遅延実行を実現します。appengine/urlfetch
: GAEが提供するURLフェッチサービス。アプリケーションから外部のHTTP(S)リソースにアクセスするために使用されます。このコミットでは、RietveldのAPIにアクセスするために利用されています。appengine/user
: GAEが提供するユーザー認証サービス。現在のユーザー情報を取得するために使用されます。
- CL (Change List): コードレビューの対象となる一連の変更。Rietveldでは、各変更のまとまりをCLと呼びます。
- LGTM (Looks Good To Me): コードレビューにおいて、レビュアーが変更を承認する際に使用する略語。Rietveldでは、LGTMのコメントが付くと、その変更が承認されたと見なされます。
- JSON (JavaScript Object Notation): 軽量なデータ交換フォーマット。RietveldのAPIからのレスポンスはJSON形式で提供されます。
- Go言語の標準ライブラリ:
net/http
(HTTPサーバーとクライアント)、encoding/json
(JSONのエンコード/デコード)、fmt
(フォーマット済みI/O)、sort
(ソート)、strings
(文字列操作)、time
(時間操作)などが使用されています。
技術的詳細
このコミットの主要な技術的変更は、misc/dashboard/codereview/dashboard/cl.go
と misc/dashboard/codereview/dashboard/people.go
の2つのファイルに集中しています。
cl.go
の変更点
-
CL
構造体の拡張:Subject
(string): CLの件名。Rietveldから取得し、メールの件名に使用されます。Recipients
([]string): CLに関連するメールの受信者リスト。Rietveldのメッセージから抽出されます。 これらのフィールドは、メール通知の際に必要な情報として追加されました。
-
sendMailLater
の導入:var sendMailLater = delay.Func("send-mail", mail.Send)
appengine/delay
パッケージのdelay.Func
を使用して、mail.Send
関数を非同期で実行するためのラッパーが定義されています。これにより、メール送信処理がHTTPリクエストの応答時間をブロックすることなく、バックグラウンドで実行されるようになります。これは、ユーザーエクスペリエンスの向上と、GAEのHTTPリクエストタイムアウトの回避に貢献します。
-
handleAssign
関数のロジック変更:- この関数は、ユーザーがCLにレビュアーをアサインする際に呼び出されます。
- レビュアーの存在チェック: アサインされたレビュアーが
preferredEmail
マップに存在するかどうかを確認します。 - Rietveld APIへのアクセス:
urlfetch.Client(c).Get(url + "?field=reviewers")
を使用して、RietveldのAPIから現在のCLのレビュアーリストを取得します。これは、アサインされたレビュアーが既にRietveld上でレビュアーとして登録されているかを確認するためです。 - JSONレスポンスのパース: Rietveld APIからのJSONレスポンスを
apiResp
構造体にデコードします。 - レビュアーの確認とメール送信:
- 取得したレビュアーリストの中に、アサインされたレビュアーが含まれていない場合 (
!found
)、以下の処理が実行されます。 - メール送信のトリガー:
mail.Message
構造体を作成し、sendMailLater.Call(c, msg)
を呼び出して、非同期でメールを送信します。 - メールの内容:
Sender
: 現在のユーザーのメールアドレス。To
: アサインされたレビュアーの優先メールアドレス。Cc
: CLの既存の受信者リスト。Subject
: CLの件名とCL番号(Rietveldのメールスレッドに合わせるため)。Body
: "R=<レビュアー名>\n\n(sent by gocodereview)" という形式のメッセージ。これは、Rietveldのコメント形式に似せており、レビュアーがこのメールに返信することでRietveld上でレビュアーとして追加されることを期待しています(ただし、このコミット時点ではダッシュボードからRietveldのレビュアーを直接追加する機能は実装されていません)。
- 取得したレビュアーリストの中に、アサインされたレビュアーが含まれていない場合 (
- トランザクション処理:
datastore.RunInTransaction
を使用して、CLの更新をアトミックに実行します。これにより、データの一貫性が保たれます。
-
updateCL
関数のロジック変更:- この関数は、RietveldからCLの最新情報を取得し、Datastoreに保存されているCLオブジェクトを更新します。
- Rietveld APIレスポンスの拡張: Rietveld APIから取得するCL情報に
Subject
フィールドが追加されました。 - メッセージ受信者の抽出: Rietveldのメッセージオブジェクトから
Recipients
フィールドを抽出し、CLオブジェクトのRecipients
フィールドに格納します。これにより、CLに関連するすべてのメール受信者(CCリスト)を追跡できるようになります。 - LGTMの重複排除 (TODO): コメントでLGTMの重複排除が将来の課題として示されています。
- 受信者リストのソート:
cl.Recipients
をソートすることで、一貫性を保ちます。
people.go
の変更点
-
preferredEmail
マップの追加:preferredEmail = make(map[string]string)
:person
(例: "rsc") からその人物の優先メールアドレス (例: "rsc@golang.org") へのマッピングを保持する新しいマップが追加されました。- これは、メール送信時にレビュアーの適切なメールアドレスを特定するために使用されます。特に、
@golang.org
と@google.com
の両方のアカウントを持つユーザーに対して、コードレビューには@golang.org
アドレスを優先して使用するという方針が反映されています。
-
init
関数の変更:gophers
リストの各人物に対して、preferredEmail[p] = p + "@golang.org"
の行が追加され、優先メールアドレスが設定されるようになりました。
これらの変更により、ダッシュボードはRietveldの情報をより詳細に取得し、レビュアーのアサイン状況に応じて適切なメール通知を自動的に行うことができるようになりました。
コアとなるコードの変更箇所
misc/dashboard/codereview/dashboard/cl.go
CL
構造体にSubject
とRecipients
フィールドが追加。sendMailLater
というdelay.Func
が定義され、mail.Send
を非同期実行可能に。handleAssign
関数内で、レビュアーがRietveldに登録されているかを確認するロジックが追加され、登録されていない場合にメールを送信する処理が実装された。- Rietveldの
/fields
APIエンドポイントをurlfetch
で呼び出し、レビュアーリストを取得。 - 取得したレビュアーリストとアサインされたレビュアーを比較。
- レビュアーがRietveldに登録されていない場合、
mail.Message
を構築し、sendMailLater.Call
でメールを送信。
- Rietveldの
updateCL
関数内で、Rietveld APIのレスポンスからSubject
とメッセージのRecipients
を抽出してCL
構造体に格納する処理が追加。Recipients
リストのソート処理が追加。
misc/dashboard/codereview/dashboard/people.go
preferredEmail
というmap[string]string
が追加され、人物名から優先メールアドレスへのマッピングを保持。init
関数内で、gophers
リストの各人物に対してpreferredEmail
マップに優先メールアドレス(@golang.org
ドメイン)を設定する処理が追加。
コアとなるコードの解説
cl.go
の handleAssign
関数におけるメール送信ロジック
// ... (既存のCL取得、ユーザー認証、レビュアー検証ロジック) ...
if rev != "" { // レビュアーが指定されている場合
// RietveldからCLのレビュアーリストを取得
url := codereviewBase + "/" + n + "/fields"
resp, err := urlfetch.Client(c).Get(url + "?field=reviewers")
// ... (エラーハンドリング) ...
var apiResp struct {
Reviewers []string `json:"reviewers"`
}
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
// ... (エラーハンドリング) ...
}
found := false
for _, r := range apiResp.Reviewers {
if emailToPerson[r] == rev { // Rietveldのレビュアーリストにアサインされたレビュアーが含まれているかチェック
found = true
break
}
}
if !found { // Rietveldにレビュアーとして登録されていない場合
c.Infof("Adding %v as a reviewer of CL %v", rev, n)
// CLの情報をDatastoreから取得(メール送信に必要なSubjectなどを取得するため)
cl := new(CL)
err := datastore.Get(c, key, cl)
// ... (エラーハンドリング) ...
// Subjectが空でない場合のみメールを送信
if cl.Subject != "" {
msg := &mail.Message{
Sender: u.Email, // 現在のユーザー(アサインした人)
To: []string{preferredEmail[rev]}, // アサインされたレビュアーの優先メールアドレス
Cc: cl.Recipients, // CLの既存の受信者リスト
Subject: cl.Subject + " (issue " + n + ")", // Rietveldのスレッドに合わせる件名
Body: "R=" + rev + "\n\n(sent by gocodereview)", // レビュアー追加を促す本文
}
sendMailLater.Call(c, msg) // 非同期でメール送信
}
}
}
// ... (DatastoreでのCL更新トランザクション) ...
このコードブロックは、レビュアーがCLにアサインされた際に、そのレビュアーがRietveld上でまだCLのレビュアーとして認識されていない場合に、自動的にメールを送信する核心部分です。urlfetch
を使ってRietveldのAPIを叩き、現在のレビュアーリストを取得し、それとアサインされたレビュアーを比較しています。もしレビュアーがRietveldにまだ登録されていないと判断された場合、mail.Message
を構築し、delay.Func
でラップされた sendMailLater
を使って非同期でメールを送信します。これにより、レビュアーはCLの存在と自身がアサインされたことをメールで通知され、レビュープロセスへの参加が促されます。
cl.go
の updateCL
関数における受信者リストの収集
// ... (Rietveld APIレスポンスのデコード) ...
cl := &CL{
// ... (既存のフィールド設定) ...
Subject: apiResp.Subject, // Rietveldから取得した件名
// ...
}
// ... (Created, Modified, Closed, Owner, Author, Description, FirstLine の設定) ...
rcpt := make(map[string]bool) // 受信者メールアドレスの重複を避けるためのマップ
for _, msg := range apiResp.Messages { // Rietveldの各メッセージをイテレート
// ... (LGTMの処理) ...
for _, r := range msg.Recipients { // 各メッセージの受信者リストをイテレート
rcpt[r] = true // マップに追加して重複を排除
}
}
for r := range rcpt { // 重複排除された受信者メールアドレスをCLのRecipientsスライスに追加
cl.Recipients = append(cl.Recipients, r)
}
sort.Strings(cl.Recipients) // 受信者リストをソート
// ... (Datastoreへの保存) ...
この部分では、Rietveldから取得したCLの全メッセージを解析し、それぞれのメッセージに含まれる受信者(Recipients
)を収集しています。map[string]bool
を一時的に使用することで、重複するメールアドレスを排除し、最終的に一意の受信者リストを cl.Recipients
に格納しています。これにより、CLに関連するすべての関係者(CCリスト)を正確に把握し、後続のメール通知などで利用できるようになります。
people.go
の preferredEmail
マップの初期化
var (
emailToPerson = make(map[string]string) // email => person
preferredEmail = make(map[string]string) // person => email
personList []string
)
func init() {
// People we assume have golang.org and google.com accounts,
// and prefer to use their golang.org address for code review.
gophers := [...]string{
// ... (gophersのリスト) ...
}
for _, p := range gophers {
personList = append(personList, p)
emailToPerson[p+"@golang.org"] = p
emailToPerson[p+"@google.com"] = p
preferredEmail[p] = p + "@golang.org" // ここで優先メールアドレスを設定
}
sort.Strings(personList)
}
preferredEmail
マップは、特定の人物(例: "bradfitz")に対して、コードレビューで優先的に使用すべきメールアドレス(例: "bradfitz@golang.org")をマッピングするために導入されました。これにより、システムはレビュアーにメールを送信する際に、常に最も適切なアドレスを選択できるようになります。これは、Goプロジェクトのメンバーが複数のメールアドレスを持つ可能性があるという現実に対応するためのものです。
関連リンク
- 元の変更リスト (CL): https://golang.org/cl/6128054
参考にした情報源リンク
- Google App Engine Documentation (当時のもの):
- Rietveld (Wikipedia): https://en.wikipedia.org/wiki/Rietveld
- Go Code Review Comments (Rietveldの慣習について): https://go.dev/doc/contribute#code_review (現在のGoプロジェクトのレビュープロセスはGerritに移行していますが、Rietveld時代の慣習を理解する上で参考になります)
- Go言語の標準ライブラリドキュメント (GoDoc):
net/http
: https://pkg.go.dev/net/httpencoding/json
: https://pkg.go.dev/encoding/jsonfmt
: https://pkg.go.dev/fmtsort
: https://pkg.go.dev/sortstrings
: https://pkg.go.dev/stringstime
: https://pkg.go.dev/time