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

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

このコミットは、Go言語のパッケージインストールツールである goinstall の機能改善に関するものです。具体的には、Google Codeのサブリポジトリのサポートを追加し、リポジトリのマッチングロジックをテストするための新しいテストケースを導入しています。さらに、goinstall がリポジトリのチェックアウトや更新が必要な場合にのみネットワークアクセスを行うように最適化されています。

コミット

commit 86c08e961136f01d34db7759166433d55e8914b2
Author: Andrew Gerrand <adg@golang.org>
Date:   Tue Nov 22 07:10:25 2011 +1100

    goinstall: support googlecode subrepos and add repo match tests
    goinstall: don't hit network unless a checkout or update is required

    R=rsc, rogpeppe
    CC=golang-dev
    https://golang.org/cl/5343042

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

https://github.com/golang/go/commit/86c08e961136f01d34db7759166433d55e8914b2

元コミット内容

goinstall: support googlecode subrepos and add repo match tests
goinstall: don't hit network unless a checkout or update is required

R=rsc, rogpeppe
CC=golang-dev
https://golang.org/cl/5343042

変更の背景

goinstall は、Go言語のパッケージをリモートリポジトリから取得し、ローカルにインストールするためのコマンドラインツールです。このコミットが行われた2011年当時、Google Codeは多くのオープンソースプロジェクトで利用されており、特にGo言語のプロジェクトも多数ホストされていました。

従来の goinstall は、project.googlecode.com/svn/trunk のような形式のGoogle Codeリポジトリはサポートしていましたが、code.google.com/p/project.subrepo/path のような形式の「サブリポジトリ」には対応していませんでした。これは、Google Codeが単一のプロジェクト内で複数のVCS(バージョン管理システム)リポジトリをホストできる機能を提供していたためです。ユーザーがこのようなサブリポジトリを goinstall で取得しようとすると、正しく認識されず、ダウンロードに失敗する問題がありました。

また、goinstall はリポジトリの存在確認やVCSタイプの特定のために、不必要にネットワークアクセスを行う可能性がありました。これは、特にオフライン環境やネットワークが不安定な環境でのパフォーマンス低下や、不必要な帯域幅の消費につながります。

このコミットは、これらの問題を解決するために、以下の2つの主要な目的を持って行われました。

  1. Google Codeサブリポジトリのサポート: code.google.com/p/project.subrepo 形式のインポートパスを正しく解析し、対応するVCS(Git, Mercurial, Subversionなど)を特定してダウンロードできるようにする。
  2. ネットワークアクセスの最適化: リポジトリが既にローカルに存在し、かつ更新が不要な場合には、ネットワークにアクセスしないように goinstall のロジックを改善する。

前提知識の解説

goinstall とは

goinstall は、Go言語の初期のパッケージ管理ツールです。現在の go get コマンドの前身にあたります。Goのソースコードは通常、import "path/to/package" の形式でインポートされますが、goinstall はこのインポートパスを解析し、対応するリモートリポジトリからソースコードをダウンロードして $GOPATH/src 以下に配置し、コンパイル・インストールまで行います。

バージョン管理システム (VCS)

  • Git: 分散型バージョン管理システム。GitHubなどで広く利用されています。
  • Mercurial (Hg): 分散型バージョン管理システム。Bitbucketなどで利用されていました。
  • Subversion (SVN): 集中型バージョン管理システム。Google Codeなどで利用されていました。
  • Bazaar (Bzr): 分散型バージョン管理システム。Launchpadなどで利用されていました。

goinstall は、これらのVCSに対応し、それぞれのコマンド(git clone, hg clone, svn checkout, bzr branch など)を内部的に呼び出してリポジトリを操作します。

Google Code Project Hosting

2016年にサービスを終了しましたが、かつてGoogleが提供していたオープンソースプロジェクトのホスティングサービスです。Git, Mercurial, SubversionのいずれかのVCSをサポートし、単一のプロジェクト内で複数のリポジトリ(メインリポジトリとサブリポジトリ)を持つことができました。サブリポジトリは通常、code.google.com/p/project.subrepo のようなURL構造を持っていました。

リポジトリの検出とマッチング

goinstall がインポートパスを受け取った際、それがどのVCSのどのリポジトリに対応するかを判断する必要があります。これには、正規表現によるURLパターンのマッチングや、特定のホスティングサービスのAPI(例: Bitbucket API)を利用したVCSタイプの検出などが含まれます。

技術的詳細

このコミットの主要な変更点は、リポジトリの検出ロジックの再設計と、ネットワークアクセスの条件付き実行です。

RemoteRepo インターフェースの導入

以前は vcsMatch という構造体がVCSとリポジトリの情報を保持していましたが、このコミットでは RemoteRepo という新しいインターフェースが導入されました。

type RemoteRepo interface {
	// IsCheckedOut returns whether this repository is checked
	// out inside the given srcDir (eg, $GOPATH/src).
	IsCheckedOut(srcDir string) bool

	// Repo returns the information about this repository: its url,
	// the part of the import path that forms the repository root,
	// and the version control system it uses. It may discover this
	// information by using the supplied client to make HTTP requests.
	Repo(_ *http.Client) (url, root string, vcs *vcs, err error)
}

このインターフェースは、以下の2つのメソッドを定義します。

  • IsCheckedOut(srcDir string) bool: 指定された srcDir 内にこのリポジトリが既にチェックアウトされているかどうかを報告します。これにより、不必要なネットワークアクセスを避けることができます。
  • Repo(_ *http.Client) (url, root string, vcs *vcs, err error): リポジトリのURL、リポジトリのルートとなるインポートパス、および使用されているVCSに関する情報を返します。このメソッドは、必要に応じてHTTPクライアントを使用してネットワークアクセスを行うことができます。

baseRepo 構造体

RemoteRepo インターフェースの基本的な実装として baseRepo 構造体が導入されました。これは、リポジトリのURL、ルート、およびVCSの基本的な情報を保持します。

type baseRepo struct {
	url, root string
	vcs       *vcs
}

特定のホスティングサービス向けのリポジトリ実装

  • googleSubrepo: code.google.com/p/project.subrepo 形式のGoogle Codeサブリポジトリを処理するための RemoteRepo 実装です。この実装は、Google CodeのソースチェックアウトページをスクレイピングしてVCSタイプ(hg, git, svn)を検出します。これは、Google CodeがAPIを提供していなかったため、HTML解析によってVCS情報を取得する必要があったためです。
  • bitbucketRepo: bitbucket.org のリポジトリを処理するための RemoteRepo 実装です。この実装は、BitbucketのAPI (https://api.bitbucket.org/1.0/repositories/) を利用して、リポジトリのVCSタイプ(git, hg)を検出します。

これらのカスタム実装により、goinstall は各ホスティングサービスの特性(APIの有無、VCS情報の提供方法など)に応じて、より正確にリポジトリ情報を取得できるようになりました。

findPublicRepofindAnyRepo の変更

  • findPublicRepo(importPath string) (RemoteRepo, error): 既知のパブリックホスティングサイト(Google Code, GitHub, Bitbucket, Launchpadなど)のパターンに importPath が一致するかどうかをチェックし、一致した場合は対応する RemoteRepo インターフェースの実装を返します。
  • findAnyRepo(importPath string) RemoteRepo: importPath 内に .git, .hg, .svn, .bzr などのVCSサフィックスが含まれている場合に、それに対応する RemoteRepo 実装を返します。

これらの関数は、RemoteRepo インターフェースを返すように変更され、リポジトリ情報の取得とVCSタイプの特定がより抽象化されました。

download 関数の最適化

download 関数は、パッケージのダウンロードと更新の主要なロジックを担っています。このコミットでは、download 関数が RemoteRepo インターフェースを利用するように変更されました。

最も重要な変更は、repo.IsCheckedOut(srcDir) を呼び出すことで、リポジトリが既にローカルに存在するかどうかを最初に確認する点です。

  • もしリポジトリが既にチェックアウトされている場合、goinstall-u (update) フラグが指定されている場合にのみ、repo.Repo(http.DefaultClient) を呼び出してネットワークアクセスを行い、リポジトリを更新します。
  • リポジトリがまだチェックアウトされていない場合のみ、repo.Repo(http.DefaultClient) を呼び出してリポジトリのURLとVCS情報を取得し、vcs.clone コマンドでリポジトリをクローンします。

これにより、goinstall は不必要なネットワークアクセスを大幅に削減し、パフォーマンスを向上させることができます。

テストの追加 (download_test.go)

新しい download_test.go ファイルが追加され、findPublicRepo 関数の動作を検証するための包括的なテストケースが導入されました。これらのテストは、Google Codeサブリポジトリ、Bitbucket、GitHub、Launchpadなど、様々なホスティングサイトのインポートパスが正しく解析され、適切なVCSタイプとリポジトリURLが返されることを確認します。特に、BitbucketやGoogle Codeサブリポジトリのように、VCSタイプを検出するためにネットワークアクセス(HTTPリクエスト)が必要なケースも模擬的にテストされています。

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

src/cmd/goinstall/download.go

  • vcs 構造体から defaultHosts フィールドが削除され、vcsMap というグローバルマップにVCS定義が移動。
  • RemoteRepo インターフェースの定義と、その実装である baseRepo, googleSubrepo, bitbucketRepo 構造体の追加。
  • knownHosts 変数の getVcs フィールドが repo フィールドに変更され、RemoteRepo を返す関数を指すように変更。
  • googleVcs, githubVcs, bitbucketVcs, launchpadVcs 関数が、それぞれ matchGoogleRepo, matchGithubRepo, matchBitbucketRepo, matchLaunchpadRepo にリファクタリングされ、RemoteRepo を返すように変更。
  • findPublicRepo および findAnyRepo 関数が RemoteRepo を返すように変更。
  • download 関数が RemoteRepo インターフェースを利用するように変更され、IsCheckedOut メソッドによるネットワークアクセスの最適化が実装。
  • checkoutRepo 関数が RemoteRepo を引数に取るように変更され、リポジトリのチェックアウトと更新ロジックをカプセル化。
  • isDir ヘルパー関数の追加。

src/cmd/goinstall/download_test.go (新規ファイル)

  • FindPublicRepoTests というテストデータ構造の定義。
  • TestFindPublicRepo 関数による findPublicRepo のテスト実装。
  • testTransport 構造体と RoundTrip メソッドによるHTTPトランスポートのモック実装。これにより、ネットワークアクセスを伴うリポジトリ検出ロジック(Google Codeスクレイピング、Bitbucket API呼び出し)をテスト可能に。

src/cmd/goinstall/doc.go

  • Google Codeサブリポジトリのインポートパス形式 (code.google.com/p/project.subrepo/sub/directory) に関するドキュメントの追加。

src/cmd/goinstall/main.go

  • install 関数内で findPublicRepo の呼び出しと、public フラグの設定方法が RemoteRepo インターフェースの利用に合わせて調整。

コアとなるコードの解説

src/cmd/goinstall/download.go の変更点

// download checks out or updates the specified package from the remote server.
func download(importPath, srcDir string) (public bool, err error) {
	if strings.Contains(importPath, "..") {
		err = errors.New("invalid path (contains ..)")
		return
	}

	repo, err := findPublicRepo(importPath) // まずパブリックリポジトリとして検出を試みる
	if err != nil {
		return false, err
	}
	if repo != nil { // パブリックリポジトリとして検出された場合
		public = true
	} else { // パブリックリポジトリとして検出されなかった場合、任意のVCSサフィックスを持つリポジトリとして検出を試みる
		repo = findAnyRepo(importPath)
	}
	if repo == nil { // どちらの方法でも検出できなかった場合
		err = errors.New("cannot download: " + importPath)
		return
	}
	err = checkoutRepo(srcDir, repo) // リポジトリのチェックアウトまたは更新を実行
	return
}

// checkoutRepo checks out repo into srcDir (if it's not checked out already)
// and, if the -u flag is set, updates the repository.
func checkoutRepo(srcDir string, repo RemoteRepo) error {
	if !repo.IsCheckedOut(srcDir) { // リポジトリがまだチェックアウトされていない場合
		// do checkout
		url, root, vcs, err := repo.Repo(http.DefaultClient) // ネットワークアクセスを伴う可能性のあるRepoメソッドを呼び出し
		if err != nil {
			return err
		}
		repoPath := filepath.Join(srcDir, root)
		parent, _ := filepath.Split(repoPath)
		if err = os.MkdirAll(parent, 0777); err != nil {
			return err
		}
		// クローンコマンドを実行
		if err = run(string(filepath.Separator), nil, vcs.cmd, vcs.clone, url, repoPath); err != nil {
			return err
		}
		return vcs.updateRepo(repoPath) // クローン後、リポジトリを更新
	}
	if *update { // リポジトリが既にチェックアウトされており、かつ-uフラグが指定されている場合
		// do update
		_, root, vcs, err := repo.Repo(http.DefaultClient) // ネットワークアクセスを伴う可能性のあるRepoメソッドを呼び出し
		if err != nil {
			return err
		}
		repoPath := filepath.Join(srcDir, root)
		// pullコマンドを実行(VCSがサポートしている場合)
		if vcs.pull != "" {
			if vcs.pullForceFlag != "" {
				if err = run(repoPath, nil, vcs.cmd, vcs.pull, vcs.pullForceFlag); err != nil {
					return err
				}
			} else if err = run(repoPath, nil, vcs.cmd, vcs.pull); err != nil {
				return err
			}
		}
		return vcs.updateRepo(repoPath) // リポジトリを更新
	}
	return nil // リポジトリが既にチェックアウトされており、-uフラグも指定されていない場合、何もしない
}

download 関数は、まず findPublicRepo で既知のホスティングサイトのリポジトリとして検出を試みます。成功すれば public フラグを true に設定します。失敗した場合は findAnyRepo でVCSサフィックスによる検出を試みます。どちらも失敗すればエラーを返します。

最も重要なのは checkoutRepo 関数です。この関数は、repo.IsCheckedOut(srcDir) を呼び出すことで、リポジトリがローカルに存在するかどうかを最初に確認します。

  • リポジトリがローカルに存在しない場合: repo.Repo(http.DefaultClient) を呼び出してリポジトリのURLとVCS情報を取得し、vcs.clone コマンドでリポジトリをクローンします。この際に初めてネットワークアクセスが発生します。
  • リポジトリがローカルに存在する場合: *update グローバル変数(コマンドラインの -u フラグに対応)が true でない限り、repo.Repo を呼び出さず、したがってネットワークアクセスも行いません。これにより、不必要なネットワークヒットが回避されます。-utrue の場合のみ、repo.Repo を呼び出して最新の情報を取得し、vcs.pullvcs.update コマンドでリポジトリを更新します。

src/cmd/goinstall/download_test.go の変更点

// testTransport は http.RoundTripper インターフェースを実装し、HTTPリクエストをモックする。
type testTransport struct {
	expectURL    string // 期待されるリクエストURL
	responseBody string // 返すレスポンスボディ
}

func (t *testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	if g, e := req.URL.String(), t.expectURL; g != e {
		return nil, errors.New("want " + e) // URLが一致しない場合はエラー
	}
	body := ioutil.NopCloser(bytes.NewBufferString(t.responseBody))
	return &http.Response{
		StatusCode: http.StatusOK,
		Body:       body,
	}, nil
}

func TestFindPublicRepo(t *testing.T) {
	for _, test := range FindPublicRepoTests {
		client := http.DefaultClient
		if test.transport != nil { // testTransport が設定されている場合、カスタムHTTPクライアントを使用
			client = &http.Client{Transport: test.transport}
		}
		repo, err := findPublicRepo(test.pkg)
		// ... (エラーチェックと結果の検証)
		url, root, vcs, err := repo.Repo(client) // カスタムクライアントをRepoメソッドに渡す
		// ... (エラーチェックと結果の検証)
	}
}

download_test.go では、testTransport というカスタム http.RoundTripper 実装が導入されています。これにより、findPublicRepo が内部で http.Client を使用してネットワークアクセスを行う場合でも、実際のネットワークに接続することなく、事前に定義されたレスポンスを返すことができます。これは、Google CodeのスクレイピングやBitbucket APIの呼び出しといった、ネットワーク依存のロジックを単体テストする上で非常に重要です。TestFindPublicRepo は、この testTransport を利用して、様々なインポートパスに対する findPublicRepo の動作を検証しています。

関連リンク

  • Go言語の公式ドキュメント (当時の goinstall に関する情報が含まれている可能性がありますが、現在は go get に置き換わっています): https://go.dev/
  • Google Code Project Hosting (サービス終了済み): https://code.google.com/

参考にした情報源リンク

  • Go言語のソースコード (GitHub): https://github.com/golang/go
  • このコミットのGoレビューシステム上の変更リスト (CL): https://golang.org/cl/5343042 (現在はアクセスできない可能性があります)
  • Go言語のパッケージ管理の歴史に関する記事 (例: "Go Modules: The Story So Far" など)
  • Git, Mercurial, Subversion, Bazaar の各VCSの公式ドキュメント
  • Bitbucket API ドキュメント (当時のバージョン): https://developer.atlassian.com/bitbucket/api/1/ (当時のAPIバージョンは異なる可能性があります)