EXPEDIA GROUP TECHNOLOGY — ENGINEERING
How to Handle Multiple Git Accounts
The easy way to get your Gits together
When you need to work with multiple Git identities, use Git’s ssh-command
configuration option to control both your SSH credentials and your Git identity with a single configuration based on your directory path.
If that sentence triggered your eureka moment, stop reading and go create something wonderful. If you want to learn more about how and why, read on.
A grove of Gits doesn’t make you weird
Working for a bring-your-own-device (BYOD) company is an obvious reason one might use multiple Git accounts, but there are plenty of others. For example, if your company participates in open source, you might have an account in your company’s GitHub Enterprise for internal work product and another in the public GitHub.com for your open source contributions. Or you may have a dedicated GitHub.com account you use for contributions to your employer’s open source products separate from the one you’d use to contribute to other open source projects. Or perhaps you are self-employed and use a different GitHub.com account for each of your ventures.
Managing these multiple configurations efficiently is painful without a carefully-considered solution.
Why multiple Git accounts are hard to manage
The short answer to this question is that two things need to be managed in most Git usages:
- Identity: Information about you attached to a commit inside a repo. This is managed by the Git application.
- Authentication: Verifying that you are in fact you before you are allowed to interact with a repo. This is managed by your local computer and the service hosting your remote repos.
These two mechanisms are configured separately. They both support some conditionality in their configuration files, but they don’t consider the same factors, which leads to maintaining two parallel sets of configuration if not configured cleverly.
Your identity in Git
When you use Git locally on your computer, you have elements of identity that you configure locally that are used to mark your commits, common examples of which are:
- User name
- Email address
- GPG signing key
You do use a GPG signing key, right? You should.
These are managed by Git itself in a config file, usually ~/.gitconfig
. Here’s a pretty typical example that contains the identity fields above as well as some global preferences:
[merge]
ff = false
[pull]
ff = only
[core]
editor = vim
[commit]
gpgsign = true[user]
name = Russell Brown
email = r***@***.com
signingkey = A0***9F
The user name and email address are then consumed from that file and placed into the commits you make on your local repo. Git does not validate or constrain these values, which opens the door to “commit spoofing”. Using a GPG signing key allows a GitHub hosting service to ensure the identity included in the commit is legitimate. This is important in public repositories and especially in open source projects.
Conditional configuration
Your .gitconfig
doesn't need to be your only configuration for Git. That file's syntax supports including additional files containing Git configuration with include
. In addition, you can make it conditional using the includeIf
directive. Its documentation lays out a couple of options, but the one that is most interesting for this use case is gitdir
.
If you commit (tee hee) to storing repos in directories that correspond to identities, you can use includeIf
to set your identity in the repos below them. I use a structure similar to:
$HOME
+- git
+- work
+- oss
+- personal
Then my .gitconfig
looks like this:
[merge]
ff = false
[pull]
ff = only
[core]
editor = vim
# The trailing slashes below are required to match
# any subdirectory and not just the exact path
[includeIf "gitdir:~/git/work/"]
path = ~/.gitconfig.work
[includeIf "gitdir:~/git/oss/"]
path = ~/.gitconfig.oss
[includeIf "gitdir:~/git/personal/"]
path = ~/.gitconfig.personal
The .gitconfig.work
configuration file looks like this (the others are similar with different emails and signing keys):
[user]
name = Russell Brown
email = r***@***.com
signingkey = A0***9F
[commit]
gpgsign = true
Whenever I invoke Git from any subdirectory of ~/git/work
(that's what the trailing slash does in the gitdir
spec), this will be the effective configuration:
[merge]
ff = false
[pull]
ff = only
[core]
editor = vim
[user]
name = Russell Brown
email = r***@***.com
signingkey = A0***9F
[commit]
gpgsign = true
Which solves the problem of including the proper name, email, and signing key for commits to my work repos.
This solves the identity management problem. It does not account for authenticating to multiple Git hosts, however.
Hosted Git
In most professional settings, Git’s server is used behind a hosting platform such as GitHub (or GitHub Enterprise), GitLab, or BitBucket. These hosting platforms provide authentication, role-based access control (RBAC), organizational structures, issues, and collaboration features like pull requests. I’m most familiar with GitHub and it is the market leader, so I’ll focus on that product, but the other products have similar capabilities.
Authenticating to GitHub
Because GitHub in all its flavors is designed to be a central authority for Git repos that are shared among developers, but local to none, users must be authenticated to have access to the repos it hosts. You can configure a local Git client to use HTTPS to interact with repos hosted in GitHub, but Git has no ability to store credentials and no daemon to cache them — so you’ll need to log in for each clone, push, and pull.
To work around this, Git can use the operating system’s secure shell (SSH) facility to authenticate you and encrypt your communications with a remote. This works by using an RSA key pair:
- The secret key is stored on your computer
- Its corresponding public key is stored in the remote
Because the remote is managed by a GitHub server, you enter the text of the public key using the GitHub UI; you don’t have a config file for this. When you push, pull, or clone, your connection to the remote is crafted with the private key and verified by the remote using the public key.
Your private key is stored in the file ~/.ssh/id_rsa
by default. The paired public key is stored under the same name and place as the private key, with .pub
appended to its name. SSH will use these names unless you specify otherwise when you use them.
You can make multiple keys if you supply different names for each.
Choosing an SSH key
SSH keeps a config file, ~/.ssh/config
by default, that allows it to choose which key to use for a particular host as well as some other configuration options. Here's an example of what goes in it:
Host personal
Hostname github.com
User rcbrownHost *
IdentityFile ~/.ssh/id_rsa
That Hostname
is picked out of the Git URL used when cloning:
git@github.com:rcbrown/grpc-node-examples.git
It’s the portion between @
and :
. The Host *
configurations will be added to the matching host's entry, or used exclusively if nothing else matches. In this case, the effective configuration of SSH will be:
Hostname github.com
User rcbrown
IdentityFile ~/.ssh/id_rsa
If you were to use SSH with two different GitHub servers, your SSH config might look something like this:
Host work
Hostname github.myemployer.com
User rbrownHost personal
Hostname github.com
User rcbrownHost *
IdentityFile ~/.ssh/id_rsa
Using the same SSH public key on multiple accounts is poor practice — so much so that GitHub prevents it. You will need two different SSH keypairs. You can handle that by moving the IdentityFile
setting out of the common block and into host-specific ones:
Host work
Hostname github.myemployer.com
User rbrown
IdentityFile ~/.ssh/id_rsa_workHost personal
Hostname github.com
User rcbrown
IdentityFile ~/.ssh/id_rsa_personalHost *
IdentityFile ~/.ssh/id_rsa
What happens when you have two accounts on the same server? For example, a GitHub.com for your open source contributions, and another for your personal stuff? This is where it gets interesting. Setting up the config file is easy enough:
Host work
Hostname github.myemployer.com
User rbrown
IdentityFile ~/.ssh/id_rsa_workHost personal
Hostname github.com
User rcbrown
IdentityFile ~/.ssh/id_rsa_personalHost oss
Hostname github.com
User open_rcbrown
IdentityFile ~/.ssh/id_rsa_ossHost *
IdentityFile ~/.ssh/id_rsa
But how do you decide whether to use personal
or oss
when accessing Github.com?
A red herring
Put “multiple GitHub accounts” in your favorite search engine and the first few results will probably propose creating the .ssh/config
file as I showed above, then modifying the hostname to choose the correct key when Git interacts with SSH:
git clone git@personal:rcbrown/grpc-node-examples.git
In my opinion, this solution has serious drawbacks.
- GitHub makes it easy to copy the clone URL to the clipboard, but you will have to modify the hostname in the command line that you must actually run. This is easy to forget. It’s even more annoying if you are forking a repo and maintaining an upstream.
- It’s a hierarchy parallel to your GitHub identity; adding a new account requires updating both
.gitconfig
and~/.ssh/config
. - Building on the last point, because separate directories are required to vary the Git identity configuration, the SSH server name must be set in a hostname that will be used only under the directory that provides the correct identity for that host. In other words, for this to work right, every repo needs both to have the right name and to be in the right place.
The fragmented config is difficult to manage:
What we need to solve these problems is a way to unify the identity and authentication configurations. To do that, we need to find a decision point that intersects both Git identity and SSH host identity.
Rescued by sshCommand
The Git config files that you include from ifInclude
directives in .gitconfig
that control identity by directory can control more. You can take the choice of SSH key away from SSH and let Git choose the key and the identity using the includeIf
mechanism.
- You can tell Git what SSH command to use in a config file by using the
sshCommand
setting. - Since you know how to tell Git to use specific configs per directory, you can configure an SSH command per directory.
- You can construct an SSH command that uses a specific identity with the
-i
option.
Here’s what .gitconfig.work
would look like:
[user]
name = Russell Brown
email = r***@***.com
signingkey = A0***9F
[commit]
gpgsign = true
[core]
sshCommand = "ssh -i ~/.ssh/id_rsa_work
Therefore, you can control both Git identity and SSH authentication using .gitconfig
and appropriate directories. No need for strange Git hostnames. A side effect is that the usual SSH identity search rules (described in SSH's man page) are no longer relevant because the identity to use is always explicit — less to reason about, a good thing in my opinion.
Alternatives
There are other ways to do this. Aside from using modified hostnames, which I rejected above, you can include a repo-level Git config in <repo>/.git/config
. This configuration file could be a symbolic link to one of the environment-specific Git configs we created above (such as ~/.gitconfig.work
). You will have to remember to create the link in each repo you clone, but once you've done so, you don't have to touch it again. This solution makes the most sense if you're only using an SSH key for one repo or if you have a large number of Git hosts to work with and relatively few repos in each.
Do you have more ideas? I’d love to hear them!