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

[インデックス 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のレビューに参加していない場合、通知が不足し、レビューの開始が遅れる可能性がありました。

このコミットの背景には、以下のような課題があったと考えられます。

  1. 通知の不足: レビュアーがCLにアサインされても、Rietveldの通知システムだけでは見落とす可能性があった。
  2. レビュープロセスの遅延: レビュアーがCLの存在に気づかないことで、レビュー開始が遅れ、結果として変更がマージされるまでの時間が長くなる。
  3. 手動でのフォローアップの負担: アサインした側がレビュアーに手動で連絡を取る必要があり、手間がかかる。

このコミットは、これらの課題を解決し、コードレビュープロセスをよりスムーズかつ効率的に進めることを目的としています。特に、ダッシュボード側でレビュアーのアサインを検知し、必要に応じて自動的にメールを送信することで、レビュアーの早期関与を促します。

前提知識の解説

このコミットを理解するためには、以下の技術や概念に関する知識が必要です。

  • 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.gomisc/dashboard/codereview/dashboard/people.go の2つのファイルに集中しています。

cl.go の変更点

  1. CL 構造体の拡張:

    • Subject (string): CLの件名。Rietveldから取得し、メールの件名に使用されます。
    • Recipients ([]string): CLに関連するメールの受信者リスト。Rietveldのメッセージから抽出されます。 これらのフィールドは、メール通知の際に必要な情報として追加されました。
  2. sendMailLater の導入:

    • var sendMailLater = delay.Func("send-mail", mail.Send)
    • appengine/delay パッケージの delay.Func を使用して、mail.Send 関数を非同期で実行するためのラッパーが定義されています。これにより、メール送信処理がHTTPリクエストの応答時間をブロックすることなく、バックグラウンドで実行されるようになります。これは、ユーザーエクスペリエンスの向上と、GAEのHTTPリクエストタイムアウトの回避に貢献します。
  3. 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の更新をアトミックに実行します。これにより、データの一貫性が保たれます。
  4. updateCL 関数のロジック変更:

    • この関数は、RietveldからCLの最新情報を取得し、Datastoreに保存されているCLオブジェクトを更新します。
    • Rietveld APIレスポンスの拡張: Rietveld APIから取得するCL情報に Subject フィールドが追加されました。
    • メッセージ受信者の抽出: Rietveldのメッセージオブジェクトから Recipients フィールドを抽出し、CLオブジェクトの Recipients フィールドに格納します。これにより、CLに関連するすべてのメール受信者(CCリスト)を追跡できるようになります。
    • LGTMの重複排除 (TODO): コメントでLGTMの重複排除が将来の課題として示されています。
    • 受信者リストのソート: cl.Recipients をソートすることで、一貫性を保ちます。

people.go の変更点

  1. preferredEmail マップの追加:

    • preferredEmail = make(map[string]string): person (例: "rsc") からその人物の優先メールアドレス (例: "rsc@golang.org") へのマッピングを保持する新しいマップが追加されました。
    • これは、メール送信時にレビュアーの適切なメールアドレスを特定するために使用されます。特に、@golang.org@google.com の両方のアカウントを持つユーザーに対して、コードレビューには @golang.org アドレスを優先して使用するという方針が反映されています。
  2. init 関数の変更:

    • gophers リストの各人物に対して、preferredEmail[p] = p + "@golang.org" の行が追加され、優先メールアドレスが設定されるようになりました。

これらの変更により、ダッシュボードはRietveldの情報をより詳細に取得し、レビュアーのアサイン状況に応じて適切なメール通知を自動的に行うことができるようになりました。

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

misc/dashboard/codereview/dashboard/cl.go

  • CL 構造体に SubjectRecipients フィールドが追加。
  • sendMailLater という delay.Func が定義され、mail.Send を非同期実行可能に。
  • handleAssign 関数内で、レビュアーがRietveldに登録されているかを確認するロジックが追加され、登録されていない場合にメールを送信する処理が実装された。
    • Rietveldの /fields APIエンドポイントを urlfetch で呼び出し、レビュアーリストを取得。
    • 取得したレビュアーリストとアサインされたレビュアーを比較。
    • レビュアーがRietveldに登録されていない場合、mail.Message を構築し、sendMailLater.Call でメールを送信。
  • updateCL 関数内で、Rietveld APIのレスポンスから Subject とメッセージの Recipients を抽出して CL 構造体に格納する処理が追加。
  • Recipients リストのソート処理が追加。

misc/dashboard/codereview/dashboard/people.go

  • preferredEmail という map[string]string が追加され、人物名から優先メールアドレスへのマッピングを保持。
  • init 関数内で、gophers リストの各人物に対して preferredEmail マップに優先メールアドレス(@golang.org ドメイン)を設定する処理が追加。

コアとなるコードの解説

cl.gohandleAssign 関数におけるメール送信ロジック

// ... (既存の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.goupdateCL 関数における受信者リストの収集

// ... (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.gopreferredEmail マップの初期化

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プロジェクトのメンバーが複数のメールアドレスを持つ可能性があるという現実に対応するためのものです。

関連リンク

参考にした情報源リンク