📦 Multiple Git Configurations and Identities with Folder-Dependent Includes for GitLab, GitHub et al#

People, shout out to some of my friends and collegues, like to configure their development environments in a myriad of ways. Even setting one’s Git author identity, a seemingly boring and mundane task, is sometimes approached in the most creative ways. This, of course, includes not only configuring git, the tool itself, but also any relevant repositories as well as the platforms where those eventually end up hosted. In this brief tutorial I show how to properly handle multiple identities and configurations as well as how to manage Git projects which might be spread throughout multiple Git backends.

Please beware this tutorial will likely only be relevant or interesting to you if you already have some experience with Git, otherwise this sort of setup may feel like unnecessary or overly complicated. In case you are new to Git, I highly recommend you go through Git for Beginners: Zero to Hero 🐙 and also have a look at Git Cheatsheet: Commands, Tips and Tricks 📝.

Addendum#

Once you’re done reading this tutorial, you can continue your git journey by having a look at the related content I’ve linked below.

Discussion#

This article was discussed on Reddit and Tildes.net

Feel free to let me know in case you think I ought to include any other conversations related to this publication.

Introduction#

Some people, I myself do so sporadically, like to painstakingly configure Git repositories manually and individually; including setting different identities via git config user.name Jayson and git config user.email noreply@jdsalaro.com. Others prefer to have one .gitconfig to rule them all under ~/.gitconfig and be done with it; which for reasons that will become apparent stoped working for me a long time ago.

What’s more, some people prefer to use open-source Git hosting and management platforms such as GitLab and Gitea, while others prefer proprietary ones like GitHub or BitBucket. There’s also some who, like me, have projects on several such platforms, some private coporate instances and even some “headless” Git servers.

If you have ever been in charge of managing or even contributing to repositories involving more than a couple of participants and multiple Git “backends”, you will recognize these scenarios as a recipe for problems at best and extremely inefficient workflows at worst. Add to this the fact that there might be quirks and conventions which depend on every Operating System, Git hosting and management platform, repository or each individual’s preferences and you will end up having a really hard time as a maintainer or user.

Git’s CLI is already verbose, intrusive and terrible enough, so much so that I have shortcuts for pretty much everything as per Wrist-friendly Git Shortcuts and Aliases #️⃣. Any of the circumstances mentioned above are only posed to complicate matters further. Also, if it’s Git and source code version control adoption that we’re after, there’s no better way to hinder that goal than forcing repetitive tasks onto Git newbies and busy professionals alike.

Thus, I often get questions about whether it’s possible to setup Git to use multiple configurations depending on different conditions. This desire usually stems from the fact that both having a global ~/.gitconfig as well as configuring each and every new repository one clones and engages with are far from optimal solutions.

What Are Git Committer Identities?#

Usually, the most common request is, at first, to have Git configurations that are specific to GitLab and GitHub in order for commits and other actions on said platforms to be properly, attributed, tied, to each user’s identity despite using different, possibly private and hence platform-specific, committer email addresses on the different Git backends [1] [2].

commit 83c74b2643e05833cde547df13fd578431b3471c
Author: Jayson Salazar Rodriguez <noreply@jdsalaro.com>
Date:   Mon Nov 6 11:43:35 2023 +0100

    modified:   git-different-gitconfig-conditional-includes.md

You might wonder what a committer’s email has to do with anything and how it is related to a user’s identity on GitLab, GitHub, BitBucket’s et al. The simple explanation is that, yes, as you would expect those platforms authenticate you by relying on your username-password combination, token, key pair, etc. Information about these authentication mechanisms, however, is not present at all in Git’s history and cannot, nor should, be embedded in commits within as to verify them. Therefore, each of those platforms uses the information found in each commit itself in order to somehow, in a best-effort kind of way, attribute it to a user and display their avatar, change their contribution heatmap, gather statistics, etc.

Therefore, regardless of whether the repository holding commit 83c74b2643e05833cd, shown above, is hosted on GitLab, GitHub, BitBucket, etc, noreply@jdsalaro.com will be used to find a user on the platform hosting said repository. Should I have an account with said email address on all platforms, then everything will work as expected and this article would end here. In case I don’t or I fake it, on purpose or inadvertently, I might end up committing changes as Linus Torvalds and my repository will have a very unexpected commit history:

../../_images/linus-fake-commit.png

A Commit with Spoofed Identity#

As depicted below, this issue is as old as sliced bread[3], but it is expected and should not, generally speaking, be a problem:

../../_images/hackernews-linus-did-not-commit-this.png

A Discussion of the Commit Shown Above on HackerNews#

For security minded Git users, there’s always the possibility to cryptographically sign your commits[4].

A Realistic Use Case and further Scenarios#

Going back to how one can manage multiple Git configurations and do so in a somewhat comfortable way, let’s consider the following ~/.gitconfig; which as the path implies is a global Git configuration:

[user]
    name = global
    email = global.noreply@jdsalaro.com

Should I have global.noreply@jdsalaro.com set up as committer email on both GitLab and GitHub, there’ll be no problems. However, and as is often the case, let’s imagine we have a user who wants to use the noreply private committer email addresses automatically generated by GitLab and GitHub which look like *@users.noreply.gitlab.com and *@users.noreply.github.com respectively. Here’s where the trouble starts. GitLab uses *@users.noreply.gitlab.com to recognize commits by our user while GitHub uses *@users.noreply.github.com. The global ~/.gitconfig approach is of no use here.

Let’s complicate matters further, let’s assume our user not only has an account and repository on both GitLab and GitHub, but also on a university or corporate Git backend instance. Since this scenario is more similar to my own, I’d personally organize my repositories as follows:


user@system:/tmp$ tree jdsalaro/

jdsalaro/
│
├── gitlab
│    └── repo
│
├── gitlab-university
│    └── repo
│
└── github
    └── repo

Now, both of the common solutions mentioned above are insufficient in this scenario. Use a global ~/.gitconfig and you sooner or later end up having commits with incorrect committer email addresses in them. If you use git config --global user.email "user@noreply.gitlab.com", all your commits to gitlab-university/repo and github/repo would end up having wrong identities in the commits.

Yes, you could use git config user.email "REPLACE" to manually configure each of the repositories for which the global configuration doesn’t hold, but, again, who wants that? Also, I guarantee you will forget at some point, back to square one.

For me GitLab and GitHub are easy usecases, I could simply use noreply@jdsalaro.com for both or a similar address for the sole purpose of comitting code to said platforms. However, I might simply want to associate commits on GitLab and GitHub with gitlab@jdsalaro.com and github@jdsalaro.com respectively, nothing wrong with that. What about gitlab-university, though? It might not have noreply.myuniversity.edu private addresses properly configured or I might want my commits on that private instance to be associated with my employee or student email. Also, what about users who, as mentioned above, want to use private platform-dependent committer identities whenever they are available? The list of corner cases goes on and on; and we’re only discussing identities without signing, merging strategies and more.

Solving the Problem at the Root#

Here is where Git’s conditionally included[5] configurations can greatly simplify our lives! They offer us a way to include Git configuration files depending on certain conditions. Conditional includes can be used in your global .gitconfig file by using includeIf configuration sections to list your desired conditions and configuration files which should be included.

The snippet shown below presents a global .gitconfig which will have a default user identity global <global.noreply@jdsalaro.com>. However, for repositories under /tmp/jdsalaro/gitlab/, /tmp/jdsalaro/github and /tmp/jdsalaro/gitlab-university, the respective gitconfig file will also be considered and will override ~/.gitconfig when applicable.

📦 The .gitconfig used here as well as a script to generate and configure the exemplary directory hierarchy shown previously are available on GitHub and also on GitLab in case you prefer bookmarks. The same files can be downloaded directly here and here.

# configure the user's global identity

[user]
    name = global
    email = global.noreply@jdsalaro.com

# include this gitconfig only if the repository exists within
# the /tmp/jdsalaro/gitlab/ directory

[includeIf "gitdir:/tmp/jdsalaro/gitlab/**"]
    path = /tmp/jdsalaro/gitlab/gitconfig

# include this gitconfig only if the repository exists within
# the /tmp/jdsalaro/gitlab-university/ directory

[includeIf "gitdir:/tmp/jdsalaro/gitlab-university/"]
    path = /tmp/jdsalaro/gitlab-university/gitconfig

#   path = ~/.gitconfig.gitlab 
#   the location of the configuration
#   file can be arbitrary. Note that in this case, it is located
#   in the user's home directory.

#   unlike global and local .gitconfig files which must respect
#   git's naming expectations, note this is not the case with
#   path directives in conditional include sections:

#   path = /tmp/jdsalaro/gitlab-university/gitconfig
#   path = /tmp/jdsalaro/gitlab-university/.gitconfig

# include this gitconfig only if the repository exists within
# the /tmp/jdsalaro/github/ directory

[includeIf "gitdir:/tmp/jdsalaro/github/"]
    path = /tmp/jdsalaro/github/gitconfig

Although it hopefully is self-evident, I will paraphrase what the snippet above does. It first configures, as is commonly known and done, the user’s global identity. This identity will be their default one whenever no includeIf section can be found which matches a given repository. Then, we come to the interesting sections. In the three following includeIf sections we define three paths where git is told to search folder-specific Git configurations and apply them to the repositories that match the respective condition.

For example, due to [includeIf "gitdir:/tmp/jdsalaro/gitlab/**"] any repository which we clone inside of /tmp/jdsalaro/gitlab/ will be configured according to the global and local .gitconfig files as well as path = /tmp/jdsalaro/gitlab/gitconfig. Of course, since the user’s identity is defined both globally and in the conditionally included Git configuration the former’s user section will be overriden by the latter.

Go ahead and use script.out to generate the test directory and repository hierarchy so you can experiment yourself:

user@system:/tmp$ bash jdsalaro-folder-dependent-git-config-setup-script.out 
.
├── github
│   ├── gitconfig
│   └── repo
├── gitlab
│   ├── gitconfig
│   └── repo
└── gitlab-university
    ├── gitconfig
    └── repo

6 directories, 5 files

Note that, as expected, each gitconfig file contains a sample identity which will override the global one and will be echoed back to us, thus allowing us to verify everything is working properly.

user@system:/tmp/jdsalaro$ cat */gitconfig
...
[user]
	name = github
	email = github.noreply@jdsalaro.com
...

[user]
	name = gitlab
	email = gitlab.noreply@jdsalaro.com
...

[user]
	name = gitlab-university
	email = gitlab-university.noreply@jdsalaro.com

Testing Everything#

As noted previously, I usually group my cloned repositories per platform and corporate instance on my file system:

user@system:/tmp/jdsalaro$ ls

github/
gitlab/
gitlab-university/

First and foremost, make sure your /tmp/jdsalaro directory looks exactly the same as above. This is given if you used script.out.

Now, modify your global ~/.gitconfig, which my included setup script doesn’t touch or modify, so it contains the snippet listed previously as shown below:

user@system:/tmp/jdsalaro$ cat ~/.gitconfig

[user]
    name = global
    email = global.noreply@jdsalaro.com

[includeIf "gitdir:/tmp/jdsalaro/gitlab/**"]
    path = /tmp/jdsalaro/gitlab/gitconfig

[includeIf "gitdir:/tmp/jdsalaro/gitlab-university/"]
    path = /tmp/jdsalaro/gitlab-university/gitconfig
#   path = ~/.gitconfig.gitlab
#   path = /tmp/jdsalaro/gitlab-university/gitconfig
#   path = /tmp/jdsalaro/gitlab-university/.gitconfig

[includeIf "gitdir:/tmp/jdsalaro/github/"]
    path = /tmp/jdsalaro/github/gitconfig

This is all we need in order to verify that, as desired, the global identity is not overriden by the specified paths while we are inside of /tmp/jdsalaro:

user@system:/tmp/jdsalaro$ git config --get user.email

global.noreply@jdsalaro.com

Nor when we are within /tmp/jdsalaro/github or other directories we have neither configured nor are valid git repositories:

user@system:/tmp/jdsalaro$ cd github/
user@system:/tmp/jdsalaro/github$ git config --get user.email

global.noreply@jdsalaro.com

However, the moment we go into one of the repositories located within /tmp/jdsalaro/github, /tmp/jdsalaro/gitlab or /tmp/jdsalaro/gitlab-university, everything falls into place:


user@system:/tmp/jdsalaro/github$ cd repo/
user@system:/tmp/jdsalaro/github/repo$ git config --get user.email

github.noreply@jdsalaro.com
user@system:/tmp/jdsalaro/github/repo$ cd ../../gitlab/repo/
user@system:/tmp/jdsalaro/gitlab/repo$ git config --get user.email

gitlab.noreply@jdsalaro.com
user@system:/tmp/jdsalaro/gitlab-university/repo$ cd ../../gitlab-university/repo
user@system:/tmp/jdsalaro/gitlab-university/repo$ git config --get user.email

gitlab-university.noreply@jdsalaro.com

That brings us to the end, of this tutorial, but it will certainly only be the beginning of your Git customization journey. Do you want to use different merge strategies depending on repositories, their remote URLs, the folder they have been cloned into, etc? You’re right, this is the basis for achieving that. Do you want to use backend-, folder- or even repository specific key pairs and algorithms for signing your commits? Yep, you can also use conditionally included Git configurations to achieve that!

Final Words#

That’s it, hopefully you enjoyed this tutorial and will find it useful!

Stay in touch by entering your email below and receive updates whenever I post something new:

As always, thank you for reading and remember that feedback is welcome and appreciated; you may contact me via email or social media. Let me know if there's anything else you'd like to know, something you'd like to have corrected, translated, added or clarified further.

Footnotes#