aboutsummaryrefslogtreecommitdiff
path: root/posts/curing_a_case_of_git-UX.md
diff options
context:
space:
mode:
Diffstat (limited to 'posts/curing_a_case_of_git-UX.md')
-rw-r--r--posts/curing_a_case_of_git-UX.md320
1 files changed, 320 insertions, 0 deletions
diff --git a/posts/curing_a_case_of_git-UX.md b/posts/curing_a_case_of_git-UX.md
new file mode 100644
index 0000000..556df1b
--- /dev/null
+++ b/posts/curing_a_case_of_git-UX.md
@@ -0,0 +1,320 @@
1Git worktrees are great, but they fall behind the venerable
2`git checkout` sometimes. I attempted to fix that with
3[fzf](https://github.com/junegunn/fzf) and
4a bit of bash.
5
6[![](https://asciinema.org/a/D297ztKRzpE4gAHbPTPmkqYps.svg)](https://asciinema.org/a/D297ztKRzpE4gAHbPTPmkqYps)
7
8Fear not if you haven't heard of "worktrees", I have
9included a primer here.
10[Skip the primer
11->](#what-makes-them-clunky).
12
13### Why Worktrees?
14
15Picture this. You are whacking away on a feature branch.
16Halfway there, in fact. Your friend asks you fix something
17urgently. You proceed to do one of three things:
18
19- create a temporary branch, make a WIP commit, begin
20 working on the fix
21- stash away your changes, begin working on the fix
22- unfriend said friend for disturbing your flow
23
24All of these options are ... subpar. With the temporary
25branch, you are forced to create a partial, non-working
26commit, and then reset said commit once done with the fix.
27With the stash approach, you are required to now keep a
28mental model of the stash, be aware of untracked files that
29don't get stashed by default, etc. Why won't git just let
30you work on two things at the same time without _thinking_
31so much?
32
33That is exactly what worktrees let you do. Worktrees let you
34have more than one checkout at a time, each checkout in a
35separate directory. Like creating a new clone, but safer (it
36disallows checking out the same branch twice) and a lot more
37space efficient (the new working tree is "linked" to the
38"main" worktree, and a good amount of stuff is shared). When
39your friend asks you to make the fix, you proceed like so:
40
411. Create a new working tree with:
42 ```bash
43 # git worktree add -b <branch-name> <path> <from>
44 git worktree add -b fix-stuff /path/to/tree master
45 ```
462. `cd` into `/path/to/tree`
473. Fix, test, commit, push, party
484. Go back to your work, `cd -`
49
50Easy as cake. You didn't have to settle for a partially
51working commit, you didn't to deal with this "stash" thing,
52_and_ you didn't have to unfriend your friend. Treating each
53branch as a directory just _feels_ more intuitive, more
54UNIX-y.
55
56A few weeks later, you find yourself singing in praise of
57worktrees, working on several things simultaneously. And at
58the same time, cursing them for being a little ... clunky.
59
60### What makes them clunky?
61
62Worktrees are great at what they claim to do. They stay out
63of the way when you need a checkout posthaste. However, as
64you start using them regularly, you realize they are not as
65flexible as `git checkout` or `git switch`.
66
67#### Branch-hopping
68You can `git checkout <branch>` from anywhere within a git
69repository. You can't "jump" to a worktree in the same
70fashion. The closest you can get, is to run `git worktree
71list`, copy the path corresponding to your branch, and `cd`
72into it.
73
74Branch-hopping with the good ol' git-checkout:
75```bash
76# anywhere, anytime
77λ git checkout feature/is-ascii-octdigit
78```
79
80Meanwhile, in worktree world:
81```bash
82# keeping these paths in your head is hard
83λ git worktree list
84~/worktrees/rustc/master eac6c33bc63 [master]
85~/worktrees/rustc/improve-std-char-docs 94cba88553e [improve-std-char-docs]
86~/worktrees/rustc/is-ascii-octdigit bc57be3af7a [feature/is-ascii-octdigit]
87~/my/other/path/oh/god op57or3ns7n [fix/some-error]
88
89λ cd ~/worktrees/rustc/is-ascii-octdigit
90```
91
92#### Branch-previewing
93
94You can "preview" branches with `git branch -v`. However, to
95get an idea of what "recent activity" on a worktree looks
96like, you might need some juggling. You can't glean much
97info about a worktree in a jiffy.
98
99Branch-previewing with the good ol' git-branch:
100```bash
101λ git branch -v
102+ feature/is-ascii-octdigit bc57be3af7a introduce {char, u8}::is_ ...
103+ improve-std-char-docs 94cba88553e add whitespace in assert ...
104* master eac6c33bc63 Auto merge of #100869 - n ...
105```
106
107Meanwhile in worktree wonderland:
108```
109λ git worktree list
110~/worktrees/rustc/master eac6c33bc63 [master]
111~/worktrees/rustc/improve-std-char-docs 94cba88553e [improve-std-char-docs]
112~/worktrees/rustc/is-ascii-octdigit bc57be3af7a [feature/is-ascii-octdigit]
113
114# aha, so ../is-ascii-octdigit corresponds to `feature/is-ascii-octdigit`
115λ git log feature/is-ascii-octdigit
116bc57be3af7a introduce {char, u8}::is_ascii_octdigit
117eac6c33bc63 Auto merge of #100869 - nnethercote:repl ...
118b32223fec10 Auto merge of #100707 - dzvon:fix-typo, ...
119aa857eb953e Auto merge of #100537 - petrochenkov:pic ...
120
121# extra work to make the branch <-> worktree correspondence
122```
123
124#### Shell completions
125
126Lastly, you can bank on shell completions to fill in your
127branch whilst using `git checkout`. Worktrees have no such
128conveniences.
129
130We can mend these minor faults with fzf.
131
132### Unclunkifying worktrees
133
134I'd suggest looking up
135[fzf](https://github.com/junegunn/fzf) (or
136[skim](https://github.com/lotabout/skim) or
137[fzy](https://github.com/jhawthorn/fzy)). These things make
138it cake-easy to add interactivity to your shell. Onto fixing
139the first minor fault, the inability to "jump" to a worktree
140from anywhere within a git repository.
141
142I have a little function called `gwj` which stands for "git
143worktree jump". The idea is to list all the worktrees,
144select one with fzf, and `cd` to it upon selection:
145
146```bash
147gwj () {
148 local out
149 out=$(git worktree list | fzf | awk '{print $1}')
150 cd $out
151}
152```
153
154That is all of it really. Head into a git repository:
155
156```bash
157# here, "master" is a directory, which contains my main
158# worktree: a checkout of the master branch on rust-lang/rust
159λ cd ~/worktrees/rustc/master/library/core/src
160λ # hack away
161```
162
163Preferably one with a few worktrees:
164
165```bash
166λ git worktree list
167~/worktrees/rustc/master eac6c33bc63 [master]
168~/worktrees/rustc/improve-std-char-docs 94cba88553e [improve-std-char-docs]
169~/worktrees/rustc/is-ascii-octdigit bc57be3af7a [feature/is-ascii-octdigit]
170```
171
172And hit `gwj` (pretend that the pipe, |, is your cursor):
173
174```bash
175λ gwj
176> |
177 4/4
178> ~/worktrees/rustc/master eac6c33bc63 [master]
179 ~/worktrees/rustc/improve-std-char-docs 94cba88553e [improve-std-char-docs]
180 ~/worktrees/rustc/is-ascii-octdigit bc57be3af7a [feature/is-ascii-octdigit]
181```
182
183Approximately type in your branch of choice:
184
185```bash
186λ gwj
187> docs|
188 4/4
189> ~/worktrees/rustc/improve-std-char-docs 94cba88553e [improve-std-char-docs]
190```
191
192And hit enter. You should find yourself in the selected
193worktree.
194
195Onward, to the next fault, lack of preview-bility. We can
196utilize fzf's aptly named `--preview` flag, to, well,
197preview our worktree before performing a selection:
198
199```bash
200gwj () {
201 local out
202 out=$(
203 git worktree list |
204 fzf --preview='git log --oneline -n10 {2}' |
205 awk '{print $1}'
206 )
207 cd $out
208}
209```
210
211Once again, hit `gwj` inside a git repository with linked worktrees:
212
213```bash
214λ gwj
215╭─────────────────────────────────────────────────────────╮
216│ eac6c33bc63 Auto merge of 100869 nnethercote:replace... │
217│ b32223fec10 Auto merge of 100707 dzvon:fix-typo, r=d... │
218│ aa857eb953e Auto merge of 100537 petrochenkov:picche... │
219│ 3892b7074da Auto merge of 100210 mystor:proc_macro_d... │
220│ db00199d999 Auto merge of 101249 matthiaskrgr:rollup... │
221│ 14d216d33ba Rollup merge of 101240 JohnTitor:JohnTit... │
222│ 3da66f03531 Rollup merge of 101236 thomcc:winfs-noze... │
223│ 0620f6e90af Rollup merge of 101230 davidtwco:transla... │
224│ c30c42ee299 Rollup merge of 101229 mgeisler:link-try... │
225│ e5356712b9e Rollup merge of 101165 ldm0:drain_to_ite... │
226╰─────────────────────────────────────────────────────────╯
227>
228 4/4
229> /home/np/worktrees/compiler/master eac6c...
230 /home/np/worktrees/compiler/improve-std-char-docs 94cba...
231 /home/np/worktrees/compiler/is-ascii-octdigit bc57b...
232```
233
234A fancy preview of the last 10 commits on the branch that
235the selected worktree corresponds to. In other words, sight
236for sore eyes. Our little script is already shaping up to be
237useful, you hit `gwj`, browse through your worktrees,
238preview each one and automatically `cd` to your selection.
239But we are not done yet.
240
241The last fault was lack shell completions. A quick review of
242what a shell completion really does:
243
244```bash
245λ git checkout f<tab>
246feature/is-ascii-octdigit
247fix/some-error
248format-doc-tests
249
250λ git checkout feat<tab>
251
252λ git checkout feature/is-ascii-octdigit
253```
254
255Each time you hit "tab", the shell produces a few
256"completion candidates", and once you have just a single
257candidate left, the shell inserts that for you directly into
258your edit line. Of course, this process varies from shell to
259shell.
260
261fzf narrows down your options as you type into the prompt,
262but you still have to:
263
2641. Type `gwj`
2652. Hit enter
2663. Type out a query and narrow down your search
2674. Hit enter
268
269We can speed that up a bit, have fzf narrow down the
270candidates on startup, just like our shell does:
271
272```bash
273gwj () {
274 local out query
275 query="${1:- }"
276 out=$(
277 git worktree list |
278 fzf --preview='git log --oneline -n10 {2}' --query "$query" -1 |
279 awk '{print $1}'
280 )
281 cd $out
282}
283```
284
285The change is extremely tiny, blink-and-you'll-miss-it kinda
286tiny. We added a little `--query` flag, that allows you to
287prefill the prompt, and the `-1` flag, that avoids the
288interactive finder if only one match exists on startup:
289
290```bash
291# skip through the fzf prompt:
292λ gwj master
293# cd -- ~/worktrees/rustc/master
294
295# more than one option, we end up in the interactive finder
296λ gwj improve
297╭─────────────────────────────────────────────────────────╮
298│ eac6c33bc63 Auto merge of 100869 nnethercote:replace... │
299│ b32223fec10 Auto merge of 100707 dzvon:fix-typo, r=d... │
300│ aa857eb953e Auto merge of 100537 petrochenkov:picche... │
301╰─────────────────────────────────────────────────────────╯
302> improve
303 2/2
304> /home/np/worktrees/compiler/improve-const-perf eac6c...
305 /home/np/worktrees/compiler/improve-std-char-docs 94cba...
306```
307
308Throw some error handling in there, hook up a similar script
309to improve the UX of `git worktree remove`, go wild. A few
310more helpers I've got:
311
312```bash
313# gwa /path/to/branch-name
314# creates a new branch and "switches" to it
315function gwa () {
316 git worktree add "$1" && cd "$1"
317}
318
319alias gwls="git worktree list"
320```