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

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

このコミットは、Go言語のビルドシステムの一部である gobuilder ツールに対する重要な変更を含んでいます。具体的には、ダッシュボードとの通信プロトコルの更新、パッケージごとのコミット処理の導入、そして一時的なパッケージモードの無効化が主な内容です。

コミット

commit 263c955f2fff2016b5ff77d787d8e1b50555930a
Author: Andrew Gerrand <adg@golang.org>
Date:   Mon Dec 5 16:44:10 2011 +1100

    gobuilder: use new dashboard protocol
    gobuilder: -commit mode for packages
    gobuilder: cripple -package mode temporarily
    
    R=golang-dev, dsymonds
    CC=golang-dev
    https://golang.org/cl/5450092

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

https://github.com/golang/go/commit/263c955f2fff2016b5ff77d787d8e1b50555930a

元コミット内容

このコミットの元の内容は以下の通りです。

  • gobuilder: 新しいダッシュボードプロトコルを使用する。
  • gobuilder: パッケージ向けの -commit モードを導入する。
  • gobuilder: -package モードを一時的に無効化する。

変更の背景

このコミットの背景には、Go言語のビルドインフラストラクチャにおけるダッシュボードとの連携方法の進化があります。gobuilder は、Goプロジェクトの継続的インテグレーション(CI)システムの一部として機能し、コミットのビルドとテストの結果をダッシュボードに報告する役割を担っています。

変更の主な動機は以下の点が考えられます。

  1. 新しいダッシュボードプロトコルへの移行: ダッシュボード側のAPIまたは通信方式が変更されたため、gobuilder もそれに合わせて更新する必要がありました。これにより、より効率的で堅牢なデータ交換が可能になったと考えられます。
  2. パッケージごとのコミット処理の導入: Go言語のエコシステムが拡大し、多くの独立したパッケージが開発される中で、モノリシックなリポジトリ全体のビルドだけでなく、個々のパッケージの変更をより細かく追跡し、ビルド結果を報告するニーズが高まったと考えられます。-commit モードの導入は、この粒度の細かい管理を可能にするためのものです。
  3. 一時的な -package モードの無効化: 新しいプロトコルやパッケージごとのコミット処理の導入に伴い、既存の -package モードが新しいシステムと互換性がなかったか、あるいは新しい機能の開発中に一時的に競合を避けるために無効化されたと考えられます。これは、大規模なシステム変更における一般的な開発プラクティスであり、段階的な移行を示唆しています。

この変更は、GoプロジェクトのCI/CDパイプラインの柔軟性とスケーラビリティを向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下の技術的背景知識が役立ちます。

  • Go言語: Googleによって開発されたオープンソースのプログラミング言語。並行処理に強く、シンプルで効率的なコード記述が可能です。
  • Mercurial (Hg): 分散型バージョン管理システム(DVCS)。Gitと同様に、コードの変更履歴を管理するために使用されます。Goプロジェクトの初期にはMercurialが主要なバージョン管理システムとして使用されていました。コミットログの解析やリポジトリのクローンに hg コマンドが使われていることから、Mercurialが深く関わっていることがわかります。
  • 継続的インテグレーション (CI): ソフトウェア開発手法の一つで、開発者がコードベースに加えた変更を頻繁にメインブランチにマージし、自動的にビルドとテストを行うことで、統合の問題を早期に発見し解決します。gobuilder はこのCIプロセスの一部を自動化しています。
  • ダッシュボード: CIシステムにおいて、ビルドのステータス、テスト結果、コードカバレッジなどの情報を視覚的に表示するウェブインターフェース。開発者はダッシュボードを通じてプロジェクトの健全性を一目で確認できます。Goプロジェクトには build.golang.org のような公式のビルドダッシュボードが存在します。
  • HTTP/JSON:
    • HTTP (Hypertext Transfer Protocol): ウェブ上でデータを交換するためのプロトコル。クライアント(gobuilder)とサーバー(ダッシュボード)間の通信に使用されます。
    • JSON (JavaScript Object Notation): 軽量なデータ交換フォーマット。人間が読み書きしやすく、機械が解析しやすい構造を持っています。このコミットでは、ダッシュボードとの間でJSON形式のデータがやり取りされるように変更されています。
  • Goの net/http パッケージ: Go言語の標準ライブラリで、HTTPクライアントおよびサーバーを実装するための機能を提供します。
  • Goの encoding/json パッケージ: Go言語の標準ライブラリで、JSONデータのエンコード(Goの構造体からJSONへ)およびデコード(JSONからGoの構造体へ)を行う機能を提供します。
  • url.Values: Goの net/url パッケージにある型で、URLクエリパラメータやフォームデータを扱うためのマップのような構造です。キーと値のペアを管理し、URLエンコードされた文字列に変換するのに便利です。

技術的詳細

このコミットにおける技術的な変更は多岐にわたりますが、特に misc/dashboard/builder/http.gomisc/dashboard/builder/main.go の変更が重要です。

misc/dashboard/builder/http.go の変更

  • dash 関数の大幅な変更:
    • 以前は param map[string]string を引数として取り、クエリパラメータまたはPOSTフォームデータを扱っていました。
    • 新しい dash 関数は、url.Values (クエリパラメータ用) と、req (POSTリクエストのJSONボディ用)、resp (レスポンスのJSONデコード用) というより汎用的な引数を取るようになりました。
    • これにより、GETリクエストではクエリパラメータを、POSTリクエストではJSON形式のボディを送信できるようになり、新しいダッシュボードプロトコルに合わせた柔軟な通信が可能になりました。
    • JSONのエンコード/デコード処理が json.Marshaljson.Unmarshal を直接使用するように変更され、エラーハンドリングも改善されています。特に、レスポンスボディ全体を読み込んでからJSONデコードを行うように変更されています。
    • dashStatus 関数が削除され、そのエラーチェックロジックは新しい dash 関数のレスポンス処理に統合されました。これは、ダッシュボードからのレスポンスに Status フィールドと Error フィールドが含まれるという従来のプロトコルから、より一般的なJSONレスポンスとHTTPステータスコードに基づくエラーハンドリングへの移行を示唆しています。
  • todo 関数の変更: ダッシュボードから次にビルドすべきリビジョンを取得する関数です。以前はハッシュのリストを期待していましたが、新しいプロトコルでは単一のハッシュ文字列を直接返すようになりました。
  • recordResult 関数の変更: ビルド結果をダッシュボードに送信する関数です。新しい dash 関数のシグネチャに合わせて、リクエストボディを obj (map[string]interface{}) としてJSONエンコードして送信するようになりました。
  • packages および updatePackage 関数の無効化: これらの関数は一時的に return nil, nilreturn nil を返すようにスタブ化されています。これはコミットメッセージの「cripple -package mode temporarily」に対応しており、新しいプロトコルやパッケージ処理の設計が完了するまで、これらの機能が一時的に利用できないことを示しています。
  • postCommit 関数の変更: コミット情報をダッシュボードに通知する関数です。
    • 引数に pkg (パッケージパス) が追加され、ダッシュボードに送信するデータに PackagePath フィールドが含まれるようになりました。これにより、特定のパッケージに関連するコミットをダッシュボードが認識できるようになります。
    • 返り値が error から bool に変更され、成功/失敗を直接示すようになりました。
  • dashboardCommit 関数の変更: ダッシュボードが特定のコミットを認識しているかを確認する関数です。引数に pkg が追加され、パッケージごとのコミット存在チェックが可能になりました。
  • dashboardPackages 関数の追加: ダッシュボードからビルドすべきパッケージのリストを取得するための新しい関数です。これにより、gobuilder は複数のパッケージを監視し、それぞれに対してコミット処理を行うことができるようになります。
  • repoURL 関数の追加: インポートパスからリポジトリのURLをデコードするための正規表現ベースのヘルパー関数です。これは、パッケージのソースコードをクローンする際に使用されると考えられます。

misc/dashboard/builder/main.go の変更

  • main 関数の変更:
    • リポジトリのクローン処理が hgClone という新しいヘルパー関数に分離されました。
    • fullHash 関数の呼び出しに goroot または pkgRoot が引数として追加され、特定のパス内でハッシュを解決するようになりました。
  • commitWatcher 関数の変更:
    • この関数は、新しいコミットを定期的にポーリングする役割を担っています。
    • 変更後、commitPoll を空のパッケージパス ("") で呼び出すだけでなく、dashboardPackages() から取得した各パッケージに対しても commitPoll を呼び出すようになりました。これにより、gobuilder はGoリポジトリ全体だけでなく、ダッシュボードが認識している個々のパッケージのコミットも監視するようになりました。
  • hgClone および hgRepoExists 関数の追加: Mercurialリポジトリのクローンと存在チェックをカプセル化するためのヘルパー関数です。
  • commitPoll 関数の変更:
    • 引数に pkg が追加されました。
    • pkg が空でない場合、pkgRoot を計算し、そのパスにMercurialリポジトリが存在しない場合は hgClone を試みます。これにより、gobuilder は必要に応じて個々のパッケージリポジトリを動的にクローンできるようになります。
    • hg pullhg log の実行パスが goroot から pkgRoot に変更され、パッケージごとのリポジトリ操作が可能になりました。
    • fullHash の呼び出しも pkgRoot を使用するように変更されています。
  • addCommit 関数の変更:
    • 引数に pkg が追加されました。
    • dashboardCommitpostCommit の呼び出しも pkg を含むように変更され、パッケージごとのコミット情報をダッシュボードに送信するようになりました。
    • 親コミットの追加ロジックも、パッケージごとのコンテキストで実行されるようになりました。
  • fullHash 関数の変更: 引数に root (リポジトリのルートパス) が追加され、指定されたリポジトリ内でハッシュを解決するようになりました。

これらの変更は、gobuilder がGo言語のモノリポジトリだけでなく、個々のパッケージリポジトリも効率的に管理し、ダッシュボードにそのビルド状況を報告できるようにするためのアーキテクチャ的な進化を示しています。

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

misc/dashboard/builder/http.go

--- a/misc/dashboard/builder/http.go
+++ b/misc/dashboard/builder/http.go
@@ -8,96 +8,108 @@ import (
 	"bytes"
 	"encoding/json"
 	"errors"
-	"fmt"
+	"io"
 	"log"
 	"net/http"
 	"net/url"
-	"strconv"
 )
 
-type param map[string]string
+type obj map[string]interface{}
 
 // dash runs the given method and command on the dashboard.
-// If args is not nil, it is the query or post parameters.
-// If resp is not nil, dash unmarshals the body as JSON into resp.
-func dash(meth, cmd string, resp interface{}, args param) error {
+// If args is non-nil it is encoded as the URL query string.
+// If req is non-nil it is JSON-encoded and passed as the body of the HTTP POST.
+// If resp is non-nil the server's response is decoded into the value pointed
+// to by resp (resp must be a pointer).
+func dash(meth, cmd string, args url.Values, req, resp interface{}) error {
 	var r *http.Response
 	var err error
 	if *verbose {
-		log.Println("dash", cmd, args)
+		log.Println("dash", meth, cmd, args, req)
 	}
 	cmd = "http://" + *dashboard + "/" + cmd
-	vals := make(url.Values)
-	for k, v := range args {
-		vals.Add(k, v)
+	if len(args) > 0 {
+		cmd += "?" + args.Encode()
 	}
 	switch meth {
 	case "GET":
-		if q := vals.Encode(); q != "" {
-			cmd += "?" + q
+		if req != nil {
+			log.Panicf("%s to %s with req", meth, cmd)
 		}
 		r, err = http.Get(cmd)
 	case "POST":
-		r, err = http.PostForm(cmd, vals)
+		var body io.Reader
+		if req != nil {
+			b, err := json.Marshal(req)
+			if err != nil {
+				return err
+			}
+			body = bytes.NewBuffer(b)
+		}
+		r, err = http.Post(cmd, "text/json", body)
 	default:
-		return fmt.Errorf("unknown method %q", meth)
+		log.Panicf("%s: invalid method %q", cmd, meth)
+		panic("invalid method: " + meth)
 	}
 	if err != nil {
 		return err
 	}
-
 	defer r.Body.Close()
-	var buf bytes.Buffer
-	buf.ReadFrom(r.Body)
-	if resp != nil {
-		if err = json.Unmarshal(buf.Bytes(), resp); err != nil {
-			log.Printf("json unmarshal %#q: %s\n", buf.Bytes(), err)
-			return err
-		}
+	body := new(bytes.Buffer)
+	if _, err := body.ReadFrom(r.Body); err != nil {
+		return err
 	}
-	return nil
-}
-
-func dashStatus(meth, cmd string, args param) error {
-	var resp struct {
-		Status string
-		Error  string
+
+	// Read JSON-encoded Response into provided resp
+	// and return an error if present.
+	var result = struct {
+		Response interface{}
+		Error    string
+	}{
+		// Put the provided resp in here as it can be a pointer to
+		// some value we should unmarshal into.
+		Response: resp,
 	}
-	err := dash(meth, cmd, &resp, args)
-	if err != nil {
+	if err = json.Unmarshal(body.Bytes(), &result); err != nil {
+		log.Printf("json unmarshal %#q: %s\n", body.Bytes(), err)
 		return err
 	}
-	if resp.Status != "OK" {
-		return errors.New("/build: " + resp.Error)
+	if result.Error != "" {
+		return errors.New(result.Error)
 	}
+
 	return nil
 }
 
 // todo returns the next hash to build.
 func (b *Builder) todo() (rev string, err error) {
-	var resp []struct {
-		Hash string
-	}
-	if err = dash("GET", "todo", &resp, param{"builder": b.name}); err != nil {
+	// TODO(adg): handle packages
+	args := url.Values{"builder": {b.name}}
+	var resp string
+	if err = dash("GET", "todo", args, nil, &resp); err != nil {
 		return
 	}
-	if len(resp) > 0 {
-		rev = resp[0].Hash
+	if resp != "" {
+		rev = resp
 	}
 	return
 }
 
 // recordResult sends build results to the dashboard
 func (b *Builder) recordResult(buildLog string, hash string) error {
-	return dash("POST", "build", nil, param{
-		"builder": b.name,
-		"key":     b.key,
-		"node":    hash,
-		"log":     buildLog,
-	})
+	// TODO(adg): handle packages
+	return dash("POST", "result", url.Values{"key": {b.key}}, obj{
+		"Builder": b.name,
+		"Hash":    hash,
+		"Log":     buildLog,
+	}, nil)
 }
 
 // packages fetches a list of package paths from the dashboard
 func packages() (pkgs []string, err error) {
+	return nil, nil
+	/* TODO(adg): un-stub this once the new package builder design is done
 	var resp struct {
 		Packages []struct {
 			Path string
@@ -111,10 +123,13 @@ func packages() (pkgs []string, err error) {
 	\tpkgs = append(pkgs, p.Path)\n \t}\n \treturn\n+\t*/
 }
 
 // updatePackage sends package build results and info dashboard
 func (b *Builder) updatePackage(pkg string, ok bool, buildLog, info string) error {
+\treturn nil
+\t/* TODO(adg): un-stub this once the new package builder design is done
 	return dash("POST", "package", nil, param{
 		"builder": b.name,
 		"key":     b.key,
 		"package": pkg,
@@ -123,26 +138,44 @@ func (b *Builder) updatePackage(pkg string, ok bool, buildLog, info string) erro
 		"log":     buildLog,
 		"info":    info,
 	})
+\t*/
 }
 
-// postCommit informs the dashboard of a new commit
-func postCommit(key string, l *HgLog) error {
-	return dashStatus("POST", "commit", param{
-		"key":    key,
-		"node":   l.Hash,
-		"date":   l.Date,
-		"user":   l.Author,
-		"parent": l.Parent,
-		"desc":   l.Desc,
-	})
-}
-
-// dashboardCommit returns true if the dashboard knows about hash.
-func dashboardCommit(hash string) bool {
-	err := dashStatus("GET", "commit", param{"node": hash})
+func postCommit(key, pkg string, l *HgLog) bool {
+	err := dash("POST", "commit", url.Values{"key": {key}}, obj{
+		"PackagePath": pkg,
+		"Hash":        l.Hash,
+		"ParentHash":  l.Parent,
+		// TODO(adg): l.Date as int64 unix epoch secs in Time field
+		"User": l.Author,
+		"Desc": l.Desc,
+	}, nil)
 	if err != nil {
-		log.Printf("check %s: %s", hash, err)
+		log.Printf("failed to add %s to dashboard: %v", key, err)
 		return false
 	}
 	return true
 }
+
+func dashboardCommit(pkg, hash string) bool {
+	err := dash("GET", "commit", url.Values{
+		"packagePath": {pkg},
+		"hash":        {hash},
+	}, nil, nil)
+	return err == nil
+}
+
+func dashboardPackages() []string {
+	var resp []struct {
+		Path string
+	}
+	if err := dash("GET", "packages", nil, nil, &resp); err != nil {
+		log.Println("dashboardPackages:", err)
+		return nil
+	}
+	var pkgs []string
+	for _, r := range resp {
+		pkgs = append(pkgs, r.Path)
+	}
+	return pkgs
+}

misc/dashboard/builder/main.go

--- a/misc/dashboard/builder/main.go
+++ b/misc/dashboard/builder/main.go
@@ -13,6 +13,7 @@ import (
 	"log"
 	"os"
 	"path"
+	"path/filepath"
 	"regexp"
 	"runtime"
 	"strconv"
@@ -93,7 +94,7 @@ func main() {
 	if err := os.Mkdir(*buildroot, mkdirPerm); err != nil {
 		log.Fatalf("Error making build root (%s): %s", *buildroot, err)
 	}\n-\tif err := run(nil, *buildroot, "hg", "clone", hgUrl, goroot); err != nil {
+\tif err := hgClone(hgUrl, goroot); err != nil {
 		log.Fatal("Error cloning repository:", err)
 	}
 
@@ -107,7 +108,7 @@ func main() {
 
 	// if specified, build revision and return
 	if *buildRevision != "" {
-\t\thash, err := fullHash(*buildRevision)\n+\t\thash, err := fullHash(goroot, *buildRevision)\n 		if err != nil {
 			log.Fatal("Error finding revision: ", err)
 		}
@@ -246,7 +247,7 @@ func (b *Builder) build() bool {
 	}
 	// Look for hash locally before running hg pull.
 
-\tif _, err := fullHash(hash[:12]); err != nil {
+\tif _, err := fullHash(goroot, hash[:12]); err != nil {
 		// Don't have hash, so run hg pull.
 		if err := run(nil, goroot, "hg", "pull"); err != nil {
 			log.Println("hg pull failed:", err)
@@ -425,11 +426,16 @@ func commitWatcher() {
 	if err != nil {
 		log.Fatal(err)
 	}
+\tkey := b.key
+\n \tfor {
 \t\tif *verbose {
 \t\t\tlog.Printf("poll...")
 \t\t}
-\t\tcommitPoll(b.key)\n+\t\tcommitPoll(key, "")
+\t\tfor _, pkg := range dashboardPackages() {
+\t\t\tcommitPoll(key, pkg)
+\t\t}
 \t\tif *verbose {
 \t\t\tlog.Printf("sleep...")
 \t\t}
@@ -437,6 +443,18 @@ func commitWatcher() {
 	}
 }
 
+func hgClone(url, path string) error {
+	return run(nil, *buildroot, "hg", "clone", url, path)
+}
+
+func hgRepoExists(path string) bool {
+	fi, err := os.Stat(filepath.Join(path, ".hg"))
+	if err != nil {
+		return false
+	}
+	return fi.IsDir()
+}
+
 // HgLog represents a single Mercurial revision.
 type HgLog struct {
 	Hash   string
@@ -467,7 +485,7 @@ const xmlLogTemplate = `
 
 // commitPoll pulls any new revisions from the hg server
 // and tells the server about them.
-func commitPoll(key string) {
+func commitPoll(key, pkg string) {
 	// Catch unexpected panics.
 	defer func() {
 	\tif err := recover(); err != nil {
@@ -475,14 +493,29 @@ func commitPoll(key string) {
 		}\n \t}()
 \n-\tif err := run(nil, goroot, "hg", "pull"); err != nil {
+\tpkgRoot := goroot
+\n+\tif pkg != "" {
+\t\tpkgRoot = path.Join(*buildroot, pkg)
+\t\tif !hgRepoExists(pkgRoot) {
+\t\t\tif err := hgClone(repoURL(pkg), pkgRoot); err != nil {
+\t\t\t\tlog.Printf("%s: hg clone failed: %v", pkg, err)
+\t\t\t\tif err := os.RemoveAll(pkgRoot); err != nil {
+\t\t\t\t\tlog.Printf("%s: %v", pkg, err)
+\t\t\t\t}\n+\t\t\t\treturn
+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tif err := run(nil, pkgRoot, "hg", "pull"); err != nil {
 		log.Printf("hg pull: %v", err)
 		return
 	}
 
 	const N = 50 // how many revisions to grab
 
-\tdata, _, err := runLog(nil, "", goroot, "hg", "log",
+\tdata, _, err := runLog(nil, "", pkgRoot, "hg", "log",
 		"--encoding=utf-8",
 		"--limit="+strconv.Itoa(N),
 		"--template="+xmlLogTemplate,
@@ -511,14 +544,11 @@ func commitPoll(key string) {
 	\tif l.Parent == "" && i+1 < len(logs) {
 	\t\tl.Parent = logs[i+1].Hash
 	\t} else if l.Parent != "" {
-\t\t\tl.Parent, _ = fullHash(l.Parent)\n+\t\t\tl.Parent, _ = fullHash(pkgRoot, l.Parent)\n 	\t}
-\t\tlog.Printf("hg log: %s < %s\n", l.Hash, l.Parent)
-\t\tif l.Parent == "" {
-\t\t\t// Can't create node without parent.
-\t\t\tcontinue
+\t\tif *verbose {
+\t\t\tlog.Printf("hg log %s: %s < %s\n", pkg, l.Hash, l.Parent)
 	\t}
-\n \t\tif logByHash[l.Hash] == nil {
 \t\t\t// Make copy to avoid pinning entire slice when only one entry is new.\n \t\t\tt := *l
@@ -528,17 +558,14 @@ func commitPoll(key string) {
 
 	for i := range logs {
 	\tl := &logs[i]
-\t\tif l.Parent == "" {
-\t\t\tcontinue
-\t\t}\n-\t\taddCommit(l.Hash, key)\n+\t\taddCommit(pkg, l.Hash, key)
 	}
 }
 
 // addCommit adds the commit with the named hash to the dashboard.
 // key is the secret key for authentication to the dashboard.
 // It avoids duplicate effort.
-func addCommit(hash, key string) bool {
+func addCommit(pkg, hash, key string) bool {
 	l := logByHash[hash]
 	if l == nil {
 		return false
@@ -548,7 +575,7 @@ func addCommit(hash, key string) bool {
 	}
 
 	// Check for already added, perhaps in an earlier run.
-\tif dashboardCommit(hash) {
+\tif dashboardCommit(pkg, hash) {
 	\tlog.Printf("%s already on dashboard\n", hash)
 	\t// Record that this hash is on the dashboard,\n \t\t// as must be all its parents.\n@@ -560,26 +587,24 @@ func addCommit(hash, key string) bool {
 	}
 
 	// Create parent first, to maintain some semblance of order.
-\tif !addCommit(l.Parent, key) {
-\t\treturn false
+\tif l.Parent != "" {
+\t\tif !addCommit(pkg, l.Parent, key) {
+\t\t\treturn false
+\t\t}
 	}
 
 	// Create commit.
-\tif err := postCommit(key, l); err != nil {
-\t\tlog.Printf("failed to add %s to dashboard: %v", key, err)
-\t\treturn false
-\t}\n-\treturn true
+\treturn postCommit(key, pkg, l)
 }
 
 // fullHash returns the full hash for the given Mercurial revision.
-func fullHash(rev string) (hash string, err error) {
+func fullHash(root, rev string) (hash string, err error) {
 	defer func() {
 	\tif err != nil {
 	\t\terr = fmt.Errorf("fullHash: %s: %s", rev, err)
 	\t}
 	}()
-\ts, _, err := runLog(nil, "", goroot,
+\ts, _, err := runLog(nil, "", root,
 		"hg", "log",
 		"--encoding=utf-8",
 		"--rev="+rev,
@@ -617,9 +642,21 @@ func firstTag(re *regexp.Regexp) (hash string, tag string, err error) {
 	\t\tcontinue
 	\t}
 	\ttag = s[1]
-\t\thash, err = fullHash(s[2])
+\t\thash, err = fullHash(goroot, s[2])
 	\treturn
 	}
 	err = errors.New("no matching tag found")
 	return
 }
+
+var repoRe = regexp.MustCompile(`^code\.google\.com/p/([a-z0-9\\-]+(\\.[a-z0-9\\-]+)?)(/[a-z0-9A-Z_.\\-/]+)?$`)
+
+// repoURL returns the repository URL for the supplied import path.
+func repoURL(importPath string) string {
+	m := repoRe.FindStringSubmatch(importPath)
+	if len(m) < 2 {
+		log.Printf("repoURL: couldn't decipher %q", importPath)
+		return ""
+	}
+	return "https://code.google.com/p/" + m[1]
+}

コアとなるコードの解説

このコミットの核となる変更は、gobuilder がダッシュボードと通信する方法と、コミットを処理する粒度を根本的に変更した点にあります。

http.godash 関数

新しい dash 関数は、ダッシュボードとのHTTP通信の汎用的なインターフェースとして機能します。

  • 柔軟なリクエスト/レスポンス処理: 以前は param マップを使ってクエリパラメータとPOSTフォームデータを区別していましたが、新しい dash 関数は url.Values でクエリパラメータを、req interface{} でJSONエンコードされるリクエストボディを、resp interface{} でJSONデコードされるレスポンスボディを扱います。これにより、ダッシュボードの新しいAPIがJSONベースのボディを期待する場合でも、柔軟に対応できるようになりました。
  • エラーハンドリングの改善: レスポンスボディ全体を読み込んでからJSONデコードを行うことで、部分的な読み込みによる問題を防ぎます。また、ダッシュボードからのエラーレスポンスが result.Error フィールドに含まれる場合、それをGoのエラーとして返すことで、より明確なエラー伝播を実現しています。

main.gocommitWatchercommitPoll 関数

これらの関数は、gobuilder が新しいコミットをどのように監視し、処理するかを定義しています。

  • パッケージごとのポーリング: commitWatcher は、従来のGoリポジトリ全体のポーリング (commitPoll(key, "")) に加えて、dashboardPackages() から取得した各パッケージに対しても commitPoll(key, pkg) を呼び出すようになりました。これは、ダッシュボードが管理する個々のGoパッケージの変更も gobuilder が追跡し、ビルド結果を報告できるようになったことを意味します。
  • 動的なリポジトリクローン: commitPoll 関数内で、特定のパッケージ (pkg) が指定された場合、そのパッケージのリポジトリ (pkgRoot) が存在しない場合は hgClone を使って動的にクローンを試みます。これにより、gobuilder は必要に応じて新しいパッケージリポジトリを自動的にセットアップし、監視対象に加えることができます。これは、Goエコシステムにおけるパッケージの増加に対応するための重要な機能です。
  • パッケージコンテキストでのMercurial操作: hg pullhg logfullHash といったMercurial操作が、goroot (Goリポジトリ全体) だけでなく、pkgRoot (個々のパッケージリポジトリ) のコンテキストでも実行されるようになりました。これにより、各パッケージの変更履歴を独立して追跡し、ダッシュボードに報告することが可能になります。

postCommitaddCommit 関数

これらの関数は、コミット情報をダッシュボードに送信するロジックをカプセル化しています。

  • パッケージ情報の追加: postCommitaddCommit の両方に pkg 引数が追加され、ダッシュボードに送信されるコミット情報に PackagePath が含まれるようになりました。これにより、ダッシュボードはどのパッケージに属するコミットであるかを正確に識別し、関連するビルド結果と紐付けることができます。

これらの変更は、gobuilder がGo言語のビルドシステムにおいて、よりきめ細かく、スケーラブルなコミットおよびパッケージ管理を可能にするための基盤を構築しています。

関連リンク

参考にした情報源リンク

このコミットは、Go言語のビルドシステムの一部である gobuilder ツールに対する重要な変更を含んでいます。具体的には、ダッシュボードとの通信プロトコルの更新、パッケージごとのコミット処理の導入、そして一時的なパッケージモードの無効化が主な内容です。

コミット

commit 263c955f2fff2016b5ff77d787d8e1b50555930a
Author: Andrew Gerrand <adg@golang.org>
Date:   Mon Dec 5 16:44:10 2011 +1100

    gobuilder: use new dashboard protocol
    gobuilder: -commit mode for packages
    gobuilder: cripple -package mode temporarily
    
    R=golang-dev, dsymonds
    CC=golang-dev
    https://golang.org/cl/5450092

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

https://github.com/golang/go/commit/263c955f2fff2016b5ff77d787d8e1b50555930a

元コミット内容

このコミットの元の内容は以下の通りです。

  • gobuilder: 新しいダッシュボードプロトコルを使用する。
  • gobuilder: パッケージ向けの -commit モードを導入する。
  • gobuilder: -package モードを一時的に無効化する。

変更の背景

このコミットの背景には、Go言語のビルドインフラストラクチャにおけるダッシュボードとの連携方法の進化があります。gobuilder は、Goプロジェクトの継続的インテグレーション(CI)システムの一部として機能し、コミットのビルドとテストの結果をダッシュボードに報告する役割を担っています。

変更の主な動機は以下の点が考えられます。

  1. 新しいダッシュボードプロトコルへの移行: ダッシュボード側のAPIまたは通信方式が変更されたため、gobuilder もそれに合わせて更新する必要がありました。これにより、より効率的で堅牢なデータ交換が可能になったと考えられます。
  2. パッケージごとのコミット処理の導入: Go言語のエコシステムが拡大し、多くの独立したパッケージが開発される中で、モノリシックなリポジトリ全体のビルドだけでなく、個々のパッケージの変更をより細かく追跡し、ビルド結果を報告するニーズが高まったと考えられます。-commit モードの導入は、この粒度の細かい管理を可能にするためのものです。
  3. 一時的な -package モードの無効化: 新しいプロトコルやパッケージごとのコミット処理の導入に伴い、既存の -package モードが新しいシステムと互換性がなかったか、あるいは新しい機能の開発中に一時的に競合を避けるために無効化されたと考えられます。これは、大規模なシステム変更における一般的な開発プラクティスであり、段階的な移行を示唆しています。

この変更は、GoプロジェクトのCI/CDパイプラインの柔軟性とスケーラビリティを向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下の技術的背景知識が役立ちます。

  • Go言語: Googleによって開発されたオープンソースのプログラミング言語。並行処理に強く、シンプルで効率的なコード記述が可能です。
  • Mercurial (Hg): 分散型バージョン管理システム(DVCS)。Gitと同様に、コードの変更履歴を管理するために使用されます。Goプロジェクトの初期にはMercurialが主要なバージョン管理システムとして使用されていました。コミットログの解析やリポジトリのクローンに hg コマンドが使われていることから、Mercurialが深く関わっていることがわかります。
  • 継続的インテグレーション (CI): ソフトウェア開発手法の一つで、開発者がコードベースに加えた変更を頻繁にメインブランチにマージし、自動的にビルドとテストを行うことで、統合の問題を早期に発見し解決します。gobuilder はこのCIプロセスの一部を自動化しています。
  • ダッシュボード: CIシステムにおいて、ビルドのステータス、テスト結果、コードカバレッジなどの情報を視覚的に表示するウェブインターフェース。開発者はダッシュボードを通じてプロジェクトの健全性を一目で確認できます。Goプロジェクトには build.golang.org のような公式のビルドダッシュボードが存在します。
  • HTTP/JSON:
    • HTTP (Hypertext Transfer Protocol): ウェブ上でデータを交換するためのプロトコル。クライアント(gobuilder)とサーバー(ダッシュボード)間の通信に使用されます。
    • JSON (JavaScript Object Notation): 軽量なデータ交換フォーマット。人間が読み書きしやすく、機械が解析しやすい構造を持っています。このコミットでは、ダッシュボードとの間でJSON形式のデータがやり取りされるように変更されています。
  • Goの net/http パッケージ: Go言語の標準ライブラリで、HTTPクライアントおよびサーバーを実装するための機能を提供します。
  • Goの encoding/json パッケージ: Go言語の標準ライブラリで、JSONデータのエンコード(Goの構造体からJSONへ)およびデコード(JSONからGoの構造体へ)を行う機能を提供します。
  • url.Values: Goの net/url パッケージにある型で、URLクエリパラメータやフォームデータを扱うためのマップのような構造です。キーと値のペアを管理し、URLエンコードされた文字列に変換するのに便利です。

技術的詳細

このコミットにおける技術的な変更は多岐にわたりますが、特に misc/dashboard/builder/http.gomisc/dashboard/builder/main.go の変更が重要です。

misc/dashboard/builder/http.go の変更

  • dash 関数の大幅な変更:
    • 以前は param map[string]string を引数として取り、クエリパラメータまたはPOSTフォームデータを扱っていました。
    • 新しい dash 関数は、url.Values (クエリパラメータ用) と、req (POSTリクエストのJSONボディ用)、resp (レスポンスのJSONデコード用) というより汎用的な引数を取るようになりました。
    • これにより、GETリクエストではクエリパラメータを、POSTリクエストではJSON形式のボディを送信できるようになり、新しいダッシュボードプロトコルに合わせた柔軟な通信が可能になりました。
    • JSONのエンコード/デコード処理が json.Marshaljson.Unmarshal を直接使用するように変更され、エラーハンドリングも改善されています。特に、レスポンスボディ全体を読み込んでからJSONデコードを行うように変更されています。
    • dashStatus 関数が削除され、そのエラーチェックロジックは新しい dash 関数のレスポンス処理に統合されました。これは、ダッシュボードからのレスポンスに Status フィールドと Error フィールドが含まれるという従来のプロトコルから、より一般的なJSONレスポンスとHTTPステータスコードに基づくエラーハンドリングへの移行を示唆しています。
  • todo 関数の変更: ダッシュボードから次にビルドすべきリビジョンを取得する関数です。以前はハッシュのリストを期待していましたが、新しいプロトコルでは単一のハッシュ文字列を直接返すようになりました。
  • recordResult 関数の変更: ビルド結果をダッシュボードに送信する関数です。新しい dash 関数のシグネチャに合わせて、リクエストボディを obj (map[string]interface{}) としてJSONエンコードして送信するようになりました。
  • packages および updatePackage 関数の無効化: これらの関数は一時的に return nil, nilreturn nil を返すようにスタブ化されています。これはコミットメッセージの「cripple -package mode temporarily」に対応しており、新しいプロトコルやパッケージ処理の設計が完了するまで、これらの機能が一時的に利用できないことを示しています。
  • postCommit 関数の変更: コミット情報をダッシュボードに通知する関数です。
    • 引数に pkg (パッケージパス) が追加され、ダッシュボードに送信するデータに PackagePath フィールドが含まれるようになりました。これにより、特定のパッケージに関連するコミットをダッシュボードが認識できるようになります。
    • 返り値が error から bool に変更され、成功/失敗を直接示すようになりました。
  • dashboardCommit 関数の変更: ダッシュボードが特定のコミットを認識しているかを確認する関数です。引数に pkg が追加され、パッケージごとのコミット存在チェックが可能になりました。
  • dashboardPackages 関数の追加: ダッシュボードからビルドすべきパッケージのリストを取得するための新しい関数です。これにより、gobuilder は複数のパッケージを監視し、それぞれに対してコミット処理を行うことができるようになります。
  • repoURL 関数の追加: インポートパスからリポジトリのURLをデコードするための正規表現ベースのヘルパー関数です。これは、パッケージのソースコードをクローンする際に使用されると考えられます。

misc/dashboard/builder/main.go の変更

  • main 関数の変更:
    • リポジトリのクローン処理が hgClone という新しいヘルパー関数に分離されました。
    • fullHash 関数の呼び出しに goroot または pkgRoot が引数として追加され、特定のパス内でハッシュを解決するようになりました。
  • commitWatcher 関数の変更:
    • この関数は、新しいコミットを定期的にポーリングする役割を担っています。
    • 変更後、commitPoll を空のパッケージパス ("") で呼び出すだけでなく、dashboardPackages() から取得した各パッケージに対しても commitPoll を呼び出すようになりました。これにより、gobuilder はGoリポジトリ全体だけでなく、ダッシュボードが認識している個々のパッケージのコミットも監視するようになりました。
  • hgClone および hgRepoExists 関数の追加: Mercurialリポジトリのクローンと存在チェックをカプセル化するためのヘルパー関数です。
  • commitPoll 関数の変更:
    • 引数に pkg が追加されました。
    • pkg が空でない場合、そのパスにMercurialリポジトリが存在しない場合は hgClone を使って動的にクローンを試みます。これにより、gobuilder は必要に応じて個々のパッケージリポジトリを自動的にセットアップし、監視対象に加えることができます。これは、Goエコシステムにおけるパッケージの増加に対応するための重要な機能です。
    • hg pullhg log の実行パスが goroot から pkgRoot に変更され、パッケージごとのリポジトリ操作が可能になりました。
    • fullHash の呼び出しも pkgRoot を使用するように変更されています。
  • addCommit 関数の変更:
    • 引数に pkg が追加されました。
    • dashboardCommitpostCommit の呼び出しも pkg を含むように変更され、パッケージごとのコミット情報をダッシュボードに送信することができます。
    • 親コミットの追加ロジックも、パッケージごとのコンテキストで実行されるようになりました。
  • fullHash 関数の変更: 引数に root (リポジトリのルートパス) が追加され、指定されたリポジトリ内でハッシュを解決するようになりました。

これらの変更は、gobuilder がGo言語のモノリポジトリだけでなく、個々のパッケージリポジトリも効率的に管理し、ダッシュボードにそのビルド状況を報告できるようにするためのアーキテクチャ的な進化を示しています。

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

misc/dashboard/builder/http.go

--- a/misc/dashboard/builder/http.go
+++ b/misc/dashboard/builder/http.go
@@ -8,96 +8,108 @@ import (
 	"bytes"
 	"encoding/json"
 	"errors"
-	"fmt"
+	"io"
 	"log"
 	"net/http"
 	"net/url"
-	"strconv"
 )
 
-type param map[string]string
+type obj map[string]interface{}
 
 // dash runs the given method and command on the dashboard.
-// If args is not nil, it is the query or post parameters.
-// If resp is not nil, dash unmarshals the body as JSON into resp.
-func dash(meth, cmd string, resp interface{}, args param) error {
+// If args is non-nil it is encoded as the URL query string.
+// If req is non-nil it is JSON-encoded and passed as the body of the HTTP POST.
+// If resp is non-nil the server's response is decoded into the value pointed
+// to by resp (resp must be a pointer).
+func dash(meth, cmd string, args url.Values, req, resp interface{}) error {
 	var r *http.Response
 	var err error
 	if *verbose {
-		log.Println("dash", cmd, args)
+		log.Println("dash", meth, cmd, args, req)
 	}
 	cmd = "http://" + *dashboard + "/" + cmd
-	vals := make(url.Values)
-	for k, v := range args {
-		vals.Add(k, v)
+	if len(args) > 0 {
+		cmd += "?" + args.Encode()
 	}
 	switch meth {
 	case "GET":
-		if q := vals.Encode(); q != "" {
-			cmd += "?" + q
+		if req != nil {
+			log.Panicf("%s to %s with req", meth, cmd)
 		}
 		r, err = http.Get(cmd)
 	case "POST":
-		r, err = http.PostForm(cmd, vals)
+		var body io.Reader
+		if req != nil {
+			b, err := json.Marshal(req)
+			if err != nil {
+				return err
+			}
+			body = bytes.NewBuffer(b)
+		}
+		r, err = http.Post(cmd, "text/json", body)
 	default:
-		return fmt.Errorf("unknown method %q", meth)
+		log.Panicf("%s: invalid method %q", cmd, meth)
+		panic("invalid method: " + meth)
 	}
 	if err != nil {
 		return err
 	}
-
 	defer r.Body.Close()
-	var buf bytes.Buffer
-	buf.ReadFrom(r.Body)
-	if resp != nil {
-		if err = json.Unmarshal(buf.Bytes(), resp); err != nil {
-			log.Printf("json unmarshal %#q: %s\n", buf.Bytes(), err)
-			return err
-		}
+	body := new(bytes.Buffer)
+	if _, err := body.ReadFrom(r.Body); err != nil {
+		return err
 	}
-	return nil
-}
-
-func dashStatus(meth, cmd string, args param) error {
-	var resp struct {
-		Status string
-		Error  string
+
+	// Read JSON-encoded Response into provided resp
+	// and return an error if present.
+	var result = struct {
+		Response interface{}
+		Error    string
+	}{
+		// Put the provided resp in here as it can be a pointer to
+		// some value we should unmarshal into.
+		Response: resp,
 	}
-	err := dash(meth, cmd, &resp, args)
-	if err != nil {
+	if err = json.Unmarshal(body.Bytes(), &result); err != nil {
+		log.Printf("json unmarshal %#q: %s\n", body.Bytes(), err)
 		return err
 	}
-	if resp.Status != "OK" {
-		return errors.New("/build: " + resp.Error)
+	if result.Error != "" {
+		return errors.New(result.Error)
 	}
+
 	return nil
 }
 
 // todo returns the next hash to build.
 func (b *Builder) todo() (rev string, err error) {
-	var resp []struct {
-		Hash string
-	}
-	if err = dash("GET", "todo", &resp, param{"builder": b.name}); err != nil {
+	// TODO(adg): handle packages
+	args := url.Values{"builder": {b.name}}
+	var resp string
+	if err = dash("GET", "todo", args, nil, &resp); err != nil {
 		return
 	}
-	if len(resp) > 0 {
-		rev = resp[0].Hash
+	if resp != "" {
+		rev = resp
 	}
 	return
 }
 
 // recordResult sends build results to the dashboard
 func (b *Builder) recordResult(buildLog string, hash string) error {
-	return dash("POST", "build", nil, param{
-		"builder": b.name,
-		"key":     b.key,
-		"node":    hash,
-		"log":     buildLog,
-	})
+	// TODO(adg): handle packages
+	return dash("POST", "result", url.Values{"key": {b.key}}, obj{
+		"Builder": b.name,
+		"Hash":    hash,
+		"Log":     buildLog,
+	}, nil)
 }
 
 // packages fetches a list of package paths from the dashboard
 func packages() (pkgs []string, err error) {
+	return nil, nil
+	/* TODO(adg): un-stub this once the new package builder design is done
 	var resp struct {
 		Packages []struct {
 			Path string
@@ -111,10 +123,13 @@ func packages() (pkgs []string, err error) {
 	\tpkgs = append(pkgs, p.Path)\n \t}\n \treturn\n+\t*/
 }
 
 // updatePackage sends package build results and info dashboard
 func (b *Builder) updatePackage(pkg string, ok bool, buildLog, info string) error {
+\treturn nil
+\t/* TODO(adg): un-stub this once the new package builder design is done
 	return dash("POST", "package", nil, param{
 		"builder": b.name,
 		"key":     b.key,
 		"package": pkg,
@@ -123,26 +138,44 @@ func (b *Builder) updatePackage(pkg string, ok bool, buildLog, info string) erro
 		"log":     buildLog,
 		"info":    info,
 	})
+\t*/
 }
 
-// postCommit informs the dashboard of a new commit
-func postCommit(key string, l *HgLog) error {
-	return dashStatus("POST", "commit", param{
-		"key":    key,
-		"node":   l.Hash,
-		"date":   l.Date,
-		"user":   l.Author,
-		"parent": l.Parent,
-		"desc":   l.Desc,
-	})
-}
-
-// dashboardCommit returns true if the dashboard knows about hash.
-func dashboardCommit(hash string) bool {
-	err := dashStatus("GET", "commit", param{"node": hash})
+func postCommit(key, pkg string, l *HgLog) bool {
+	err := dash("POST", "commit", url.Values{"key": {key}}, obj{
+		"PackagePath": pkg,
+		"Hash":        l.Hash,
+		"ParentHash":  l.Parent,
+		// TODO(adg): l.Date as int64 unix epoch secs in Time field
+		"User": l.Author,
+		"Desc": l.Desc,
+	}, nil)
 	if err != nil {
-		log.Printf("check %s: %s", hash, err)
+		log.Printf("failed to add %s to dashboard: %v", key, err)
 		return false
 	}
 	return true
 }
+
+func dashboardCommit(pkg, hash string) bool {
+	err := dash("GET", "commit", url.Values{
+		"packagePath": {pkg},
+		"hash":        {hash},
+	}, nil, nil)
+	return err == nil
+}
+
+func dashboardPackages() []string {
+	var resp []struct {
+		Path string
+	}
+	if err := dash("GET", "packages", nil, nil, &resp); err != nil {
+		log.Println("dashboardPackages:", err)
+		return nil
+	}
+	var pkgs []string
+	for _, r := range resp {
+		pkgs = append(pkgs, r.Path)
+	}
+	return pkgs
+}

misc/dashboard/builder/main.go

--- a/misc/dashboard/builder/main.go
+++ b/misc/dashboard/builder/main.go
@@ -13,6 +13,7 @@ import (
 	"log"
 	"os"
 	"path"
+	"path/filepath"
 	"regexp"
 	"runtime"
 	"strconv"
@@ -93,7 +94,7 @@ func main() {
 	if err := os.Mkdir(*buildroot, mkdirPerm); err != nil {
 		log.Fatalf("Error making build root (%s): %s", *buildroot, err)
 	}\n-\tif err := run(nil, *buildroot, "hg", "clone", hgUrl, goroot); err != nil {
+\tif err := hgClone(hgUrl, goroot); err != nil {
 		log.Fatal("Error cloning repository:", err)
 	}
 
@@ -107,7 +108,7 @@ func main() {
 
 	// if specified, build revision and return
 	if *buildRevision != "" {
-\t\thash, err := fullHash(*buildRevision)\n+\t\thash, err := fullHash(goroot, *buildRevision)\n 		if err != nil {
 			log.Fatal("Error finding revision: ", err)
 		}
@@ -246,7 +247,7 @@ func (b *Builder) build() bool {
 	}
 	// Look for hash locally before running hg pull.
 
-\tif _, err := fullHash(hash[:12]); err != nil {
+\tif _, err := fullHash(goroot, hash[:12]); err != nil {
 		// Don't have hash, so run hg pull.
 		if err := run(nil, goroot, "hg", "pull"); err != nil {
 			log.Println("hg pull failed:", err)
@@ -425,11 +426,16 @@ func commitWatcher() {
 	if err != nil {
 		log.Fatal(err)
 	}
+\tkey := b.key
+\n \tfor {
 \t\tif *verbose {
 \t\t\tlog.Printf("poll...")
 \t\t}
-\t\tcommitPoll(b.key)\n+\t\tcommitPoll(key, "")
+\t\tfor _, pkg := range dashboardPackages() {
+\t\t\tcommitPoll(key, pkg)
+\t\t}
 \t\tif *verbose {
 \t\t\tlog.Printf("sleep...")
 \t\t}
@@ -437,6 +443,18 @@ func commitWatcher() {
 	}
 }
 
+func hgClone(url, path string) error {
+	return run(nil, *buildroot, "hg", "clone", url, path)
+}
+
+func hgRepoExists(path string) bool {
+	fi, err := os.Stat(filepath.Join(path, ".hg"))
+	if err != nil {
+		return false
+	}
+	return fi.IsDir()
+}
+
 // HgLog represents a single Mercurial revision.
 type HgLog struct {
 	Hash   string
@@ -467,7 +485,7 @@ const xmlLogTemplate = `
 
 // commitPoll pulls any new revisions from the hg server
 // and tells the server about them.
-func commitPoll(key string) {
+func commitPoll(key, pkg string) {
 	// Catch unexpected panics.\n \tdefer func() {
 	\tif err := recover(); err != nil {
 	\t\tlog.Println("commitPoll: panic:", err)
@@ -475,14 +493,29 @@ func commitPoll(key string) {
 		}\n \t}()
 \n-\tif err := run(nil, goroot, "hg", "pull"); err != nil {
+\tpkgRoot := goroot
+\n+\tif pkg != "" {
+\t\tpkgRoot = path.Join(*buildroot, pkg)
+\t\tif !hgRepoExists(pkgRoot) {
+\t\t\tif err := hgClone(repoURL(pkg), pkgRoot); err != nil {
+\t\t\t\tlog.Printf("%s: hg clone failed: %v", pkg, err)
+\t\t\t\tif err := os.RemoveAll(pkgRoot); err != nil {
+\t\t\t\t\tlog.Printf("%s: %v", pkg, err)
+\t\t\t\t}\n+\t\t\t\treturn
+\t\t\t}\n+\t\t}\n+\t}\n+\n+\tif err := run(nil, pkgRoot, "hg", "pull"); err != nil {
 		log.Printf("hg pull: %v", err)
 		return
 	}
 
 	const N = 50 // how many revisions to grab
 
-\tdata, _, err := runLog(nil, "", goroot, "hg", "log",
+\tdata, _, err := runLog(nil, "", pkgRoot, "hg", "log",
 		"--encoding=utf-8",
 		"--limit="+strconv.Itoa(N),
 		"--template="+xmlLogTemplate,
@@ -511,14 +544,11 @@ func commitPoll(key string) {
 	\tif l.Parent == "" && i+1 < len(logs) {
 	\t\tl.Parent = logs[i+1].Hash
 	\t} else if l.Parent != "" {
-\t\t\tl.Parent, _ = fullHash(l.Parent)\n+\t\t\tl.Parent, _ = fullHash(pkgRoot, l.Parent)\n 	\t}
-\t\tlog.Printf("hg log: %s < %s\n", l.Hash, l.Parent)
-\t\tif l.Parent == "" {
-\t\t\t// Can't create node without parent.
-\t\t\tcontinue
+\t\tif *verbose {
+\t\t\tlog.Printf("hg log %s: %s < %s\n", pkg, l.Hash, l.Parent)
 	\t}
-\n \t\tif logByHash[l.Hash] == nil {
 \t\t\t// Make copy to avoid pinning entire slice when only one entry is new.\n \t\t\tt := *l
@@ -528,17 +558,14 @@ func commitPoll(key string) {
 
 	for i := range logs {
 	\tl := &logs[i]
-\t\tif l.Parent == "" {
-\t\t\tcontinue
-\t\t}\n-\t\taddCommit(l.Hash, key)\n+\t\taddCommit(pkg, l.Hash, key)
 	}
 }
 
 // addCommit adds the commit with the named hash to the dashboard.
 // key is the secret key for authentication to the dashboard.
 // It avoids duplicate effort.
-func addCommit(hash, key string) bool {
+func addCommit(pkg, hash, key string) bool {
 	l := logByHash[hash]
 	if l == nil {
 		return false
@@ -548,7 +575,7 @@ func addCommit(hash, key string) bool {
 	}
 
 	// Check for already added, perhaps in an earlier run.
-\tif dashboardCommit(hash) {
+\tif dashboardCommit(pkg, hash) {
 	\tlog.Printf("%s already on dashboard\n", hash)
 	\t// Record that this hash is on the dashboard,\n \t\t// as must be all its parents.\n@@ -560,26 +587,24 @@ func addCommit(hash, key string) bool {
 	}
 
 	// Create parent first, to maintain some semblance of order.
-\tif !addCommit(l.Parent, key) {
-\t\treturn false
+\tif l.Parent != "" {
+\t\tif !addCommit(pkg, l.Parent, key) {
+\t\t\treturn false
+\t\t}
 	}
 
 	// Create commit.
-\tif err := postCommit(key, l); err != nil {
-\t\tlog.Printf("failed to add %s to dashboard: %v", key, err)
-\t\treturn false
-\t}\n-\treturn true
+\treturn postCommit(key, pkg, l)
 }
 
 // fullHash returns the full hash for the given Mercurial revision.
-func fullHash(rev string) (hash string, err error) {
+func fullHash(root, rev string) (hash string, err error) {
 	defer func() {
 	\tif err != nil {
 	\t\terr = fmt.Errorf("fullHash: %s: %s", rev, err)
 	\t}
 	}()
-\ts, _, err := runLog(nil, "", goroot,
+\ts, _, err := runLog(nil, "", root,
 		"hg", "log",
 		"--encoding=utf-8",
 		"--rev="+rev,
@@ -617,9 +642,21 @@ func firstTag(re *regexp.Regexp) (hash string, tag string, err error) {
 	\t\tcontinue
 	\t}
 	\ttag = s[1]
-\t\thash, err = fullHash(s[2])
+\t\thash, err = fullHash(goroot, s[2])
 	\treturn
 	}
 	err = errors.New("no matching tag found")
 	return
 }
+
+var repoRe = regexp.MustCompile(`^code\.google\.com/p/([a-z0-9\\-]+(\\.[a-z0-9\\-]+)?)(/[a-z0-9A-Z_.\\-/]+)?$`)
+
+// repoURL returns the repository URL for the supplied import path.
+func repoURL(importPath string) string {
+	m := repoRe.FindStringSubmatch(importPath)
+	if len(m) < 2 {
+		log.Printf("repoURL: couldn't decipher %q", importPath)
+		return ""
+	}
+	return "https://code.google.com/p/" + m[1]
+}

コアとなるコードの解説

このコミットの核となる変更は、gobuilder がダッシュボードと通信する方法と、コミットを処理する粒度を根本的に変更した点にあります。

http.godash 関数

新しい dash 関数は、ダッシュボードとのHTTP通信の汎用的なインターフェースとして機能します。

  • 柔軟なリクエスト/レスポンス処理: 以前は param マップを使ってクエリパラメータとPOSTフォームデータを区別していましたが、新しい dash 関数は url.Values でクエリパラメータを、req interface{} でJSONエンコードされるリクエストボディを、resp interface{} でJSONデコードされるレスポンスボディを扱います。これにより、ダッシュボードの新しいAPIがJSONベースのボディを期待する場合でも、柔軟に対応できるようになりました。
  • エラーハンドリングの改善: レスポンスボディ全体を読み込んでからJSONデコードを行うことで、部分的な読み込みによる問題を防ぎます。また、ダッシュボードからのエラーレスポンスが result.Error フィールドに含まれる場合、それをGoのエラーとして返すことで、より明確なエラー伝播を実現しています。

main.gocommitWatchercommitPoll 関数

これらの関数は、gobuilder が新しいコミットをどのように監視し、処理するかを定義しています。

  • パッケージごとのポーリング: commitWatcher は、従来のGoリポジトリ全体のポーリング (commitPoll(key, "")) に加えて、dashboardPackages() から取得した各パッケージに対しても commitPoll(key, pkg) を呼び出すようになりました。これは、ダッシュボードが管理する個々のGoパッケージの変更も gobuilder が追跡し、ビルド結果を報告できるようになったことを意味します。
  • 動的なリポジトリクローン: commitPoll 関数内で、特定のパッケージ (pkg) が指定された場合、そのパッケージのリポジトリ (pkgRoot) が存在しない場合は hgClone を使って動的にクローンを試みます。これにより、gobuilder は必要に応じて新しいパッケージリポジトリを自動的にセットアップし、監視対象に加えることができます。これは、Goエコシステムにおけるパッケージの増加に対応するための重要な機能です。
  • パッケージコンテキストでのMercurial操作: hg pullhg logfullHash といったMercurial操作が、goroot (Goリポジトリ全体) だけでなく、pkgRoot (個々のパッケージリポジトリ) のコンテキストでも実行されるようになりました。これにより、各パッケージの変更履歴を独立して追跡し、ダッシュボードに報告することが可能になります。

postCommitaddCommit 関数

これらの関数は、コミット情報をダッシュボードに送信するロジックをカプセル化しています。

  • パッケージ情報の追加: postCommitaddCommit の両方に pkg 引数が追加され、ダッシュボードに送信されるコミット情報に PackagePath が含まれるようになりました。これにより、ダッシュボードはどのパッケージに属するコミットであるかを正確に識別し、関連するビルド結果と紐付けることができます。

これらの変更は、gobuilder がGo言語のビルドシステムにおいて、よりきめ細かく、スケーラブルなコミットおよびパッケージ管理を可能にするための基盤を構築しています。

関連リンク

参考にした情報源リンク