From c3669921a8ab3fd4953f0ac8b8d50827c4c80191 Mon Sep 17 00:00:00 2001 From: Akshay Date: Sun, 4 Sep 2022 12:35:38 +0530 Subject: new post: curing a case of git-ux --- posts/curing_a_case_of_git-UX.md | 320 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 posts/curing_a_case_of_git-UX.md (limited to 'posts') 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 @@ +Git worktrees are great, but they fall behind the venerable +`git checkout` sometimes. I attempted to fix that with +[fzf](https://github.com/junegunn/fzf) and +a bit of bash. + +[![](https://asciinema.org/a/D297ztKRzpE4gAHbPTPmkqYps.svg)](https://asciinema.org/a/D297ztKRzpE4gAHbPTPmkqYps) + +Fear not if you haven't heard of "worktrees", I have +included a primer here. +[Skip the primer +->](#what-makes-them-clunky). + +### Why Worktrees? + +Picture this. You are whacking away on a feature branch. +Halfway there, in fact. Your friend asks you fix something +urgently. You proceed to do one of three things: + +- create a temporary branch, make a WIP commit, begin + working on the fix +- stash away your changes, begin working on the fix +- unfriend said friend for disturbing your flow + +All of these options are ... subpar. With the temporary +branch, you are forced to create a partial, non-working +commit, and then reset said commit once done with the fix. +With the stash approach, you are required to now keep a +mental model of the stash, be aware of untracked files that +don't get stashed by default, etc. Why won't git just let +you work on two things at the same time without _thinking_ +so much? + +That is exactly what worktrees let you do. Worktrees let you +have more than one checkout at a time, each checkout in a +separate directory. Like creating a new clone, but safer (it +disallows checking out the same branch twice) and a lot more +space efficient (the new working tree is "linked" to the +"main" worktree, and a good amount of stuff is shared). When +your friend asks you to make the fix, you proceed like so: + +1. Create a new working tree with: + ```bash + # git worktree add -b + git worktree add -b fix-stuff /path/to/tree master + ``` +2. `cd` into `/path/to/tree` +3. Fix, test, commit, push, party +4. Go back to your work, `cd -` + +Easy as cake. You didn't have to settle for a partially +working commit, you didn't to deal with this "stash" thing, +_and_ you didn't have to unfriend your friend. Treating each +branch as a directory just _feels_ more intuitive, more +UNIX-y. + +A few weeks later, you find yourself singing in praise of +worktrees, working on several things simultaneously. And at +the same time, cursing them for being a little ... clunky. + +### What makes them clunky? + +Worktrees are great at what they claim to do. They stay out +of the way when you need a checkout posthaste. However, as +you start using them regularly, you realize they are not as +flexible as `git checkout` or `git switch`. + +#### Branch-hopping +You can `git checkout ` from anywhere within a git +repository. You can't "jump" to a worktree in the same +fashion. The closest you can get, is to run `git worktree +list`, copy the path corresponding to your branch, and `cd` +into it. + +Branch-hopping with the good ol' git-checkout: +```bash +# anywhere, anytime +λ git checkout feature/is-ascii-octdigit +``` + +Meanwhile, in worktree world: +```bash +# keeping these paths in your head is hard +λ git worktree list +~/worktrees/rustc/master eac6c33bc63 [master] +~/worktrees/rustc/improve-std-char-docs 94cba88553e [improve-std-char-docs] +~/worktrees/rustc/is-ascii-octdigit bc57be3af7a [feature/is-ascii-octdigit] +~/my/other/path/oh/god op57or3ns7n [fix/some-error] + +λ cd ~/worktrees/rustc/is-ascii-octdigit +``` + +#### Branch-previewing + +You can "preview" branches with `git branch -v`. However, to +get an idea of what "recent activity" on a worktree looks +like, you might need some juggling. You can't glean much +info about a worktree in a jiffy. + +Branch-previewing with the good ol' git-branch: +```bash +λ git branch -v ++ feature/is-ascii-octdigit bc57be3af7a introduce {char, u8}::is_ ... ++ improve-std-char-docs 94cba88553e add whitespace in assert ... +* master eac6c33bc63 Auto merge of #100869 - n ... +``` + +Meanwhile in worktree wonderland: +``` +λ git worktree list +~/worktrees/rustc/master eac6c33bc63 [master] +~/worktrees/rustc/improve-std-char-docs 94cba88553e [improve-std-char-docs] +~/worktrees/rustc/is-ascii-octdigit bc57be3af7a [feature/is-ascii-octdigit] + +# aha, so ../is-ascii-octdigit corresponds to `feature/is-ascii-octdigit` +λ git log feature/is-ascii-octdigit +bc57be3af7a introduce {char, u8}::is_ascii_octdigit +eac6c33bc63 Auto merge of #100869 - nnethercote:repl ... +b32223fec10 Auto merge of #100707 - dzvon:fix-typo, ... +aa857eb953e Auto merge of #100537 - petrochenkov:pic ... + +# extra work to make the branch <-> worktree correspondence +``` + +#### Shell completions + +Lastly, you can bank on shell completions to fill in your +branch whilst using `git checkout`. Worktrees have no such +conveniences. + +We can mend these minor faults with fzf. + +### Unclunkifying worktrees + +I'd suggest looking up +[fzf](https://github.com/junegunn/fzf) (or +[skim](https://github.com/lotabout/skim) or +[fzy](https://github.com/jhawthorn/fzy)). These things make +it cake-easy to add interactivity to your shell. Onto fixing +the first minor fault, the inability to "jump" to a worktree +from anywhere within a git repository. + +I have a little function called `gwj` which stands for "git +worktree jump". The idea is to list all the worktrees, +select one with fzf, and `cd` to it upon selection: + +```bash +gwj () { + local out + out=$(git worktree list | fzf | awk '{print $1}') + cd $out +} +``` + +That is all of it really. Head into a git repository: + +```bash +# here, "master" is a directory, which contains my main +# worktree: a checkout of the master branch on rust-lang/rust +λ cd ~/worktrees/rustc/master/library/core/src +λ # hack away +``` + +Preferably one with a few worktrees: + +```bash +λ git worktree list +~/worktrees/rustc/master eac6c33bc63 [master] +~/worktrees/rustc/improve-std-char-docs 94cba88553e [improve-std-char-docs] +~/worktrees/rustc/is-ascii-octdigit bc57be3af7a [feature/is-ascii-octdigit] +``` + +And hit `gwj` (pretend that the pipe, |, is your cursor): + +```bash +λ gwj +> | + 4/4 +> ~/worktrees/rustc/master eac6c33bc63 [master] + ~/worktrees/rustc/improve-std-char-docs 94cba88553e [improve-std-char-docs] + ~/worktrees/rustc/is-ascii-octdigit bc57be3af7a [feature/is-ascii-octdigit] +``` + +Approximately type in your branch of choice: + +```bash +λ gwj +> docs| + 4/4 +> ~/worktrees/rustc/improve-std-char-docs 94cba88553e [improve-std-char-docs] +``` + +And hit enter. You should find yourself in the selected +worktree. + +Onward, to the next fault, lack of preview-bility. We can +utilize fzf's aptly named `--preview` flag, to, well, +preview our worktree before performing a selection: + +```bash +gwj () { + local out + out=$( + git worktree list | + fzf --preview='git log --oneline -n10 {2}' | + awk '{print $1}' + ) + cd $out +} +``` + +Once again, hit `gwj` inside a git repository with linked worktrees: + +```bash +λ gwj +╭─────────────────────────────────────────────────────────╮ +│ eac6c33bc63 Auto merge of 100869 nnethercote:replace... │ +│ b32223fec10 Auto merge of 100707 dzvon:fix-typo, r=d... │ +│ aa857eb953e Auto merge of 100537 petrochenkov:picche... │ +│ 3892b7074da Auto merge of 100210 mystor:proc_macro_d... │ +│ db00199d999 Auto merge of 101249 matthiaskrgr:rollup... │ +│ 14d216d33ba Rollup merge of 101240 JohnTitor:JohnTit... │ +│ 3da66f03531 Rollup merge of 101236 thomcc:winfs-noze... │ +│ 0620f6e90af Rollup merge of 101230 davidtwco:transla... │ +│ c30c42ee299 Rollup merge of 101229 mgeisler:link-try... │ +│ e5356712b9e Rollup merge of 101165 ldm0:drain_to_ite... │ +╰─────────────────────────────────────────────────────────╯ +> + 4/4 +> /home/np/worktrees/compiler/master eac6c... + /home/np/worktrees/compiler/improve-std-char-docs 94cba... + /home/np/worktrees/compiler/is-ascii-octdigit bc57b... +``` + +A fancy preview of the last 10 commits on the branch that +the selected worktree corresponds to. In other words, sight +for sore eyes. Our little script is already shaping up to be +useful, you hit `gwj`, browse through your worktrees, +preview each one and automatically `cd` to your selection. +But we are not done yet. + +The last fault was lack shell completions. A quick review of +what a shell completion really does: + +```bash +λ git checkout f +feature/is-ascii-octdigit +fix/some-error +format-doc-tests + +λ git checkout feat + +λ git checkout feature/is-ascii-octdigit +``` + +Each time you hit "tab", the shell produces a few +"completion candidates", and once you have just a single +candidate left, the shell inserts that for you directly into +your edit line. Of course, this process varies from shell to +shell. + +fzf narrows down your options as you type into the prompt, +but you still have to: + +1. Type `gwj` +2. Hit enter +3. Type out a query and narrow down your search +4. Hit enter + +We can speed that up a bit, have fzf narrow down the +candidates on startup, just like our shell does: + +```bash +gwj () { + local out query + query="${1:- }" + out=$( + git worktree list | + fzf --preview='git log --oneline -n10 {2}' --query "$query" -1 | + awk '{print $1}' + ) + cd $out +} +``` + +The change is extremely tiny, blink-and-you'll-miss-it kinda +tiny. We added a little `--query` flag, that allows you to +prefill the prompt, and the `-1` flag, that avoids the +interactive finder if only one match exists on startup: + +```bash +# skip through the fzf prompt: +λ gwj master +# cd -- ~/worktrees/rustc/master + +# more than one option, we end up in the interactive finder +λ gwj improve +╭─────────────────────────────────────────────────────────╮ +│ eac6c33bc63 Auto merge of 100869 nnethercote:replace... │ +│ b32223fec10 Auto merge of 100707 dzvon:fix-typo, r=d... │ +│ aa857eb953e Auto merge of 100537 petrochenkov:picche... │ +╰─────────────────────────────────────────────────────────╯ +> improve + 2/2 +> /home/np/worktrees/compiler/improve-const-perf eac6c... + /home/np/worktrees/compiler/improve-std-char-docs 94cba... +``` + +Throw some error handling in there, hook up a similar script +to improve the UX of `git worktree remove`, go wild. A few +more helpers I've got: + +```bash +# gwa /path/to/branch-name +# creates a new branch and "switches" to it +function gwa () { + git worktree add "$1" && cd "$1" +} + +alias gwls="git worktree list" +``` -- cgit v1.2.3