Hacker News new | past | comments | ask | show | jobs | submit login
Git and Jujutsu: In Miniature (lottia.net)
89 points by todsacerdoti 7 months ago | hide | past | favorite | 72 comments



> git really forces me to make a lot of decisions I don’t actually care about. How will I save my WIP while I’m off on this quest? Do I want to juggle stashes and my working tree, or throw commits around? How will I get a commit where I want it? Do I need to come up with a branch name? And how much of all this needs to just sit in my head or pasteboard, lest I forget what I was in the middle of?

I am hearing this argument a lot, but I don't really get it. My grocery store forces me to make a lot of decision when it offers me 7 kinds of ketchup and 20+ kinds of cereal, but no one says "I am avoiding large grocery stores because they force me to make too many decisions". If you are feeling adventurous, try something new. If you just want to get some food and go, grab your favorite brands. And yes, a smaller store won't force you to choose, but there is good chance you won't like the food you buy there.

(For the problem described, I'd do as following: Save WIP work as temporary commit. No, you don't want to juggle stashes. You can do interactive rebase with "edit" mode and then cherry-pick extra commit - this'll let you make sure test passes after rebase too. Yes, you need to come up with branch name, any will go, like c_s_test. You will need to keep the name of previous branch in your head, but given author uses "develop" it should not be too bad. But this is all my preferences, if someone wants to do something else - more power to them.)


> offers me 7 kinds of ketchup and 20+ kinds of cereal, but no one says "I am avoiding

They even write articles about decision paralysis and being overwhelmed by choice and all that, so yes the exact same issue apparently exists in that domain


> You will need to keep the name of previous branch in your head

Not even that - "git checkout -" is like "cd -", but for branches.

I have it on a short alias - cor. I do "cob asdf", do some work, then "cor". (I have 28 aliases for git alone, 2 or 3 letters each. And I still get to autocomplete any options when necessary, thanks to https://github.com/cykerway/complete-alias).


Don't you think having 28 aliases for the git incantations in your worflow is a clear signal something is wrong with git?


This argument makes sense only if you think Git has, like, 2 uses.


It's mostly because of my attempts to minimize keypresses due to RSI. I would end up with similar, albeit lower, number of aliases if I were to use jj.


Magit (for Emacs) is geared towards one-key-per-action/flag. E.g. `c` when you are in the Git Status window.


Meant to say that `c` triggers the “commit” interaction. Just `cc` if you want to commit without options.


Don't you think having 300+ commands in /bin is a clear signal something is wrong with Unix? No?


There are certain cases that - isn't allowed in though, like git branch -d.


I've only ever needed it for checkout, but in other commands I believe the full syntax @{-1} should work.


> no one says "I am avoiding large grocery stores because they force me to make too many decisions"

I say that. I 100% prefer going to the smaller store if I don't need anything "fancy" because the large offerings stress me out.

That said I'm fine with git and don't care about Jujutsu, so I don't know what that says about me :)


But jj is as powerful as git. The choices are meaningless.

If you have one piece of code that does what you need with four conceptually different types of state that interact in various ways when state is mutated, and another that does it with one type of state and a lot fewer interactions, what's the better architecture?


A car and a bicycle fundamentally get you from point a to point b.

Is the decision which you take meaningless if your destination is 50 miles away?


You missed the point. The analogy was: Git has more choice and like having access to exotic Ketchups that can be nice. But jj can do anything that git can, so in the metaphor, git gives you a ton of choice but jj gives you all the same flavors.

Now if jj makes you assemble several pieces and makes it more difficult to get the flavors, while with git you could just grab the correct bottle, you could argue that the choice git offers is not meaningless. But that's a hard sell.

I think by now, there is pretty good evidence that the complexity of operating git is incidental. It's the result of badly designed primitives the user has to interact with (index, stash, commits, branches). As always, once you have internalized the primitives and their interactions, the complexity (mostly) disappears. But if you frequently train new students in using git, you stay painfully aware of it.


Or you're missing the point ( • ‿ • )

I think you've straight up reinforced my example. The end result is the same, but the way to get there is different.

Depending on what your destination is, a different tool might ultimately turn out to be better for the job. Even if they're both representing their state in a compatible manner on disk.

You're probably just not realizing that the way you interact with git isn't necessarily how everyone does - and that other people might have usecases that you've never thought about... Because you're not in their situation. So you're only seeing one way to reach the destination, which is why the one tool always looks like the correct choice.

To be clear, my current day job really wouldn't get any value out of any of these features either. We're doing simple trunk development on various repositories with roughly 5 authors providing commits.

Almost no merge conflicts, the most anyone does is git blame to figure out when the line was edited etc. under these circumstances, commits are essentially fire and forget and you're fine only interacting with the repository through your ide even.


Nobody, including you, have produced an example of a workforce that's substantially easier due to gits model.

Either gits design is unnecessarily complex, or there should be a pay off to the complexity somewhere. Some flavor you can't get, or can't get easily, with jj.

I wouldn't need these features either, but I believe teaching jj to beginners will be easier than teaching git. This is further evidence that the problem is git, not the problem space and the need for different workflows: If a different set of concepts serves both power users and beginners better, at what point do we stop pretending that it's all just tradeoffs?


I’m working solo on a project, with trunk based development. I still find jj nicer to use than git. I do do pull requests but just to ensure that CI passes on every commit.

Everybody is different, and that’s okay.


This is the point!


Almost anyone (above the age of 5) on this planet is familiar with food, not the case with `git` and it's obvious non-obviousness


I would argue that any professional developer should be familiar with basic manipulations in git...


You would be surprised. Google uses "g4" (e.g. "p4"-like), much like many game developers like me. It's not like we don't use "git" (we have devops/infra groups) - but lots of folks use Perforce/PlasticSCM/other systems that are not "git".

Just because "git" is super-popular, does not mean that ANY PROFESSIONAL DEVELOPER SHOULD BE FAMILIAR with it... Sorry but no!


Well if you can use jj, it means that you work against a git repository, doesn't it? Or does jj work with g4 repositories?


I guess I have to give `jj` a try then!


A better analogy would be: You have 7 ways to the ketchup section with varying lengths.


Some will lead you into a universe from which you will never return.


That can work too... "To get to the ketchup section, I can go via fresh produce row or via bread row or via canned food row... Which path do I pick? Going via produce row is faster but I need to pick up some hot dog buns..."

On the other hand IKEA did design their showrooms so there is exactly one recommended way to get to any section. I find this highly annoying, but they keep doing this, so I am guessing some people like it?


> I am hearing this argument a lot, but I don't really get it. My grocery store forces me to make a lot of decision when it offers me 7 kinds of ketchup and 20+ kinds of cereal, but no one says "I am avoiding large grocery stores because they force me to make too many decisions".

I say that all the time. I hate decision paralysis. I'll specifically buy stuff in ways (such as online order + pickup) that I don't have to look at tons of different products.


When people find out I use jj (Jujutsu), I often get asked some version of "how's it better than Git?" And while I can list a number of reasons why I think it's better and you could argue whether or not that reason is contrived or not, I think it's all missing the point.

I think it's better -- in the most pessimistic case -- to look at jj as reframing how you think about branches and commits in the same way that learning a type of Lisp reframes your thinking even if you're a full time Python developer and have zero intention of ever using a Lisp.

The idea of shuffling commits around without fear, changing your working train of thought mid-branch, etc. is natural... mindless, even. It's one command away and you get so much muscle memory executing that command you just do it automatically. (There's no fear because `jj undo` undoes any operation you did if you regret it. Of course there a ways to undo N operations back and so on too).

I use jj full time now, but even when I periodically go back to using git (for older projects I don't have a jj clone out for), it has altered the way I look at my stream of work. I think there's value in that.

That's the pessimistic case. The optimistic case is you should be using jj because it's better and there's almost zero downside to doing it (your coworkers don't even need to know).

(This blog post was great, I just expect and already see some people focusing on the minutia of how to Git golf your way to achieving the same thing easily when that doesn't invalidate that jj is good, in my opinion).


> I think it's better -- in the most pessimistic case -- to look at jj as reframing how you think about branches and commits in the same way that learning a type of Lisp reframes your thinking even if you're a full time Python developer and have zero intention of ever using a Lisp.

For some reason this actually had me pause in fear. I've been using git deeply for years and find it very natural and mindless to use at this point. The thought that I might again be trapped in Plato's cave like I found I had been prior to learning Common Lisp is actually disturbing...

Now I don't have a choice but to give jj a shot


Bruce Lee has that quote: “ I fear not the man who has practiced 10,000 kicks once, but I fear the man who has practiced one kick 10,000 times.”

If Lisp’s “one kick” is the cons list, then jj’s is the commit: by using them for everything (they replace, at least, the git stash, index, and working copy) you actually get really fluent in manipulating them and they become more powerful than special-cased tools.


I loved git for a long time. I never understood the folks that said git’s UX was too hard.

I’ll never go back after jj. Be warned, haha.


Can’t wait to hear your experience. I felt the same way and I’m a convert now.


I didn’t know you were a jj fan! I’ve been a convert for a long time now. I fully agree with what you’ve said here.


I am! I think I heard about it through you somehow, actually. And then I kept getting PRs from people I respected with weird branch names and thought "what the hell is going on" and both of these things pushed me to look into it.

I switched cold turkey in one afternoon after reading your tutorial in about 30 minutes and never touched Git ever again (except in the very rare cases noted in my previous post). And also... bisect.


That’s awesome, it’s such a small world.


This subthread has convinced me to try it, I'm going through your tutorial now, thanks!


Nice! I’m working on a second version that reads very differently, it’s taking me a while though. Here’s the opening: https://gist.github.com/steveklabnik/53b51724920dac76fc623d9...


Thanks for your efforts. Tutorials like this is a must-have on this new frontier (for many of us).


The crazy thing to me having made the change is how utterly fearless I am doing long chains of that I would have double- or triple-checked with git. Reordering commits, fixing an earlier commit, even doing these things with multiple unmerged ancestor branches… are all trivial. I don't even have to think about it. There's no case where I'm dumped into a conflict state that must be resolved right here and right now (and where I don't even get to use any of the tools in my VCS until I fix it).

It's so fucking freeing.


What do you mean by "jj clone" ? I assumed it was possible to start using jj on an existing Git repository, and continue using Git (and jj) afterwards. Isn't this the case ?


It is. I assume they just mean an existing repo that hasn't had jj bootstrapped in it. Which is trivial, but maybe you just don't want to do it for whatever reason.


Why won't this work to achieve the exact same thing?

  # on branch `feature`
  # code code code
  # whoops! Test missing
  git add wip/ changes/ && git commit -m lol
  git checkout develop
  # write the test
  git add test/ files/ && git commit -m lol2
  # back to WIP branch
  git checkout feature
  # open vim and put the lol2 commit in the right place
  git rebase -i develop
  ?lol2<cr>dd/3e7<cr>P:wq
  # undo the original lol commit
  git reset HEAD~1
 
You could use git stash instead of making and resetting the `lol` commit but i just prefer to do it this way.


You can, but this sort of glosses over all the ways this can quickly get confusing.

I’ve had this exact scenario happen where develop had multiple commits and the rebase caused a chain of having to resolve rebase conflicts in each commit. Then I fuck up one of those and I have to completely start over. And during this process I’m “stuck” in the rebase state and can’t use regular tools to manage and track my work.


The sibling comment mentions rerere. But you can solve this even without that.

I assume you mean develop has a few extra commits from remote meaning feature branch diverges a few commits earlier.

Let's say the commit where feature branch diverges from has hash `abc`.

So what I would do is

  git checkout abc
  # write the test, then suppose hash with test files is xyz
  git checkout feature
  git rebase --onto xyz abc
You will now have test commit as root of feature branch. And now you can move it wherever you want just like in my original answer.


Yes, I am familiar with git rebase. I can virtually assure you that everyone here evangelizing jj is aware of it too. Most of us were git power users. Steve Klabnik certainly was. I was too.

Actually using git rebase is a pain in the ass when things don't go according to plan. Long chains of rebase conflicts suck. Making a mistake during a rebase sucks. Stashing sucks when you forget what branch the changes were on. Stashing also sucks when you have stash apply conflicts.

None of this is ever a problem in jj due to the fundamental design.

And you get some additional superpowers too. Today I was working on a feature that needs to be merged and deployed one piece at a time. There are (right now) four separate pieces, each comprising multiple commits. In jj it's just one linear chain of commits on top of `main` with four branch names assigned to the relevant interim commits. Each of those branches has an associated PR. When I need to make a change to something in one of the commits early in the chain, I just do it. If I need to make a new commit somewhere else in the middle, I just do it. It takes zero effort, and all later commits are automatically kept up to date with changes earlier in the tree. When I push, all the PRs are updated. When I end up merging the first of those four stages, there is zero work necessary to fix up the later three branches.

Try doing that with git.


rerere in git config helps, but if jj just does it all more sanely, it's probably worth learning for people who don't yet have the git-fu.


Rerere sort of helps, sometimes, but like so many things with git (the stash, wip commits, fixup commits, squash rebasing) it’s yet more hacky fixes over the design being not quite right.


The git example break away from how I use git immediately. I've been burned enough times by stash to not ever use it anymore - just create a new branch immediately.


I'd be interested to know how it's burned you. I use stash a lot.


Forgetting which branch a set of changes was originally stashed on, having stash conflicts (and forgetting to drop the top of the stash once you've fixed it) are common.


I've probably just learned to ignore a lot of that. Stash conflicts are annoying. And I have a long list of stashed changes that I should just purge at this point.

All the discussion here has me very interested to look at jj, though. Although most of my git interaction these days is through magit, and changing that will probably be hard.


This is all enormously complicated for no reason. If you find the missing tests, just send a PR to add the tests immediately. Why go through all the weirdness of stitching into this development branch? Just add the tests and then rebase your WIP branch on it.


I don’t know what is supposed to be more straightforward about Just Send a PR. This is a short process broken down into every little step. And every step is just within git/jj. Just Send a PR would involve more steps and indirection.


I mean that's the point of the article, no?

With Jujutsu it's not at all complicated. Sure, you may not agree with the example (and I would say its a little contrived), but rebasing into history to keep a clean progression of commits in a feature branch that is unreleased is something that many people are keen on.

Jujutsu also has a bunch of other really useful features like `jj fix` which can run a code linter over a linear commit history (in parallel) and integrate the changes into the commits that should contain the change. This avoids a litany of 'fix formatting' style commits littered through your history.


> rebasing into history to keep a clean progression of commits in a feature branch that is unreleased is something that many people are keen on

To give some specific examples, "many people" includes popular open-source projects like Linux and Git itself, as well as large tech companies like Google and Meta, which employ "trunk-based development" (see e.g. https://trunkbaseddevelopment.com).


This was just one example, maybe not a perfect one. I found it to be relatable, though.

Sometimes the "add the missing tests" step depends on other work on your branch.


I think the pedantic answer is "if so, your branches/commits are too big."

In practice, what you describe is often the case, and having a quick shortcut to do "stick $small_fix in main via a separate branch, but yoink that commit into my branch before that first branch lands on main so I can keep working" would be very useful.

Historically, though, I usually addressed this need by copy/pasting the code into separate commits in both branches, and being careful to keep the $small_fix-to-main code and its twin in my larger branch un-drifted and textually discrete to make the eventual merge conflict a rubber-stamp to resolve. Ugly, yes. Maybe I should feel guilty about it, who knows?


That sounds like a lot of work.

And it is a lot of work. Even in the best case. I know it is because I used to do exactly this. But more often than not the small fix ends up getting affected by your changes in the branch and then you're in rebase hell again.


Always blows my mind how much time we waste talking about Git instead of just using it properly. Cheatsheet:

- It's distributed horticulture. Your tools come saran-wrapped to the tree.

- Rebase flow. Keep your branches linear.

- Merge commits are BS

- There is no source control problem that rebase, rebase --onto, reset, and reflog can't fix

- Topology is everything. Add this to your aliases: https://stackoverflow.com/questions/1838873/visualizing-bran...

TL;DR: Keep your branches up to date. Feature flags are a huge red flag. Constant conflicts and the inability to have long lived branches is indicative of poor architecture and project layout.

Use "rerere" as a bandaid.

Why won't anyone think about the release managers.


To make it overcomplicated for git just to fake a point doesn't do it for me.

I am yet to see a real value to use JJ. Those kind of built-up posts I am seeing about JJ on HN tend to push me in the opposite direction.

1) do your test in your current branch, it's related to what you are doing. 2) commit, co master, co -b newtestbranch, PR it, merge in your branch after. You're going to squash rebase at the end anyway...


> To make it overcomplicated for git just to fake a point doesn't do it for me

I have experienced this exact scenario hundreds of times using git over the past two decades. I’m in the middle of some work, I realize I need to task switch to do some other work, then the work that was mid-flight should depend upon that interim thread of changes.

In git it is doable but it is a process, and one that is easily derailed (oh god a chain of failed automatic rebases) that requires keeping state in my head. In jj it is so trivial it doesn’t even register that I did something interesting or special, except when I remember how awful it would have been doing that in git.

> You're going to squash rebase at the end anyway

No? I’m not?


It took me a number of years of working with git to realize it really really wants you to squash things. Especially if you're sharing branches.

git lets you create as complicated a commit tree as you want, and lets you do merges so you can have a separate commit where multiple changes come together, but realistically it really only works if you use it for a rebase workflow in your branches, squashing all changes into a single commit each time. Merge is an outlier when you need a more powerful tool to join branches but want to keep both of the originals still intact. But that shouldn't be your everyday workflow or you're guaranteed to end up with an unreadable, untraceable, and unmanageable commit graph.


Someone should let Linus know that git prefers this workflow. Someone should also let the maintainers of git itself know as well. Both happily employ a merge-based workflow.

What’s actually the case is that your workflow seems best with squash merging. There are a lot of project and teams where this is not the case.


I’m using worktrees for this. Makes more sense IMHO.


Yes, there are a million first-party and third-party bandaids you can use on top of git to try and approximate a sensible workflow. I was a heavy user of `git-revise`.

All of these workarounds just… don't exist in jj, nor do they need to.


It’s not a bandaid, it’s working on two features which should be done on two separate clones. Worktrees avoid having to fully clone the repository, which is cool, but I’d use multiple clones if this did not exist.


(But to be clear Jj has worktrees if you do like that workflow)


Whoops, TIL jj has worktrees!


I don’t think the article is overcomplicating the Git commands to falsely represent how complicated Git is. I can easily believe that in some contexts, a Git user really would need to do all the steps the author describes.

> 1) do your test in your current branch, it's related to what you are doing.

The article describes a scenario where you “feel reasonably assured that green CI will mean you’re on the right track, and you’ve been removing parts of the old parser as you introduce the new”. But this one parser feature you discovered was not under test. “The original parser is half-taken to bits,” so perhaps you already deleted the implementation of that feature, not knowing it was depended on. If the implementation were missing, of course you couldn’t test it on your current branch.

Okay, let’s say you didn’t delete the implementation yet – but the implementation calls some of the methods you already modified. If you write a test on your current branch, it would only show that your already-modified parser passes the test. You couldn’t be confident that the expectations in the test you wrote actually match the behavior of the old parser. If the old parser acted differently from your tests (even if it was due to a bug), you need to know about that so you can update callers of the old parser to not rely on that behavior any more.

To be confident that your new test accurately describes behavior you need to support, you need to run the new test against the “develop” branch.

> 2) … You're going to squash rebase at the end anyway...

Why would you assume that? Some teams prefer to express changes as a series of independent commits when possible, and they merge PRs while keeping those individual commits. My current team is one example. On such teams, keeping a Git feature branch’s history organized is a real need.

If you wonder why a team wouldn’t just squash rebase, I think the goals of keeping commits separate on the main branch are to make debugging tools such as `git log`, `git blame`, and `git bisect` more useful.


> are to make debugging tools such as `git log`, `git blame`, and `git bisect` more useful.

*actually work.

No point using `git bisect` if you switch to a commit that literally doesn't even compile.


If you have a commit that doesn't compile, it should have been squashed into another commit before merging the PR. Every commit should be in a valid state. I'm not talking about a full squash merge, just `git rebase -i` to make sure the history makes sense. The final branch history doesn't have to be the same as your development one.




Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: