Remix.run Logo
zingar 9 hours ago

"It's more powerful and easier" is a great claim, but I need examples in this opening page to convince me of the pain I could save myself or the awesome things I'm living without.

EliasWatson 8 hours ago | parent | next [-]

A couple things off the top of my head:

- You aren't forced to resolve rebase/merge conflicts immediately. You can switch branches halfway through resolving conflicts and then come back later and pick up where you left off. You can also just ignore the conflicts and continue editing files on the conflicted branch and then resolve the conflicts later.

- Manipulating commits is super easy (especially with jjui). I reorder commits all the time and move them between branches. Of course you can also squash and split commits, but that's already easy in git. Back when I was using git, I would rarely touch previous commits other than the occasional squash or rename. But now I frequently manipulate the commit history of my branch to make it more readable and organized.

- jj acts as a VCS for your VCS. It has an operation log that is a history of the state of the git repository. So anything that would be destructive in git (e.g. rebase, pull, squash, etc) can be undone.

- Unnamed branches is the feature that has changed my workflow the most. It's hard to explain, so I probably won't do it justice. Basically you stop thinking about things in terms of branches and instead just see it as a graph of commits. While I'm experimenting/exploring how to implement or refactor something, I can create "sub-branches" and switch between them. Similar to stashes, but each "stash" is just a normal branch that can have multiple commits. If I want to test something but I have current changes, I just `jj new`. And if I want to go back, I just make a new commit off of the previous one. And all these commits stick around, so I can go back to something I tried before. Hopefully this made some sense.

Also note that jj is fully compatible with git. I use it at work and all my coworkers use git. So it feels more like a git client than a git replacement.

coldtea 8 hours ago | parent | next [-]

All of these features sound like the recipe for a confusing nightmare!

"You can switch branches halfway through resolving conflicts and then come back later and pick up where you left off. You can also just ignore the conflicts and continue editing files on the conflicted branch and then resolve the conflicts later."

"Similar to stashes, but each "stash" is just a normal branch that can have multiple commits. If I want to test something but I have current changes, I just `jj new`. And if I want to go back, I just make a new commit off of the previous one. And all these commits stick around, so I can go back to something I tried before."

nickisnoble 8 hours ago | parent | next [-]

I thought the same until I started using it.

Turns out, git sorta trains you to be very, very afraid of breaking something.

jj answers this in a few ways:

1. everything is easily reversible, across multiple axes.

2. yes, everything is basically a stash, and it's a live stash — as in, I don't have to think about it because if it's in my editor, it's already safely stored as the current change. I can switch to a different one, create a new one, have an agent work on another one, etc, all without really caring about "what if I forgot to commit or stash something". Sounds like insanity from a git POV but it really is freeing.

3. Because of 2, you can just leave conflicts alone and go work on something else (because they are, like you said, essentially stashed). It's fine and actually very convenient.

The thing the article doesn't mention, that makes this all safe, is that trunk / "main" is strictly immutable. All this flexibility is *just* for unmerged WIP. (There are escape hatches though, naturally!)

steveklabnik 8 hours ago | parent | prev | next [-]

The "you don't need to worry about resolving conflicts" thing is confusing when you hear it with words, so let me show you what it looks like in practice.

Let's say I have two branches off of trunk. They each have one commit. That looks like this (it looks so much nicer with color, I'm going to cut some information out of the default log so that it's easier to read without the color):

    @  vvxvznow 
    │  (empty) (no description set)
    │ ○  uuowqquz 
    ├─╯  foo
    │ ○  uvlpytpm 
    ├─╯  bar
    ◆  lwywpyls trunk
    │  feat: blah blah blah
So both `foo` and `bar` are on top of trunk, and I'm also working on a third branch on top of trunk (@). Those vvxv and such are the change ids, and you can also see the named trunk there as well.

Now, I fetch from my remote, and want to rebase my work on top of them: a `jj git fetch`, and then let's rebase `foo` first: that's `jj rebase uu -o trunk` (you only need uu instead of uuowqquz because it's a non-ambiguous prefix, just like git). Uh oh! a conflict!

    @  vvxvznow
    │  (empty) (no description set)
    │ ×  uuowqquz (conflict)
    │ │  foo
    │ ◆  tswtuqmu
    ├─╯  chore: whatever
    ~  (elided revisions)
    │ ○  uvlpytpm
    ├─╯  bar
    
Note that jj did not put us into a "hey there's a conflict, you need to resolve it" state. It just did what you asked: it rebased it, there's a conflict, it lets you know.

So why is this better? Well, for a few reasons, but I think the simplest is that we now have choice: with git, I would be forced to deal with this conflict right now. But maybe I don't want to deal with this conflict right now: I'm trying to update my branches in general. Is this conflict going to be something easy to resolve? In this case, it's one commit. But what if each of these branches had ten commits, with five of them conflicted and five not? It might be a lot of work to fix this conflict. So the cool thing is: we don't actually have to. We could continue our "let's rebase all the branches" task and rebase bar as well. Maybe it doesn't have a conflict, and we'd rather go work on bar before we come back and deal with foo. Heck, sometimes, I've had a conflicted branch, and then a newer version of trunk makes the conflict go away! I only have to choose to address the conflict at the moment I want to return to work on foo.

There's broader implications here, but in practice, it's just that it's simply nicer to have choice.

ersatz-matty 8 hours ago | parent | prev | next [-]

In practice, it isn't. What you're identifying as potentially nightmarish - and no doubt quite tedious in git - are things that JJ enables you to do with a small subset of commands that work exactly how you expect them to work _in every workflow context_ in which they are needed.

Thinking specifically about conflicts: being able to defer conflicts until you're ready to deal with them is actually great. I might not be done with what I am actually working on and might want to finish that first. being forced into a possibly complicated conflict resolution when I'm in the middle of something is what I'd actually consider nightmarish.

When you want to solve the conflict: `jj new <rev>`, solve the conflict, then `jj squash`, your conflict resolution is automatically propagated to the chain of child commits from the conflict.

BeetleB 3 hours ago | parent | prev | next [-]

Remember when you used SVN or whatever before git, and you loved git because of how easy it is to make branches?

With branches, jj is to git what git was to SVN. It's an order of magnitude less friction to do branching in jj than git.

Not long ago, I pulled from main and rebased my branch onto it - merge conflicts. But I wanted to work on some other feature at the moment. Why should I have to fix this merge conflict to work on a feature on a totally different branch? With jj, I don't. I just switch to the other branch (that has no conflict), and code my new feature. Whenever I need to work on the conflicted branch, I'll go there and fix the conflict.

Once I started using jj, I realized how silly it was for git to have separate concepts for stash and index. And it's annoying that stash/index is not version controlled in git. Or is it? I have no idea.

In jj, a stash is simply yet another unnamed branch. Do whatever you want there. Add more commits. Then apply it to any branch(es) that you would like to. Or not.

Why does git need a separate concept of a stash? And wouldn't you like a version controlled stash in git?

Have you ever made a ton of changes, done a "git add", accidentally deleted some of the changes in one file, done a "git add", and thought "Oh crap!" I suppose that information can be recovered from the reflog. But wouldn't you wish "git add" was version controlled in the same way everything else is?

That's the appeal of jj. You get a better stash. You get a better index. And all with fewer concepts. You just need to understand what a branch (or graph) is, and you get all of it. Why give it a name like "stash" or "index"?

Why does git insist on giving branches names? Once you get used to unnamed branches, the git way just doesn't make sense. In jj you'll still give names wherever you need to.

ncphillips 8 hours ago | parent | prev [-]

Yeah I legit do not understand the appeal. I’m willing to be wrong but it’s not clicking with me at all

tcoff91 7 hours ago | parent | next [-]

Anonymous branches are amazing for when you are trying out a bunch of different approaches to a problem. As I search the space of possible solutions for what I'm really looking for, I end up with a tree of various approaches.

Then when you rebase, the entire tree of anonymous branches can be rebased onto main in 1 command. This is why the first class conflicts and not having to resolve conflicts immediately is so important: when i'm rebasing, an entire tree of branches is getting rebased and so if you had to resolve conflicts right away it would be incredibly cumbersome, because I'm rebasing like 30+ commits and a bunch of anonymous branches in a single operation.

I work on top of an octopus merge of all my in-flight PRs. ON top of that merge commit i have a bunch of anonymous branches with various things going on. When I'm ready to submit a PR, I take one of those anonymous branches and rebase it onto main and make it an additional parent of my 'dev-base' merge commit. Then i give that branch a name and submit it as a PR.

Every day when I start working, I rebase this entire subgraph of branches in a single command onto main. all my PRs are up to date, all my anonymous branches are up to date, etc... Takes like 2 seconds. If some of my anonymous branches are in a conflicted state, that's ok, i don't have to deal with it until I want to work on that change again.

These anonymous branches aren't confusing because they all show up in the default revset that is shown when looking at the jj log. I can easily browse through them with jjui TUI and instantly see which ones are what. It's really not confusing at all.

https://ofcr.se/jujutsu-merge-workflow

baq 7 hours ago | parent | prev [-]

typical for experienced git users who already 'just don't do' things which git punishes you for; after a decade it's hard to even imagine any other way, not to mention that it might be better. been there, done that, jj is legit after letting go of (some of) git.

capitainenemo 8 hours ago | parent | prev | next [-]

I also like the powerful revision querying mechanisms that they pulled in from mercurial. They seem to work just like mercurial revset queries which can be used in various operations on sets of revisions.

I would like them to have mercurial's awesome hg fa --deleted when it comes to history trawling, but apparently for it to work well, they also need to swap out git's diff format for mercurial's smarter one, so I'll be waiting on that for a while I suppose.

rasguanabana 8 hours ago | parent | prev | next [-]

> So anything that would be destructive in git (e.g. rebase, pull, squash, etc) can be undone.

It’s possible to recover from these with git reflog, though.

BeetleB 6 hours ago | parent | next [-]

I've used git for years, and used reflog once or twice.

I've used jj for only a year, and have used "jj undo" countless times.

There's a huge benefit to having a simpler mental model.

ersatz-matty 7 hours ago | parent | prev [-]

`jj undo` compared to what exactly?

k4rnaj1k 8 hours ago | parent | prev [-]

[dead]

latortuga 9 hours ago | parent | prev | next [-]

Yeah we moved on from SVN to git because SVN branches were truly a pain in the ass to work with. I truly do not have any rough edges or big pains in my day to day git workflow.

baq 9 hours ago | parent | prev | next [-]

does trivially working on 3 PRs in a single checkout and pushing focused changes to each one independently without thinking twice count?

if you don't need this, you might not see any value in jj and that's ok. you might use magit to get the same workflow (maybe? haven't used magit personally) and that's also ok.

tinco 9 hours ago | parent | next [-]

It might count, but it is easy with git as well, what is the feature in jj that makes this easier? Switching branches and pushing changes to remotes is the core feature of git and in my opinion really easy so I'm curious how jj improves on it.

baq 8 hours ago | parent [-]

rebases don't lose branches and jj absorb trivially squashes changes to the correct head (or leaves changes alone if it can't find where to squash).

is it possible in git? yeah, I've done it; there's a reason I haven't done it more than a few times with git, though. ergonomics matter.

VanTodi 9 hours ago | parent | prev | next [-]

Guess he was talking about the presentation, not what the tool can achieve. It has no hard proof on the first page, which could easily just be a LinkedIn pitch, but not on hackernews

VonGallifrey 9 hours ago | parent | prev | next [-]

Can you show how you would do this in jj?

I know how I would do this in git, but don't really see how this would be in jj. I currently don't use it in my workflow, but if it is super easy in jj then I could see myself switching.

bilkow 8 hours ago | parent | next [-]

This is how I'd do it:

    jj new branch1 branch2 branch3
This creates an empty commit that merges all 3 branches, you can think of this as your staging area.

When you want to move specific changes to an existing commit, let's say a commit with an ID that starts with `zyx` (all jj commands highlights the starting characters that make the commit / change unambiguous):

    jj squash -i --to zyx
Then select your changes in the TUI. `-i` stands for interactive.

If you want to move changes to a new commit on one of the branches:

    jj split -i -A branch1
Then select the changes you want moved. `-A` is the same as `--insert-after`, it inserts the commit between that commit and any children (including the merge commit you're on).

There's one thing that's a bit annoying, the commit is there but the head of the branch hasn't been moved, you have to move it manually (I used + to get the child to be clearer, but I usually just type the first characters of the new change id):

    jj bookmark move branch1 --to branch1+
baq 8 hours ago | parent | prev [-]

the beauty of it is there's not much to show; I use a crude jjui approach where I have an octopus merge working tree commit (in command line terms, jj new PR_A PR_B PR_C) and either use native jj absorb (S-A in jjui) which guesses where to squash based on the path or, when I'm feeling fancy, rebase the octopus via jjui set parents (S-M) function (also handy to clean up parents when one of the PRs gets merged).

alphabetag675 9 hours ago | parent | prev [-]

Actually it is a anti-demo, because while software allows you to do it, I don't think many software engineers can work on this.

baq 8 hours ago | parent [-]

in large enough monorepos and teams and big enough changes you either do it like this or have a humongous giga-PR which eventually starts conflicting with everything.

surajrmal 8 hours ago | parent | prev | next [-]

Specific commands don't really showcase the appeal of jj. If anything they might scare someone at first glance. It's the fact that the workflows are intuitive and you never find yourself reaching for help to get something done. You really need to try it to understand it.

tomnipotent 8 hours ago | parent | next [-]

> that the workflows are intuitive

It can't be both intuitive and yet too complicated to show examples at the same time.

WesolyKubeczek 8 hours ago | parent | prev [-]

The only intuitive interface is the nipple. All other things are learned.

I feel very comfortable using git. Maybe jj is better, but not seeing is not believing.

baq 8 hours ago | parent | next [-]

jj is better for some workflows, which, if you're a git expert as you claim, you conciously or subconciously avoid as 'too much work' or 'too brittle'.

if you don't care about them after accepting this realization... it's fine. git is good enough.

ncphillips 8 hours ago | parent [-]

I’m not a fit expert by any means. The workflows being described do not appeal to me but not because of the way fit works. They sound confusing and I don’t understand what benefit I’m getting out of them. Like, it’s a solution to a problem I’m not sure exists (for me)

BeetleB 8 hours ago | parent | prev [-]

> but not seeing is not believing.

Classic denying the antecedent :-)

https://en.wikipedia.org/wiki/Denying_the_antecedent

coldtea 8 hours ago | parent [-]

It's not supposed to be a modus ponens deduction.

Just an expression of what the Missouri state's nickname says.

Rebelgecko 7 hours ago | parent | prev | next [-]

"jj undo" is worth the price of admission by itself.

See the current top thread on HN about backblaze not backing up .git repos. People are flaming OP like they're an idiot for putting a git repo in a bad state. With jj, it's REALLY HARD to break your repo in a way that can't be fixed by just running "jj undo" a couple times.

ersatz-matty 9 hours ago | parent | prev | next [-]

Consider using the table of contents on the left of the page to view "Real World Workflows", "Branching, Merging, and Conflicts", and then "Sharing Your Code with Others" and then evaluate how JJ does things against your current git workflow. This requires some minor effort on your part.

The official JJ docs also have a "bird's eye view" introduction and tutorial available here: https://docs.jj-vcs.dev/latest/tutorial/.

EDIT: Jujutsu for Git experts: <https://docs.jj-vcs.dev/latest/git-experts/>. This outlines some of the main advantages relatively succinctly.

steveklabnik 9 hours ago | parent | prev | next [-]

This is good feedback, for sure, thank you. It's sometimes hard to come up with truly concise examples, but that's also why they're so valuable.

qznc 9 hours ago | parent | prev [-]

There is no index anymore. I guess that is the "easier" part.

steveklabnik 8 hours ago | parent [-]

That is, but not directly.

The general idea here is that jj has fewer and more orthogonal concepts than git. This makes it more regular, which is what I mean by "easy."

So for example, there is no index as a separate concept. But if you like to stage changes, you can accomplish this through a workflow, rather than a separate feature. This makes various things less complex: the equivalent of git reset doesn't need --hard, --soft, --mixed, because the index isn't a separate concept: it's just a commit. This also makes it more powerful: you can use any command that works on commits on your index.

This is repeated across jj's design in general.