Managing two (or more) accounts on one git-forge
Tags: git, github, ssh
Published: 2023-08-07 03:02:45

A single account of a git-forge such as gitlab or github is quite common and only requires a little configuration. But when your employer or client uses the same forge and requires you to have a second account on that forge, it could become a bit tricky, especially when you have them within the same repository. There are ways to solve this problem. This post will explain at least three of them.

Let's assume you are using the following remotes for the same project; one is the upstream project, and the other is your fork. You can use your account for one remote (`git@gitforge.com:waterkip/themoneymaker.git`), and you have to use another account for the other (`git@gitforge.com:client/themoneymaker.git`).

## Option 1: Change the remote URI via ssh_config

And by ssh_config, I mean `$HOME/.ssh/config`.

The idea behind this is that by using `.ssh/config`, you can use aliases for a gitforge and configure a specific identity for that alias. In this case, you will use `clientforge` as your client's host and the original name gitforge.com for your projects. Your `.ssh/config` will look a little something like this:

```
Host gitforge.com
   User git
   Hostname gitforge.com
   IdentityFile ~/.ssh/id_rsa

Host clientforge
    User git
    Hostname gitforge.com
    IdentityFile ~/.ssh/id_client
```

In your project's repository, you'll need to change the remote of your liking with the set-url command for remotes:

```
$ git remote set-url upstream git@clientforge:client/themoneymaker.git
```

The downside to this approach is that you need to remember which host to use for which project, and you can no longer copy/paste specific commands from the forges after you've created a new repository. The upside is that it doesn't require any additional git configuration changes.

## Option 2: Use a different SSH command (or options) for a project

Assuming you have two projects, one for your own project and one for your client project, you can opt for this solution.

Git configs have several locations; you have a system-wide configuration in `/etc/git/config`,  a global one in `$HOME/.gitconfig`, and a local one in your repository `.git/config`. For a single project, you can set a custom [`core.sshCommand`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-coresshCommand) command or ssh-options so you only use the key associated with your work account. You can do this by running the following command:

```shell
# in ~/code/themoneymaker
git config core.sshCommand \
  '-i ~/.ssh/id_client -o IdentityOnly=yes -F /dev/null'
```

This should result in a line similar to this in `.git/config`:

```ini
[core]
  sshCommand = -i ~/.ssh/id_client -o IdentityOnly=yes -F /dev/null
```

The `identityOnly=yes` is only there to prevent ssh from looping over all your ssh-keys and potentially using a different ssh key. The `-F /dev/null` disables using your `.ssh/config`. You could also use a different SSH config for just the `sshCommand`, e.g. `sshCommand = -F ~/.ssh/config-client` and set the correct ssh options in that file. You could also use the entire command here, e.g. `ssh -i ~/.ssh/id_client -o IdentityOnly=yes -F /dev/null`.

This option only scales well when you have only one or two projects, which leads me to the next option.

## Option 3: Different SSH commands based on the path of your projects

You can use different configs for git depending on the project directory that you are in. With the [`includeIf`](https://git-scm.com/docs/git-config#_includes) directive, you can include a separate configuration based on the path of your projects. So all projects in `$HOME/work/client` share the same git configuration. In your `$HOME/.gitconfig`, you need to add the following snippet:

```
[includeIf "gitdir:~/work/client/"]
   path = ~/.config/git/client.config
```

Now in `$HOME/.config/git/client.config`, you can configure the `core.sshCommand` as done previously in option 2. You can also use this for configuring other bits of git for your project(s), such as a different e-mail address, name, etc.


## Option 4: Custom ssh wrapper

The nicest of them all and can work as a combination of options 2 and 3. The problem is that I have two accounts on the same git-forge, and I have a ton of repositories. Because the hostnames of the repositories are placed in the myrepos configuration file, I cannot just quickly change the host in `.ssh/config`. And I also don't want to change host for all my personal projects. Since I already use the `includeIf` directives for various directories, I wanted to be able to select a different ssh key for specific remotes. Now there is an `includeIf` directive that supports including a configuration when a remote has a specific endpoint: `[includeIf "hasconfig:remote.*.url:https://example.com/**"]`. This will not work as expected because it will include the configuration, regardless of where you push your changes to. So we will use an ssh-wrapper script and configure it as the `core.sshCommand` command. 

First, you need to know how git sends its ssh command. You can test this by making a script and printing the things to STDERR or using `set -x`. Printing to STDOUT from within the script will issue warnings: `protocol error: bad line length character: git@`. You can also use `GIT_TRACE=1` while issuing a command that triggers something on your remote (`git fetch`, `git pull`, `git push`, etc). Git has several options on how and what it sends to ssh. You can change these options by setting the [`ssh.variant`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-sshvariant) option. I started by setting it to `simple`, changed it to `ssh`, and now it is back to the default `auto`.

A small script like this will do the job for testing purposes:

```shell
set -x

echo $@ >&2
ssh $*
```

And you issue the command `git config --local core.sshCommand /path/to/wrapper-script.sh`. After which you run `git fetch`:

```
+ echo -o SendEnv=GIT_PROTOCOL git@gitlab.com git-upload-pack 'waterkip/themoneymaker.git'
-o SendEnv=GIT_PROTOCOL git@gitlab.com git-upload-pack 'waterkip/themoneymaker.git'
+ ssh -o SendEnv=GIT_PROTOCOL git@gitlab.com git-upload-pack 'waterkip/themoneymaker.git'
```

As you can see, the last bit of the arguments is what we are interested in. 
It has the repository, and we can select the correct ssh-key based on that.
We want to tell our script that if the owner/group of the repository is `client`, we want to use the ssh-key, `~/.ssh/id_client`. And we supply this as arguments to the wrapper script.

```ini
# Your relevant git config file should have the following snippet
[core]
  sshCommand = /path/to/custom-ssh-wrapper client ~/.ssh/id_client
```

The wrapper script:

```zsh
#!/usr/bin/env zsh

name=$1
identity=$2
shift;
shift;

# $@ = git@host 'git-cmd \'user/repo.git\''
git_cmd=${@[$#]}
git_cmd=("${(@s: :)git_cmd}")
# git_cmd = ( git-cmd 'user/repo.git' )
repo=$(echo "${git_cmd[${#git_cmd[@]}]}" | sed -e "s/'//g");
# repo = user/repo.git
repo=("${(@s:/:)repo}")
# repo = ( user repo.git )

group_oder_user=$repo[1]

ssh_opts=""
[ $group_oder_user = $name ] \
  && ssh_opts="-i $identity -o IdentitiesOnly=yes -F /dev/null"

eval ssh $ssh_opts $*
```

This allows me to use different accounts for each remote within the same repository. Support for a third account is also possible. It requires more work on the argument handling in the wrapper script. Or by taking a different approach and having a configuration that maps the repo-user to ssh-keys. But I'll leave that as an exercise for the reader.