Your Git Config Is Just A File

Config commands look like a database API and turn out to be a text file with sections in brackets. Which explains the one behaviour that surprises everybody.

July 25, 20265 min read1 / 6

Walking objects by hand took the mystery out of commits. Config gets the same treatment, and it takes about a minute.

We covered the two keys you need to commit earlier. This is the rest of it, and the reason it is worth a second pass is that config has one genuinely surprising behaviour that will bite you eventually.

Adding Values

The command is --add, and the key is always section.key:

Bash
git config --add team.lead "Denver" git config --add team.reviewer "James"

Quote the values. You may not always need to, but a value with a space in it will fall apart without quotes, and forming the habit costs nothing.

Reading Them Back

Two ways, and I will be honest about which one I actually use.

Bash
git config --list

That dumps everything. To find one section in it:

Bash
git config --get-regexp team

That is the proper way. It takes a regular expression, so t.am would also match, along with anything else that happens to fit the pattern. Regular expressions being over-eager is the usual reason a search returns more than you wanted.

The other way is to give up and pipe it:

Bash
git config --list | grep team

Not elegant. Completely fine. Nobody is grading your shell usage, and the first time you run --list | grep on a config you have not touched in three years, you will probably find some stale entries you forgot about.

The Surprise

Now change a value. You know --add, --list, and --get, so use those.

Bash
git config --add team.lead "Neal" git config --get-regexp team

Two team.lead entries. Both still there.

--add means add. It does not mean set. And there is no --set, which is genuinely irritating once you notice it: git gives you --add and --unset as a pair, and the obvious middle option simply does not exist.

So duplicate keys are not an error state. They are a thing git allows, and you will create them by accident. It is entirely possible to have three usernames sitting in your config right now.

Three config commands on the left writing into a .git/config file on the right, showing that adding team.lead twice produces two separate lines, plus what --get, --get-all, --unset, --unset-all, and --remove-section each do about it. ExpandThree config commands on the left writing into a .git/config file on the right, showing that adding team.lead twice produces two separate lines, plus what --get, --get-all, --unset, --unset-all, and --remove-section each do about it.

Reading a duplicated key gives you one answer:

Bash
git config --get team.lead

You get Neal, the last one written. Later values win, which is the only sensible rule. To see all of them:

Bash
git config --get-all team.lead

Removing Them Is Fussier Than You Expect

Try the obvious thing:

Bash
git config --unset team.lead

Git refuses. team.lead has multiple values, and git will not guess which one you meant.

List it again and both are still there. The failed unset changed nothing, which is the right call but does mean you have to be explicit:

Bash
git config --unset-all team.lead

Now they are both gone. And to remove an entire namespace:

Bash
git config --remove-section team

There are many more options than this, including unsetting by index. You will probably configure git about three times in your life, so knowing that the options exist matters more than memorising them. The manual has the rest.

It Is a File

Here is the payoff, and it is the same payoff as every other post in this series.

Bash
cat .git/config

There it is. Sections in square brackets, keys indented underneath, values after an equals sign. It looks a lot like TOML, and it is exactly as unremarkable as it looks.

Your config commands are editing a text file. Every --add appends a line. --remove-section deletes a block. That is why duplicate keys are possible in the first place: nothing was ever stopping two lines from having the same name.

Run --remove-section and then cat the file again, and you can watch the block disappear. The magic is gone, and what is left is disappointingly simple, which is the best possible outcome.

Locations

Config exists at several levels: system, global, local, worktree, and you can point at an arbitrary file too.

In practice you will use two.

  • --global applies to every repository on your machine
  • --local applies to the current repository only, and is the default when you omit a flag

Add the same key to both and watch what happens:

Bash
git config --add --global team.lead "Denver" git config --add --local team.lead "Peter"

Both exist. You can see each one on its own:

Bash
git config --list --local

But ask for the value and there is only one answer:

Bash
git config --get team.lead

Local wins. The more specific layer always does, which is the layering from the earlier config post working exactly as advertised.

Rename the Default Branch

Time to actually change something about how git behaves for you.

Bash
git config --add --global init.defaultBranch trunk

This has to be global. A local setting cannot affect repositories that do not exist yet, and the whole point is that every new repository gets the name.

Now make the repository the next several posts will use. The name matters here, because everything that follows builds on it:

Bash
mkdir hello-git cd hello-git git init

The branch is called trunk.

A warning that is only half a joke: you have now changed this permanently. Months from now you will create a repository, see trunk instead of master, and have no memory of why. Now you have a note.

What This Sets Up

You have a fresh repository on a branch called trunk and no commits in it yet.

Next: branches are just a file with a SHA in it, which explains why creating one appears to do nothing at all.

The Essentials

  1. --add adds, it does not replace, and there is no --set.
  2. Duplicate keys are legal, and you will create them by accident.
  3. --get returns the last value, --get-all returns every one.
  4. --unset refuses when a key has multiple values. Use --unset-all, or --remove-section for a whole block.
  5. The config is a plain text file with sections in brackets, which is why all of the above is possible.
  6. Global and local are the two levels you will actually use, and local wins.
  7. init.defaultBranch must be global to affect repositories you have not made yet.

Further Reading and Watching