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

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

このコミットは、Go言語プロジェクトのダッシュボードシステムにおけるコードレビュー機能の改善に関するものです。具体的には、Google App Engine上で動作するダッシュボードが、放棄された(abandoned)Change List (CL) を適切に処理できるようにする変更が加えられています。これにより、存在しないCLに対する不必要な更新試行を防ぎ、ダッシュボードのデータ整合性を向上させます。

コミット

commit 935d8d16d402d3721a2c80ffb0d0e16262566a48
Author: David Symonds <dsymonds@golang.org>
Date:   Fri Jun 1 10:55:55 2012 +1000

    misc/dashboard/codereview: handle abandoned CLs.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/6257082

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

https://github.com/golang/go/commit/935d8d16d402d3721a2c80ffb0d0e16262566a48

元コミット内容

misc/dashboard/codereview: handle abandoned CLs.

このコミットは、Goプロジェクトのダッシュボードシステムにおいて、コードレビューのChange List (CL) が放棄された場合にそれを適切に処理する機能を追加します。

変更の背景

Goプロジェクトでは、Gerritのようなコードレビューシステム(当時はGoogleの内部システムが使われていた可能性が高い)を使用してコード変更(Change List, CL)のレビューを行っていました。ダッシュボードはこれらのCLの状態を追跡し、開発者に表示する役割を担っています。

しかし、CLが作成された後に何らかの理由で削除されたり、完全に放棄されたりする場合があります。このような「放棄されたCL」は、コードレビューシステム上にはもはや存在しません。従来のダッシュボードシステムでは、これらの存在しないCLに対しても定期的に更新を試みていました。その結果、APIからの404 Not Foundエラーを受け取り、不必要なエラーログの生成や、ダッシュボード上のデータが実際の状態と乖離する問題が発生していました。

このコミットの目的は、ダッシュボードがコードレビューシステムから「CLが存在しない」という明確なシグナル(HTTP 404ステータスコードと特定のエラーメッセージ)を受け取った際に、そのCLをダッシュボードのデータストアから削除し、システムをクリーンアップすることです。これにより、ダッシュボードの堅牢性と正確性が向上します。

前提知識の解説

  • Change List (CL): ソフトウェア開発におけるコード変更の単位。通常、単一の機能追加、バグ修正、リファクタリングなど、論理的にまとまった変更を指します。Goプロジェクトでは、Gerritのようなシステムで管理されます。
  • Google App Engine (GAE): Googleが提供するPaaS (Platform as a Service)。ウェブアプリケーションやモバイルバックエンドを構築・ホストするためのプラットフォームです。このダッシュボードはGAE上で動作していました。
  • Datastore: Google App Engineが提供するNoSQLデータベースサービス。スケーラブルで高可用性を持つデータストレージです。ダッシュボードはCLの情報をDatastoreに保存していました。
  • urlfetch: Google App EngineのGo SDKに含まれるパッケージで、外部URLへのHTTPリクエストを行うためのクライアントを提供します。GAE環境下で外部サービス(この場合はコードレビューAPI)と通信するために使用されます。
  • encoding/json: Go言語の標準ライブラリで、JSONデータのエンコードとデコードを提供します。
  • io/ioutil: Go言語の標準ライブラリで、I/O操作に関するユーティリティ関数を提供します。特にReadAllio.Readerから全てのデータを読み込むために使われます。
  • bytes: Go言語の標準ライブラリで、バイトスライスを操作するための関数を提供します。Containsはバイトスライスが別のバイトスライスを含むかどうかをチェックします。
  • HTTP 404 Not Found: クライアントが要求したリソースがサーバー上で見つからなかったことを示すHTTPステータスコード。
  • json.NewDecoder vs json.Unmarshal:
    • json.NewDecoder(r io.Reader): io.Readerから直接JSONデータをストリームとして読み込み、デコードします。これは大きなJSONデータを効率的に処理するのに適しています。resp.Bodyio.Readerインターフェースを実装しています。
    • json.Unmarshal([]byte, interface{}): バイトスライスとしてメモリにロードされたJSONデータをデコードします。ioutil.ReadAllで読み込んだデータはバイトスライスになるため、Unmarshalが適しています。resp.Bodyは一度しか読み込めないため、ioutil.ReadAllで読み込んだ後はNewDecoderを使うことはできません。

技術的詳細

このコミットの主要な変更は、misc/dashboard/codereview/dashboard/cl.go ファイル内の updateCL 関数に集中しています。この関数は、特定のCLの情報をコードレビューシステムから取得し、ダッシュボードのDatastoreに更新する役割を担っています。

変更前は、updateCL 関数はコードレビューAPIからHTTPレスポンスを受け取った後、直接 json.NewDecoder(resp.Body).Decode(&apiResp) を使用してJSONデータをデコードしていました。しかし、この方法では、APIが404エラーを返した場合に、そのレスポンスボディに特定のメッセージが含まれているかどうかをチェックすることができませんでした。resp.Body は一度しか読み込めないため、ステータスコードをチェックした後に再度ボディの内容を読み込もうとするとエラーになります。

この問題を解決するため、以下の変更が導入されました。

  1. レスポンスボディの完全な読み込み: raw, err := ioutil.ReadAll(resp.Body) を追加し、APIからのレスポンスボディ全体を raw というバイトスライスに読み込みます。これにより、ボディの内容を複数回参照できるようになります。
  2. 放棄されたCLの検出と処理: if resp.StatusCode == 404 && bytes.Contains(raw, []byte("No issue exists with that id")) という条件分岐が追加されました。
    • resp.StatusCode == 404: HTTPステータスコードが404 (Not Found) であることを確認します。
    • bytes.Contains(raw, []byte("No issue exists with that id")): レスポンスボディの内容 (raw) が、特定の文字列 "No issue exists with that id" を含んでいることを確認します。この文字列は、コードレビューシステムがCLが見つからない場合に返す典型的なエラーメッセージです。 この両方の条件が満たされた場合、そのCLは放棄されたものと判断されます。
    • datastore.Delete(c, key): Datastoreから該当するCLのエントリを削除します。
    • c.Infof("Deleted abandoned CL %v", n): ログに削除されたCLの情報を出力します。
    • return nil: 正常に処理が完了したとして関数を終了します。
  3. JSONデコード方法の変更: if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { の行が if err := json.Unmarshal(raw, &apiResp); err != nil { に変更されました。これは、resp.Body が既に ioutil.ReadAll によって読み込まれているため、raw バイトスライスからJSONをデコードする必要があるためです。

これらの変更により、ダッシュボードは放棄されたCLを検出し、Datastoreからその情報を削除することで、データの整合性を保ち、不必要なエラー処理を回避できるようになりました。

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

misc/dashboard/codereview/dashboard/cl.go ファイルの updateCL 関数内。

--- a/misc/dashboard/codereview/dashboard/cl.go
+++ b/misc/dashboard/codereview/dashboard/cl.go
@@ -7,10 +7,12 @@ package dashboard
 // This file handles operations on the CL entity kind.
 
 import (
+\t"bytes"
 \t"encoding/json"
 \t"fmt"
 \t"html/template"
 \t"io"
+\t"io/ioutil"
 \t"net/http"
 \tnetmail "net/mail"
 \t"net/url"
@@ -256,6 +258,7 @@ func handleUpdateCL(w http.ResponseWriter, r *http.Request) {
 // updateCL updates a single CL. If a retryable failure occurs, an error is returned.\n func updateCL(c appengine.Context, n string) error {\n \tc.Debugf("Updating CL %v", n)\n+\tkey := datastore.NewKey(c, "CL", n, 0, nil)\n \n \turl := codereviewBase + "/api/" + n + "?messages=true"\n \tresp, err := urlfetch.Client(c).Get(url)\n@@ -263,6 +266,20 @@ func updateCL(c appengine.Context, n string) error {\n \t\treturn err\n \t}\n \tdefer resp.Body.Close()\n+\n+\traw, err := ioutil.ReadAll(resp.Body)\n+\tif err != nil {\n+\t\treturn fmt.Errorf("Failed reading HTTP body: %v", err)\n+\t}\n+\n+\t// Special case for abandoned CLs.\n+\tif resp.StatusCode == 404 && bytes.Contains(raw, []byte("No issue exists with that id")) {\n+\t\t// Don't bother checking for errors. The CL might never have been saved, for instance.\n+\t\tdatastore.Delete(c, key)\n+\t\tc.Infof("Deleted abandoned CL %v", n)\n+\t\treturn nil\n+\t}\n+\n \tif resp.StatusCode != 200 {\n \t\treturn fmt.Errorf("Update: got HTTP response %d", resp.StatusCode)\n \t}\n@@ -281,7 +298,7 @@ func updateCL(c appengine.Context, n string) {\n \t\t\tApproval   bool     `json:\"approval\"`\n \t\t} `json:\"messages\"`\n \t}\n-\tif err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {\n+\tif err := json.Unmarshal(raw, &apiResp); err != nil {\n \t\t// probably can\'t be retried\n \t\tc.Errorf("Malformed JSON from %v: %v", url, err)\n \t\treturn nil\n@@ -341,7 +358,6 @@ func updateCL(c appengine.Context, n string) error {\n \tsort.Strings(cl.LGTMs)\n \tsort.Strings(cl.Recipients)\n \n-\tkey := datastore.NewKey(c, "CL", n, 0, nil)\n \terr = datastore.RunInTransaction(c, func(c appengine.Context) error {\n \t\tocl := new(CL)\n \t\terr := datastore.Get(c, key, ocl)\n```

## コアとなるコードの解説

1.  **`import ("bytes", "io/ioutil")`**:
    `bytes` パッケージはバイトスライス操作のために、`io/ioutil` パッケージはHTTPレスポンスボディを読み込むためにそれぞれ追加されました。
2.  **`key := datastore.NewKey(c, "CL", n, 0, nil)` の移動**:
    この行は、Datastoreのキーを生成するもので、以前は関数の後半にありました。放棄されたCLを削除する新しいロジックでこのキーが必要になるため、関数の冒頭に移動されました。
3.  **`raw, err := ioutil.ReadAll(resp.Body)`**:
    `urlfetch.Client(c).Get(url)` で取得したHTTPレスポンスのボディ (`resp.Body`) を全て読み込み、`raw` というバイトスライスに格納します。これにより、ボディの内容を後で解析できるようになります。
4.  **放棄されたCLの検出ロジック**:
    ```go
    if resp.StatusCode == 404 && bytes.Contains(raw, []byte("No issue exists with that id")) {
        datastore.Delete(c, key)
        c.Infof("Deleted abandoned CL %v", n)
        return nil
    }
    ```
    このブロックがこのコミットの核心です。
    *   HTTPステータスコードが `404` (Not Found) であることを確認します。
    *   さらに、読み込んだレスポンスボディ `raw` が、コードレビューシステムが返す特定のメッセージ `"No issue exists with that id"` を含んでいることを `bytes.Contains` で確認します。
    *   両方の条件が真の場合、そのCLはもはや存在しない(放棄された)と判断し、`datastore.Delete(c, key)` を呼び出してDatastoreからそのCLの情報を削除します。
    *   `c.Infof` でログに削除したことを記録し、`return nil` で関数を正常終了します。これにより、後続のJSONデコードやDatastore更新処理がスキップされます。
5.  **`if err := json.Unmarshal(raw, &apiResp); err != nil {`**:
    以前は `json.NewDecoder(resp.Body).Decode(&apiResp)` を使用していましたが、`resp.Body` は既に `ioutil.ReadAll` によって読み込まれているため、代わりに `raw` バイトスライスを `json.Unmarshal` でデコードするように変更されました。これにより、既にメモリにロードされたJSONデータを効率的に処理できます。

これらの変更により、ダッシュボードはコードレビューシステムとの同期をより正確に行い、存在しないCLに関する不必要な処理やエラーを回避できるようになりました。

## 関連リンク

*   Go言語のコードレビュープロセスに関する情報(当時のシステムはGerritベースまたは類似のカスタムシステム):
    *   [https://go.dev/doc/contribute](https://go.dev/doc/contribute) (現在のGoプロジェクトの貢献ガイドライン)
*   Google App Engine Datastoreのドキュメント:
    *   [https://cloud.google.com/appengine/docs/standard/go/datastore/](https://cloud.google.com/appengine/docs/standard/go/datastore/) (Go用Datastoreクライアントライブラリの概要)

## 参考にした情報源リンク

*   Go言語の標準ライブラリドキュメント:
    *   `bytes` パッケージ: [https://pkg.go.dev/bytes](https://pkg.go.dev/bytes)
    *   `io/ioutil` パッケージ (Go 1.16以降は `io` および `os` に移行): [https://pkg.go.dev/io/ioutil](https://pkg.go.dev/io/ioutil)
    *   `encoding/json` パッケージ: [https://pkg.go.dev/encoding/json](https://pkg.go.dev/encoding/json)
*   Google App Engineの公式ドキュメント (当時のバージョン):
    *   `urlfetch` サービス: [https://cloud.google.com/appengine/docs/standard/go/reference/services/urlfetch](https://cloud.google.com/appengine/docs/standard/go/reference/services/urlfetch)
    *   `datastore` サービス: [https://cloud.google.com/appengine/docs/standard/go/reference/services/datastore](https://cloud.google.com/appengine/docs/standard/go/reference/services/datastore)
*   Go言語のコードレビューシステムに関する一般的な情報 (Gerritなど):
    *   [https://gerrit-review.googlesource.com/Documentation/](https://gerrit-review.googlesource.com/Documentation/) (Gerritの公式ドキュメント)
*   コミットに記載されているCLリンク:
    *   [https://golang.org/cl/6257082](https://golang.org/cl/6257082) (このリンクは古いGoのコードレビューシステムのものであり、現在はリダイレクトされるか、直接アクセスできない可能性がありますが、当時のCLのIDを示しています。)
*   Go言語のダッシュボードプロジェクトのソースコード:
    *   [https://github.com/golang/go/tree/master/misc/dashboard](https://github.com/golang/go/tree/master/misc/dashboard) (現在のGoリポジトリ内のダッシュボード関連コード)
*   `json.NewDecoder` と `json.Unmarshal` の違いに関する一般的なプログラミング記事。# [インデックス 13241] ファイルの概要

このコミットは、Go言語プロジェクトのダッシュボードシステムにおけるコードレビュー機能の改善に関するものです。具体的には、Google App Engine上で動作するダッシュボードが、放棄された(abandoned)Change List (CL) を適切に処理できるようにする変更が加えられています。これにより、存在しないCLに対する不必要な更新試行を防ぎ、ダッシュボードのデータ整合性を向上させます。

## コミット

commit 935d8d16d402d3721a2c80ffb0d0e16262566a48 Author: David Symonds dsymonds@golang.org Date: Fri Jun 1 10:55:55 2012 +1000

misc/dashboard/codereview: handle abandoned CLs.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6257082

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

[https://github.com/golang/go/commit/935d8d16d402d3721a2c80ffb0d0e16262566a48](https://github.com/golang/go/commit/935d8d16d402d3721a2c80ffb0d0e16262566a48)

## 元コミット内容

`misc/dashboard/codereview: handle abandoned CLs.`

このコミットは、Goプロジェクトのダッシュボードシステムにおいて、コードレビューのChange List (CL) が放棄された場合にそれを適切に処理する機能を追加します。

## 変更の背景

Goプロジェクトでは、Gerritのようなコードレビューシステム(当時はGoogleの内部システムが使われていた可能性が高い)を使用してコード変更(Change List, CL)のレビューを行っていました。ダッシュボードはこれらのCLの状態を追跡し、開発者に表示する役割を担っています。

しかし、CLが作成された後に何らかの理由で削除されたり、完全に放棄されたりする場合があります。このような「放棄されたCL」は、コードレビューシステム上にはもはや存在しません。従来のダッシュボードシステムでは、これらの存在しないCLに対しても定期的に更新を試みていました。その結果、APIからの404 Not Foundエラーを受け取り、不必要なエラーログの生成や、ダッシュボード上のデータが実際の状態と乖離する問題が発生していました。

このコミットの目的は、ダッシュボードがコードレビューシステムから「CLが存在しない」という明確なシグナル(HTTP 404ステータスコードと特定のエラーメッセージ)を受け取った際に、そのCLをダッシュボードのデータストアから削除し、システムをクリーンアップすることです。これにより、ダッシュボードの堅牢性と正確性が向上します。

## 前提知識の解説

*   **Change List (CL)**: ソフトウェア開発におけるコード変更の単位。通常、単一の機能追加、バグ修正、リファクタリングなど、論理的にまとまった変更を指します。Goプロジェクトでは、Gerritのようなシステムで管理されます。
*   **Google App Engine (GAE)**: Googleが提供するPaaS (Platform as a Service)。ウェブアプリケーションやモバイルバックエンドを構築・ホストするためのプラットフォームです。このダッシュボードはGAE上で動作していました。
*   **Datastore**: Google App Engineが提供するNoSQLデータベースサービス。スケーラブルで高可用性を持つデータストレージです。ダッシュボードはCLの情報をDatastoreに保存していました。
*   **`urlfetch`**: Google App EngineのGo SDKに含まれるパッケージで、外部URLへのHTTPリクエストを行うためのクライアントを提供します。GAE環境下で外部サービス(この場合はコードレビューAPI)と通信するために使用されます。
*   **`encoding/json`**: Go言語の標準ライブラリで、JSONデータのエンコードとデコードを提供します。
*   **`io/ioutil`**: Go言語の標準ライブラリで、I/O操作に関するユーティリティ関数を提供します。特に`ReadAll`は`io.Reader`から全てのデータを読み込むために使われます。Go 1.16以降では非推奨となり、`io.ReadAll`や`os.ReadFile`に置き換えられています。
*   **`bytes`**: Go言語の標準ライブラリで、バイトスライスを操作するための関数を提供します。`Contains`はバイトスライスが別のバイトスライスを含むかどうかをチェックします。
*   **HTTP 404 Not Found**: クライアントが要求したリソースがサーバー上で見つからなかったことを示すHTTPステータスコード。
*   **`json.NewDecoder` vs `json.Unmarshal`**:
    *   `json.NewDecoder(r io.Reader)`: `io.Reader`から直接JSONデータをストリームとして読み込み、デコードします。これは大きなJSONデータを効率的に処理するのに適しています。`resp.Body`は`io.Reader`インターフェースを実装しています。
    *   `json.Unmarshal([]byte, interface{})`: バイトスライスとしてメモリにロードされたJSONデータをデコードします。`ioutil.ReadAll`で読み込んだデータはバイトスライスになるため、`Unmarshal`が適しています。`resp.Body`は一度しか読み込めないため、`ioutil.ReadAll`で読み込んだ後は`NewDecoder`を使うことはできません。

## 技術的詳細

このコミットの主要な変更は、`misc/dashboard/codereview/dashboard/cl.go` ファイル内の `updateCL` 関数に集中しています。この関数は、特定のCLの情報をコードレビューシステムから取得し、ダッシュボードのDatastoreに更新する役割を担っています。

変更前は、`updateCL` 関数はコードレビューAPIからHTTPレスポンスを受け取った後、直接 `json.NewDecoder(resp.Body).Decode(&apiResp)` を使用してJSONデータをデコードしていました。しかし、この方法では、APIが404エラーを返した場合に、そのレスポンスボディに特定のメッセージが含まれているかどうかをチェックすることができませんでした。`resp.Body` は一度しか読み込めないため、ステータスコードをチェックした後に再度ボディの内容を読み込もうとするとエラーになります。

この問題を解決するため、以下の変更が導入されました。

1.  **レスポンスボディの完全な読み込み**:
    `raw, err := ioutil.ReadAll(resp.Body)` を追加し、APIからのレスポンスボディ全体を `raw` というバイトスライスに読み込みます。これにより、ボディの内容を複数回参照できるようになります。
2.  **放棄されたCLの検出と処理**:
    `if resp.StatusCode == 404 && bytes.Contains(raw, []byte("No issue exists with that id"))` という条件分岐が追加されました。
    *   `resp.StatusCode == 404`: HTTPステータスコードが404 (Not Found) であることを確認します。
    *   `bytes.Contains(raw, []byte("No issue exists with that id"))`: レスポンスボディの内容 (`raw`) が、特定の文字列 "No issue exists with that id" を含んでいることを確認します。この文字列は、コードレビューシステムがCLが見つからない場合に返す典型的なエラーメッセージです。
    この両方の条件が満たされた場合、そのCLは放棄されたものと判断されます。
    *   `datastore.Delete(c, key)`: Datastoreから該当するCLのエントリを削除します。
    *   `c.Infof("Deleted abandoned CL %v", n)`: ログに削除されたCLの情報を出力します。
    *   `return nil`: 正常に処理が完了したとして関数を終了します。
3.  **JSONデコード方法の変更**:
    `if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {` の行が `if err := json.Unmarshal(raw, &apiResp); err != nil {` に変更されました。これは、`resp.Body` が既に `ioutil.ReadAll` によって読み込まれているため、`raw` バイトスライスからJSONをデコードする必要があるためです。

これらの変更により、ダッシュボードは放棄されたCLを検出し、Datastoreからその情報を削除することで、データの整合性を保ち、不必要なエラー処理を回避できるようになりました。

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

`misc/dashboard/codereview/dashboard/cl.go` ファイルの `updateCL` 関数内。

```diff
--- a/misc/dashboard/codereview/dashboard/cl.go
+++ b/misc/dashboard/codereview/dashboard/cl.go
@@ -7,10 +7,12 @@ package dashboard
 // This file handles operations on the CL entity kind.
 
 import (
+\t"bytes"
 \t"encoding/json"
 \t"fmt"
 \t"html/template"
 \t"io"
+\t"io/ioutil"
 \t"net/http"
 \tnetmail "net/mail"
 \t"net/url"
@@ -256,6 +258,7 @@ func handleUpdateCL(w http.ResponseWriter, r *http.Request) {
 // updateCL updates a single CL. If a retryable failure occurs, an error is returned.\n func updateCL(c appengine.Context, n string) error {\n \tc.Debugf("Updating CL %v", n)\n+\tkey := datastore.NewKey(c, "CL", n, 0, nil)\n \n \turl := codereviewBase + "/api/" + n + "?messages=true"\n \tresp, err := urlfetch.Client(c).Get(url)\n@@ -263,6 +266,20 @@ func updateCL(c appengine.Context, n string) error {\n \t\treturn err\n \t}\n \tdefer resp.Body.Close()\n+\n+\traw, err := ioutil.ReadAll(resp.Body)\n+\tif err != nil {\n+\t\treturn fmt.Errorf("Failed reading HTTP body: %v", err)\n+\t}\n+\n+\t// Special case for abandoned CLs.\n+\tif resp.StatusCode == 404 && bytes.Contains(raw, []byte("No issue exists with that id")) {\n+\t\t// Don't bother checking for errors. The CL might never have been saved, for instance.\n+\t\tdatastore.Delete(c, key)\n+\t\tc.Infof("Deleted abandoned CL %v", n)\n+\t\treturn nil\n+\t}\n+\n \tif resp.StatusCode != 200 {\n \t\treturn fmt.Errorf("Update: got HTTP response %d", resp.StatusCode)\n \t}\n@@ -281,7 +298,7 @@ func updateCL(c appengine.Context, n string) {\n \t\t\tApproval   bool     `json:\"approval\"`\n \t\t} `json:\"messages\"`\n \t}\n-\tif err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {\n+\tif err := json.Unmarshal(raw, &apiResp); err != nil {\n \t\t// probably can\'t be retried\n \t\tc.Errorf("Malformed JSON from %v: %v", url, err)\n \t\treturn nil\n@@ -341,7 +358,6 @@ func updateCL(c appengine.Context, n string) error {\n \tsort.Strings(cl.LGTMs)\n \tsort.Strings(cl.Recipients)\n \n-\tkey := datastore.NewKey(c, "CL", n, 0, nil)\n \terr = datastore.RunInTransaction(c, func(c appengine.Context) error {\n \t\tocl := new(CL)\n \t\terr := datastore.Get(c, key, ocl)\n```

## コアとなるコードの解説

1.  **`import ("bytes", "io/ioutil")`**:
    `bytes` パッケージはバイトスライス操作のために、`io/ioutil` パッケージはHTTPレスポンスボディを読み込むためにそれぞれ追加されました。
2.  **`key := datastore.NewKey(c, "CL", n, 0, nil)` の移動**:
    この行は、Datastoreのキーを生成するもので、以前は関数の後半にありました。放棄されたCLを削除する新しいロジックでこのキーが必要になるため、関数の冒頭に移動されました。
3.  **`raw, err := ioutil.ReadAll(resp.Body)`**:
    `urlfetch.Client(c).Get(url)` で取得したHTTPレスポンスのボディ (`resp.Body`) を全て読み込み、`raw` というバイトスライスに格納します。これにより、ボディの内容を後で解析できるようになります。
4.  **放棄されたCLの検出ロジック**:
    ```go
    if resp.StatusCode == 404 && bytes.Contains(raw, []byte("No issue exists with that id")) {
        datastore.Delete(c, key)
        c.Infof("Deleted abandoned CL %v", n)
        return nil
    }
    ```
    このブロックがこのコミットの核心です。
    *   HTTPステータスコードが `404` (Not Found) であることを確認します。
    *   さらに、読み込んだレスポンスボディ `raw` が、コードレビューシステムが返す特定のメッセージ `"No issue exists with that id"` を含んでいることを `bytes.Contains` で確認します。
    *   両方の条件が真の場合、そのCLはもはや存在しない(放棄された)と判断し、`datastore.Delete(c, key)` を呼び出してDatastoreからそのCLの情報を削除します。
    *   `c.Infof` でログに削除したことを記録し、`return nil` で関数を正常終了します。これにより、後続のJSONデコードやDatastore更新処理がスキップされます。
5.  **`if err := json.Unmarshal(raw, &apiResp); err != nil {`**:
    以前は `json.NewDecoder(resp.Body).Decode(&apiResp)` を使用していましたが、`resp.Body` は既に `ioutil.ReadAll` によって読み込まれているため、代わりに `raw` バイトスライスを `json.Unmarshal` でデコードするように変更されました。これにより、既にメモリにロードされたJSONデータを効率的に処理できます。

これらの変更により、ダッシュボードはコードレビューシステムとの同期をより正確に行い、存在しないCLに関する不必要な処理やエラーを回避できるようになりました。

## 関連リンク

*   Go言語のコードレビュープロセスに関する情報(当時のシステムはGerritベースまたは類似のカスタムシステム):
    *   [https://go.dev/doc/contribute](https://go.dev/doc/contribute) (現在のGoプロジェクトの貢献ガイドライン)
*   Google App Engine Datastoreのドキュメント:
    *   [https://cloud.google.com/appengine/docs/standard/go/datastore/](https://cloud.google.com/appengine/docs/standard/go/datastore/) (Go用Datastoreクライアントライブラリの概要)

## 参考にした情報源リンク

*   Go言語の標準ライブラリドキュメント:
    *   `bytes` パッケージ: [https://pkg.go.dev/bytes](https://pkg.go.dev/bytes)
    *   `io/ioutil` パッケージ (Go 1.16以降は `io` および `os` に移行): [https://pkg.go.dev/io/ioutil](https://pkg.go.dev/io/ioutil)
    *   `encoding/json` パッケージ: [https://pkg.go.dev/encoding/json](https://pkg.go.dev/encoding/json)
*   Google App Engineの公式ドキュメント (当時のバージョン):
    *   `urlfetch` サービス: [https://cloud.google.com/appengine/docs/standard/go/reference/services/urlfetch](https://cloud.google.com/appengine/docs/standard/go/reference/services/urlfetch)
    *   `datastore` サービス: [https://cloud.google.com/appengine/docs/standard/go/reference/services/datastore](https://cloud.google.com/appengine/docs/standard/go/reference/services/datastore)
*   コミットに記載されているCLリンク:
    *   [https://golang.org/cl/6257082](https://golang.org/cl/6257082) (このリンクは古いGoのコードレビューシステムのものであり、現在はリダイレクトされるか、直接アクセスできない可能性がありますが、当時のCLのIDを示しています。)
*   Go言語のダッシュボードプロジェクトのソースコード:
    *   [https://github.com/golang/go/tree/master/misc/dashboard](https://github.com/golang/go/tree/master/misc/dashboard) (現在のGoリポジトリ内のダッシュボード関連コード)
*   Web検索結果: "Go App Engine dashboard codereview"
    *   `codereview.appspot.com` は、Goプロジェクトで使われていた古いコードレビューシステムであり、App Engine上でホストされていたことが示唆されています。
    *   現代のGoプロジェクトのコードレビューは、GitHub, GitLabなどのプラットフォームで行われています。