Switch to Jujutsu already: a tutorial
91 points by keybits
91 points by keybits
I don't get it. All those sound like anti features to me, and a messy way to work. Maybe git adapts to my mental model better
I think of it as a shift from taking each commit as the unit of work and the code files as the object of manipulation to taking the entire series of commits as the object of manipulation. Some people already use git this way, squashing and rebasing aggressively, but jj makes it easier.
There are two camps, each commit is atomic and represents work, and a series of commits are a change and the intermediate commits are just checkpoints to be squashed or a merge-commit at worst.
I find it really hard to convincingly explain why Jujutsu is nicer to use because several features sound scary or weird in isolation but work together to make a great UX:
If it still sounds weird, I encourage you to try it on a pet project for a couple days. Most people seem to have one of three reactions:
I’ve no doubt some people try it earnestly and decide it’s not for them, but that doesn’t seem to be the norm.
I tried it, based on the Klabnik tutorial, and I was struggling. My mental model now is that jujutsu
is two version control systems; one for managing the local unpushed commits, and one (basically git
) for managing the commits already shared with other developers.
Git may have its problems, but once I understood it's object model, everything clicked into place. And yes, I'm aware that it's actual object model is a lot more complicated than its conceptual one. So far, I haven't had this experience with jujutsu
. And I'm a bit worried that it's so aggressively pushed as a "succesor" to git
.
Keep in mind that when using git, I treat rebase
and cherry picks as the occasional operations they're supposed to be. git
works best when it has the full merge history available, and I don't think rebase
was ever supposed to be a core part of the workflow.
pijul
makes a lot of sense to me, but I've yet to use it in a project myself. But I think its failing might be that it's even more "git-like" than git
.
git works best when it has the full merge history available
I used to work on source control. This is actually not true, because (among other things) having a full merge history leads to the likelihood of what are known as criss-cross merges: situations where there isn't a single nearest common ancestor/merge base between two commits. (Merge bases are a crucial piece of information for many source control algorithms.) Criss-cross merges are a pain to deal with, both within the version control system itself and within tools written on top of the VCS. As a result, many tools simply consider criss-cross merges to be outside of their design parameters.
In contrast, a linear history can never have criss-cross merges, greatly simplifying many algorithms.
In general, linear history is friendlier for both tools and humans.
edit: to give an example, a tool I helped build for work needs to load files from the merge base of a commit so it can ensure that "blessed" files don't change in nontrivial ways. The tool simply (and correctly) considers criss-cross merges to be out of scope: https://github.com/oxidecomputer/dropshot-api-manager/blob/bb71488cfa2f642b232ef619bbb3d2c287c8e336/crates/dropshot-api-manager/src/git.rs#L22
Opting into a history full of merges means imposing a burden on the entire ecosystem.
There is no total ordering of commits; that's just a fact of people working in parallel. I accept that pretending that such a total ordering exists simplifies things in people's minds and in code, but it doesn't simplify the actual problem.
I'm not sure what this has to do with criss-cross merges. There absolutely is a total ordering of when commits were integrated into main, and linear histories do simplify the actual problem.
git works best when it has the full merge history available
I think this is something on which reasonable people may disagree, and may be a function of what work they’re involved with. I’m happiest looking at a linear history of cleaned-up, well-described atomic commits. And, I’ve never worked on a project like Linux where a maintainer is pulling from multiple independent lines of development that may not have been recently rebased as a matter of course. As such jj is an ergonomic improvement for me, having previously spent a lot of hours doing things in git rebase -i
.
I see. In my case, i don't rely in other than the base features (commit, branch, remotes), don't edit history often, and normally --amend is enough if I need it. What i see is that people happy with it do an awful lot of local history editing, maybe that's the big point
Well, since your working directory is auto-commited, everything you do in jj is technically local history editing.
As a side effect, this makes history editing intuitive/easier and changes your behaviour in subtle ways.
In git I tend to avoid history editing, even when I think I'd possibly benefit from it in the future, because I can't be bothered to google the commands required for what I want to do (It's rare enough that I don't keep it memorized). In jj, the commands are intuitive enough that I can figure it out on my own, so I just do the edits when it makes sense.
Then there are several small things that I really appreciate, like not having to create branches anymore because most of them was created for the purpose of creating a PR anyway (I just do jj git push --change @
), or not needing git stash
due to the auto-commit feature.
It's probably easier to start by comparing mercurial to git, and then take the learnings and apply them to JJ. The undo/redo feature just by itself is worth it (although if you're a git expert, may be less relevant), and you need to do a rebase in JJ to see how much better it is.
I can't even count anymore the number of bogus commits jj has prevented me from making. I just edit the thing I'm working in place and push back to github.
Might just be the 4am of it all, but this is the first post I'm seeing that's motivating me to actually try out jj
Edit: specifically the mutability of commits and conflicts stuff
Friday I rewrote and force pushed an entire repo (to scrub a secret). You can easily find the point of conflict and fix that after which the change will propagate automatically.
once a secret is out there, for any amount of time, you should consider it compromised and have a workflow to create and propagate a new one.
you cannot rely on force pushing a scrubbed repo since there are bots searching for these things being pushed to public repos all the time
Interestingly, I think many of these bots must not be very high quality. I recently had a breach in my DigitalOcean account where an attacker was able to run up $50 of charges before I stopped them (I got lucky and saw the slew of "VM created" emails right before I went to bed).
It turned out that they'd found an API token that had been sitting out in the open for about a decade before anyone noticed it. It just so happened that it was hidden away in a feature branch instead of the main one. Which suggests that a lot of these bots are only scanning the main branch.
As you say, though, I would still consider a secret in a side branch leaked, even if bots seem to be less aggressive about scanning those.
The secret has already been deleted and will be recreated. I just want to get it out of the repo for my own feelings of tidiness.
If you are using GitHub, be aware that force pushing might not be enough: https://trufflesecurity.com/blog/how-to-scan-force-pushed-commits-for-secrets
If I read that correctly, it looks like even deleting your repo would not be enough. Does that match your read?
More or less right.
If you push a secret, rotate the secret. There are so many ways that secrets can be leaked that planning for it is an essential part of a good security posture.
This is the right thing to do. Once it's out, you never know who has it. You absolutely need to rotate it, don't risk it.
I discussed it with people as well and it also wasn't clear whether it's ever really possible to get them out of the reflog and other git specific files using any measure of garbage collection. And even then whether or not Github itself might have proprietary implementation details that would cache stuff and leak it somewhere else on the site.
So yeah, the secret has also been rotated but I thought it nice to remove it from history entirely as far as I could.
I started to use JJ a couple of months ago. I like it and will continue to use it. I think it's a step up from Git. But for me it hasn't been a huge improvement, and I don't think they completely have solved Git's usability problem of having to remember what flags work with what sub-commands. In JJ, I still constantly need to read the --help
page for different sub-commands to look up if I'm supposed to use -t
(to), -f
(from), -c
(change), -r
(revset), -s
(source), -d
(destination), etc. I suppose there's a reason behind all these different flags for different sub-commands, but I haven't understood the logic behind it yet, so for me it appears as a bit random.
This has been my experience too, and it is perhaps somewhat exacerbated by the options changing over version (though, I do understand this is the cost of using a rapidly evolving tool).
I think that using a UI fixes this, because a rebase is just drag-and-drop instead of remembering to and from.
I just use jjui
for that, I haven't needed to remember any flags as I only use the commit command in the cli.
The heuristic that seems to work for me is
-r
if I'm specifying a single revision argument, like to log
, diff
, etc-s
and -d
if I'm specifying two revisions, I can only think of rebase
here-t
and -f
if I'm moving around changes within commits, which is maybe just squash
?Although tbh since the CLI help is quick enough to read it's not much of a bother to check it for a less-used command.
You also have -c
in: jj git push -c @
. And no flag in: jj show @
. And -r
that you mentioned in like: jj split -r @
I haven’t checked the docs to confirm, but -r
is (generally?) short for —revisions
(note the plural, it’ll accept a revset expression) where -c
is short for —change
which is singular.
And yes this does mean that when you pass -r
to describe
you can edit the description of multiple commits at once for example
Ahh yep, I have an alias for pushing which always does --all
, so I don't run into that one.
I do get caught out by diff
and split
taking files as positional arguments when I always expect them to take a revision (I almost never want to pass a file path to them).
whenever it gets into a weird state because I fat-fingered something, I have my trusty alias, f**kgit, that deletes the .git directory, clones the repo again into a temp folder, and moves the .git directory from that into my directory, and I’ve managed to eke out a living for my family this way.
This sounds absurd. I'm curious what that weird state really is.
What if you had stashes? Branches? Anything not synced to a remote??? Very bad advice. Not trying to be a jerk, but people read blog posts and assume authority. The author should at least call out what the side effects of deleting the .git directory are for those who don’t know better. It is NOT a “git rebase —abort” which I guess is how it’s being explained here?
At my previous job, we versioned the app configuration with git on a "shared server" where everyone was amending files and commiting as root with their name in the commit.
One day I found out that people had been casually making commits in the middle of an interactive rebase. Like, for weeks. And git casually reminded "hey, don't forget to git rebase --continue
". I consider myself not that bad with git but in this situation I simply nuked the repo and granted it with an "initial commit" again 🥹
Yeah I've fat-fingered a commit in the middle of a rebase before. Situations like that are why I wrote fuckgit
. I don't tend to store state in my repo, so it worked for me.
In git it can happen because of some rebase conflicts which force you into the special state.
The good news is that JJ doesn’t have these special states. Conflicts are the normal thing here. You can deal with them calmly and safely
It was definitely when I was rebasing things, trying to fix the conflicts, and ended up in such a mess that it was quicker to just rebuild the repo.
I use git add -p
constantly—and so I love git's distinction between the working directory and the index. What do people who use jj do as an equivalent? (I tried jj split
, and it gave me a weird quasi-curses interface. Maybe that's because I don't have jj configured much at all. Update: I tried making Neovim my diff editor, but then jj split
gives me an empty buffer when I try to edit the diff. I'm sure that this is PEBCAK, but it's still annoying.)
Quasi curses is the default, that’s right.
I tend to work one commit ahead and squash the bits I want into the parent, which I build up over time, rather than split.
I tend to work one commit ahead and squash the bits I want into the parent, which I build up over time, rather than split.
I'm not sure what you mean by "work one commit ahead." Do you mean that you create empty commits for each distinct change that you want to make in a file or project?
You don’t need a commit for each distinct change because after each bit of work, you squash it into the parent, which makes the working commit empty again. You build up the parent bit by bit just like you’re building up the staging area.
Also, though I haven’t tried Jujutsu, I remember once reading that it exposes the parent of the working copy commit to Git as the index / staging area. If that is correct, then if you don’t like jj squash
for whatever reason, you could alternatively move changes into the parent with git add -p
or your editor’s equivalent. Jujutsu will detect those changes the next time you run any jj
command.
Edit: I tried to confirm my memory of this feature, but I didn’t find anything about it in Jujutsu’s documentation. In fact, the Git compatibility page contradicts this by saying “The staging area will be ignored.” Though another part of that page just says “TODO”, so maybe it’s that page that is out of date.
Jujutsu updates the git index so git diff
shows the same thing as jj diff
as much as possible.
But, it never reads the git index. If you stage changes, jj simply overwrites it next time you run a jj command.
Not 100% sure as I haven’t done it, but I suspect interacting with the git stage on purpose like that while using jj would be asking for pain. The parent is the git head, not the staging area. I don’t think it uses the staging area.
Interacting with git in a colocated jj repo is fine, I do it a lot (I use lazygit sometimes to switch branches or whatnot) and I've never had any issues.
Staging is probably different since jj doesn’t use staging. Check out jjui btw.
jj just unstages everything when you run a command, IIRC. I don't use staging in colocated repos as I know jj will ignore/reset it, so I'm not entirely sure, but it definitely isn't asking for pain. At worst, it'll do nothing.
I do use jjui a lot, it's great (I mention it multiple times in the post as well).
Unstaging everything you’ve painstakingly staged sounds pretty bad! That’s the kind of thing I meant.
crespo is correct, here’s a longer form explanation of the workflow https://steveklabnik.github.io/jujutsu-tutorial/real-world-workflows/the-squash-workflow.html
Thanks for the link (and thanks @davidcrespo for the explanation). What you describe in that part of your tutorial definitely feels more familiar to me (in a good way), though for the full git add -p
experience, I'm back to the TUI.
Yeah, I should explore the alternatives for the TUI… it doesn’t bug me but for those it does, having active alternatives at the ready would be nice.
By the same token, I should give it a chance. The UI for git add -p
is not a masterpiece of design. I'm just used to it.
Oh yeah, part of why I don’t use the TUI a ton is that you can pass a file or directory to squash, so you only need the TUI for “staging” part of a file, which has been rare for me as of late.
"weird quasi-curses interface" is actually quite nice!
I usually work differently now: I create several empty commits and then just switch between them when I want to make changes. But I know that it doesn't feel right for a lot of people.
Another common approach is to use jj absorb
(see https://steveklabnik.github.io/jujutsu-tutorial/advanced/simultaneous-edits.html) — I should probably do this myself
"weird quasi-curses interface" is actually quite nice!
Thanks for your answer, but we will have to agree to disagree about the quasi-curses thing. :)
I'll think about the other two approaches that you mention. It's absolutely possible that I might grow to like them, but I'm not sure I want or need to put in that effort. That is, so far, jj requires me to change a ton of my patterns and I'm not seeing much benefit from doing so. That's not meant to be a criticism of jj. But all of the recent jj-boosting on Lobsters has made me realize that I like git just fine.
Yeah, set the diff editor to meld
for a GUI experience.
Thanks, and I may try it, but I don't actually want a GUI experience. I want to stay in the terminal, but I find the TUI default awkward.
In a way it is mostly how I use git already. I recognize it may be cleaner so I need to check jj out. I use rebase interactive often and it is the most clunky of the things I use a lot.
One thing that is important for me is git gui, it is my preferred way of making commits (I also do a lot of amends), because I can easily commit things on per line basis. I use also gitk a lot to shuffle branches. Sometimes I would like to be able to edit the diff directly.
I don't understand things around this paragraph:
Jujutsu calls these labels “bookmarks”, and they correspond to whatever git uses to tag branches. Bookmarks are what you’ll tag your commits with to tell git what your branches are.
"Tag" in this context is not the "commit tag" for sure? In jj if you have my-branch coming of main, you have linear history, then it branches out of you commit to main. How is this different from git? Git log will also show a linear history. Unless when you commit to main my-branch automatically rebases, but it is not like that AFAIU with jj and it would probably make things awkward for changes on remote.
git gui
I use gg to change descriptions and squash commits together. Moving stuff around I use the command line though I think gg does support drag and drop.
Does anyone have a good tutorial about how to use JJ in a git team without bothering others?
Probably Using named branches in jj and Working with remotes.
The biggest giveaway is the auto-named branches, which I know some people don't like, but you can just give them meaningful names yourself.
Yep, really the only tells are the weird generated branch names and frequent force pushing. You can just not do either of those.
What others said, use bookmarks in the same way others are using “branches.”
Here’s the other thing though. When JJ tells you something is immutable, RESPECT IT. It gives you the tools to punch through that. But don’t.
Finally, if you’re required to use tags, you’ll need to revert to git for that (including the push).
Do these things and your collaborators will never have ANY idea you’re using jj. It’s perfectly invisible.
And it’s a fucking amazing experience.
Enjoy!
I'm using it as is and I don't think I've really bothered others much. But also we don't use collaborative branches, so others aren't really exposed to the autogenerated branch names.
I’d love to see the in-depth guide on JUST merge conflicts, since that’s such a common pain point for juniors.
I get that jj allows you to“resolve them later” which sounds great, but what does resolving them look like? Is that part actually better?
At worst, doesn’t deferred conflict resolution mean the code won’t build, or (worse) otherwise function incorrectly? A broken build would force you to resolve conflicts immediately. I could see juniors wasting tons of time developing against half broken states because they are “allowed to” but obviously that code isn’t representative of what ends up on the main branch.
Not saying git’s forced immediate conflict resolution is perfect, but I’m not (yet?) convinced that jj “solved” the problem once and for all. I could be missing something, but I struggle to see how…
It won’t build if you’re on that change, just like if you were in the middle of a merge or rebase in git. The difference is that it lets you work on other things, if you’d prefer, before you do the resolution.
Why is this nice? Well, imagine I have two branches, and I fetch my main branch from upstream. Maybe I rebase both branches, and one of them is conflicted, but the other isn’t. I can choose to work on the one that’s not, and defer fixing the one that is until I feel like it. With two branches this is no big deal, but if I had three, where two are conflicted, I can’t actually know how many are conflicted and how many aren’t until I resolve at least one of the conflicts, whereas with jj, I have a “rebase-all” alias that rebases all of my outstanding branches in one command, and I immediately know which ones are okay and which ones aren’t, and can choose what to do next.
I’ll file it away in my head to write more about this though, it’s not an uncommon question, and for good reason.
The improvements are about managing the conflict resolution process. JJ doesn’t make major changes to actually resolving the conflicts. There are either two or three versions of the file to compare, and it will pass them to your favorite resolution tool, just like git. Or you can open the working version of the file and it will have conflict markers in it you can resolve manually in your editor. When you’re done fixing, jj notices the lack of conflicts.
One helpful difference from git is that you can do this in as many incremental sessions as you want, and never block any other operations. Like, I can fix half the conflicts today, work on other unconflicted changes, and come back to finish tomorrow.
The other big difference is once you’ve fixed a conflict, jj automatically rebases every descendant for you and the conflict disappears from them too.
This piece does not have an auspicious start:
As all developers, I’ve been using git since the dawn of time
Guess I'm not a developer, because my first experience with svn. Git has only been around since 2005.
Needless to say, I just don’t get git. I never got it, even though I’ve read a bunch of stuff on how it represents things internally. I’ve been using it for years knowing what a few commands do, and whenever it gets into a weird state because I fat-fingered something, I have my trusty alias, fuckgit
While "I couldn't figure out git, but I like jujutsu" might be a good indication that jujutsu is easier to use than git, it also means that this is a very bad piece to read if you want a mental model for either.
Git has only been around since 2005.
So you could have 20 years of developer experience and still have used nothing but git.
Where I work, developers with 20+ years of developer experience is rare.
Mine was CVS, I just didn't realize you SVN newbies would take things in the post so literally.
It's written in a pretty literal-sounding way. Guess I missed the intended facetious tone?
If so, you may want to reconsider the phrasing. It makes you sound like someone who is a lot less experienced and really has only encountered git. (There are assuredly many such people...)
So far I've considered jujitsu not worth the time to learn, but the "no stashing" perspective makes me more curious now.
I've lost stashed code a bunch of times due to weird stashing behavior user error, and I can see how committing the in-progress work would fix that. The only reason I didn't do that with plain git was the headache of dealing with those in-progress commits once you need to work on them again.
Recommend setting up a daemon like Watchman to help jj autocommit to an unnamed/unlabeled commit on every file change, to get "infinite undo": https://news.ycombinator.com/item?id=45426787
sounds a bit of extreme. I worry that op log
would be filled with hundreds of micro-changes and it would become difficult to find the real key-points.
oo I'd not seen this tip before, thanks for mentioning it!
,*
One other handy (imo) thing I do is add this to the .gitignore ... so all files pre-fixed with a comma are ignored.
I mainly find it useful when working with secrets / other stuff I don't want stored in the history - though I could see it being even more handy if using jj + watchman too!
I really dont understand the jj hype
Feels straightforward to me:
That’s the Rust world-domination playbook: fundamental improvement (memory safety) + broad compatibility (ffi & llvm toolchain) + nice things (https://robert.ocallahan.org/2016/08/random-thoughts-on-rust-cratesio-and.html)
I‘m very interested in trying Jujutsu but I like the integration I have with Git in the editor and potentially using a Mac app as a GUI for it. Is the first part maybe solved by using co-located repositories in jj? Is anybody working on a GUI app?
https://github.com/gulbanana/gg ?
As well as,
I’ve bounced off jj a couple times, but this article is making it click! Also, jjui is really frickin nice. I’ve gotten used to using lazygit from inside neovim, and having a visual editor for it is going to make switching costs way lower. Also great that we don’t have to —colocate all the time anymore.
Only downside is that jjui doesn’t work inside of Neovim. Just whipped up a plugin for it, gonna figure out some sensible defaults and hopefully publish it in a day or two.