Hacker News new | ask | show | jobs
by lytigas 497 days ago
I've read a few small overviews of jj. One thing that's off-putting as a git lover is that while git is truly append-only (except refs), jj seems quite "mutable" by comparison.

Say I'm messing around with the commit that introduced a bug, somewhere deep in the history. With git, it's basically impossible to mess up the repo state. Even if I commit, or commit --amend, my downstream refs still point to the old history. This kind of sucks for making stacked PRs (hello git rebase -i --autosquash --update-refs) but gives me a lot of confidence to mess around in a repo.

With jj, it seems like all I would have to do is forget to "jj new" before some mass find+replace, and now my repo is unfixable. How does jj deal with this scenario?

8 comments

As others have pointed out there's `jj undo` and other tools, but they all rely on the fact that JJ is less mutable than it seems.

Internally, JJ is still backed by an append-only tree of commits. You don't normally get to see these commits directly, but they're there. A change (i.e. the thing you see in `jj log`) is always backed by one or more commits. You can see the latest commit in the log directly (the ID on the left is the change ID, the ID on the right is the commit ID), but you can also look back in history for a single change using `jj evolog`, and you can see all commits using `jj op log`.

This ensures that even if you were to exclusively use `jj edit`, never made a new commit, and kept all your work and history in a single change, you could still track the history of your project (or the "evolution", hence the name evolog). It would be kind of impractical, but it would work.

The only caveat here is that, by default, JJ only creates new snapshots/commits from the working directory whenever you run the CLI. So if you made a large change, didn't run any JJ command at all, then made a second large change, JJ would by default see that as a single change to the working directory. To catch these issues, you can use a file watcher to automatically run JJ whenever any file changes, which typically means that JJ makes much more frequent snapshots and therefore you're less likely to lose work (at the cost of having a file watcher running, and also potentially bloating your repository with lots of tiny snapshots).

Note also that the above is all local. When using the git backend, Jujutsu will only sync one commit for each change when pushing to a remote repository, so the people you're collaborating with will not see all these minor edits and changes, they'll only see the finished, edited history that you've built. But locally, all of those intermediate snapshots exist, which is why Jujutsu should never lose your data.

> you can use a file watcher to automatically run JJ whenever any file changes, which typically means that JJ makes much more frequent snapshots and therefore you're less likely to lose work (at the cost of having a file watcher running, and also potentially bloating your repository with lots of tiny snapshots).

For example? Or should I create my own in C using inotify/kqueue? Is there a library for jj?

See the documentation here: https://jj-vcs.github.io/jj/latest/config/#filesystem-monito...

The default behaviour (i.e. `core.fsmonitor = "watchman"`) is to only use the file watcher as an optimisation — rather than scanning the entire folder every time JJ wants to make a snapshot, the watch keeps a list of which files have changed, and then when creating a snapshot, JJ only needs to check those files.

However, you can also add `core.watchman.register_snapshot_trigger = true` to the configuration, and this will make it so that every time the watcher sees that a file has changed, it automatically makes the new snapshot.

That said, neither of these are active by default, and neither are necessary by default. But if you're the sort of person who uses VS Code's "Timeline" view to see exactly how each file you've worked with has changed over time, then you might also appreciate the automatic snapshotting feature.

It’s a simple config flag.
The first time I’ve tried to prepare a set of commits to push out I found out the hard way that merges cannot be undone - and I’m not sure which of the commands were doing a merge.
Merges can be undone. Either you can manually remediate by `jj abandon`ing the merge commit so that it's not visible anymore, or you can restore the entire repo state to a previous point in time with `jj undo` or `jj op restore`, or you can do some remediation in between those two extremes.

Off of the top of my head, `jj new` and occasionally `jj rebase` can create merge commits; I don't recall any others.

You can always undo the most recent action using `jj undo`. To undo older actions, the easiest solution is to look for the state you want to get back to in the operations log `jj op log`, and then restore that state directly using `jj op restore <hash of state>`.

You really can undo every action in Jujutsu (and if you can't, that's a bug), but the `undo` mechanism can be a bit surprising - it doesn't behave like a stack, where undoing multiple times will take you further back in history. Instead, undo always undoes the most recent action, and if the most recent action is an undo, then it undoes the undo. This often catches people off-guard - in future versions, JJ will show a warning when this happens, and in even further future versions, there's a plan to make undo behave more like expected.

But if you use `jj op log` and `jj op restore`, you can always get back to any previous state, including undoing merges and other complicated changes.

Thanks, that makes sense. The cheat sheets either didn’t contain the op log-reflog analog or I missed it.

For the record I’ve seen ‘cannot undo a merge’ after jj undo 3-4 times, can’t remember now. I was trying to squash a change into a single commit for a GitHub pr for the first time and couldn’t figure out how to map jj commits into something acceptable, then decided to undo the whole thing and actually managed to overwrite some of my changes in a way I couldn’t find them, fortunately only a few lines of boilerplate.

> With git, it's basically impossible to mess up the repo state.

I must be a wizard because I’ve lost count of the number of times I’ve messed up my repo’s state.

I jest. Kinda. I know that git’s state might technically not be messed up and someone skilled in the ways of git could get me out of my predicament. But the fact that I’m able to easily dig myself into a hole that I don’t know how to get out of is one of git’s biggest issues, in my opinion.

Totally.

That hole is very easily dug with git:

use ctrl x ctrl v to move files around commit boom, you've lost history for those files (file tracking only works in theory, not in real life), let's say you don't notice (very easy to not notice)

commit some more, merge

discover your mistake tons of commits back

good luck fixing that, without digging a bigger hole.

And that's one of 100's of examples in which git just is really really not fun or user friendly.

> use ctrl x ctrl v to move files around commit boom, you've lost history for those files (file tracking only works in theory, not in real life), let's say you don't notice (very easy to not notice)

This is one of the things git excels at: You didn't lose your history because that's not how git handles it. Git might be the only version control that can actually handle your case (renaming files without using a special command) - it looks for file similarity between a deleted file and an added file in the same commit, with various flags to make this look in broader places.

"git mv file1 file2" is almost identical to "mv file1 file2" + "git add file1 file2" (it also handles unstaged changes instead staging them)

I keep reading explanation similar to yours, yet it obviously doesn't work in my case.

I'm probably doing something dumb/wrong, some setting somewhere, wrong OS, whatever it is: it proves my point that git is harder than it should be.

If you want to “checkout” some previous commit, jj has your back in three ways

- first, that commit that’s been merged to main is marked as immutable and, unless you add a flag to say “I know this is immutable and I want to mutate it anyway”, you can’t mutate it

- second, as part of your regular workflow, you haven’t actually checked out that historical commit. You created a new, empty commit when you “checked it out” using “jj new old_commit”

- third, you can use jj undo. Or, you can use “jj obs log” to see how a change has evolved over time (read: undo your mass find+replace by reverting to a previous state)

jj is pretty much just safer than Git in terms of the core architecture.

There's several things Git can't undo, such as if you delete a ref (in particular, for commits not observed by `HEAD`), or if you want to determine a global ordering between events from different reflogs: https://github.com/arxanas/git-branchless/wiki/Architecture#...

In contrast, jj snapshots the entire repo after each operation (including the assignment of refs to commits), so the above issues are naturally handled as part of the design. You can check the historical repo states with the operation log: https://jj-vcs.github.io/jj/latest/operation-log/ (That being said, there may be bugs in jj itself.)

“jj op log” shows you the operation history, which you can then “jj op restore” and point to where you want to restore to :) (disclaimer: im still jj newbie, but this has gotten me out of the snafus ive put myself into while learning g)
Can I ask what your motivation was for trying jj?

I'm always keen to explore new things but I don't have many complaints about git. I'm wondering what this solves that made it attractive for you.

I was always very frustrated with git workflow for working on multiple features/bugfixes simultaneously (multiple branches [1]). Changing between them, or combining them for testing is tedious -- constant stashing, switching, cherry-picking. Conflicts fit very poorly into git's version control model - you can't just tell git to ignore some conflict and continue for the moment so you can take care of it later. You have to stop the thing you wanted to focus on and instead babysit git because it found a conflict. Etc.

These are less of an issue once you've molded yourself to fit into git's strange ways, but jj feels like a much nicer tool -- especially for beginners, but feels like it frees up cognitive space even for more experienced folks. You can focus less on the tool and focus more on what you actually want to do.

[1]: I've tried using multiple working trees, but that workflow never really "stuck" with me.

I've done the multiple trees thing too, and agree it didn't work very well.

jj solved the biggest problem for me, which is how much time you spend rebasing when you have 1 PR = 1 stack of commits on top of main. It's easy enough to work on multiple branches this way, but it's a lot of repeated pain when `main` diverges and your changes on top are still out for review. (I honestly just started squashing all of my commits before review, so I would only have to resolve conflicts once.) jj fixes all of this. I especially enjoy working on a 3rd pending change that refers to the previous 2 pending changes; `jj new june/feature-1 june/feature-2` and then you add feature 3 there. You can even `jj squash --into june/feature-1` if something makes more sense being in a prior commit. It's all very wonderful if you are working with other people and you can't immediately mutate `main` upon finishing some work.

i’ve always been interested in improving the git workflow i have for my small team (usually 2-3 other programmers), i think particularly hopping branches/commits, merging changes, rebasing and reorganizing history, i think git does suffice for that stuff if you know the commands, but jj makes it feel so fluid and easy to do, and having enough depth to get as expressive as you need to be

a lot of other tools ive found were lacking for one reason or another, and mightve not been git compatible. with jj you can hop between git and jj commands as you please, essentially full compatibility with git

I didn’t really have any complaints about git, but people I trust told me to check out jj anyway. Now I’m not going back. Something can still be nicer without another thing having to be bad, basically.
> With git, it's basically impossible to mess up the repo state

I'd like to introduce you to a couple of my former colleagues...

As other commenters have mentioned, there's `jj undo`, but you can also configure a set of immutable commits (by default it's the main branch and heads of untracked branches) and jj will stop you from changing those unintentionally (there's a flag you have to explicitly set if you want to force it).
Er, well, never type `jj edit` and this will never be an issue?

I exclusively move in a jj repo with `jj new` and `jj squash` or `jj squash --to <rev>` as appropriate. I've been using it 8+ hours daily for months and have never, ever even thought of having this issue.

I find it quite funny that this is exactly the kind of the thing "just don't use ... 8+hours daily ... Never had issues ..." that people who remain with git say.

And now people are saying it for jj.

Endless cycle it seems, with every new tool.

`jj edit` is specifically and exclusively for mutating existing commits.

Thus, if you're worried about mutating existing commits, don't use it.

What exactly is so hard to understand here? You're not making the gotcha point you seem to think you are - it's not like it's some common command that is hyper-overloaded and has to be used specially.

Just another example of the usual HN skepticism that isn't even skepticism, it's just smug ignorance. It's so exhausting. But sure, the countless people that keep claiming its the single biggest tool improvement in some time are just idiots? suckers? hype-beasts? making it up? or what?

Like, the irony of you assuming that it must be as convoluted and hard to use as git is just... awesome. I love the Git defenders that literally can't fathom that there is actually a better mental model or simpler tool, and can't even be arsed to try it and see.

With JJ you can even override the ref spec of what should be considered a mutable commit. Feel free to set it to all commits and JJ will not allow you to mutate anything unless you pass the --ignore-mutable flag.

For example, I've configured it for me to make any commit by anyone else mutable regardless of branch:

    [revset-aliases]
    # To always consider changes by others immutable:
    "immutable_heads()" = "builtin_immutable_heads() | (trunk().. & ~mine())"