📚

同一PC内で複数Githubアカウントを自動で切り替える

5 min read 2

モチベ

会社でGitHub Enterpriseを導入している関係で、
個人用のGitHubのアカウントとの切り替えをしたい。

が、しかし

git config --global user.name = "hoge"
git config --global user.email = "fuga@example.com"

だの

git clone git@github.com:ore/repo
cd repo
git config --local user.name = "hoge"
git config --local user.email = "fuga@example.com"

だの毎回打ちたくないので自動で切り替えて欲しい。

そこで、今回はGit Hookを利用してなんとかしたものを備忘録がてら共有します。

筆者環境

  • MacBook Pro (16-inch, 2019)
  • macOS 10.15.7
  • git version 2.31.1

概要

cloneしてくる時にアカウントを判別して、local configの user.name, user.email を自動でセットする。

準備

今回はGitHubとGitHub Enterpriseを切り替える程でやりますが、GitHubのアカウント2つの切り替えも同じような手順でできると思います。

SSHの設定

とりあえず ssh-keygen でkeyを生成。
別にここでアカウントごとにkeyを分ける必要はないですが、今回は分けるということで。

ssh-keygen -f ~/.ssh/ghe_rsa
ssh-keygen -f ~/.ssh/github_rsa
ls ~/.ssh # ghe_rsa ghe_rsa.pub github_rsa github_rsa.pub

生成した公開鍵 *.pub は各アカウントで登録しておきます。

次に ~/.ssh/config に以下を追記します。

~/.ssh/config
Host primary.ghe
    HostName ghe.hoge.com
    IdentityFile ~/.ssh/ghe_rsa
    User git

Host secondary.github
    HostName github.com
    IdentityFile ~/.ssh/github_rsa
    User git

ここで注意、SSH的にはHost名にアンダースコアが使えますが、後でgit側でエラーが出るので区切る場合はピリオドを使うことをお勧めします。

gitconfig

次に ~/.gitconfig に以下を追記します。

~/.gitconfig
[user "ssh://git@primary.ghe/"]
    name = hoge
    email = hoge@example.com
[user "ssh://git@secondary.github/"]
    name = fuga
    email = fuga@example2.com

今回はアカウントを使い分けることが目的なので、globalのuserはhost別に定義しておきます。

Git hook

Git hookについてはこちらを参照してください。

まずはhookを置く場所を決めます。
~/.gitconfig にパスを書けばいいのでどこでも大丈夫です。

cd {hookを置きたい場所}
mkdir -p .git_template/hooks

hookを置く場所を決めてディレクトリを作ったら、 ~/.gitconfig にパスを追記します。

git config --global init.templatedir '{hookを置いたディレクトリ}/.git_template'

今回は git clone を叩いた時にhookが発火して欲しいのですが、cloneした時だけ発火するhookは存在しません。そこでcloneした後はcheckoutする仕様を利用して、checkoutした後に発火するhook内でcloneかどうかを判別することにします。

hookは実行権限が必要なので、先に与えてしまいましょう。

cd .git_template/hooks
touch post-checkout
chmod +x post-checkout

post-checkoutの中身は以下のようにします。

.git_template/hooks/post-checkout
#!/bin/sh

readonly PREVIOUS_HEAD=$1
readonly NOW_HEAD=$2
readonly BRANCH_SWITCH=$3 # ブランチをチェックアウトした場合は1, ファイルをチェックアウトした場合は0

readonly Z40="0000000000000000000000000000000000000000"

readonly origin_name="$(git remote | head -1)"
current_remote_url="$(git config --get --local remote.$origin_name.url)"
local_name="$(git config --local --get user.name)"
local_email="$(git config --local --get user.email)"

# git cloneした場合のPREVIOUS_HEADは0が40個で、BRANCH_SWITCHは1
if [ "$PREVIOUS_HEAD" = "$Z40" -o "$BRANCH_SWITCH" = "1" ]; then
    if [ -z "$current_remote_url" ]; then
        echo "\033[31mfatal: No remote URL\033[m"
    fi
    case $current_remote_url in
    *://*)
        # Normalize URL: remove leading "git+"
        # e.g.) "git+ssh://user@host/path/" ==> "ssh://user@host/path/"
        current_remote_url=$(echo $current_remote_url | sed 's/^git\+//')
        ;;
    *:*)
        # Convert scp-style URL to normal-form
        # e.g.) "user@host:path/" ==> "ssh://user@host/path/"
        current_remote_url=$(echo $current_remote_url | sed 's/\(.*\):ssh:\/\/\1\//')
        ;;
    esac
    if [ -z "$local_name" ]; then
        local_name="$(git config --get-urlmatch user.name $current_remote_url)"
        git config --local user.name "$local_name"
        echo "config: set local user.name $local_name"
    fi
    if [ -z "$local_email" ]; then
        local_name="$(git config --get-urlmatch user.email $current_remote_url)"
        git config --local user.email "$local_email"
        echo "config: set local user.email $local_email"
    fi
else
    # 通常のgit checkout後のhookを書く場合にはここに書く
fi

これで終わりでもいいんですが、 git init した時などは自動的にlocalの user.name, user.email が設定されないので、globalの値が使われることになります。
そこで、pre-commit hookを作ることでlocalに user.name, user.email が設定されていない場合commitできないようにします。
この場合pre-commitにも実行権限をつけることを忘れないようにしましょう。

.git_template/hooks/pre-commit
#!/bin/sh

if [ -z "$(git config --local --get user.name)" ]; then
    echo "\033[31mfatal: user.name is not set locally\033[m"
    exit 1
fi

if [ -z "$(git config --local --get user.email)" ]; then
    echo "\033[31mfatal: user.email is not set locally\033[m"
    exit 1
fi

使い方

git clone するときはあらかじめ ~/.ssh/config に記述してあるhostを指定してcloneする。

git clone git@primary.ghe:hoge/repo

注意点

  • git initした場合はcheckoutが行われないのでpost-checkout hookが発火しません
  • 新しく空のレポジトリを作って、それをそのままcloneした場合も同様にcheckoutが行われないのでpost-checkout hookが発火しません

追記

@ulwluさんからご指摘いただきましたが、アカウントごとにディレクトリを分けるのであれば includeif を用いた方がよりシンプルで良いと思います。

https://zenn.dev/joy_m1k1/articles/multiple-github-accounts#comment-a5d25687ca1bd8

参考

https://git-scm.com/docs/git-config

https://git-scm.com/book/ja/v2/Git-のカスタマイズ-Git-フック

https://www.klab.com/jp/blog/tech/2015/1033121546.html

Discussion

良い記事でした。
もし目的と違ったら申し訳ないですが、includeifを用いるともう少しシンプルに実現できると思います。

[includeIf "gitdir:~/ghe"]
path = .gitconfig-ghe
[includeIf "gitdir:~/github"]
path = .gitconfig-github

こうして、各gitconfigにuserとemailや、templateディレクトリも切り分けることが出来ます。明示的かつ処理の流れを読みとかなくて良いのでシンプルかもしれないです。

ご指摘ありがとうございます!

確かにディレクトリをアカウントごとに分けてincludeifを用いた方がよりシンプルでいいですね。
大変勉強になります🙇‍♂️

ログインするとコメントできます