goコマンドを特定のGitHub Enterprise向けに改造する

  • 17
    Like
  • 1
    Comment
More than 1 year has passed since last update.

要約

goコマンドを改造することにより、

$ go get pfi/nobu/owakon

のように カスタムのimportパス を指定しても、全然違うアドレスにある GitHub Enterprise のリポジトリを sshで cloneできるようになりました。もちろんコード中に記述するimportも

import (
    "io"
    "fmt"
    "pfi/nobu/repo1"
    "pfi/nobu/repo2"
)

のように書くことができ、go get時に自動で依存関係が解決されます。また、GitHub EnterpriseにHTTPSでアクセスしなくなったため、余計な認証を求められることもなくなりました。

なおこの文書はgo1.2向けに書かれています。

改造方法

変更対象はsrc/cmd/go/vcs.goです。vcs.goにはvcsPathsという変数が定義されています。

var vcsPaths = []*vcsPath{
    // Google Code - new syntax
    {
        prefix: "code.google.com/",
        re:     `^(?P<root>code\.google\.com/p/(?P<project>[a-z0-9\-]+)(\.(?P<subrepo>[a-z0-9\-]+))?)(/[A-Za-z0-9_.\-]+)*$`,
        repo:   "https://{root}",
        check:  googleCodeVCS,
    },

    // Google Code - old syntax
    {
        re:    `^(?P<project>[a-z0-9_\-.]+)\.googlecode\.com/(git|hg|svn)(?P<path>/.*)?$`,
        check: oldGoogleCode,
    },

    // Github
    {
        prefix: "github.com/",
        re:     `^(?P<root>github\.com/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)*$`,
        vcs:    "git",
        repo:   "https://{root}",
        check:  noVCSSuffix,
    },
    ... 省略 ...
}

ここに次のようなものを追加しました。

var vcsPaths = []*vcsPath{
    // PFI
    {
        prefix: "pfi/",
        re:     `^(?P<root>pfi/(?P<subroot>[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`,
        vcs:    "git",
        repo:   "git@<内緒だよ>:{subroot}.git",
    },

    // Google Code - new syntax
    ... 省略 ...
}

これだけです。以下、必要な部分を補足します。

(2015/8/20 追記)

Go 1.5では同じくvcs.goの中にある isSecure メソッドに対して次のような変更を追加で行う必要があります。

func (v *vcsCmd) isSecure(repo string) bool {
    if strings.HasPrefix(repo, "git@<GitHub Enterpriseのアドレス>:") {
        return true
    }

    ...省略...

prefixとは?

importパスがprefixで与えられた文字列から始まっていると、対応するルールが適用されます。例えば上記の例では"pfi/"で始まるimportに関しては、gitリポジトリであり、リポジトリのパスは"git@<内緒だよ>:{subroot}.git"になっているよ、という情報が書かれています。他にもGoogle CodeやGitHub、Bitbucketなどメジャーなものは事前に定義されています。

reとは?

importパスにマッチさせたい正規表現を re2形式 で指定します。後述の名前付きグループの活用だけでなく、バリデーションにも利用できます。

vcsとは?

使用するVCSの コマンド名 を指定します。今回は既存のもので問題なかったのでgitをそのまま使っています。

ちょっと脇道にそれますが、ここはコマンド名じゃなくて vcsCmd.name から引けるようにして欲しかったですね。そうしたら、既存のコマンドの挙動定義に都合が悪い点があっても、同一コマンド用に別の挙動を定義しやすくなると思います。

rootとは?

正規表現の中にrootという名前付きのグループがあります。このrootにマッチした文字列は、そのまま"$GOPATH/src/{root}"として展開されます。例えば"github.com/nobu-k/hoge"の場合は"$GOPATH/src/github.com/nobu-k/hoge"というディレクトリが掘られます。そこにリポジトリの中身がダウンロードされます。

subrootとは?

リポジトリのパスに指定するユーザ名とリポジトリ名を抽出するために、適当に用意した名前付きグループです。reの中で名前付きグループを用意すると、repoの中でマッチした値を参照できます。詳しくはvcs.goの repoRootForImportPathStatic 関数を参照すると良いでしょう。

repoとは?

リポジトリのURIみたいなもんだと思います(適当)。今回はping(scheme推定)の機能は使わないため、sshを使って貰えるようにschemeを明示的に指定しました。具体的にその値がどう使われるかは指定したVCSによって異なり、gitの場合はここで書いたrepoの値が以下のように参照されます。

// vcsGit describes how to use Git.
var vcsGit = &vcsCmd{
    name: "Git",
    cmd:  "git",

    createCmd:   "clone {repo} {dir}",
    downloadCmd: "pull --ff-only",

    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 master",

    scheme:  []string{"git", "https", "http", "git+ssh"},
    pingCmd: "ls-remote {scheme}://{repo}",
}

今回意味を持つのはcreateCmdのところだけですね。

補足: goコマンドとGitHub Enterprise

今回なぜこんなことをしたかというと、goコマンドとGitHub Enterpriseの相性が(会社的、個人的に)悪かったためです。goコマンドは How to Write Go Code に従って開発をしていると非常に便利です。しかし、GitHub Enterpriseとセットで利用しようとするといくつか問題が生じたので共有しておきます。

importパスにGitHub EnterpriseのURIが含まれる

GitHub Enterpriseからimportするということは、importパスにリポジトリのURIを書かなければならないということを意味します。これは開発がGitHub Enterpriseで閉じている分には問題ありません。しかし、ひとたびそのリポジトリをGitHubなどで公開しようとすると、GitHub Enterpriseのアクセス先が公になってしまうという問題が生じます。

リポジトリをgithub.comに移すときにimportパスを書き換えつつgitの履歴を全消去すれば良いのかもしれませんが、できれば履歴は消したくないです。変更説明だけでなく、コントリビューションの情報も消えちゃいますしね。なので、履歴に残しておいても問題ないような形でimportパスを記述できると良いですよね。

HTTPSでのアクセス

goコマンドでgitリポジトリを取得する際には、すべての操作がHTTPSで行われます。そのため、go get時にGitHub Enterpriseのユーザ名とパスワードを入力しなければならないこともしばしば・・・。どうせもう鍵は登録されてるんだし、ssh使って欲しいですよね。

importパスがかっこわるくなる

importやgo getコマンドがVCSを推測できるようにパスを記述すると、GitHub Enterpriseの場合は "ghe.hoge.com/user/repo.git.git" のように末尾に1個余分な.gitをつけなければなりません。ダサい。

どうせホスト名を隠すのであれば、"pfi/<user>/<repo>"みたいな感じで書けると良いですよね。どっちにしろ社外から参照されることは意図していないので、こんな感じで良いのではないかと思いました。

他の解決策案

色々やってみた結果、今回紹介したgoコマンドのソースを書き換えるのが楽でしたが、他にも以下の方法を検討しました。

metaタグを利用する

適当なサーバを用意してそこから Remote Import Paths に書かれているようなmetaタグを返すようにすると、importの挙動を若干制御できます。しかし、そんなサーバを用意するのが面倒だったので没になりました。また、いずれにせよそのサーバにアクセスするとGitHub Enterpriseのアドレスが割れてしまうので、リポジトリを公開したくなったときに前述の問題が発生します。

GitHub Enterpriseでgo用にmetaタグを返せるようになると、上記の.git.git問題だけはお手軽に解決できるかもしれませんね。

goコマンドのラッパーを作る

とりあえずsshでgo getだけできればいいや!という感じで、importパスを拾い上げつつ適当にgit cloneするようなコマンドを書けば良いんじゃないかと一瞬だけ思いました。が、もちろんgoコマンドが中でやってるようなロジックをもう一度実装することになるのでやめました。なにげに色んなタイミングでimportの解釈が行われるので大変ですね。

結果としてgoコマンドのソースをいじった方がメンテコストも低くて良かったです。

まとめ

GitHub EnterpriseでGoによる開発を快適に行うためにちょっとした改造を加えました。コマンド自体も非常に綺麗に書かれており、想像以上に簡単に変更できてさすがGo言語だと思いました!