summaryrefslogtreecommitdiffstats
path: root/content/en/articles/my-git-workflow/index.md
blob: 60e6bbdc65de0a864b393443bf45ccd31f2fdfb0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
+++
title = "The Git Config Settings I Actually Use"
author = "Danilo M."
type = "tech"
date = "2026-04-28T19:53:46+02:00"
draft = false
excerpt = "A tour through my ~/.gitconfig, covering the settings I've grown to rely on daily and why each one earns its place."
tags = ["git", "workflow", "linux", "gpg", "hugo", "configuration", "howto"]
categories = ["DIY", "Code"]
+++

A few years back, I set up a two-repository system for this site: one repo for content, another for the theme (living as a submodule). It's a clean architecture in theory, but in practice, managing two repos means you're constantly juggling branch updates, resolving submodule pointer conflicts, and pushing to two remotes in sequence. I started carrying around a `.gitconfig` file between machines, tweaking it bit by bit as I discovered settings that eliminated friction.

This isn't a guide to every git option, that would be a book. Instead, it's a personal tour through the settings I've actually integrated into my workflow, why they matter, and what changed once each one was on.

<!--more-->

## The Setup Glue: `push.autoSetupRemote` and `pull.rebase`

These two live together because they're about the boundaries of your branches: pushing them up and pulling them back down.

### push.autoSetupRemote

The old frustration: cut a new branch locally, write code, run `git push`, and get:

```
fatal: The current branch feature/my-new-branch has no upstream branch.
To push the current and set the upstream branch, use:

    git push --set-upstream origin feature/my-new-branch
```

It's boilerplate ceremony every time you push a new branch. The fix:

```ini
[push]
    autoSetupRemote = true
```

Now `git push` on a new branch just works. It infers the tracking branch automatically. The first time I used this with Hugo content branches, cutting `content/new-article` on Monday and pushing immediately, I realized I'd eliminated maybe fifty typos a year of `--set-upstream`.

### pull.rebase

Linear history matters more on a single-author site. Without `pull.rebase = true`, pulling changes creates merge commits like "Merge branch 'main' of origin/main" even when you're just syncing. These commits don't represent work; they're noise. They clutter `git log`, make bisecting slower, and add confusion to the graph.

```ini
[pull]
    rebase = true
```

With this on, pulling rebases your local work on top of the remote tip. When you're managing a Hugo site with a theme submodule, this is especially helpful: submodule pointer updates stay tidy in the log instead of hiding inside merge commits.

## Identity and Trust: `commit.gpgsign`

GPG signing commits isn't about compliance or passing audits. It's about provenance. Every commit on my repos carries my cryptographic signature, proof that it came from me and hasn't been tampered with.

```ini
[user]
    signingkey = YOUR_GPG_KEY_ID
    email = danix@danix.xyz
    name = Danilo M.

[commit]
    gpgsign = true
```

The first time you set this up, know that `gpg-agent` needs to be running and your key needs to be imported locally. Once both are in place, every commit is signed automatically. If you want to dive deeper into GPG keys, I wrote about [managing passwords with password-store](/en/articles/manage-your-passwords-with-password-store/), which covers key setup.

## Reducing Friction: `help.autocorrect`, `fetch.prune`, and `fetch.prunetags`

Three small settings that save daily typing and mental overhead.

### help.autocorrect

Typos happen. I often type `git statsu` instead of `git status`, or `git comit` instead of `git commit`. The old behavior is an error message and a suggested fix. With:

```ini
[help]
    autocorrect = 10
```

Git autocorrects and runs the command after a 1-second countdown:

```
$ git statsu
WARNING: You called a Git command named 'statsu', which does not exist.
Continuing in 0.9 seconds, assuming that you meant 'status'.
```

The `10` here means 1 second (git measures in tenths). It's long enough to notice and interrupt if the correction is wrong, but short enough not to interrupt workflow.

### fetch.prune and fetch.prunetags

When teammates delete branches on the remote, stale refs accumulate in your local repository. These settings clean them up:

```ini
[fetch]
    prune = true
    prunetags = true
```

Now `git fetch` silently removes local branches that no longer exist upstream, and does the same for tags. For a Hugo theme repo, old release tags get cleaned up on the server, and `prunetags` means they don't linger in your local `git tag` output.

## When Conflicts Happen: `merge.conflictstyle` and `rerere`

Two settings that make merge conflicts less painful.

### merge.conflictstyle = zdiff3

Conflict markers normally show you two sides:

```
<<<<<<< HEAD
your code
=======
their code
>>>>>>> branch
```

You see what you wrote and what they wrote, but not what you both started from. The `zdiff3` conflict style shows the ancestor:

```
<<<<<<< HEAD
your code
||||||| base
original code
=======
their code
>>>>>>> branch
```

The `||||||| base` section is what both sides changed from. Now you can see the full story: you changed this line to X, they changed it to Y, and the original was Z. Intent becomes obvious.

```ini
[merge]
    conflictstyle = zdiff3
```

### rerere.enabled and rerere.autoupdate

`rerere` stands for "reuse recorded resolution." When you resolve a conflict manually, git remembers the resolution. The next time you hit the same conflict:

```ini
[rerere]
    enabled = true
    autoupdate = true
```

Git resolves it automatically. This is invaluable when rebasing a content branch over a main that includes a submodule pointer update. The first time there's a conflict in the submodule reference, you resolve it. On a second branch with the same conflict, `rerere` handles it silently, and `autoupdate` stages the resolution so you don't have to `git add` it manually.

## Looking Around: Branch Display, Logs, and Diffs

Four settings that make reading your repository history faster.

### branch.sort and column.ui

```ini
[branch]
    sort = -committerdate

[column]
    ui = auto
```

By default, `git branch` lists branches alphabetically. Useless past ten branches. The `-committerdate` sort shows newest first, so the branch you're probably about to switch to is at the top.

`column.ui = auto` displays branches in multiple columns when your terminal is wide enough, cutting down on scrolling through a long list.

### log.abbrevCommit and log.follow

```ini
[log]
    abbrevCommit = true
    follow = true
```

`abbrevCommit` shows short SHAs (7 chars) instead of full 40-character hashes. Cleaner logs, faster to read.

`follow` tracks files through renames. When you run `git log -- path/to/article.md` and that file was once named `old-article.md`, log doesn't break, it follows the file through the rename and shows the full history.

### diff.mnemonicPrefix, diff.renames, and diff.wordRegex

```ini
[diff]
    mnemonicPrefix = true
    renames = true
    wordRegex = [^[:space:]]
```

`mnemonicPrefix` changes the diff headers from `a/` and `b/` to more descriptive labels like `i/` (index), `w/` (working tree), `o/` (object), `c/` (commit). More information at a glance.

`renames = true` detects file renames and shows them as `file.old => file.new` instead of a delete and a create. Cleaner diffs.

`wordRegex` defines what counts as a "word" in word-level diffs (`git diff --word-diff`). The pattern `[^[:space:]]` treats any non-whitespace as a word, which means punctuation gets its own boundaries, useful for prose-heavy files like Markdown articles.

## Shorthand That Earns Its Place: Aliases

I keep aliases minimal on purpose. Aliases that hide what git is doing are noise; these two are worth the keystroke savings.

### lg: Pretty-print log with graph

```ini
[alias]
    lg = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)' --all
```

Running `git lg` shows a colored graph with commit hashes, dates, messages, authors, and branch pointers:

```
* abc1234 - (3 hours ago) Add git config article - Danilo M. (HEAD -> master)
* def5678 - (1 day ago) Fix theme CSS - Danilo M.
|\
| * ghi9012 - (2 days ago) WIP: new feature - Danilo M. (content/draft)
|/
* jkl3456 - (5 days ago) Bump theme submodule - Danilo M. (origin/master)
```

Much better than plain `git log`.

### sw: Switch shorthand

```ini
[alias]
    sw = switch
```

`git sw main` is faster than `git switch main`, and `git sw -c feature/new` creates and switches in one command.

## A Typical Day

Here's how these settings work together in practice.

Morning: sync with the repo. `git fetch` fires silently in the background, `fetch.prune` and `prunetags` cleaning up any deleted branches and old tags without you thinking about it.

I'm starting the week's Hugo article. Cut a new content branch:

```bash
git sw -c content/git-config-deep-dive
```

No typing `--set-upstream` later. `push.autoSetupRemote` has me covered.

Write, commit. The commit is GPG-signed automatically. No extra steps.

```bash
git commit -m "Add first draft of git config article"
```

Push when done:

```bash
git push
```

No upstream error. Just works.

Later, the main branch gets a theme submodule update. I rebase my work on top:

```bash
git fetch
git rebase main
```

There's a conflict in the submodule pointer, the theme repo moved forward. `merge.conflictstyle = zdiff3` shows me exactly what changed. I resolve it once.

Later, a second branch has the same submodule conflict. `rerere` remembers my resolution and applies it automatically with `rerere.autoupdate`. No re-resolving.

Before merging back to main, check the graph:

```bash
git lg
```

Pretty output shows the timeline clearly.

Merge to main:

```bash
git switch main
git merge content/git-config-deep-dive
```

If there's a conflict here (unlikely given we rebased), `zdiff3` shows the ancestor context.

Done. All the settings worked together transparently.

## The Full Config

Here's the complete block as it sits in my `~/.gitconfig`:

```ini
[user]
    name = Danilo M.
    email = danix@danix.xyz
    signingkey = YOUR_KEY_ID_HERE

[pull]
    rebase = true

[push]
    autoSetupRemote = true

[commit]
    gpgsign = true

[help]
    autocorrect = 10

[fetch]
    prune = true
    prunetags = true

[merge]
    conflictstyle = zdiff3

[rerere]
    enabled = true
    autoupdate = true

[branch]
    sort = -committerdate

[column]
    ui = auto

[log]
    abbrevCommit = true
    follow = true

[diff]
    mnemonicPrefix = true
    renames = true
    wordRegex = [^[:space:]]

[alias]
    lg = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)' --all
    sw = switch
```

Copy what you need. Leave what doesn't fit your workflow.

## A Config That Evolves

This isn't the "correct" git configuration, it's the one that fits how I work. I've been refining it for years, and it'll keep evolving as I hit new friction points and discover new settings. The point isn't to follow mine exactly, but to think about what's slowing you down and look for the git setting that fixes it.

If you use any of these settings, or if you've found others that changed your workflow, let me know in the comments. I'm always curious how other people configure their tools.