1

I have a personal github account and then another for work. My Directory structure:

~/Projects/work
~/Projects/personal

Within each of those directories are various repos corresponding to work or personal projects.

My work Github has a separate username and email than my personal one.

I followed this guide to create two separate ssh keys. I got as far as step 4:

  1. Step1: Generated two ssh keys. A default one and then one where I passed my work username and pw e.g. ssh-keygen -t rsa -C "email@work_mail.com" -f "id_rsa_work_user1"
  2. Added the corresponding keys to the relevant github accounts in gh settings.
  3. Registered the new keys: ssh-add ~/.ssh/id_rsa; ssh-add ~/.ssh/id_rsa_work_user1
  4. Set up an ssh config file per the example

<I have to add this here or my code block below won't format after a numbered list/>

# Personal account, - the default config
Host github.com
   HostName github.com
   User git
   IdentityFile ~/.ssh/id_rsa
   
# Work account-1
Host github.com-work_user1    
   HostName github.com
   User git
   IdentityFile ~/.ssh/id_rsa_work_user1

In the terminal, in any directory, whether home or in one of the paths above, work or personal projects, I get my personal Github details witht he following commands:

git config user.name
git config user.email

If I try to push to a work repo I get a permission denied error. I could, I guess, overwrite each time with e.g. git config user.name "User 1", my work username and then the same for my email. But that seems prone to accident and me forgetting which one is currently set when pushing projects.

What's the 'right' way to handle multiple GH accounts on the same device. Ideally I'd have them set at the repo or directory level. E.g. any repos in ~/Projects/work should use my work gh settings while any in ~/Projects/personal should use my personal settings.

What's the recommended solution? How can I set this up?

Doug Fir
  • 19,971
  • 47
  • 169
  • 299
  • 1
    Either have two different users on the computer (you can local-ssh to the other user when you work with it) or set the `user.name` and `user.email` per repo by not giving the `--global` parameter when setting it. – fredrik May 22 '21 at 19:15
  • @fredrik could you expand on your second option? Perhaps as an answer? How could I do that, set user name and email at the repo level? When you say 'by not setting the global paramter, I don't know where to write or not write that? Is there a way to make this permanent per repo? – Doug Fir May 22 '21 at 19:50

1 Answers1

2

TL;DR

You'll probably want to use both includeIf and carefully-adjusted URLs.

To see how this works (plus various Git version caveats), read through the long section.

Long

We need to separate things into two parts here. First, let's describe it: what program uses what, when, and how. Then let's worry about how to use these conveniently.

  • There's the stuff Git uses when you run git commit. This is what user.name and user.email are for.1

  • Then, independent of any of that, there's the stuff that ssh uses when you run git fetch or git push with an ssh://git@github.com/path/to/repo.git or git@github.com:path/to/repo.git URL.2

You configure Git settings with git config. You configure ssh settings by editing files in $HOME/.ssh/, or wherever your system's ssh, or the auxiliary ssh, puts them.3

Let's now dive into the ssh part.


1Whenever you make a new commit with Git, Git needs to know several things to put into the new commit:

  • Your name, e.g., "A. U. Thor".
  • Your email address, e.g., thor@example.com.
  • The current date-and-time.
  • A log message.
  • A snapshot: the exact contents for every file, to put into the new commit.

Git finds the date-and-time on its own, using your computer's clock. Use whatever software is appropriate to keep your computer's clock correct to make this work right.

Git gets the log message from you. Use whatever you like to provide the right log message.

Git gets the snapshot from whatever is in Git's index. Use git add to adjust Git's index, so that you have the correct snapshot. (This is why you have to git add files all the time, even if they're not new files: it's not that Git isn't going to put them in the commit, it's that Git will put the old version in the file, because when Git checked out a commit earlier, that's the version it got out of the commit, and put in Git's index. The git add command has Git update its index copy of the file.)

The user.name and user.email settings are just for these items: author/committer name, and author/committer email address. You can put anything you want here; Git does not know or care who you are, and does not verify these. (Other software may, eventually, try to figure out who you are, and might care, so it's generally just a loss to you if you fake these, but Git itself won't care.)

2I've written these as git@github.com but you can use just github.com, or something even shorter, depending on how you configure ssh itself. See the section on ssh below. The github.com:path spelling is shorter and has the same effect as ssh://github.com/path, but I prefer the explicit ssh:// myself. They really do mean the same thing to Git—not to ssh, but Git strips this part off before invoking ssh—so you can use whichever you prefer here.

3On systems that come with a working ssh, Git generally just uses the system's ssh. On Windows, however, a lot of older Windows systems have either a defective ssh, or no ssh at all, so a lot of Git-for-Windows installations come with a private ssh implementation as well. You must find out which ssh your system uses, if you are in this situation, and see where to configure it. You may be able to configure your Git to use the system ssh; if you have a modern Windows system, this may be a good idea. I don't use Windows and hence don't have all the details here.


How ssh works, in a nutshell

While ssh has lots of little side jobs to perform, its one big thing is to establish a secure communications stream. To do this, ssh needs to authenticate. For our purposes here, we'll skip over all but the simple public/private-key authentication method, and in particular how GitHub will decide who you are. This lets us ignore a lot of irrelevant-to-us details. (They do matter if you're using ssh more generally, but not if you're fetching from, and pushing to, GitHub.)

What ssh will do is:

  • look up a host name to find an IP address, or take an IP address directly;
  • connect to a server at that IP address, at some numeric port (default 22, which is the standard ssh port);
  • collect a "fingerprint" from the server there, and see if it matches the known fingerprint from the last connection;4
  • if all looks good, send enough information for the other end to figure out who you are: they'll send a challenge, your ssh will send a response, and at the end of all of this, they will have your public key and be reasonably certain that you have the corresponding private key.

In the end, then, they figure out who you are from your public key. They don't believe you just because you have this key: the public key is, after all, available to the public. But this particular public key is paired with a private key, which you don't give out to anyone else. They send a challenge, using your public key that you've already sent to them, and you—or your ssh—answer back, using your private key, and the fact that you can respond indicates that you hold the right private key.

What all this boils down to is:

  • You must give GitHub your public key, using their web interface. This is how they will decide that you are you. If you want to be two people, "you-at-home" and "you-at-work", you need to give them two public keys.

  • Later, when you come in to access some particular repository, you must give GitHub the right public key. You don't give them just any public key: if you want access to "you-at-work" repositories, you have to give them the you-at-work public key (only!).

So: how do we do that? If you have two (or more) public keys, how do we make sure that ssh hands, to GitHub, one particular key? This is where the .ssh/config entries come in. Let's look at this one:

# Work account-1
Host github.com-work_user1    
   HostName github.com
   User git
   IdentityFile ~/.ssh/id_rsa_work_user1

The line Host github.com-work_user1 means that ssh will use this information when you run:

ssh [user@]github.com-work_user1

or equivalent. This means that the URL we have Git use must resemble ssh://github.com-work_user1/path/to/repo.git, not ssh://github.com/path/to/repo.git.

(I personally recommend taking away the .com part here: make this Host gh-work for instance, or maybe Host ghwork.short or similar, to make it visually distinctive. The ssh command will match on this first, before trying to look up any host name to get an IP address.)

The HostName part after this is how ssh will look up the IP address, so here we have github.com so that ssh won't use the fake name that triggers matching this entry.

The User git part lets you omit the git@ part: instead of ssh git@gh-work or whatever, you can just ssh gh-work. Note that if you put in a user name—e.g., ssh error@gh-work—things won't work right here: the supplied user in the command itself overrides the User line in the config.

The last line shown here, IdentityFile, is where things get tricky. It's meant to say: Use the given file (containing the .pub public-key) as the public key. I'll come back to the trickiness in a bit.

There's a missing additional line that I recommend:

IdentitiesOnly yes

This line is not always required, but it means don't use additional public keys. The way I like to think about this is a simple analogy:5 if someone came to your front door and tried one key, and it didn't work, and they left, you'd probably think: Oh, they went to the wrong house. If someone comes to your front door and tries a million keys, one at a time, you might just get a bit suspicious. The IdentitiesOnly line means just try the specific keys I list in this particular entry, not any other keys you might find on some key-ring somewhere.

Now, you mentioned using ssh-add. This communicates with an agent. What I have found, on various systems, is that when using the agent, you sometimes must list the .pub file specifically, and when not using the agent, you sometimes must not list the .pub file, of the public/private key-pair. I'm not going to get into the details here but overall I think this is a bad situation. If possible, always use the agent and always list the .pub file. It is possible that some systems exist where, even when using the agent, you must leave out the .pub here; I don't know of any, but they might exist. Use ssh -Tv <github-alias> to watch ssh offer keys, to see if it is successfully offering the right ones.

If you do use the agent like this, and do not store the private keys on various intermediate hosts, you must ssh-add each key that the agent could offer based on config, before your ssh can actually use it. The agent transfers the private key from the source (whichever host that is) to each intermediate host for as long as it needs it. For more about this, see ssh-specific questions.


4If it doesn't match, you normally get an alarming message since it looks like someone else is pretending to be the people you previously agreed to talk to. If you don't have a known fingerprint for this server, ssh will normally ask you to confirm whether to go ahead and remember this fingerprint. This is all configurable, and to avoid the conversation, you can do an initial connection or use a one-time "scan" (ssh-keyscan) as you set things up. See also this ServerFault Q&A. I don't recommend the StrictHostKeyChecking=no option in the accepted answer for general use: read the comments and the other answers. For very specific cases, where you control everything, it's OK, but GitHub isn't one of those specific cases.

5Like most analogies, it's OK for a way to think about the problem, but may not stand up to scrutiny.


How Git uses ssh

For the most part, this is really very simple. You give Git a URL:

git remote add xyzzy https://example.com/path/to/repo1
git remote add plugh ssh://user@example.com/path/to/repo2

for instance. Each URL is stored under a remote, in this setup; the remote is the "short name" and otherwise just means the long URL.

When the URL is an ssh URL, Git splits it into the user@host or host part, and the path/to/repo part. Let's use the plugh remote as our example here. Then it runs:

ssh user@example.com git-upload-pack path/to/repo2

when you run git fetch plugh. The git-upload-pack command6 implements the server side work that lets your client do a git fetch. A similar command, git-receive-pack, runs on the server to let your client do a git push.

When you connect to GitHub, the authentication step they use figures out who you are using the public key you send. Then they look up the "who you are" that they figured out, and decide whether that "you" has access to the repository given in the upload or receive pack command (and they also make sure that you only use legitimate commands, of course). Note that Git has no work to do here; nothing you set in Git can affect this. All of the work happens in ssh and on the server, and it's all based on the public key you have your ssh send to the server.

Again, the way to test what Git is going to do is to take Git out of the picture, and run ssh -Tv host-alias. Add more v letters to make your particular ssh command even more verbose, to track what it's doing, step by step. Make sure it's sending the desired public key. If GitHub have that public key, and if your ssh then passes the challenge/response part, you'll see a message of the form:

Hi _____! You've successfully authenticated, but GitHub does not
provide shell access.

with the blank here filled in with the person they think you are, based on the public key you sent.


6At some point long ago, this was vaguely intended to be changed to git upload-pack instead of git-upload-pack. Recent versions of Git stopped installing git-foo for every git foo and there was a brief period during which git-upload-pack could be removed without being reinstalled, during an upgrade. If you ever see a problem of this form, it means someone on the server forgot to put the git-upload-pack link back and your Git client is still using the old git-upload-pack form. In any case, some clients may now be using git upload-pack now.


Configuring Git, part 1

Now that you have Git connecting to GitHub as the right person—or before that point, if you like—it's time to figure out how to get the right name and email address into new commits you will make. That is, we now want to make sure that Git gets the right user.name and user.email.

If you only have one name and email address to use, this is easy: use the --global setting. If you have more than one, well, there are many options here.

Git's configuration system is very flexible, and supports three or more "levels" of configuration (the exact number depends on how you want to count these):

  • There's a system configuration file. Often this file simply doesn't exist, i.e., there's no system-wide configurations set. If it does exist, only the system administrator has permission to modify it. So you wouldn't put your own settings here anyway.

  • There is a per-user configuration file. When you log in to your own computer, you're picking a user to use. Even if your computer has only one user, modern OSes still have "administrator" or "sysadm" or "root" or something as a concept, so this distinguishes you from "stuff owned by the system". If your system is designed correctly, this lets you upgrade the system without messing with your own settings and stuff. Git's per-user configuration file is the --global one, and you can edit it directly if you like, with git config --global --edit. Note that this will run your chosen editor—usually from your core.editor setting—on that file. If you haven't chosen some editor yet, you'll get the system choice or the built-in default. One way to fix this is to run git config --global core.editor name, for whatever editor you want to use, first.

  • Then, there is a per-repository configuration. This has the same format and use as the global configuration, but anything you set for any given repository are only for that repository. Note that git config --edit, with no other option, edits this per-repository configuration; git config user.name Me sets the setting in the per-repository configuration.

  • In modern Git, there is a per-worktree configuration. Use git config --worktree to work with this configuration. This is only used in added work-trees and it's almost always wrong, or at least fishy, to set something here (Git uses this configuration for its own internal purposes).

  • You can also use git config --file *filename to point git config at a particular file. This is mainly meant for Git's submodule stuff, but can be used for whatever purpose you like.

  • If nothing is configured at all for some setting, Git has whatever default it has built in, if any. For instance, if core.editor is not set (and none of the environment variables is set either7), Git always has a compiled-in default editor such as vim or nano.

  • Finally, you can override the setting for one command using git -c variable=value, e.g., git -c user.name="Scary Spice" -c user.email=melb@virgin-records.com commit.

The way this is intended to be used is straightforward, but not simple: you can configure each setting in every configuration file, and the "most local" setting will override. That is, you could set user.name to Anonymous in the system configuration, User in the per-user configuration, and Me in one specific repository. A new commit made by you, in that repository, will have as its author and committer, Me <email-address>, because the per-repository user.name is Me. The email address part will come from the user.email setting.

For user.name and user.email specifically, there are some extra complications:

  • If they're not set at all, your Git may gripe and quit (not make a new commit). Or, it may use OS-level facilities to guess or find your name and/or email address. Whether and how well it can do this depends on the Git version and on your OS, and several Git versions have a bug here where they can crash while trying to do this guessing.

  • Another setting, user.useConfigOnly, will suppress this guessing, so you can use git config --global user.useConfigOnly true to avoid having the guessing happen.

By defeating the guessing, you can make sure that if you forgot to configure them, Git won't make commits with the wrong name and email address. So you might want to do this, depending on how you want to set everything else up.

With this in mind, you have a couple of options:

  • You can git config --global user.useConfigOnly true and force yourself to configure user.name and user.email locally for each repository. Note, however, that this particular feature was new in Git version 2.8; older Git versions will ignore the setting (though they may still force you to configure these, depending on their other defaults).

  • Then you just have to remember to git config the user.name and user.email settings in every repository (ugh).

  • Or, you can git config --global the settings you want to use most of the time, and just have to remember to git config the user.name and user.email settings in each non-default repository. That's still kind of "ugh", but perhaps better.

With Git 2.13, however, there's a perhaps-still-better way.


7Partly for historical reasons, Git will choose the editor to run based on the first of:

  • $GIT_EDITOR
  • core.editor
  • $VISUAL
  • $EDITOR
  • compiled-in default

The $-prefixed names here are environment variables, which sh/bash style command line interpreters set with simple assignments:

$ GIT_EDITOR=emacs git config --global --edit

will run emacs on the global Git configuration file, for instance.

You can also use:

$ git -c core.editor=emacs git config --global --edit

which requires a bit more typing, but might be easier to remember: it has no special cases. (To remember the special cases, see the git var documentation.)


Configuring Git, part 2: using includeIf

Git 2.13 learned a new directive in configuration files, spelled includeIf (you can use all-lowercase includeif, if you prefer, but the camelCase spelling is a nominal standard here). The includeIf directive can test certain conditions, and an interesting one here is gitdir.

Here, gitdir stands in for the full path name to the repository you're working in. In your case, you have put each project under some appropriate prefix:

~/Projects/work
~/Projects/personal

This means that if the path to some repository is ~/Projects/work/foo/bar/.git, this must be a work project. If it's ~/Projects/personal/baz/quux/.git, it must be a personal project.

If you want all work projects to use a particular user.name and user.email configuration, you'll want, e.g.:

[includeIf "gitdir:~/Projects/work/"]
    path = ~/.git-work

and then you'll want to create a git config format file in ~/.git-work in which those settings are set. You can use git config --file ~/.git-work user.name work-name, for instance, to adjust the work setting, or run git config --edit --file ~/.git-work to run your favorite editor on that file.

Likewise, you can add an includeIf for personal projects, or you can just set user.name and user.email before doing the includeIf part (because for these settings, later values override earlier ones).

Be careful with whitespace here (see Git includeIf for personal and work profiles doesn't work for an example where whitespace broke the intended use). Be aware, too, that the way the gitdir: matching works is a little complicated. See the git config documentation for complete details, but in short, the gitdir:~/path/ notation "means" do the include if the .git directory has a glob match against ~/path/** after doing tilde expansion.

Conclusion

You'll want to use Git configurations to set user.name and user.email appropriately for when you make commits. This has no effect on push and fetch.

You'll want to control the URL for when you run push and fetch, so that Git passes the desired fake host name to ssh, so that ssh can supply github.com and the correct public key. I did not mention it above, but Git configuration items of the form url.base.insteadOf can be used to rewrite the URLs. You can, in theory, combine this with the includeIf trick so that you don't have to fake out the URLs. I have not tested this myself, and I imagine the debugging could be ... "interesting".

torek
  • 448,244
  • 59
  • 642
  • 775