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

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

このコミットは、Go言語のコマンドラインツール cmd/go におけるバージョン管理システム (VCS) の統合、特にGitリポジトリの扱いに関するものです。具体的には、go get コマンドなどがリポジトリから特定のバージョン(タグ)を取得する際に、従来のGitタグに加えてGitブランチも「リリース用のタグ」として扱えるように拡張する変更が加えられています。これにより、開発者はGitのブランチ名を指定して特定のリリースバージョンを取得できるようになり、より柔軟なバージョン管理が可能になります。

コミット

commit 7207898fe489e967e42877f9a8dc092636c206dc
Author: Gustavo Niemeyer <gustavo@niemeyer.net>
Date:   Fri Feb 3 03:03:13 2012 -0200

    cmd/go: add support for release tags via git branches
    
    R=rsc, remyoudompheng, gustavo, dsymonds
    CC=golang-dev
    https://golang.org/cl/5617057

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

https://github.com/golang/go/commit/7207898fe489e967e42877f9a8dc092636c206dc

元コミット内容

このコミットは、cmd/go ツールがGitリポジトリからバージョンを取得する際の挙動を変更します。以前は、go get -u などで特定のバージョンを指定する際、Gitの「タグ」のみが考慮されていました。この変更により、Gitの「ブランチ」もリリースバージョンを示すものとして認識され、指定されたブランチをチェックアウトできるようになります。これは、特に長期サポート (LTS) ブランチや、特定の機能セットを安定版として提供するブランチ運用を行っているプロジェクトにとって有用です。

変更の背景

Goの go get コマンドは、指定されたパッケージのソースコードをダウンロードし、ビルドするために使用されます。このコマンドは、VCS (Version Control System) と連携して動作し、リポジトリから適切なバージョンを取得します。

このコミットが導入された背景には、当時のGoエコシステムにおけるバージョン管理の慣習と、go get の機能的な制約がありました。

  1. タグとブランチの使い分け: Gitでは、タグは通常、特定のコミットに対する不変の参照(例: v1.0.0)として使用され、ブランチは開発の並行ライン(例: master, develop, feature/x)として使用されます。しかし、一部のプロジェクトでは、安定版のリリースを特定のブランチ(例: release-1.x)で管理し、そのブランチの最新コミットを「最新の安定版」として提供する運用モデルを採用していました。
  2. go get のタグ依存: 従来の go get は、特定のバージョンを指定する際にGitのタグを優先的に探していました。そのため、ブランチ名を指定しても、それがタグとして認識されない限り、意図したバージョンのコードを取得できないという問題がありました。
  3. 柔軟性の向上: 開発者が go get を通じて、タグだけでなくブランチもバージョン指定の手段として利用できるようにすることで、より多様なプロジェクトのバージョン管理戦略に対応し、ツールの柔軟性を高める必要がありました。

この変更は、go get がGitリポジトリからコードを取得する際の「タグ」の解釈を拡張し、ブランチもその対象に含めることで、これらの課題を解決しようとするものです。

前提知識の解説

このコミットの変更内容を理解するためには、以下の概念について理解しておく必要があります。

  1. Go Modules と go get:

    • go get: Go言語のパッケージ管理コマンドの一つで、リモートリポジトリからGoのソースコードをダウンロードし、依存関係を解決するために使用されます。Go Modulesが導入される前は、主に $GOPATH 内にソースコードを配置し、依存関係を管理していました。このコミットが作成された2012年時点では、Go Modulesはまだ存在せず、go get はVCSと直接連携して動作していました。
    • Go Modules (補足): 現在のGoでは、Go Modulesが標準の依存関係管理システムとなっています。Go Modulesでは、go.mod ファイルで依存関係が明示的に定義され、バージョンはセマンティックバージョニングに基づいて管理されます。しかし、このコミットの時点では、go get がVCSのタグやブランチを直接解釈する機能が重要でした。
  2. Gitのタグとブランチ:

    • ブランチ (Branch): Gitにおける開発の並行ラインです。新しい機能開発やバグ修正を行う際にブランチを作成し、独立した作業を進めることができます。ブランチは常に最新のコミットを指し示し、新しいコミットが追加されるとブランチの参照も移動します。
    • タグ (Tag): Gitにおける特定のコミットに対する不変の参照です。通常、リリースバージョン(例: v1.0.0)や重要なマイルストーンを示すために使用されます。タグは一度作成されると、その参照先が移動することはありません。
    • git show-ref: Gitコマンドの一つで、リポジトリ内の参照(ブランチ、タグ、リモート追跡ブランチなど)を一覧表示します。例えば、git show-ref --tags はタグの一覧を、git show-ref --heads はローカルブランチの一覧を表示します。git show-ref tags/v1.0.0 のように特定の参照を検索することも可能です。
  3. 正規表現 (Regular Expression):

    • 文字列のパターンを記述するための強力なツールです。このコミットでは、Gitコマンドの出力からタグやブランチの名前を抽出するために正規表現が使用されています。
    • ^(\S+)$: 行の先頭 (^) から末尾 ($) まで、空白文字以外の任意の文字 (\S+) が1回以上続くパターンにマッチし、その部分をキャプチャグループ (()) で抽出します。
    • (?:tags|origin)/(\S+)$: tags/ または origin/ のいずれか ((?:...) は非キャプチャグループ) の後に、空白文字以外の任意の文字 (\S+) が1回以上続くパターンにマッチし、その \S+ の部分をキャプチャグループで抽出します。
  4. Go言語の regexp パッケージ:

    • Go言語の標準ライブラリで、正規表現を扱うための機能を提供します。regexp.MustCompile は正規表現をコンパイルし、FindAllStringFindAllStringSubmatch は文字列内でパターンにマッチする部分を検索します。

技術的詳細

このコミットの主要な変更は、src/cmd/go/vcs.go ファイル内の vcsGit 構造体の定義と、それに付随する tags および tagSync メソッドのロジック変更にあります。

  1. vcsCmd 構造体の拡張:

    • vcsCmd 構造体に tagLookupCmd []tagCmd という新しいフィールドが追加されました。これは、特定のタグ(またはブランチ名)が与えられたときに、それが実際にGitリポジトリ内のどの参照(タグまたはブランチ)に対応するかを検索するためのコマンドを定義します。
  2. vcsGittagCmd の変更:

    • vcsGit はGitリポジトリを扱うための vcsCmd の実装です。
    • 以前の tagCmd{"tag", ^(\S+)$} で、git tag コマンドの出力からタグ名を直接抽出していました。
    • 新しい tagCmd{"show-ref", (?:tags|origin)/(\S+)$} となりました。これは git show-ref コマンドを使用し、その出力から tags/ または origin/ で始まる参照(つまり、Gitタグまたはリモート追跡ブランチ)を抽出し、その後の名前部分をキャプチャするように変更されました。これにより、go get はGitタグだけでなく、origin/master のようなブランチも「タグ」として認識できるようになります。
  3. vcsGittagLookupCmd の追加:

    • tagLookupCmd{"show-ref tags/{tag} origin/{tag}", ((?:tags|origin)/\S+)$} が追加されました。
    • このコマンドは、ユーザーが指定した {tag} が、tags/{tag}(Gitタグ)または origin/{tag}(リモート追跡ブランチ)のいずれかに対応するかを git show-ref を使って確認します。
    • 正規表現 ((?:tags|origin)/\S+)$ は、マッチした参照の完全な名前(例: tags/v1.0.0origin/my-branch)をキャプチャします。
  4. run1 関数のリファクタリング:

    • runrunOutput の内部で呼び出される run1 関数から、output というブーリアン引数が削除されました。これは機能的な変更ではなく、runrunOutput がそれぞれ run1 を呼び出す際に、出力の扱いをより明確にするためのリファクタリングです。runOutputrun1 の結果をそのまま返し、run は結果を破棄します。
  5. tags メソッドの変更:

    • vcsCmd.tags メソッドは、tagCmd で定義されたコマンドを実行し、その出力からタグ名を抽出します。
    • 変更前は re.FindAllString を使用していましたが、これは正規表現にマッチする文字列全体を返します。
    • 変更後は re.FindAllStringSubmatch を使用し、その結果の m[1](最初のキャプチャグループ)を tags スライスに追加するように変更されました。これは、新しい tagCmd の正規表現が (\S+) というキャプチャグループを持つため、そのグループ内の値(実際のタグ/ブランチ名)を抽出するために必要です。
  6. tagSync メソッドの変更:

    • vcsCmd.tagSync メソッドは、指定された tag にリポジトリを同期(チェックアウト)する役割を担います。
    • このコミットの最も重要な変更点の一つは、tagSync の冒頭に tagLookupCmd を使用した新しいロジックが追加されたことです。
    • もし tag が指定されている場合、tagLookupCmd に定義されたコマンド(例: git show-ref tags/{tag} origin/{tag})を実行し、その出力から正規表現を使って実際のGit参照(tags/v1.0.0origin/my-branch など)を検索します。
    • もし有効な参照が見つかれば、その参照で tag 変数を更新し、その後の v.run(dir, v.tagSyncCmd)git checkout {tag} が実行される際に、正しいGit参照が使用されるようにします。
    • これにより、ユーザーが v1.0.0 と指定した場合でも、master と指定した場合でも、go get が適切にGitリポジトリをチェックアウトできるようになります。

これらの変更により、cmd/go はGitリポジトリのブランチを「リリースバージョン」として認識し、go get コマンドでブランチ名を指定して特定のコードベースを取得する機能が実現されました。

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

src/cmd/go/vcs.go ファイルにおける主要な変更箇所は以下の通りです。

  1. vcsCmd 構造体への tagLookupCmd フィールドの追加:

    --- a/src/cmd/go/vcs.go
    +++ b/src/cmd/go/vcs.go
    @@ -24,6 +24,7 @@ type vcsCmd struct {
     	downloadCmd string // command to download updates into an existing repository
     
     	tagCmd         []tagCmd // commands to list tags
    +	tagLookupCmd   []tagCmd // commands to lookup tags before running tagSyncCmd
     	tagSyncCmd     string   // command to sync to specific tag
     	tagSyncDefault string   // command to sync to default tag
     }
    
  2. vcsGit 定義における tagCmd の変更と tagLookupCmd の追加:

    --- a/src/cmd/go/vcs.go
    +++ b/src/cmd/go/vcs.go
    @@ -83,7 +84,14 @@ var vcsGit = &vcsCmd{
     	createCmd:   "clone {repo} {dir}",
     	downloadCmd: "fetch",
     
    -	tagCmd:         []tagCmd{{"tag", `^(\S+)$`}},
    +	tagCmd: []tagCmd{
    +		// tags/xxx matches a git tag named xxx
    +		// origin/xxx matches a git branch named xxx on the default remote repository
    +		{"show-ref", `(?:tags|origin)/(\S+)$`},
    +	},
    +	tagLookupCmd: []tagCmd{
    +		{"show-ref tags/{tag} origin/{tag}", `((?:tags|origin)/\S+)$`},
    +	},
     	tagSyncCmd:     "checkout {tag}",
     	tagSyncDefault: "checkout origin/master",
     }
    
  3. run1 関数のシグネチャ変更 (リファクタリング):

    --- a/src/cmd/go/vcs.go
    +++ b/src/cmd/go/vcs.go
    @@ -128,17 +136,17 @@ func (v *vcsCmd) String() string {
     // command's combined stdout+stderr to standard error.
     // Otherwise run discards the command's output.
     func (v *vcsCmd) run(dir string, cmd string, keyval ...string) error {
    -	_, err := v.run1(dir, false, cmd, keyval)
    +	_, err := v.run1(dir, cmd, keyval)
     	return err
     }
     
     // runOutput is like run but returns the output of the command.
     func (v *vcsCmd) runOutput(dir string, cmd string, keyval ...string) ([]byte, error) {
    -	return v.run1(dir, true, cmd, keyval)
    +	return v.run1(dir, cmd, keyval)
     }
     
     // run1 is the generalized implementation of run and runOutput.
    -func (v *vcsCmd) run1(dir string, output bool, cmdline string, keyval []string) ([]byte, error) {
    +func (v *vcsCmd) run1(dir string, cmdline string, keyval []string) ([]byte, error) {
     	m := make(map[string]string)
     	for i := 0; i < len(keyval); i += 2 {
     		m[keyval[i]] = keyval[i+1]
    
  4. tags メソッドでの正規表現マッチングの変更:

    --- a/src/cmd/go/vcs.go
    +++ b/src/cmd/go/vcs.go
    @@ -187,7 +195,9 @@ func (v *vcsCmd) tags(dir string) ([]string, error) {
     			return nil, err
     		}
     		re := regexp.MustCompile(`(?m-s)` + tc.pattern)
    -		tags = append(tags, re.FindAllString(string(out), -1)...)
    +		for _, m := range re.FindAllStringSubmatch(string(out), -1) {
    +			tags = append(tags, m[1])
    +		}
     	}
     	return tags, nil
     }
    
  5. tagSync メソッドでの tagLookupCmd を使用したタグ解決ロジックの追加:

    --- a/src/cmd/go/vcs.go
    +++ b/src/cmd/go/vcs.go
    @@ -198,6 +208,20 @@ func (v *vcsCmd) tagSync(dir, tag string) error {
     	if v.tagSyncCmd == "" {
     		return nil
     	}
    +	if tag != "" {
    +		for _, tc := range v.tagLookupCmd {
    +			out, err := v.runOutput(dir, tc.cmd, "tag", tag)
    +			if err != nil {
    +				return err
    +			}
    +			re := regexp.MustCompile(`(?m-s)` + tc.pattern)
    +			m := re.FindStringSubmatch(string(out))
    +			if len(m) > 1 {
    +				tag = m[1]
    +				break
    +			}
    +		}
    +	}
     	if tag == "" && v.tagSyncDefault != "" {
     		return v.run(dir, v.tagSyncDefault)
     	}
    

コアとなるコードの解説

このコミットの核心は、go コマンドがGitリポジトリの「タグ」をどのように解釈し、同期するかというロジックの変更にあります。

  1. vcsCmd 構造体と tagLookupCmd:

    • vcsCmd は、Goがサポートする様々なVCS(Git, Mercurial, Subversionなど)の操作を抽象化するための構造体です。
    • tagLookupCmd は、ユーザーが指定した文字列(例: v1.0.0master)が、Gitリポジトリ内のどの実際の参照(tags/v1.0.0origin/master)に対応するかを特定するためのコマンドと正規表現のペアを定義します。これにより、go get はユーザーの意図をより正確に解釈できるようになります。
  2. vcsGittagCmdtagLookupCmd の連携:

    • vcsGit.tagCmd は、リポジトリ内の利用可能な「タグ」(この変更後はブランチも含む)を列挙するために git show-ref を使用します。正規表現 (?:tags|origin)/(\S+)$ は、tags/ または origin/ で始まる参照から、その後の実際の名前(例: v1.0.0master)を抽出します。
    • vcsGit.tagLookupCmd は、特定の名前が与えられたときに、それがGitタグなのか、それともリモート追跡ブランチなのかを明示的に確認するために git show-ref tags/{tag} origin/{tag} を実行します。これにより、例えばユーザーが master と指定した場合に、それが origin/master ブランチとして正しく解決されるようになります。
  3. tags メソッドの正規表現キャプチャ:

    • tags メソッドは、tagCmd の結果を処理し、利用可能なタグ(およびブランチ名)のリストを返します。
    • re.FindAllStringSubmatch(string(out), -1)m[1] の使用は、正規表現のキャプチャグループの重要性を示しています。tagCmd の正規表現 (?:tags|origin)/(\S+)$ において、(\S+) が実際のタグ/ブランチ名をキャプチャするグループであり、この変更によってそのキャプチャされた値が正しく抽出されるようになりました。
  4. tagSync メソッドのタグ解決ロジック:

    • tagSync メソッドは、指定された tag にリポジトリをチェックアウトする最終的な処理を行います。
    • このメソッドの冒頭に追加されたループは、tagLookupCmd を利用して、ユーザーが指定した tag 文字列を実際のGit参照(例: tags/v1.0.0origin/my-branch)に解決しようとします。
    • もし解決に成功すれば、tag 変数は解決された完全な参照名で更新され、その後の git checkout {tag} コマンドが正確なターゲットに対して実行されることを保証します。これにより、go get はブランチ名を指定された場合でも、そのブランチの最新コミットを正しく取得できるようになります。

この一連の変更により、go コマンドはGitリポジトリのブランチを「リリースバージョン」として扱い、開発者がより柔軟なバージョン指定を行えるようになりました。

関連リンク

  • Gerrit Code Review: https://golang.org/cl/5617057
    • このコミットに対応するGoプロジェクトのGerritコードレビューページです。当時の議論やレビューコメント、変更の経緯などを確認できます。

参考にした情報源リンク