[インデックス 13577] ファイルの概要
このコミットは、Go言語のコードレビューダッシュボード(misc/dashboard/codereview
)に対する機能改善とUI調整を含んでいます。主な目的は、コードレビューのステータスを一目で把握しやすくすること、特にレビュー担当者と作者のどちらが次のアクションを待っているのかを明確にすることです。
コミット
commit ab058b35402aded8579e3e9653c0d78c5c4e9e5e
Author: Russ Cox <rsc@golang.org>
Date: Sun Aug 5 14:35:35 2012 -0400
misc/dashboard/codereview: show first line of last message in thread
This line helps me to tell whether the CL is waiting for me or I'm waiting for the author.
Also:
- vertical-align table cells so buttons are always aligned with CL headers.
- add email= to show front page for someone else.
Demo at http://rsc.gocodereview.appspot.com/.
Until this is deployed for real, some recently changed CLs may be
missing the 'first line of last message' part.
R=dsymonds
CC=golang-dev
https://golang.org/cl/6446065
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ab058b35402aded8579e3e9653c0d78c5c4e9e5e
元コミット内容
misc/dashboard/codereview: show first line of last message in thread
このコミットは、コードレビューのスレッドにおける最後のメッセージの最初の行を表示するように変更します。これにより、CL(Change List)が自分からの返信を待っているのか、それとも作者からの返信を待っているのかを判断しやすくなります。
追加の変更点として、以下のものが含まれます。
- テーブルセルの垂直方向の配置を調整し、ボタンが常にCLヘッダーと揃うようにします。
email=
パラメータを追加し、他のユーザーのフロントページを表示できるようにします。
デモは http://rsc.gocodereview.appspot.com/
で利用可能です。この変更が実際にデプロイされるまでは、最近変更されたCLでは「最後のメッセージの最初の行」が表示されない場合があります。
変更の背景
この変更の主な背景は、コードレビューの効率化とユーザーエクスペリエンスの向上です。Go言語のプロジェクトでは、Rietveldのようなコードレビューシステムが利用されており、多くのCLが同時に進行します。レビュー担当者やCLの作者は、自分が次に何をすべきか、あるいは誰からのアクションを待っているのかを迅速に把握する必要があります。
従来のダッシュボードでは、CLのステータスを判断するために各CLの詳細ページにアクセスする必要がありました。これは、特に多数のCLを管理している場合に非効率的です。最後のメッセージの最初の行を表示することで、ダッシュボード上でCLの最新のやり取りの概要を把握できるようになり、レビューのボトルネックを特定しやすくなります。
また、UIの改善(垂直方向の配置)は、ダッシュボードの視認性と使いやすさを向上させます。email=
パラメータの追加は、チームリーダーやマネージャーが特定のメンバーのレビュー状況を簡単に確認できるようにするためのもので、これもレビュープロセスの管理を支援します。
前提知識の解説
- Go言語 (Golang): Googleによって開発されたオープンソースのプログラミング言語。シンプルさ、効率性、並行処理のサポートが特徴です。
- コードレビュー (Code Review): ソフトウェア開発プロセスにおいて、他の開発者が書いたコードをレビューし、品質向上、バグの発見、知識共有などを目的とする活動です。
- CL (Change List): コードレビューシステムにおける変更の単位。Gitのコミットに相当しますが、レビュープロセスにおける一連の変更を指すことが多いです。RietveldやGerritなどのシステムで使われる用語です。
- Rietveld: Googleが開発したオープンソースのコードレビューツール。Pythonで書かれており、Google App Engine上で動作するように設計されています。Go言語プロジェクトの初期のコードレビューにも利用されていました。
- Google App Engine (GAE): Googleが提供するPaaS (Platform as a Service)。ウェブアプリケーションやモバイルバックエンドを構築・ホストするためのプラットフォームで、スケーラブルなインフラを提供します。このダッシュボードもApp Engine上で動作していることが示唆されています。
datastore
タグ: Go言語のApp EngineデータストアAPIで使用される構造体タグ。datastore:",noindex"
は、そのフィールドがデータストアのインデックス作成の対象外であることを示します。これにより、クエリのパフォーマンスが向上したり、ストレージコストが削減されたりする場合があります。json
タグ: Go言語のencoding/json
パッケージで使用される構造体タグ。JSONとのマーシャリング/アンマーシャリング時にフィールド名を指定します。- 正規表現 (Regular Expression): 文字列のパターンを記述するための強力なツール。このコミットでは、メッセージから不要な部分を削除するために使用されています。
- HTMLテンプレート (html/template): Go言語の標準ライブラリの一部で、HTMLを安全に生成するためのテンプレートエンジン。クロスサイトスクリプティング (XSS) 攻撃を防ぐためのエスケープ処理が自動的に行われます。
技術的詳細
このコミットは、主にGo言語で書かれたコードレビューダッシュボードのバックエンドとフロントエンドの両方に変更を加えています。
バックエンド (cl.go
) の変更点
-
CL
構造体の拡張:LastUpdateBy string
: 最新のレビューメッセージの送信者(メールアドレス)を格納するフィールドが追加されました。LastUpdate string
: 最新のレビューメッセージの最初の「興味深い」行を格納するフィールドが追加されました。このフィールドにはdatastore:",noindex"
タグが付与されており、データストアのインデックス作成から除外されます。これは、このフィールドが検索やソートのキーとして使用されることが少なく、単に表示目的であるため、パフォーマンスとコストの最適化のためと考えられます。
-
Reviewed()
メソッドの追加:CL
構造体にReviewed()
メソッドが追加されました。このメソッドは、現在のCLがレビュー担当者によって返信されたかどうかを判断するためのヒューリスティックを提供します。- 判断基準は以下のいずれかです:
- 最新のメッセージの送信者がレビュー担当者である場合。
LGTMs
(Looks Good To Me) リストにレビュー担当者が含まれている場合。
emailToPerson
マップを使用して、メールアドレスから個人IDへの変換も考慮されています。
-
Rietveld APIメッセージの処理強化:
apiMessage
構造体が定義され、Rietveldから返されるメッセージのJSON構造をマッピングします。これにはDate
,Text
,Sender
,Recipients
,Approval
などのフィールドが含まれます。byDate
型がsort.Interface
を実装し、apiMessage
のスライスを日付順(最も古いものから)にソートできるようにしました。Rietveld APIから取得したメッセージが必ずしも時系列順ではない可能性があるため、このソートは最新のメッセージを正確に特定するために重要です。updateCL
関数内で、Rietveld APIから取得したメッセージがsort.Sort(byDate(apiResp.Messages))
によってソートされるようになりました。これにより、常に最新のメッセージがスライスの最後に来ることが保証されます。
-
firstLine()
関数の導入:- このコミットの核心的な変更の一つで、与えられたテキスト(レビューメッセージ)から「最初の興味深い行」を抽出するための新しい関数
firstLine(text string)
が追加されました。 - この関数は、以下の処理を行います:
- 末尾の空白文字を削除します。
removeRE
という正規表現を使用して、メッセージの冒頭にある不要な部分(例: 「Hello so-and-so,」、引用されたテキスト(>
で始まる行)、文字を含まない行、コメントやファイル情報へのリンクなど)をスキップします。これは、Rietveldやコードレビュープラグインが自動的に追加する定型文や、ユーザーが引用した過去のメッセージなどを取り除くことで、実際のメッセージ内容の冒頭を抽出することを目的としています。- 最初の改行文字でメッセージを切り詰めます。
- もし行の長さが74文字を超える場合、70文字で切り詰め、末尾に
...
を追加します。これは、ダッシュボード上での表示スペースを考慮したものです。
- このコミットの核心的な変更の一つで、与えられたテキスト(レビューメッセージ)から「最初の興味深い行」を抽出するための新しい関数
-
正規表現の定義:
trailingSpaceRE
: 末尾の空白文字をマッチさせるための正規表現。removeRE
: メッセージの冒頭からスキップすべきパターンを定義する複雑な正規表現。(?m-s)
はマルチラインモードを有効にし、.
が改行にマッチしないようにします。
フロントエンド (front.go
) の変更点
-
email
パラメータのサポート:handleFront
関数内で、HTTPリクエストのフォーム値からemail
パラメータを読み取るようになりました。このパラメータが存在する場合、ダッシュボードはログインしているユーザーではなく、指定されたメールアドレスのユーザーのCLを表示します。これにより、他のユーザーのレビュー状況を簡単に確認できるようになります。- テーブルのタイトル(例: "CLs assigned to you for review")も、この
email
パラメータの値に応じて動的に変更されるようになりました。
-
shortemail
テンプレート関数の追加:- HTMLテンプレート内で使用できる新しい関数
shortemail
が追加されました。この関数は、メールアドレス(例:user@example.com
)から@
より前の部分(例:user
)を抽出して返します。これは、ダッシュボード上での表示スペースを節約し、より簡潔な表示を実現するためです。
- HTMLテンプレート内で使用できる新しい関数
-
HTMLテンプレートのスタイルと構造の変更:
td
要素にvertical-align: top;
スタイルが追加されました。これにより、テーブルセル内のコンテンツ(特にボタンなど)が常にセルの上部に揃うようになり、UIの整合性が向上します。table
要素にborder-spacing: 0;
が追加され、テーブルセルの間隔がなくなりました。tr.unreplied td.email
という新しいCSSクラスが追加されました。このクラスが適用された行のメールアドレスのセルには、左側に青いボーダーが表示されます。これは、レビュー担当者がまだ返信していないCLを視覚的に強調するためのものです。- CLの表示行 (
<tr>
) に、Reviewed()
メソッドの結果に基づいてunreplied
クラスが動的に追加されるようになりました。 - CL情報表示部分に、
LastUpdateBy
(短縮されたメールアドレス) とLastUpdate
(最新メッセージの最初の行) が表示されるようになりました。これにより、ダッシュボード上で各CLの最新のやり取りの概要を直接確認できるようになります。
これらの変更は、Go言語のApp Engineアプリケーションにおけるデータモデルの更新、APIレスポンスの処理、そしてHTMLテンプレートを用いたUIの動的な生成とスタイリングの典型的な例を示しています。特に、正規表現を用いたテキスト処理は、ユーザーが入力する自由形式のテキストから意味のある情報を抽出する際の一般的なパターンです。
コアとなるコードの変更箇所
misc/dashboard/codereview/dashboard/cl.go
--- a/misc/dashboard/codereview/dashboard/cl.go
+++ b/misc/dashboard/codereview/dashboard/cl.go
@@ -45,11 +45,12 @@ type CL struct {
Created, Modified time.Time
- Description []byte `datastore:",noindex"`
- FirstLine string `datastore:",noindex"`
- LGTMs []string
- NotLGTMs []string
- LastUpdate string
+ Description []byte `datastore:",noindex"`
+ FirstLine string `datastore:",noindex"`
+ LGTMs []string
+ NotLGTMs []string
+ LastUpdateBy string // author of most recent review message
+ LastUpdate string `datastore:",noindex"` // first line of most recent review message
// Mail information.
Subject string `datastore:",noindex"`
@@ -61,6 +62,24 @@ type CL struct {
Reviewer string
}
+// Reviewed reports whether the reviewer has replied to the CL.
+// The heuristic is that the CL has been replied to if it is LGTMed
+// or if the last CL message was from the reviewer.
+func (cl *CL) Reviewed() bool {
+ if cl.LastUpdateBy == cl.Reviewer {
+ return true
+ }
+ if person := emailToPerson[cl.LastUpdateBy]; person != "" && person == cl.Reviewer {
+ return true
+ }
+ for _, who := range cl.LGTMs {
+ if who == cl.Reviewer {
+ return true
+ }
+ }
+ return false
+}
+
// DisplayOwner returns the CL's owner, either as their email address
// or the person ID if it's a reviewer. It is for display only.
func (cl *CL) DisplayOwner() string {
@@ -252,6 +271,23 @@ func handleUpdateCL(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "OK")
}
+// apiMessage describes the JSON sent back by Rietveld in the CL messages list.
+type apiMessage struct {
+ Date string `json:"date"`
+ Text string `json:"text"`
+ Sender string `json:"sender"`
+ Recipients []string `json:"recipients"`
+ Approval bool `json:"approval"`
+}
+
+// byDate implements sort.Interface to order the messages by date, earliest first.
+// The dates are sent in RFC 3339 format, so string comparison matches time value comparison.
+type byDate []*apiMessage
+
+func (x byDate) Len() int { return len(x) }
+func (x byDate) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
+func (x byDate) Less(i, j int) bool { return x[i].Date < x[j].Date }
+
// updateCL updates a single CL. If a retryable failure occurs, an error is returned.
func updateCL(c appengine.Context, n string) error {
c.Debugf("Updating CL %v", n)
@@ -282,19 +318,14 @@ func updateCL(c appengine.Context, n string) error {
}
var apiResp struct {
- Description string `json:"description"`
- Reviewers []string `json:"reviewers"`
- Created string `json:"created"`
- OwnerEmail string `json:"owner_email"`
- Modified string `json:"modified"`
- Closed bool `json:"closed"`
- Subject string `json:"subject"`
- Messages []struct {
- Text string `json:"text"`
- Sender string `json:"sender"`
- Recipients []string `json:"recipients"`
- Approval bool `json:"approval"`
- } `json:"messages"`
+ Description string `json:"description"`
+ Reviewers []string `json:"reviewers"`
+ Created string `json:"created"`
+ OwnerEmail string `json:"owner_email"`
+ Modified string `json:"modified"`
+ Closed bool `json:"closed"`
+ Subject string `json:"subject"`
+ Messages []*apiMessage `json:"messages"`
}\n\tif err := json.Unmarshal(raw, &apiResp); err != nil {
// probably can't be retried
c.Errorf("json.Unmarshal %v: %v", n, err)
@@ -302,6 +333,7 @@ func updateCL(c appengine.Context, n string) error {
return nil
}
//c.Infof("RAW: %+v", apiResp)
+ sort.Sort(byDate(apiResp.Messages))
cl := &CL{
Number: n,
@@ -339,6 +371,12 @@ func updateCL(c appengine.Context, n string) error {
s, rev = p, true
}
+ line := firstLine(msg.Text)
+ if line != "" {
+ cl.LastUpdateBy = msg.Sender
+ cl.LastUpdate = line
+ }
+
// CLs submitted by someone other than the CL owner do not immediately
// transition to "closed". Let's simulate the intention by treating
// messages starting with "*** Submitted as " from a reviewer as a
@@ -392,3 +430,52 @@ func updateCL(c appengine.Context, n string) error {
c.Infof("Updated CL %v", n)
return nil
}\n+\n+// trailingSpaceRE matches trailing spaces.\n+var trailingSpaceRE = regexp.MustCompile(`(?m)[ \\t\\r]+$`)\n+\n+// removeRE is the list of patterns to skip over at the beginning of a \n+// message when looking for message text.\n+var removeRE = regexp.MustCompile(`(?m-s)\\A(` +\n+ // Skip leading "Hello so-and-so," generated by codereview plugin.\n+ `(Hello(.|\\n)*?\\n\\n)` +\n+\n+ // Skip quoted text.\n+ `|((On.*|.* writes|.* wrote):\\n)` +\n+ `|((>.*\\n)+)` +\n+\n+ // Skip lines with no letters.\n+ `|(([^A-Za-z]*\\n)+)` +\n+\n+ // Skip links to comments and file info.\n+ `|(http://codereview.*\\n([^ ]+:[0-9]+:.*\\n)?)` +\n+ `|(File .*:\\n)` +\n+\n+ `)`,\n+)\n+\n+// firstLine returns the first interesting line of the message text.\n+func firstLine(text string) string {\n+ // Cut trailing spaces.\n+ text = trailingSpaceRE.ReplaceAllString(text, "")\n+\n+ // Skip uninteresting lines.\n+ for {\n+ text = strings.TrimSpace(text)\n+ m := removeRE.FindStringIndex(text)\n+ if m == nil || m[0] != 0 {\n+ break\n+ }\n+ text = text[m[1]:]\n+ }\n+\n+ // Chop line at newline or else at 74 bytes.\n+ i := strings.Index(text, "\\n")\n+ if i >= 0 {\n+ text = text[:i]\n+ }\n+ if len(text) > 74 {\n+ text = text[:70] + "..."\n+ }\n+ return text\n+}\n```
### `misc/dashboard/codereview/dashboard/front.go`
```diff
--- a/misc/dashboard/codereview/dashboard/front.go
+++ b/misc/dashboard/codereview/dashboard/front.go
@@ -11,6 +11,7 @@ import (
"html/template"
"io"
"net/http"
+ "strings"
"sync"
"time"
@@ -36,7 +37,13 @@ func handleFront(w http.ResponseWriter, r *http.Request) {
IsAdmin: user.IsAdmin(c),
}
var currentPerson string
- currentPerson, data.UserIsReviewer = emailToPerson[data.User]
+ u := data.User
+ you := "you"
+ if e := r.FormValue("email"); e != "" {
+ u = e
+ you = e
+ }
+ currentPerson, data.UserIsReviewer = emailToPerson[u]
var wg sync.WaitGroup
errc := make(chan error, 10)
@@ -59,7 +66,7 @@ func handleFront(w http.ResponseWriter, r *http.Request) {
if data.UserIsReviewer {
tableFetch(0, func(tbl *clTable) error {
q := activeCLs.Filter("Reviewer =", currentPerson).Limit(maxCLs)
- tbl.Title = "CLs assigned to you for review"
+ tbl.Title = "CLs assigned to " + you + " for review"
tbl.Assignable = true
_, err := q.GetAll(c, &tbl.CLs)
return err
@@ -68,7 +75,7 @@ func handleFront(w http.ResponseWriter, r *http.Request) {
tableFetch(1, func(tbl *clTable) error {
q := activeCLs.Filter("Author =", currentPerson).Limit(maxCLs)
- tbl.Title = "CLs sent by you"
+ tbl.Title = "CLs sent by " + you
tbl.Assignable = true
_, err := q.GetAll(c, &tbl.CLs)
return err
@@ -139,7 +146,7 @@ type frontPageData struct {
Reviewers []string
UserIsReviewer bool
- User, LogoutURL string
+ User, LogoutURL string // actual logged in user
IsAdmin bool
}\n\n@@ -156,6 +163,12 @@ var frontPage = template.Must(template.New("front").Funcs(template.FuncMap{\n }\n return ""\n },\n+ "shortemail": func(s string) string {\n+ if i := strings.Index(s, "@"); i >= 0 {\n+ s = s[:i]\n+ }\n+ return s\n+ },\n }).Parse(`\n <!doctype html>\n <html>\n@@ -175,9 +188,16 @@ var frontPage = template.Must(template.New("front").Funcs(template.FuncMap{\n color: #777;\n margin-bottom: 0;\n }\n+ table {\n+ border-spacing: 0;\n+ }\n td {\n+ vertical-align: top;\n padding: 2px 5px;\n }\n+ tr.unreplied td.email {\n+ border-left: 2px solid blue;\n+ }\n tr.pending td {\n background: #fc8;\n }\n@@ -209,15 +229,15 @@ var frontPage = template.Must(template.New("front").Funcs(template.FuncMap{\n <img id="gopherstamp" src="/static/gopherstamp.jpg" />\n <h1>Go code reviews</h1>\n \n-{{range $tbl := .Tables}}\n-<h3>{{$tbl.Title}}</h3>\n-{{if .CLs}}\n <table class="cls">\n+{{range $i, $tbl := .Tables}}\n+<tr><td colspan="5"><h3>{{$tbl.Title}}</h3></td></tr>\n+{{if .CLs}}\n {{range $cl := .CLs}}\n- <tr id="cl-{{$cl.Number}}">\n+ <tr id="cl-{{$cl.Number}}" class="{{if not $i}}{{if not .Reviewed}}unreplied{{end}}{{end}}">\n <td class="email">{{$cl.DisplayOwner}}</td>\n- {{if $tbl.Assignable}}\n <td>\n+ {{if $tbl.Assignable}}\n <select id="cl-rev-{{$cl.Number}}" {{if not $.UserIsReviewer}}disabled{{end}}>\n <option></option>\n {{range $.Reviewers}}\n@@ -243,22 +263,23 @@ var frontPage = template.Must(template.New("front").Funcs(template.FuncMap{\n });\n });\n </script>\n- </td>\n {{end}}\n+ </td>\n <td>\n <a href="http://codereview.appspot.com/{{.Number}}/" title="{{ printf "%s" .Description}}">{{.Number}}: {{.FirstLineHTML}}</a>\n {{if and .LGTMs $tbl.Assignable}}<br /><span style="font-size: smaller;">LGTMs: {{.LGTMHTML}}</span>{{end}}\n {{if and .NotLGTMs $tbl.Assignable}}<br /><span style="font-size: smaller; color: #f74545;">NOT LGTMs: {{.NotLGTMHTML}}</span>{{end}}\n+ {{if .LastUpdateBy}}<br /><span style="font-size: smaller; color: #777777;">(<span title="{{.LastUpdateBy}}">{{.LastUpdateBy | shortemail}}</span>) {{.LastUpdate}}</span>{{end}}\n </td>\n <td title="Last modified">{{.ModifiedAgo}}</td>\n- {{if $.IsAdmin}}<td><a href="/update-cl?cl={{.Number}}" title="Update this CL">⟳</a></td>{{end}}\n+ <td>{{if $.IsAdmin}}<a href="/update-cl?cl={{.Number}}" title="Update this CL">⟳</a>{{end}}</td>\n </tr>\n {{end}}\n-</table>\n {{else}}\n-<em>none</em>\n+<tr><td colspan="5"><em>none</em></td></tr>\n {{end}}\n {{end}}\n+</table>\n \n <hr />\n <address>\n```
## コアとなるコードの解説
### `cl.go` の変更点
- **`CL` 構造体への `LastUpdateBy` と `LastUpdate` の追加**:
- `LastUpdateBy` は、最新のレビューメッセージを送信したユーザーのメールアドレスを保持します。
- `LastUpdate` は、その最新メッセージの「最初の興味深い行」を保持します。これらはダッシュボード上でCLの最新の状況を簡潔に表示するために使用されます。`datastore:",noindex"` は、これらのフィールドがデータストアのインデックス作成の対象外であることを示し、ストレージとクエリの効率化に貢献します。
- **`Reviewed()` メソッド**:
- このメソッドは、CLがレビュー担当者によって「レビュー済み」と見なせるかどうかを判断します。これは、レビュー担当者がCLにLGTM(Looks Good To Me)を与えたか、または最新のメッセージがレビュー担当者からのものである場合に `true` を返します。これにより、ダッシュボード上で未返信のCLを視覚的に区別できるようになります。
- **`apiMessage` 構造体と `byDate` ソート**:
- Rietveld APIから取得するメッセージの構造を正確にマッピングするために `apiMessage` が定義されました。
- `byDate` は `sort.Interface` を実装しており、`apiMessage` のスライスを日付順にソートすることを可能にします。`updateCL` 関数内でこのソートが適用されることで、常に最新のメッセージを正確に特定し、その情報(送信者と最初の行)を `CL` 構造体に格納できるようになります。
- **`firstLine()` 関数**:
- この関数は、レビューメッセージのテキストから、表示に適した「最初の興味深い行」を抽出するロジックをカプセル化しています。
- 特に注目すべきは、`removeRE` 正規表現です。これは、コードレビューシステムが自動的に追加する定型文(例: 「Hello so-and-so,」)、引用されたテキスト、空行、コメントやファイルへのリンクなど、メッセージの冒頭にあるノイズを除去するために設計されています。これにより、ユーザーが実際に書いたメッセージの核心部分を抽出できます。
- 抽出された行は、表示スペースの制約を考慮して、改行または74文字で切り詰められます。
### `front.go` の変更点
- **`email` パラメータによるユーザー切り替え**:
- `handleFront` 関数は、URLクエリパラメータ `email` をチェックし、その値が存在すれば、ログインユーザーではなく指定されたメールアドレスのユーザーのCLを表示するように動作を変更します。これにより、他のユーザーのレビュー状況を簡単に監視できるようになります。テーブルのタイトルもこれに合わせて動的に変更されます。
- **`shortemail` テンプレート関数**:
- HTMLテンプレート内で使用される新しいヘルパー関数です。メールアドレスの `@` より前の部分を抽出し、ダッシュボード上での表示を簡潔にします。例えば、`rsc@golang.org` は `rsc` と表示されます。
- **HTML/CSSの変更**:
- `vertical-align: top;` を `td` 要素に適用することで、テーブル内のコンテンツ(特にボタンやテキスト)がセルの上部に揃うようになり、UIの視覚的な整合性が向上します。
- `tr.unreplied td.email` というCSSルールは、`Reviewed()` メソッドが `false` を返すCLの行に `unreplied` クラスが適用された際に、メールアドレスのセルに青い左ボーダーを追加します。これにより、レビュー担当者からの返信がまだないCLが視覚的に強調され、注意を引くことができます。
- CLの表示部分に `LastUpdateBy` と `LastUpdate` が追加され、各CLの最新のやり取りの概要がダッシュボード上で直接確認できるようになりました。
これらの変更は、Go言語のWebアプリケーション開発における一般的なパターン、すなわちデータモデルの拡張、ビジネスロジックの追加、APIレスポンスの処理、そしてHTMLテンプレートを用いた動的なUI生成とスタイリングを効果的に組み合わせています。特に、ユーザーエクスペリエンスを向上させるための細かなUI/UXの調整と、それを支えるバックエンドのデータ処理ロジックが密接に連携している点が特徴です。
## 関連リンク
- Go言語公式サイト: [https://golang.org/](https://golang.org/)
- Google App Engine: [https://cloud.google.com/appengine](https://cloud.google.com/appengine)
- Rietveld (Wikipedia): [https://en.wikipedia.org/wiki/Rietveld](https://en.wikipedia.org/wiki/Rietveld)
- Go言語のコードレビューシステムに関する議論(当時の状況を理解するのに役立つかもしれません): [https://groups.google.com/g/golang-dev/c/y_1_2_3_4_5/m/6_7_8_9_0](https://groups.google.com/g/golang-dev/c/y_1_2_3_4_5/m/6_7_8_9_0) (これは一般的な例であり、特定の議論を指すものではありません)
## 参考にした情報源リンク
- コミットハッシュ: `ab058b35402aded8579e3e9653c0d78c5c4e9e5e`
- GitHub上のコミットページ: [https://github.com/golang/go/commit/ab058b35402aded8579e3e9653c0d78c5c4e9e5e](https://github.com/golang/go/commit/ab058b35402aded8579e3e9653c0d78c5c4e9e5e)
- Go言語のCLリンク: [https://golang.org/cl/6446065](https://golang.org/cl/6446065)
- Go言語の `html/template` パッケージドキュメント: [https://pkg.go.dev/html/template](https://pkg.go.dev/html/template)
- Go言語の `regexp` パッケージドキュメント: [https://pkg.go.dev/regexp](https://pkg.go.dev/regexp)
- Go言語の `sort` パッケージドキュメント: [https://pkg.go.dev/sort](https://pkg.go.dev/sort)
- Google App Engine Go SDK ドキュメント (当時のバージョン): (具体的なURLは時間の経過により変更されている可能性が高いですが、当時のApp Engine Go SDKのデータストアやユーザーAPIに関するドキュメントが参考になります)