Git is an enormously configurable tool: author name and e-mail, text editor, behaviour when integrating changes, output colours, shortcuts, credentials and several hundred more options. All of it is managed with a single command, git config, and stored in plain text files with a simple format.
This lesson is about the mechanism: where the configuration lives, what levels exist, who wins when two levels disagree, how to read, write and delete values, and how to keep separate profiles for work and for personal projects. We deliberately will not list yet which values are worth setting the first time round: that is the content of the next lesson, Initial Configuration. Here we learn to handle the controls; next, which buttons to press.
Ana needs this right now: the laptop she uses for her company's work is the same one she uses for task-manager, and she wants each project to carry the right e-mail without having to remember every time.
Contents
- What
git configis - The three configuration levels
- Precedence: who wins
- Reading values
- Writing values
- Deleting values
- Inside the
.gitconfigfile - Direct editing with
--edit - Conditional configuration with
includeIf - Special cases: multiple values and types
- What
git config is
git config isgit config is the command that reads and writes Git's configuration. Its basic form is:
A concrete example, without going into what the value means yet:
Breaking down each part:
git config→ the command.--global→ the level: which file gets written. If omitted, the local level is used.core.editor→ the key, made up of a section (core) and a name (editor), separated by a dot."nano"→ the value. Always quote it whenever it contains spaces or special characters.
It matters that you understand that git config does not validate keys. If you write core.editorr with two r's, Git will accept it without complaint and the option will simply have no effect whatsoever. It is the number one cause of configurations that "don't work".
A note on versions. Since Git 2.46 there is a more explicit alternative syntax:
git config get,git config set,git config unsetandgit config list. It is equivalent to the classic form and somewhat more readable. In this course we will use the classic syntax with options (--get,--unset,--list) because it works on every version and it is what you will find in existing documentation.
- The three configuration levels
Git reads its configuration from several files, organised into levels of decreasing scope. The three main ones are:
| Level | Option | Scope | File path |
|---|---|---|---|
| System | --system |
Every user on the machine | Linux: /etc/gitconfigmacOS (Homebrew): /opt/homebrew/etc/gitconfigWindows: C:\Program Files\Git\etc\gitconfig |
| Global | --global |
Every repository belonging to your user | Linux/macOS: ~/.gitconfig or ~/.config/git/configWindows: C:\Users\<user>\.gitconfig |
| Local | --local |
The current repository only | <repository>/.git/config |
There are also two less common levels worth knowing about:
| Level | Option | Scope |
|---|---|---|
| Worktree | --worktree |
The current working copy only, when several are in use (module 6) |
| Command | -c key=value |
That one command's execution only |
The command level is very handy for one-off tests, because it leaves no trace:
Notice that -c goes before the subcommand, not after: it is an option of git, not of commit.
When to use each level
| Level | Use it for |
|---|---|
| System | Machine-wide policy; in practice administrators touch it, almost never you |
| Global | Your identity and your personal preferences: editor, colours, shortcuts |
| Local | Whatever is specific to a project: a different e-mail, its own line-ending settings |
The general rule: always start with --global. Save --local for the genuine exceptions of one particular project.
- Precedence: who wins
When several levels define the same key, the most specific one wins. The order, from lowest to highest priority:
graph TD
S["/etc/gitconfig<br/>--system<br/>lowest priority"] --> G["~/.gitconfig<br/>--global"]
G --> L[".git/config<br/>--local"]
L --> W[".git/config.worktree<br/>--worktree"]
W --> C["git -c key=value<br/>highest priority"]
A concrete example on Ana's laptop:
| File | Value of user.email |
|---|---|
/etc/gitconfig |
(not defined) |
~/.gitconfig |
ana.ferrer@company.com |
task-manager/.git/config |
ana.ferrer@personal.example |
Result: inside the task-manager repository, Git will use ana.ferrer@personal.example. In any other repository on Ana's laptop, ana.ferrer@company.com.
This is exactly the tool Ana needed: the global configuration covers the majority case (her job) and the local one handles the exception (her personal project). In section 9 we will see how to automate it so she never has to remember in each new repository.
- Reading values
A key's effective value
This returns the value Git would use right now, with precedence already resolved. It also works without --get, which is the shorter and more common form:
If the key is not defined, the command prints nothing and returns a non-zero exit code. To supply a default when it does not exist:
Reading one specific level
Combining --get with a level queries one specific file, ignoring the rest:
git config --global --get user.email # the global file only
git config --local --get user.email # the repository's file only
git config --system --get user.email # the system file onlyThis is essential for debugging: if the effective value is not what you expected, checking level by level reveals where it comes from.
Listing the whole configuration
This prints every active key, one per line, as key=value:
user.name=Ana Ferrer user.email=ana.ferrer@company.com core.editor=nano init.defaultbranch=main color.ui=auto
And now the most useful command in the whole lesson:
It prefixes each line with the file it came from:
file:/etc/gitconfig core.autocrlf=input file:/home/ana/.gitconfig user.name=Ana Ferrer file:/home/ana/.gitconfig user.email=ana.ferrer@company.com file:/home/ana/.gitconfig core.editor=nano file:.git/config user.email=ana.ferrer@personal.example file:.git/config remote.origin.url=git@git.example.com:team/task-manager.git
Look at the last appearance of user.email: when a key shows up several times, the last line is the one that wins, because the files are processed from least to most specific. Here the effective value is ana.ferrer@personal.example.
An even more informative variant:
This adds the level's name as well (system, global, local), which saves you from working it out from the path.
| Command | What it shows |
|---|---|
git config <key> |
The effective value |
git config --global <key> |
The value at one specific level |
git config --list |
Every active key |
git config --list --show-origin |
Every key and the file it came from |
git config --list --show-scope |
Every key and its level |
git config --get-regexp <pattern> |
Only the keys matching a regular expression |
That last one is very practical when you are looking for something and cannot remember the exact name:
# Every key in the user section
git config --get-regexp '^user\.'
# Everything to do with aliases
git config --get-regexp '^alias\.'
- Writing values
We already know the basic form:
Points to keep in mind:
- If the key exists, it is overwritten. There is no confirmation and no warning.
- Quotes are required when the value contains spaces.
git config --global user.name Ana Ferrerwould fail, because Git would readFerreras an extra argument. - Keys are case-insensitive in the section and the name (
init.defaultBranchandinit.defaultbranchare the same key), but values are not. - If you give no level,
--localis used, which requires being inside a repository. Outside one, Git raises the errorfatal: not in a git directory.
That last point deserves an example, because it is a classic stumble:
# Inside ~/projects/task-manager: writes to .git/config
git config user.email "ana.ferrer@personal.example"
# On the desktop, outside any repository: ERROR
git config user.email "ana.ferrer@personal.example"
# fatal: not in a git directoryGet into the habit of always stating the level explicitly. It saves confusion that is hard to diagnose.
Writing at the system level
This requires administrator privileges, because the file sits outside your home folder:
On Windows you have to open Git Bash or PowerShell as administrator.
- Deleting values
To remove a key, use --unset:
If the key appears several times in the same file (which is possible for certain keys, as we will see in section 10), --unset will fail with the message warning: <key> has multiple values. In that case use --unset-all:
To remove an entire section:
| Command | Effect |
|---|---|
--unset <key> |
Deletes a key with a single value |
--unset-all <key> |
Deletes every appearance of the key |
--remove-section <section> |
Deletes the whole section |
One important nuance: deleting a key from one level does not leave it "with no value", it lets the level below win. If Ana deletes user.email from task-manager's .git/config, the e-mail becomes the one in her ~/.gitconfig, not nothing at all.
And a warning: --unset with no explicit level operates on the local file. If you meant to clean up the global one and forget --global, you will delete something you did not intend to.
- Inside the
.gitconfig file
.gitconfig fileEvery Git configuration file uses the same format: INI, plain text, readable and editable by hand. This could be Ana's ~/.gitconfig:
[user]
name = Ana Ferrer
email = ana.ferrer@company.com
[core]
editor = nano
autocrlf = input
[init]
defaultBranch = main
[color]
ui = auto
[alias]
st = status
co = checkout
last = log -1 HEAD
[pull]
rebase = falseThe rules of the format:
- Sections go in square brackets:
[user],[core]. They correspond to the part before the dot in the key. - Keys are written as
name = value, indented with a tab by convention (Git writes a tab; spaces work too). - Comments begin with
#or;. - Values with spaces need no quotes inside the file, unless you want to preserve leading or trailing spaces.
The mapping between command and file is direct:
| Command | Result in the file |
|---|---|
git config --global user.name "Ana Ferrer" |
[user] → name = Ana Ferrer |
git config --global alias.st status |
[alias] → st = status |
git config --global color.ui auto |
[color] → ui = auto |
Subsections
Some sections take one more level, in quotes. You see them mostly on remotes and branches:
[remote "origin"]
url = git@git.example.com:team/task-manager.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/mainOn the command line, the subsection goes in the middle: remote.origin.url, branch.main.remote. Git writes these entries automatically when you add a remote or set up a branch's tracking, both matters for module 4. You will rarely write them by hand, but being able to read them helps enormously when diagnosing problems.
Important: unlike keys, subsections are case-sensitive.
[remote "Origin"]and[remote "origin"]are two different remotes.
- Direct editing with
--edit
--editFor changes spanning several lines, editing the file directly is far more comfortable than stringing commands together:
This opens ~/.gitconfig in the editor set in core.editor (or in whichever the EDITOR environment variable names). Save and close, and the changes take effect immediately: Git reads the configuration on every run, it does not cache it.
The three variants:
git config --system --edit # the system file (requires permissions)
git config --global --edit # your personal file
git config --local --edit # the current repository's fileYou can also open the file with any editor, bypassing Git:
That is just as valid. The advantage of --edit is that you do not need to remember the path, which varies between systems.
Careful: if you write a configuration file with a syntax error (an unclosed bracket, say), Git will complain on every command you run from that moment on:
The fix is to reopen the file and correct the line it names. It is reversible and it damages no repository, but it is disconcerting the first time.
- Conditional configuration with
includeIf
includeIfWe come now to the most elegant feature of the configuration system, and the one that solves Ana's problem for good.
The problem
Ana uses the same laptop for two contexts:
- Her company's projects, which must be signed with
ana.ferrer@company.com. - Personal projects such as
task-manager, which must go out withana.ferrer@personal.example.
She can handle it with local configuration repository by repository, but that means remembering every time she clones or creates one. Sooner or later she will forget and make commits with the wrong e-mail, something only a history rewrite can fix.
The solution: including configuration based on the path
Git lets you include another configuration file only when a condition holds. The most useful condition is the repository's path.
Ana organises her folders like this:
/home/ana/
├── work/ ← the company's repositories
│ └── client-portal/
└── personal/ ← her own projects
└── task-manager/And she edits her ~/.gitconfig:
[user]
name = Ana Ferrer
email = ana.ferrer@company.com
[core]
editor = nano
[init]
defaultBranch = main
# If the repository sits under ~/personal/, include this other file
[includeIf "gitdir:~/personal/"]
path = ~/.gitconfig-personalAnd she creates the file ~/.gitconfig-personal:
How it works, step by step:
- Git reads
~/.gitconfigfrom top to bottom and setsuser.email = ana.ferrer@company.com. - On reaching
includeIf, it checks whether the current repository sits inside~/personal/. - If it does, it reads
~/.gitconfig-personalat that point, and itsuser.emailoverwrites the previous one. - If it does not, the line is ignored and the company e-mail stands.
Result: Ana never has to remember again. She only has to keep each project in the right folder.
Checking it:
# Inside ~/personal/task-manager
git config --get user.email
# → ana.ferrer@personal.example
# Inside ~/work/client-portal
git config --get user.email
# → ana.ferrer@company.comThe conditions available
| Condition | Holds when |
|---|---|
gitdir:<path> |
The repository sits under that path (case-sensitive) |
gitdir/i:<path> |
The same, case-insensitive (handy on Windows and macOS) |
onbranch:<name> |
The current branch matches that name or pattern |
hasconfig:remote.*.url:<pattern> |
One of the repository's remotes matches that URL pattern |
The rules for the path in gitdir:
- It must end in
/for it to apply to everything inside, recursively. Without the trailing slash it would only match that exact folder, and since Git compares against the path of the.gitdirectory, it would practically never work the way you expect. This is the most frequent mistake when usingincludeIf. - It accepts
~for the home folder. - It accepts wildcards:
gitdir:~/clients/*/matches any first-level subfolder.
The last condition, hasconfig:remote.*.url, is particularly powerful because it does not depend on how you organise your folders:
# Any repository whose remote lives on the company server
[includeIf "hasconfig:remote.*.url:git@git.company.com:**"]
path = ~/.gitconfig-workThat way, even if Ana clones a company repository onto her desktop in a hurry, the right e-mail is applied anyway.
Unconditional include
There is also a version with no condition, useful for splitting a long configuration into reusable pieces:
A common pattern in teams: keep a shared file of aliases and colours in a repository, and have each person include it from their ~/.gitconfig without giving up their own identity.
- Special cases: multiple values and types
Keys with several values
Most keys hold a single value, but some accept several. To add without overwriting, use --add:
git config --global --add safe.directory /opt/projects/shared
git config --global --add safe.directory /srv/repos/internalThe file ends up with two lines in the same section:
Reading them all requires --get-all, because --get would return only the last one:
| Command | Behaviour with multiple values |
|---|---|
git config <key> <value> |
Overwrites every existing value |
git config --add <key> <value> |
Adds one more |
git config --get <key> |
Returns the last one |
git config --get-all <key> |
Returns them all |
git config --unset-all <key> |
Deletes them all |
Value types
Git interprets values according to the type the key expects. You can force the interpretation when reading:
# Read as a boolean: accepts true/false, yes/no, on/off, 1/0
git config --type=bool core.ignorecase
# Read as an integer, allowing the suffixes k, m, g
git config --type=int core.bigFileThreshold
# Expand a path containing ~ into its absolute form
git config --type=path core.excludesFileBooleans are flexible when writing. These six lines are equivalent:
git config --global color.ui true
git config --global color.ui yes
git config --global color.ui on
git config --global color.ui 1
git config --global color.ui TRUE
git config --global color.ui TrueAnd a boolean key written with no value is read as true:
That is valid, though hardly readable. Better to write the value explicitly.
Common Mistakes and Tips
- Forgetting
--globaland writing into the repository by accident. This is the most frequent error. With no explicit level,git configwrites to.git/config, so your preference applies to one project only and you are left wondering why it does not work in the others. Get into the habit of always stating the level. - Misspelling a key's name. Git validates nothing:
core.editorrorpull.rebasseare stored without a murmur and do nothing at all. If an option has no effect, check it withgit config --get <key>and compare it against the documentation (git help config). - Not quoting values with spaces.
git config --global user.name Ana Ferrereither errors or stores justAna. Use quotes whenever there are spaces. - Forgetting the trailing slash in
includeIf "gitdir:...". Without the final/, the condition almost never holds and the included file is ignored silently, with no warning at all. It is a particularly hard failure to spot. - Editing
.git/configwith a syntax error. It leaves Git unusable in that repository until the line is fixed. It is neither serious nor destructive, but it is frightening. The message names the exact file and line. - Mixing up the levels when diagnosing. When a value is not what you expect, do not guess: run
git config --list --show-origin --show-scopeand find which file it comes from. - Tip: keep your
~/.gitconfigin a repository of your own. It is a small text file that represents hours of tuning. Many professionals keep it under version control alongside their other personal configuration files. - Tip: run
git config --list --show-originon every new machine. It is the first thing worth looking at when you start on an unfamiliar computer, and also when debugging odd behaviour on a company machine, where the system level can hold surprises.
Exercises
Exercise 1: Resolving precedence
On Bruno's laptop, the configuration files contain this:
/etc/gitconfig:
~/.gitconfig:
~/personal/task-manager/.git/config:
Answer, from inside the task-manager repository:
- What value does
user.emailhave? - And
user.name? - And
core.editor? - And
core.autocrlf? - What command would you run to find out which file each one comes from without opening them?
- If Bruno runs
git config --unset user.emailinsidetask-manager, what will the new effective value be?
Exercise 2: Separate profiles with includeIf
Carla works in three contexts on the same laptop:
- Her company's projects, in
~/work/, with the e-mailcarla.vidal@company.com. - An outside client's projects, in
~/clients/acme/, with the e-mailc.vidal@acme-contractor.example. - Personal projects, in
~/code/, with the e-mailcarla@example.com.
Her name is always "Carla Vidal" and her editor is always nano.
Write the complete contents of her ~/.gitconfig and of whatever auxiliary files she needs. Then say which command she would use to check, from inside ~/clients/acme/sales-dashboard, that the e-mail being applied is the right one.
Exercise 3: Diagnosis
Ana complains that she has configured her favourite editor but Git still opens Vim every time it asks her to write a message. This is what she ran:
- Identify two distinct errors in that command.
- Write the correct command.
- Write the commands to clean up the wrong key that was left behind.
- Write the command that would have let her spot the problem herself.
Solutions
Solution to Exercise 1
-
user.email=bruno.salas@personal.example. The local level is the most specific of the three present and beats both the global and the system one. -
user.name=Bruno Salas. It is only defined in the global file; nothing overwrites it. -
core.editor=vim. It is only in the global file. -
core.autocrlf=input. It is only at the system level, and no higher level redefines it, so it applies. -
The diagnostic command:
It will show each key preceded by its level and by the file's path. For a single key:
- The new effective value would be
bruno.salas@company.com. Deleting the key at the local level does not leave it empty: it simply lets the next level win, which is the global one. The system one would once again be masked by the global.
Solution to Exercise 2
~/.gitconfig:
[user]
name = Carla Vidal
email = carla@example.com
[core]
editor = nano
[init]
defaultBranch = main
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-company
[includeIf "gitdir:~/clients/acme/"]
path = ~/.gitconfig-acme~/.gitconfig-company:
~/.gitconfig-acme:
Decisions worth justifying:
- The personal e-mail goes in the main file as the default value, because any repository outside the two specific folders should end up with the personal one, which is the least compromising option if she forgets.
- The name and the editor go once in the main file, since they do not change between contexts.
- Every path ends in
/, an indispensable condition for the match to be recursive. - The
includeIfblocks go at the end of the file: they are processed in order, and what is included later overwrites what came before.
Checking from ~/clients/acme/sales-dashboard:
git config --get user.email
# → c.vidal@acme-contractor.example
# The diagnostic version, which also names the file responsible:
git config --show-origin --get user.email
# → file:/home/carla/.gitconfig-acme c.vidal@acme-contractor.exampleSolution to Exercise 3
-
The two errors:
- The key is misspelled:
core.editorrwith two r's. Git accepts it without validating and stores it as a meaningless key, so it has no effect at all. - The
--globallevel is missing: without it, the value would have been written only intotask-manager's.git/config, that is, for that one repository. Even with the key spelled correctly, the editor would still be Vim in all of Ana's other projects.
- The key is misspelled:
-
The correct command:
- Cleaning up the wrong key (it is at the local level, because that is how it was written):
cd ~/personal/task-manager
git config --local --unset core.editorr
# Check that nothing is left of the core section locally
git config --local --list- The diagnostic command that would have revealed the problem:
It would have returned nothing, a sign that the correct key was not defined at any level. And with:
she would have seen the line file:.git/config core.editorr=nano, where both the typo and the wrong level jump straight out at you.
Conclusion
You now have Git's configuration mechanism under control. Everything goes through git config, which reads and writes INI-format text files spread across three levels: system (the whole machine), global (your user) and local (one repository), joined by the worktree level and the -c option for a single run. When several levels define the same key, the most specific one wins, and the command git config --list --show-origin --show-scope is the definitive tool for finding out where each value comes from.
You have seen how to read (--get, --get-all, --list, --get-regexp), write (with and without --add), delete (--unset, --unset-all, --remove-section) and edit the files directly with --edit. And you have met includeIf, the piece that lets you keep separate profiles — personal, company, client — without having to remember anything in each new repository, thanks to conditions on the path (gitdir:), the branch (onbranch:) or the remote's URL (hasconfig:).
You know how to handle the controls; what remains is deciding which buttons to press. In the module's last lesson, Initial Configuration, we will apply all of this to Ana's laptop to get it ready: her identity, the default branch name, the editor, how line endings are handled, the behaviour when integrating changes, the credential store and the output colours. By the end, Ana will finally have everything in place to create the task-manager repository in module 2.
Mastering Git: From Beginner to Advanced
Module 1: Introduction to Git
- What Is Git?
- Installing Git
- Basic Git Terminology
- The Git Data Model
- Configuring Git
- Initial Configuration
Module 2: Basic Git Operations
- Creating a Repository
- Cloning a Repository
- The Basic Git Workflow
- Staging and Committing Changes
- Inspecting Changes with git diff
- Viewing Commit History
Module 3: Branching and Merging
- Understanding Branches
- Creating and Switching Branches
- Merging Branches
- Merge Strategies
- Resolving Merge Conflicts
- Branch Management
Module 4: Working with Remote Repositories
- Understanding Remote Repositories
- Adding a Remote Repository
- Authenticating with Remote Repositories
- Fetching and Pulling Changes
- Pushing Changes
- Tracking Branches
Module 5: Advanced Git Operations
Module 6: Git Tools and Techniques
- Using Git Hooks
- Git Bisect
- Git Blame
- Git Log and Aliases
- Git Submodules
- Multiple Working Copies with git worktree
Module 7: Collaboration and Workflow Strategies
- Forks and Pull Requests
- Code Reviews with Git
- The Git Flow Workflow
- GitHub Flow
- Trunk Based Development
- Continuous Integration with Git
Module 8: Git Best Practices and Tips
- Writing Good Commit Messages
- Keeping a Clean History
- Ignoring Files with .gitignore
- File Attributes with .gitattributes
- Security Best Practices
- Performance Tips
Module 9: Troubleshooting and Debugging
- Common Git Problems
- Undoing Changes
- Resolving Divergence with the Remote
- Recovering Lost Commits
- Dealing with Corrupted Repositories
- Advanced Debugging Techniques
