diff options
Diffstat (limited to 'posts')
-rw-r--r-- | posts/curing_a_case_of_git-UX.md | 320 |
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 @@ | |||
1 | Git 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 | ||
4 | a bit of bash. | ||
5 | |||
6 | [![](https://asciinema.org/a/D297ztKRzpE4gAHbPTPmkqYps.svg)](https://asciinema.org/a/D297ztKRzpE4gAHbPTPmkqYps) | ||
7 | |||
8 | Fear not if you haven't heard of "worktrees", I have | ||
9 | included a primer here. | ||
10 | [Skip the primer | ||
11 | ->](#what-makes-them-clunky). | ||
12 | |||
13 | ### Why Worktrees? | ||
14 | |||
15 | Picture this. You are whacking away on a feature branch. | ||
16 | Halfway there, in fact. Your friend asks you fix something | ||
17 | urgently. 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 | |||
24 | All of these options are ... subpar. With the temporary | ||
25 | branch, you are forced to create a partial, non-working | ||
26 | commit, and then reset said commit once done with the fix. | ||
27 | With the stash approach, you are required to now keep a | ||
28 | mental model of the stash, be aware of untracked files that | ||
29 | don't get stashed by default, etc. Why won't git just let | ||
30 | you work on two things at the same time without _thinking_ | ||
31 | so much? | ||
32 | |||
33 | That is exactly what worktrees let you do. Worktrees let you | ||
34 | have more than one checkout at a time, each checkout in a | ||
35 | separate directory. Like creating a new clone, but safer (it | ||
36 | disallows checking out the same branch twice) and a lot more | ||
37 | space efficient (the new working tree is "linked" to the | ||
38 | "main" worktree, and a good amount of stuff is shared). When | ||
39 | your friend asks you to make the fix, you proceed like so: | ||
40 | |||
41 | 1. 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 | ``` | ||
46 | 2. `cd` into `/path/to/tree` | ||
47 | 3. Fix, test, commit, push, party | ||
48 | 4. Go back to your work, `cd -` | ||
49 | |||
50 | Easy as cake. You didn't have to settle for a partially | ||
51 | working commit, you didn't to deal with this "stash" thing, | ||
52 | _and_ you didn't have to unfriend your friend. Treating each | ||
53 | branch as a directory just _feels_ more intuitive, more | ||
54 | UNIX-y. | ||
55 | |||
56 | A few weeks later, you find yourself singing in praise of | ||
57 | worktrees, working on several things simultaneously. And at | ||
58 | the same time, cursing them for being a little ... clunky. | ||
59 | |||
60 | ### What makes them clunky? | ||
61 | |||
62 | Worktrees are great at what they claim to do. They stay out | ||
63 | of the way when you need a checkout posthaste. However, as | ||
64 | you start using them regularly, you realize they are not as | ||
65 | flexible as `git checkout` or `git switch`. | ||
66 | |||
67 | #### Branch-hopping | ||
68 | You can `git checkout <branch>` from anywhere within a git | ||
69 | repository. You can't "jump" to a worktree in the same | ||
70 | fashion. The closest you can get, is to run `git worktree | ||
71 | list`, copy the path corresponding to your branch, and `cd` | ||
72 | into it. | ||
73 | |||
74 | Branch-hopping with the good ol' git-checkout: | ||
75 | ```bash | ||
76 | # anywhere, anytime | ||
77 | λ git checkout feature/is-ascii-octdigit | ||
78 | ``` | ||
79 | |||
80 | Meanwhile, 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 | |||
94 | You can "preview" branches with `git branch -v`. However, to | ||
95 | get an idea of what "recent activity" on a worktree looks | ||
96 | like, you might need some juggling. You can't glean much | ||
97 | info about a worktree in a jiffy. | ||
98 | |||
99 | Branch-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 | |||
107 | Meanwhile 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 | ||
116 | bc57be3af7a introduce {char, u8}::is_ascii_octdigit | ||
117 | eac6c33bc63 Auto merge of #100869 - nnethercote:repl ... | ||
118 | b32223fec10 Auto merge of #100707 - dzvon:fix-typo, ... | ||
119 | aa857eb953e Auto merge of #100537 - petrochenkov:pic ... | ||
120 | |||
121 | # extra work to make the branch <-> worktree correspondence | ||
122 | ``` | ||
123 | |||
124 | #### Shell completions | ||
125 | |||
126 | Lastly, you can bank on shell completions to fill in your | ||
127 | branch whilst using `git checkout`. Worktrees have no such | ||
128 | conveniences. | ||
129 | |||
130 | We can mend these minor faults with fzf. | ||
131 | |||
132 | ### Unclunkifying worktrees | ||
133 | |||
134 | I'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 | ||
138 | it cake-easy to add interactivity to your shell. Onto fixing | ||
139 | the first minor fault, the inability to "jump" to a worktree | ||
140 | from anywhere within a git repository. | ||
141 | |||
142 | I have a little function called `gwj` which stands for "git | ||
143 | worktree jump". The idea is to list all the worktrees, | ||
144 | select one with fzf, and `cd` to it upon selection: | ||
145 | |||
146 | ```bash | ||
147 | gwj () { | ||
148 | local out | ||
149 | out=$(git worktree list | fzf | awk '{print $1}') | ||
150 | cd $out | ||
151 | } | ||
152 | ``` | ||
153 | |||
154 | That 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 | |||
163 | Preferably 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 | |||
172 | And 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 | |||
183 | Approximately 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 | |||
192 | And hit enter. You should find yourself in the selected | ||
193 | worktree. | ||
194 | |||
195 | Onward, to the next fault, lack of preview-bility. We can | ||
196 | utilize fzf's aptly named `--preview` flag, to, well, | ||
197 | preview our worktree before performing a selection: | ||
198 | |||
199 | ```bash | ||
200 | gwj () { | ||
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 | |||
211 | Once 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 | |||
234 | A fancy preview of the last 10 commits on the branch that | ||
235 | the selected worktree corresponds to. In other words, sight | ||
236 | for sore eyes. Our little script is already shaping up to be | ||
237 | useful, you hit `gwj`, browse through your worktrees, | ||
238 | preview each one and automatically `cd` to your selection. | ||
239 | But we are not done yet. | ||
240 | |||
241 | The last fault was lack shell completions. A quick review of | ||
242 | what a shell completion really does: | ||
243 | |||
244 | ```bash | ||
245 | λ git checkout f<tab> | ||
246 | feature/is-ascii-octdigit | ||
247 | fix/some-error | ||
248 | format-doc-tests | ||
249 | |||
250 | λ git checkout feat<tab> | ||
251 | |||
252 | λ git checkout feature/is-ascii-octdigit | ||
253 | ``` | ||
254 | |||
255 | Each time you hit "tab", the shell produces a few | ||
256 | "completion candidates", and once you have just a single | ||
257 | candidate left, the shell inserts that for you directly into | ||
258 | your edit line. Of course, this process varies from shell to | ||
259 | shell. | ||
260 | |||
261 | fzf narrows down your options as you type into the prompt, | ||
262 | but you still have to: | ||
263 | |||
264 | 1. Type `gwj` | ||
265 | 2. Hit enter | ||
266 | 3. Type out a query and narrow down your search | ||
267 | 4. Hit enter | ||
268 | |||
269 | We can speed that up a bit, have fzf narrow down the | ||
270 | candidates on startup, just like our shell does: | ||
271 | |||
272 | ```bash | ||
273 | gwj () { | ||
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 | |||
285 | The change is extremely tiny, blink-and-you'll-miss-it kinda | ||
286 | tiny. We added a little `--query` flag, that allows you to | ||
287 | prefill the prompt, and the `-1` flag, that avoids the | ||
288 | interactive 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 | |||
308 | Throw some error handling in there, hook up a similar script | ||
309 | to improve the UX of `git worktree remove`, go wild. A few | ||
310 | more helpers I've got: | ||
311 | |||
312 | ```bash | ||
313 | # gwa /path/to/branch-name | ||
314 | # creates a new branch and "switches" to it | ||
315 | function gwa () { | ||
316 | git worktree add "$1" && cd "$1" | ||
317 | } | ||
318 | |||
319 | alias gwls="git worktree list" | ||
320 | ``` | ||