📚

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

2021/04/30に公開

モチベ

会社で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