本文へ移動
vast-cowのブログ
前のページへ戻る

GitHub複数アカウント運用で、repo ownerから自動的にghのPATを選ぶ

この記事を編集

GitHubで複数アカウントを使っていると、HTTPSでのgit pushやgit pull時に「どのアカウントのPATを使うか」が地味に面倒です。

特に、

https://github.com/aont/foo.git
https://github.com/another-user/bar.git

のように複数ownerのrepositoryを扱っていると、Git Credential Managerやgh auth git-credentialが現在のアクティブアカウントを使ってしまい、

remote: Permission to owner/repo.git denied to other-user.

のようなエラーになることがあります。

そこで、remote URLのowner部分をGitHub CLIのアカウント名として解釈し、

gh auth token --user ACCOUNT

からPATを取り出してGitに返すcredential helperを.gitconfigへ直接埋め込みます。

個人repositoryではownerをそのままaccountとして使用し、Organization配下などでownerと認証に使うaccountが異なる場合には、明示的なマッピングを追加できるようにします。

外部スクリプトも不要です。

前提

まず、利用するGitHubアカウントはあらかじめghにログインしておきます。

gh auth login

複数アカウントを登録している場合は、

gh auth status

で確認できます。

この方法では、基本的にはrepository URLが、

https://github.com/aont/foo.git

なら、

gh auth token --user aont

を実行します。

したがって、デフォルトでは、

repository owner = ghに登録したaccount名

という運用を前提にします。

この関係が成立しない場合だけ、明示的なマッピングで上書きします。

.gitconfig

Organization単位やrepository単位のマッピングにも対応する場合、設定は次のようにします。

[credential]
    useHttpPath = true

[credential "https://github.com/"]
    helper =
    helper = "!f() { \
        [ \"$1\" = get ] || exit 0; \
        path=''; \
        while IFS='=' read -r k v; do \
            [ \"$k\" = path ] && path=$v; \
        done; \
        [ -n \"$path\" ] || exit 0; \
        owner=${path%%/*}; \
        repo=${path#*/}; \
        repo=${repo%.git}; \
        case \"$owner/$repo\" in \
            my-org/special-repo) account='special-account' ;; \
            *) \
                case \"$owner\" in \
                    my-org) account='my-work-account' ;; \
                    another-org) account='another-account' ;; \
                    *) account=\"$owner\" ;; \
                esac \
                ;; \
        esac; \
        token=$(gh auth token --user \"$account\") || exit 0; \
        printf 'username=%s\\npassword=%s\\n' \"$account\" \"$token\"; \
    }; f"

[credential "https://gist.github.com/"]
    helper =
    helper = "!f() { \
        [ \"$1\" = get ] || exit 0; \
        account=''; \
        while IFS='=' read -r k v; do \
            [ \"$k\" = path ] && account=${v%%/*}; \
        done; \
        [ -n \"$account\" ] || exit 0; \
        token=$(gh auth token --user \"$account\") || exit 0; \
        printf 'username=%s\\npassword=%s\\n' \"$account\" \"$token\"; \
    }; f"

ポイントは4つあります。

credential.useHttpPath = true が重要

通常、Gitのcredential helperには、

protocol=https
host=github.com

程度しか渡されません。

しかし今回必要なのは、

aont/foo.git

というpathです。

そこで、

[credential]
    useHttpPath = true

を設定します。

これによってcredential helperの標準入力に、

protocol=https
host=github.com
path=aont/foo.git

のようにpathも渡されます。

pathからownerとrepository名を取得する

helper内では、まずpathを取得します。

path=''
while IFS='=' read -r k v; do
    [ "$k" = path ] && path=$v
done

その後、

owner=${path%%/*}

でownerを取り出します。

repository名についても、

repo=${path#*/}
repo=${repo%.git}

として取得します。

たとえば、

path=aont/foo.git

なら、

owner=aont
repo=foo

となります。

明示的なマッピングが存在しなければ、

account="$owner"

として、ownerをそのままGitHub CLIのaccountとして使用します。

その後、

token=$(gh auth token --user "$account")

で、そのアカウントに対応するPATをGitHub CLIから取得します。

つまり、

https://github.com/aont/foo.git

へのアクセス時は自動的に、

gh auth token --user aont

相当になります。

一方、

https://github.com/another-user/bar.git

なら、

gh auth token --user another-user

になります。

gh auth switchを毎回実行する必要はありません。

Organizationのownerを別のaccountへマッピングする

単純な、

owner = account

というルールは個人repositoryでは扱いやすいですが、Organization配下のrepositoryでは成立しない場合があります。

たとえばremote URLが、

https://github.com/my-org/foo.git

であっても、そのOrganizationへアクセスするGitHubアカウントが、

my-work-account

だったとします。

この場合、

repository owner = my-org
認証に使うaccount = my-work-account

なので、

gh auth token --user my-org

としても正しいcredentialは取得できません。

そこで、owner単位のマッピングを追加します。

case "$owner" in
    my-org) account='my-work-account' ;;
    another-org) account='another-account' ;;
    *) account="$owner" ;;
esac

これによって、

github.com/my-org/foo
        ↓
owner = my-org
        ↓
account = my-work-account
        ↓
gh auth token --user my-work-account

という動作になります。

明示的に指定していないownerについては、

*) account="$owner" ;;

にフォールバックするため、従来どおりownerをそのままaccountとして使用します。

つまり、個人repositoryのためにすべてのownerを列挙する必要はありません。

特定のrepositoryだけ別accountを使う

同じOrganization配下でも、repositoryによって認証に使うアカウントを変えたい場合があります。

たとえば、

https://github.com/my-org/foo.git
https://github.com/my-org/special-repo.git

があり、通常は、

my-work-account

を使うものの、special-repoだけは、

special-account

を使いたいとします。

その場合は、owner単位のマッピングより先にowner/repo単位で判定します。

case "$owner/$repo" in
    my-org/special-repo) account='special-account' ;;
    *)
        case "$owner" in
            my-org) account='my-work-account' ;;
            another-org) account='another-account' ;;
            *) account="$owner" ;;
        esac
        ;;
esac

この構成では、優先順位が、

repository単位のマッピング
        ↓
owner単位のマッピング
        ↓
ownerをそのままaccountとして使用

となります。

たとえば、

github.com/my-org/special-repo
        ↓
special-account

github.com/my-org/other-repo
        ↓
my-work-account

github.com/aont/foo
        ↓
aont

という形です。

これなら、個人repository、Organization配下のrepository、さらに一部repositoryだけの例外を同じcredential helperで扱えます。

helper = で既存credential helperをリセットする

もう一つ重要なのが、

helper =

です。

Git for Windowsなどでは、すでにGit Credential Managerやgh auth git-credentialなどが設定されている場合があります。

たとえば別のhelperが先にcredentialを返すと、今回のhelperまで処理が回ってきません。

その結果、

remote: Permission to foo/bar.git denied to wrong-account.

のようになります。

そこで、

helper =
helper = "!f() { ... }; f"

とします。

最初の空のhelper =で、それ以前に設定されていたcredential helperをリセットし、その後に今回のhelperだけを登録しています。

複数GitHubアカウントを扱う場合には、この部分がかなり重要です。

Gistにも同じ仕組みを使う

Gistにも同じcredential helperを設定できます。

ただし通常のGist clone URLは、

https://gist.github.com/GIST_ID.git

となっていて、URLからユーザー名を判断できません。

そこで、この運用ではGistのremote URLを意図的に、

https://gist.github.com/USER/GIST_ID.git

という形式にします。

たとえば、

https://gist.github.com/aont/0123456789abcdef.git

なら、credential helperから見ると、

path=aont/0123456789abcdef.git

となるので、

gh auth token --user aont

を自動的に使えます。

GitHub repositoryとGistでは多少ルールが異なりますが、どちらもURLのpathをcredential routingに利用できます。

github.com/OWNER/REPO
gist.github.com/USER/GIST
                ↓
        accountを決定
                ↓
gh auth token --user ACCOUNT

通常の個人repositoryやGistではownerまたはUSERをそのまま使い、Organization配下のrepositoryについては必要に応じてマッピングで上書きする形です。

動作確認

Gitが実際にどのcredentialを取得するかは、git credential fillで確認できます。

たとえば、

printf '%s\n' \
  'protocol=https' \
  'host=github.com' \
  'path=aont/foo.git' \
  '' |
git credential fill

を実行します。

期待する出力は、

protocol=https
host=github.com
username=aont
password=...

です。

Organization単位のマッピングについても確認できます。

printf '%s\n' \
  'protocol=https' \
  'host=github.com' \
  'path=my-org/foo.git' \
  '' |
git credential fill

上記の設定なら、期待するusernameは、

username=my-work-account

です。

さらに、repository単位のoverrideについて、

printf '%s\n' \
  'protocol=https' \
  'host=github.com' \
  'path=my-org/special-repo.git' \
  '' |
git credential fill

を実行すると、

username=special-account

となります。

もしusernameが想定と異なる場合は、

git config --show-origin --get-all credential.helper

で、別のcredential helperが残っていないか確認するとよいです。

なぜgh auth git-credentialをそのまま使わないのか

GitHub CLIには標準で、

gh auth git-credential

があります。

通常の単一アカウント運用ならこれで十分です。

ただ、複数アカウントを同一ホストgithub.comで使っている場合、「repository ownerやrepositoryそのものに応じて、どのgh accountを使うか」を明示的に制御したくなります。

今回のhelperでは、

remote URL
    ↓
owner/repositoryを抽出
    ↓
repository単位のマッピングがあれば使用
    ↓
owner単位のマッピングがあれば使用
    ↓
なければownerをaccountとして使用
    ↓
gh auth token --user account

という規則にしているため、現在どのアカウントがghでactiveになっているかを意識する必要がありません。

この構成の利点

この方法だとPATそのものを.gitconfigに保存しません。

PATは必要になるたびに、

gh auth token --user ACCOUNT

から取得します。

そのため、設定として保存されるのは「どのrepositoryにどのアカウントを使うか」というルールだけです。

また、repositoryごとに、

gh auth switch

したり、

git config credential.username ...

を設定したりする必要もありません。

remote URLそのものがcredential routingの入力になります。

個人repositoryなら、

github.com/aont/foo
        ↓
account = aont

Organization配下なら、

github.com/my-org/foo
        ↓
account = my-work-account

さらに特定repositoryだけ例外にしたければ、

github.com/my-org/special-repo
        ↓
account = special-account

という形で扱えます。

まとめ

複数GitHubアカウントをHTTPSで使う場合、remote URLのpathを使って、適切なgh accountを自動的に選択できます。

仕組みとしては、

Git remote URL
  ↓
credential.useHttpPath
  ↓
path=owner/repo.git
  ↓
ownerとrepoを抽出
  ↓
repository単位のマッピングがある?
  ├─ yes → 指定されたaccount
  └─ no
       ↓
     owner単位のマッピングがある?
       ├─ yes → 指定されたaccount
       └─ no → ownerをそのままaccountとして使用
  ↓
gh auth token --user account
  ↓
username/passwordとしてGitへ返す

という流れです。

基本ルールはこれまでと同じく、

repository owner = GitHub account

です。

そのうえで、Organization配下など、

repository owner != 認証に使うaccount

となるケースだけowner単位のマッピングを追加できます。

さらに、同じOrganization内でも認証アカウントを分ける必要があれば、repository単位でoverrideできます。

これによって、PAT自体を.gitconfigへ保存したり、外部スクリプトを用意したりすることなく、複数GitHubアカウントのcredential routingを.gitconfigだけで完結できます。


この記事を編集
この記事を共有:

コメント


前の記事
GitHubのMarkdownで使える「Alerts」記法とは?
次の記事
一時的にキーボードレイアウトを変更する方法