jj + github PR flow
Lately I've been writing more code than usual, not because of llm-coding but llm-looking-things-up / getting answers to targeted questions (much faster than reading x number of blog posts to get an easy-to-verify answer for a specific question). A bit ironic that, in the age of vibe-codeing, for my workflow, llm helps me to spend more time actually typing and crafting the code, and I like that.
Because of this, I often work on multiple stories/tickets/features at the same time. jj makes this fairly easy. unlike git, where I'd have to rebase mulitple branches (one for each feature) over and over, jj lets me just keep my single line of commits, with bookmarks pointed to the commit that is the head of the branch.
e.g. with jj I can hop to any commit below (on any branch in git's point of
view) with jj edit, make changes, and all the bookmark/branches are
automagically rebased. If conflicts happen they're handlled jj style, left in
place and non-blocking. e.g. In github say I have feature-1 -> staging, feature-2 -> feature-1, feature-3 -> feature-2. A single jj git push updates all three
branches/PRs upstream. When feature-1 merges, a single jj git fetch && jj rebase -o staging syncs me up.
OR DOES IT...
the problem
It does, if'n you use merge commits without squashing for PRs. But my team doesn't and I generally do like it that way to keep git history as "one commit per unit of work." But when devving a feature, I like manifold commits detailing every step of thought along the way. The workflow for this is less than ideal for regular git as well as jj.
e.g. Say I push a clean linear history of commits:
staging-> m -> y -> n (feature-1) -> u -> r -> z (feature-2).
Running ghpr-create feature-1 staging § then ghpr-create feature-2 feature-1 I have PRs out for:
feature-1targetingstagingfeature-2targetingfeature-1
% jj log
@ z feature-2
│ last work on 2
○ r
│ more work on 2
○ u
│ work on 2
○ n feature-1
│ last work on 1
○ y
│ more work on 1
○ m
│ work on 1
◆ k staging
│ some older feature
~
Now... let's merge and delete the branch for feature-1. Now jj git fetch to see our problem.
% jj git fetch
remote: Enumerating objects: 4, done.
remote: Total 3 (delta 1), reused 2 (delta 1), pack-reused 0 (from 0)
bookmark: feature-1@origin [deleted] untracked
bookmark: staging@origin [updated] tracked
% jj log
@ zl feature-2
│ last work on 2
○ r
│ more work on 2
○ u
│ work on 2
○ n
│ last work on 1
○ y
│ more work on 1
○ m
│ work on 1
│ ◆ zn staging
├─╯ feature-1
◆ k
│ some older feature
~
We can see my line of work is still based on the old staging head, k. And
we cannot simply rebase the whole line on the new staging because we have
commits m, y, and n that were squashed and are alreadin included in a new
commit in staging.
We also don't know where the old feature-1 ended! The bookmark was deleted
upstream and so disppears downstream too.
the solution (for now)
Until I find a better way, here's my flow.
tldr
% jj bookmark untrack feature-1
% jj git fetch
% jj abandon 'trunk()..feature-1'
% jj rebase -o 'trunk()'
details
Before I run a jj git fetch (or if I did it and see my situation, jj undo to go back), I'll untrack the bookmark for feature-1.
% jj bookmark untrack feature-1
Stopped tracking 1 remote bookmarks.
# this time fetch doesn't remove local bookmark since it's untracked
% jj git fetch
remote: Enumerating objects: 4, done.
remote: Total 3 (delta 1), reused 2 (delta 1), pack-reused 0 (from 0)
bookmark: feature-1@origin [deleted] untracked
bookmark: staging@origin [updated] tracked
# this cleans up our orphans and the no longer needed local bookmark
% jj abandon 'trunk()..feature-1'
Abandoned 3 commits:
ntqtoxkt 809d7841 feature-1 | last work on 1
ylqsrmrk a60cadbc more work on 1
mvkzrukx 094437d1 work on 1
Deleted bookmarks: feature-1
Rebased 3 descendant commits onto parents of abandoned commits
Working copy (@) now at: zlqqtzyr 525ce6ea feature-2* | last work on 2
Parent commit (@-) : rsssuzkr b935e485 more work on 2
Added 0 files, modified 0 files, removed 1 files
# now all we need is a rebase
% jj log
@ zl feature-2*
│ last work on 2
○ r
│ more work on 2
○ u
│ work on 2
│ ◆ zn staging
├─╯ feature-1
◆ k
│ some older feature
~
% jj rebase -o 'trunk()'
Rebased 3 commits to destination
Working copy (@) now at: zlqqtzyr 6743a2db feature-2* | last work on 2
Parent commit (@-) : rsssuzkr 39e54068 more work on 2
Added 1 files, modified 0 files, removed 0 files
% jj log
@ zl feature-2*
│ last work on 2
○ r
│ more work on 2
○ u
│ work on 2
◆ zn staging
│ feature-1
~
And voila! feature-2 is pointed at staging and we have no orphan commits. We
can stack PRs ad infinitum with a clean linear history. I plan to make a
helper script jj-handled-merged-bookmark which does just these things in one
command.