Top
Best
New

Posted by tigerlily 12 hours ago

jj – the CLI for Jujutsu(steveklabnik.github.io)
471 points | 404 comments
tiborsaas 9 hours ago|
Does JJ really prefer for me to think backwards? It wants me to start with the new and describe command, but with git I first make the changes and name the changeset at the end of the workflow.

I also often end up with in a dirty repo state with multiple changes belonging to separate features or abstractions. I usually just pick the changes I want to group into a commit and clean up the state.

Since it's git compatible, it feels like it must work to add files and keep files uncommitted, but just by reading this tutorial I'm unsure.

stouset 5 minutes ago||
> Does JJ really prefer for me to think backwards?

No, you run `jj new` when you’re done with your work just like you’d run `git commit`. You can even just run `jj commit` which is a shorthand for `jj describe && jj new`.

joshka 9 hours ago|||
> Does JJ really prefer for me to think backwards? It wants me to start with the new and describe command, but with git I first make the changes and name the changeset at the end of the workflow.

A good way to think of it is that jj new is an empty git staging area. There's still a `jj commit` command that allows you to desc then jj new.

> I also often end up with in a dirty repo state with multiple changes belonging to separate features or abstractions. I usually just pick the changes I want to group into a commit and clean up the state.

jj split allows you do to this pretty well.

> Since it's git compatible, it feels like it must work to add files and keep files uncommitted, but just by reading this tutorial I'm unsure.

In jj you always have a commit - it's just sometimes empty, sometimes full, has a stable changeid regardless. jj treats the commit as a calculated value based on the contents of your folder etc, rather than the unit of change.

frio 1 hour ago|||
> A good way to think of it is that jj new is an empty git staging area. There's still a `jj commit` command that allows you to desc then jj new.

This always made me feel uncomfy using `jj`. Something that I didn't realise for a while is that `jj` automatically cleans up/garbage collects empty commits. I don't write as much code as I used to, but I still have to interact with, debug and test our product a _lot_ in order to support other engineers, so my workflow was effectively:

``` git checkout master git fetch git rebase # can be just git pull but I've always preferred doing this independently _work_/investigate git checkout HEAD ./the-project # cleanup the things I changed while investigating ```

Running `jj new master@origin` felt odd because I was creating a commit, but... when I realised that those commits don't last, things felt better. When I then realised that if I made a change or two while investigating, that these were basically stashed for free, it actually improved my workflow. I don't often have to go back to them, but knowing that they're there has been nice!

greenicon 6 hours ago||||
I'm using jj exactly this way, but `jj commit -i` is still somewhat backwards compared to `git commit -i`: jj displays the commit timestamp by default instead of the author timestamp like git. In addition, in jj the author timestamp of a commit is set to the time you started and not ended a commit/change. This results in unexpected timestamps when working with git-using people or tools. Also, it's rather weird if you use a previously empty commit for your work which was created months earlier by a previous `jj commit`, resulting in a timestamp neither correlating to when you started nor ended your work.

I guess the idea of jj's authors is that jj's commits are far more squishy and can always be changed, so a fixed finished timestamp makes less sense. I still prefer git's behaviour, marking work as finished and then keep the author (but not commit) timestamps on amends.

I use this jj alias to get git's timestamp behaviour:

  [aliases]
  c = ["util", "exec", "--", "bash", "-c", """
  set -euo pipefail
  change_id=$(jj log -r @ --no-graph -T 'change_id')
  desc=$(jj log -r $change_id --no-graph -T 'description')
  commit_author=$(jj log -r $change_id --no-graph -T 'author.email()')
  configured_author=$(jj config get user.email)
  
  jj commit -i "$@"
  
  if [ -z "$desc" ] && [ z"$commit_author" = z"$configured_author" ]; then
      echo "Adjusting author date"
      jj metaedit --update-author-timestamp --quiet $change_id
  fi
  """]
  
  [templates]
  # display author timestamp instead of commit timestamp in log
  'commit_timestamp(commit)' = 'commit.author().timestamp()'
saghm 9 hours ago|||
I often will use `jj new -B@` (which I made an alias for) followed by `jj squash -i` to split changes. I had no idea about `jj split`, so I need look into that!
EliasWatson 8 hours ago|||
jj is very flexible when it comes to workflow. One thing to note is that commits don't have to have messages. What I tend to do is to run `jj new` frequently while I work on something and leave all of them without messages. Then when I'm ready to make my actual commit, I squash the temporary commits together and then add a message. If my changes are separable, I can split the commits before squashing. This workflow acts as a kind of undo history. I can easily go back to what I had 5 minutes ago and try a different approach, but then also jump back to my original changes if I want. It makes experimentation much easier compared to git.
Jenk 9 hours ago|||
It doesn't need you to think that way at all.

`jj new` simply means "create a new commit [ontop of <location>]" - you don't have to describe it immediately. I never do.

I know that the intention was to do that, and I tried forcing the habit, but I too found it counter-productive to invariably end up re-writing the description.

surajrmal 9 hours ago||
I don't usually do that right away, but I often use squash or absorb to move additional changes into a commit I already made in my stack. I think the spirit still applies if you take that course.
nchmy 36 minutes ago|||
JJ doesnt prefer you to do anything. I regularly just create description-free commits, do whatever, then name them later (or squash, split, absorb the changes into other commits). It is exceptionally flexible and forgiving. Even moreso if you use jjui, the best TUI ive ever used.
shermantanktop 7 hours ago|||
This is me! I often find that in the process of making one change, I have also made several other changes, and only recognize that they are distinct after following the ideas to their natural conclusion.

Hence I have multiple workspaces, and I shelve changes a lot (IntelliJ. I end up with dirty repos too and that can be painful to cherry-pick from. Sometimes I just create a git patch so I can squirrel the diffs into a tmp file while I cleanup the commit candidate. I often let changes sit for several days while I work on something else so that I can come back and decide if it’s actually right.

It’s chaotic and I hide all this from coworkers in a bid to seem just a bit more professional.

I admire people who are very deliberate and plan ahead. But I need to get the code under my fingers before I have conviction about it.

sfink 6 hours ago|||
I'm about the same. jj is kind of perfect for that. Example:

# I've finished something significant! Carve it out from the working "change" as its own commit.

    `jj commit --interactive` # aka `jj commit -i` or `jj split`, depending on how you prefer to think of it: making a commit for some work, or splitting a separate commit out of the working change.
# Oops, missed a piece.

    `jj squash --interactive` # aka `jj squash -i`
# Let me look at what's left.

    `jj diff`
# Oh right, I had started working on something else. I could just leave it in the working change, but let me separate it out into its own commit even though it's unfinished, since I can always add pieces to it later.

    `jj commit -i`
# Wait, no, I kind of want it to come before that thing I finished up. Shoot, I messed up.

    `jj undo`
# Let me try that again, this time putting it underneath.

    `jj split -B @-` # aka `jj split --insert-before @-`. @ is the working change, @- is its immediate parent(s), @-- is all grandparents, etc.
# Note that instead of undoing and re-selecting the parts, you could also `jj rebase -r @- -B @--` to reorder. And in practice, you'll often be doing `jj log` to see what things are and using their change ids instead of things like `@--`.

# I also have some logging code I don't need anymore. Let me discard it.

    `jj diffedit`
# Do some more work. I have some additions to that part I thought was done.

    `jj squash -i`
# And some additions to that other part.

    `jj squash -i --into @--`
# etc.

There's a lot more that you could do, but once you internalize the ideas that (1) everything is a commit, and (2) commits (and changes) can have multiple parents thus form a DAG, then almost everything else you want to do becomes an obvious application of a small handful of core commands.

Note: to figure out how to use the built-in diff viewer, you'll need to hover over the menu with the mouse, but you really just need f for fold/unfold and j/k for movement, then space for toggle.

chriswarbo 7 hours ago|||
> I often find that in the process of making one change, I have also made several other changes, and only recognize that they are distinct after following the ideas to their natural conclusion.

I do that all the time. With git, everything starts "unstaged", so I'd use magit to selectively stage some parts and turn those into a sequence of commits, one on top of another.

With jj I'd do it "backwards": everything starts off committed (with no commit message), so I'd open the diff (`D` in majutsu), selecting some parts and "split" (`S` in majutsu) to put those into a new commit underneath the remaining changes. Once the different changes are split into separate commits, I'd give each a relevant commit message.

smweber 9 hours ago|||
My preferred workflow is to start with a new change, pick the changes I want, then use jj commit to describe the change and create a new empty one on top. Feels very similar to my old git workflow.

If I end up with multiple features or abstractions in one change (equivalent to the “dirty repo”), jj split works very well as an alternative to the git add/git commit/repeat workflow tidying up one’s working copy.

pythonaut_16 9 hours ago||
I also like `jj commit [paths]` to commit just a subset of files when I don't need hunk based splitting.

Like `jj commit -m 'Feature A' file1 file2` then `jj commit -m 'Feature B' file3 file 4`

surajrmal 9 hours ago||
I use jj commit -i a lot when writing the paths is too tedious. What's nice is you can pass -i into most commands (squash, split, absorb, etc).
chriswarbo 6 hours ago|||
> It wants me to start with the new and describe command

jj doesn't "want" anything.

I always end a piece of work with `new`: it puts an empty, description-less commit as the checked-out HEAD, and is my way of saying "I'm finished with those changes (for now); any subsequent changes to this directory should go in this (currently empty) commit"

The last thing I do to a commit, once all of its contents have settled into something reasonable, is describe it.

In fact, I mostly use `commit` (pressing `C` in majutsu), which combines those two things: it gives the current commit a description, and creates a new empty commit on top.

saghm 9 hours ago|||
Nothing stops you from making changes in a commit that has no description and then at the end doing `jj commit -m` to describe them and make a new commit in one go, which is essentially the same as git. The difference is that it's essentially amending in place as you make changes rather than needing to stage first.
gcr 4 hours ago|||
Think of it this way: the current change is like a staging area/index in git. Leave it without a a description while you're working (just like git's staging area). Rely on jj's auto-snapshotting to capture all your changes. Then, when you're ready to do something else, give it a description ("jj describe") and switch to a new blank change ("jj new"), and that becomes your new "staging area"/index.

The workflows are conceptually identical.

fmckdkxkc 9 hours ago|||
Personally haven’t used jj but as far as dvcs’s are concerned Fossil is great complement to Git because it does things differently than git and truly has a decentralized feel.

The autosync feature is really nice too, and you can store backup repos in cloud storage folders and auto sync to those as well.

59nadir 1 hour ago||
Fossil is delightful and definitely nails a feeling of decentralization that I think we ruined completely with `git` by constantly centering around centralized repositories.

I also find it interesting that so many people want to switch to something that's not `git` but are simultaneously somehow super invested in it being basically just `git`.

Most teams could switch to Fossil and just have a better time overall. It's made for smaller, high-trust teams, `git` is not. Fossil also manages to actually support external contributions just fine; it's just that it's not the default.

baq 8 hours ago|||
it's actually git that makes you think backwards - in jj the working tree is a commit, in git it isn't until you at least stage it.

the working tree being a commit has wide ranging implications as all the commands that work with commits start working with the working tree by default.

benoitg 2 hours ago|||
Not necessarily, I often make changes on unrelated commits. You can always use jj split to extract the change and put it somewhere else.
miyoji 9 hours ago|||
> Does JJ really prefer for me to think backwards? It wants me to start with the new and describe command, but with git I first make the changes and name the changeset at the end of the workflow.

Yes, but this is not backwards, the way you do it in git is backwards. =)

SiempreViernes 8 hours ago||
git promises "version control", this clearly implies that the versions predate the control: in this picture the git workflow is not backwards.
miyoji 6 hours ago||
I don't think the term "version control" has any implication about precedence, and I don't understand what you mean by "the versions predate the control". In git, you add items to the worktree (control), then you commit (create a version), so doesn't that mean git does it "wrong" according to what you're saying? In jj, you are always on a committed version and the contents of that commit are controlled by your edits, if you want your edits to be on a different commit, you usually just change to that commit and make the edits, although there are other ways to move edits around (which is also true in git).

The point is that there actually isn't a correct order to do these operations, just one that you're familiar with. Other orders of operations are valid, and may be superior for your or your team's workflow.

jezzamon 9 hours ago|||
That totally works and it's how I use jj. jj commit -i does what you would want
minraws 8 hours ago||
think of jj like,

I want to build xyz,

```

jj desc -m "feat: x y & z"

```

do the work.

```

jj split

```

Split up the parts and files that you want to be separate and name them.

This will also allow you to rename stuff.

```

jj bookmark create worklabel-1 -r rev1

jj bookmark create worklabel-2 -r rev2

# Push both commits

# since we just split them they are likely not inter-dependent

# so you can rebase them both to base

# assuming rev1 is already on top of base

jj rebase -s rev2 -d base

jj git push

```

That is it.

motbus3 8 hours ago||
I am dumb. why is that better than a git branch or a git worktree ?
cornstalks 8 hours ago|||
If you're already super comfortable in git, it's not. I'm saying this as someone who recently converted from git to jj and never wants to go back.

You also don't have to follow what the GP said. I never say `jj describe` before writing code. I write the code then just say `jj commit -m "Foo stuff"`, just like I would in git.

The bigger difference I've noticed is:

1. Switching between changesets just feels more natural than git ever did. If I just run `jj` it shows me my tree of commits (think of it like showing you git's branches + their commits), and if I want to edit the code in one of them I just say `jj edit xyz`, or if I want to create a new commit on top of another one and branch it off in a new direction, I just say `jj new xyz`. It took a little bit for my brain to "get" jj and how it works because I was so used to git's branches, but I'm really enjoying the mental model.

2. `jj undo`. This alone is enough to convert me. I screwed something up when trying to sync something and had a bunch of conflicts I really didn't want to resolve and I knew could have been avoided if I did things differently, but my screwup was several operations ago! So I ran `jj undo`. And ran it again. And again. And again. And then I was back to my clean state several stages ago before I screwed up, despite having made several changes and operations since then. With git? Yeah I could have gotten it fixed and gone back. But every time I've had to do something like that in git, I'm only 25% confident I'm doing it right and I'm not screwing things up further.

3. Rebasing. When I would try to sync git to an upstream GitHub repo that used rebasing for PRs, I would always get merge conflicts. This was because I stack my changes on top of each other, but only merge in one at a time. Resyncing means my PR got a new commit hash, even though none of the code changed, and now git couldn't figure out how to merge this new unknown commit with my tree, even though it was the same commit I had locally, just a different hash. With jj? I never get merge conflicts anymore from that.

Overall the developer experience is just more enjoyable for me. I can't say jj's flow is fundamentally and objectively better than git's flow with branches, but personally and subjectively, I like it better.

bastardoperator 4 hours ago|||
It's not, you can literally do everything this tool does with Git, and 80% of the features could be replaced with commands in your shell rc file also using vanilla git.

This tool was described perfectly the other day. JJ is the Dvorak of Git. Most people could careless about Dvorak layout, 99.8% of people use qwerty just fine. Those 0.02% though, they're loud, and they want everyone to know how great the reinvention of bread is.

tom_alexander 10 hours ago||
I'm giving jj a try but one aspect of it I dislike is edits to files are automatically committed, so you need to defensively create empty new commits for your changes. As in, want to browse the repo from a commit 2 weeks ago? Well if you just checkout that commit and then edit a file, you've automatically changed that commit in your repo and rebased everything after it on top of your new changes. So instead you create a new branch off of the old commit and add an empty commit to that branch so any file changes don't end up rewriting the past 2 weeks of history. git is much nicer in that I can do whatever I want to the files and it won't change the repo until _I tell it to_.
smackmybishop 10 hours ago||
Just don't ever use `edit`, use `new` instead; then your changes are tracked without making a mess. I think that's much nicer than juggling stashes in git.
embedding-shape 9 hours ago|||
> Just don't ever use `edit`, use `new` instead

As a git-ist (?), if I'd ever move away from git, it would be to avoid tooling that has idioms like this (like git too has), if `jj` just gonna surface a bunch of new "bad ideas" (together with what seems like really good ideas), kind of makes it feel like it isn't worth picking up unless you don't already know git.

saghm 8 hours ago|||
The idiom here is use `edit` if you want to edit a commit, and use `new` if you want to make a new commit. This works identically whether you specify the commit via branch name or commit id. I'm not sure why people are saying not to use `edit` ever. It's basically just a shorthand for staging and amending changes in an existing commit, and there's still a use case for that; it's just not "I want to see the changes on this old branch".
embedding-shape 8 hours ago|||
> Just don't ever use `edit`,

> The idiom here is use `edit` if you want to edit a commit

You know, you guys have fun with that, I'll continue using git which (probably) has the same amount of warts, but I already know them. I'll continue to refer new VCS users to jj, seems a lot easier to learn, but really don't have the interest to re-learn a bunch of ever-changing idioms.

saghm 7 hours ago|||
I disagree with the people saying "never use edit". There are plenty of people saying conflicting things about git too, and I'd argue that understanding edit versus new isn't anywhere close to the level of wart that having to get people to agree on merging versus rebasing. Like you said though, have fun with that!
sswatson 5 hours ago|||
No system is perfect, but there's nothing wrong with `jj edit` and `jj new`. Both commands are completely reasonable and do what you think they would do.
joshuamorton 7 hours ago|||
I think it's because it's easy to make annoying mistakes (still easy to fix with undo) with edit. And it gains relatively little over new+squash. Edit is a useful power-feature, but I think for a novice, "never use it, only use the more well understood workflow of new+squash" is a good heuristic.
dzaima 9 hours ago||||
`edit` is still useful; just, for ..editing (!) something, instead of viewing it.

If you have some unfinished changes at the tip and want to temporarily checkout something 2 weeks ago, you `jj new` to there (similar to `git stash; git switch whatever`), and then later `jj edit your-old-tip` to go back (equivalent to `git switch main; git stash pop`; I think `jj edit` being an extended replacement for stash-popping things is a reasonable way to think about it). (and if you don't have any uncommitted changes, you always `jj new`)

jj also has a concept of immutable commits (defaulting to include tagged commits, and trunk at origin, which it'll disallow editing as a layer of defense)

surajrmal 9 hours ago|||
jj edit has good use cases, but it's not the default command you need. For instance, say you were working on some changes but had to change branches for a few minutes to do something. If you didn't manage to create a commit and want to go back to the previous staging area, you would use the jj edit command rather than jj new. It's very intuitive in my experience, something I can't say is true for managing git commits (unless you've spent years forcing it into muscle memory). I never need to run jj help. I run help commands with git all the time.
VMG 10 hours ago|||
... unless you actually want to edit a change!
throawayonthe 10 hours ago|||
well, you can do jj new <revision>, make your edit, and then do jj squash which will add the changes to the prev revision

i do this for example when i want to see a specific edit highlighted in my editor, it's a nice workflow i think

Aeolun 10 hours ago||
This is exactly how someone explained Git to me 12 years ago or so, and I’ve finally wrapped my head around it. Not changing now.
mh- 6 hours ago|||
If I'm understanding the thread correctly, I have a git alias to `git commit --amend --no-edit`, for exactly this workflow. When I'm hacking on something locally and want to just keep amending a commit. I only ever do this if it's HEAD though.
steveklabnik 5 hours ago||
Yes, one way to think about jj in a sort of low-level way is that every jj command does the equivalent of that, every time.

(You can also set up watchman and have that happen on every file change...)

hacker161 7 hours ago|||
[flagged]
BeetleB 9 hours ago||||
I go back and forth between the two approaches, but because of the whole "accidentally made some temporary changes and now it's a pain to separate/undo them because not all changes were temporary", I also usually do a jj new and then jj squash.
arccy 10 hours ago||||
still use new, and then squash your changes in. that way you can actually see what changes you made
incognito124 10 hours ago|||
then you `new` & `squash` :)
smweber 10 hours ago|||
jj edit is the biggest jj footgun I can think of, as other comments said just use jj new. But also if you do accidentally edit or change something jj undo works surprisingly well.

I found when using jj it worked best for me when I stopped thinking in commits (which jj treats as very cheap “snapshots” of your code) and instead focus on the “changes”. Felt weird for me at first, but I realized when I was rebasing with git that’s how I viewed the logical changes I made anyway, jj just makes it explicit.

jj auto-rebasing doesn’t matter until you push changes, and once you do it marks them immutable, preventing you from accidentally rebasing changes that have been shared.

saghm 8 hours ago|||
> jj edit is the biggest jj footgun I can think of

Honestly, this is only because `git checkout` is so convoluted that we've collectively changed our expectations around the UX. "checkout" can mean switching to another branch (and creating it if you specify a flag but erroring if you don't), looking at a commit (in which case you have "detached HEAD" and can't actually make changes until you make a branch) or resetting a file to the current state of HEAD (and mercy on your soul if you happen to name a branch the same as one of your files). Instead of having potentially wildly different behavior based on the "type" of the thing you pass to it, `jj edit` only accepts one type: the commit you want to edit. A branch (or "bookmark", as jj seems to call it now) is another way of specifying the commit you want to edit, but it's still saying "edit the commit" and not "edit the bookmark". Unfortunately, the expectation for a lot of people seems to be that "edit" should have the same convoluted behavior as git, and I'm not sure how to bridge that gap without giving up part of what makes jj nice in the first place.

nightski 8 hours ago||
It's not "wildly" different behavior based on the thing it's pointing to. In all 3 cases, the command is pointed at a commit and the behavior is the same. Once you know that branches/HEAD are just named pointers to commits, then it becomes obvious you are always just working on commits and branches/ids/HEAD etc are just ways of referencing them.
MrJohz 4 hours ago|||
But branches are not just named pointers to a commit. If they were, then checking out the pointer would be the same as checking out the commit itself. But I can check out a commit and I can check out a branch and depending on which I've done, I'm in two different states.

Either I'm in branch state, where making a commit bumps the branch pointer and means the commit will be visible in the default log output, or I'm in "detached head" mode, and making a commit will just create a new commit somewhere that by default is hidden into I learn what a reflog is. And the kicker is: these two states look completely identical - I can have exactly the same files in my repository, and exactly the same parent commit checked out, but the hidden mode changes how git will respond to my commands.

In fairness, none of this is so difficult that you can't eventually figure it out and learn it. But it's not intuitive. This is the sort of weirdness that junior developers stumble over regularly where they accidentally do the wrong kind of checkout, make a bunch of changes, and then suddenly seem to have lost all their work.

This is one of the ways that I think the JJ model is so much clearer. You always checkout a commit. Any argument you pass to `jj new` will get resolved to a commit and that commit will be checked out. The disadvantage is that you need to manually bump the branch pointer, but the advantage is that you don't necessarily need branch pointers unless you want to share a particular branch with other people, or give it a certain name. Creating new commits on anonymous branches is perfectly normal and you'll never struggle to find commits by accidentally checking out the wrong thing.

fragmede 4 hours ago||
> these two states look completely identical

No they don't. As you noted, one state is "detached head" and any competently set up shell PS1 will tell you that, or that you're on a branch by displaying the name of the branch vs the commit.

> Creating new commits on anonymous branches is perfectly normal

Sorry, that that's an example of more intuitive behavior on jj's partc, you've lost me. I've done that intentionally with git, but I know what I'm doing in that case. For someone new to version control, committing to an unnamed branch doesn't seem like a desired operation no matter which system you're using. What's wrong with requiring branches to be named?

steveklabnik 4 hours ago|||
> For someone new to version control, committing to an unnamed branch doesn't seem like a desired operation no matter which system you're using.

We have data on this! I can't cite anything public, but companies like Meta have to train people who are used to git to use tools like sapling, which does not require named branches. In my understanding, at first, people tend to name their branches, but because they don't have to, they quickly end up moving towards not naming.

> What's wrong with requiring branches to be named?

Because it's not necessary. It's an extra step that doesn't bring any real benefits, so why bother?

Now, in some cases, a name is useful. For example, knowing which branch is trunk. But for normal development and submitting changes? It's just extra work to name the branch, and it's going to go away anyway.

fragmede 1 hour ago||
Fascinating. The benefit it brings is you can map the branch to its name. Of the, say, 10 branches you've got checked out, how do you know which branch maps to jira-123 and which one maps to jira-234, or if you're using names, which anonymous branch maps to addFeatureA or fixBugB?

More to the point though, what tooling is there on top of raw jj/git? Specifically, there's a jira cli (well, multiple) as well as a gh cli for github as well as gitlab has one as well. When you call the script that submits the branch to jira/github/gitlab, how does it get the ticket name to submit the code to the system under? Hopefully no one's actually opening up jira/github/gitlab by hand and having to click a bunch of buttons! So I'll be totally transparent about my bias here in that my tooling relies on the branch being named jira-123 so it submits it to jira and github from the command line and uses the branch name as part of the automated PR creation and jira ticket modification.

steveklabnik 1 hour ago|||
> Of the, say, 10 branches you've got checked out, how do you know which branch maps to jira-123 and which one maps to jira-234, or if you're using names, which anonymous branch maps to addFeatureA or fixBugB?

The descriptions of the changes. I shared some jj log output in another comment, here it is with more realistic messages, taken from a project of mine:

    @  vvxvznow 
    │  (empty) (no description set)
    │ ○  uuowqquz 
    ├─╯  Fix compiler panic in error rendering for anonymous struct methods (rue-fwi9)
    │ ○  uvlpytpm 
    ├─╯  Stabilize anonymous struct methods feature
    ◆  lwywpyls trunk
    │  Fix array return type unification in type inference
That (rue-fwi9) is the equivalent of jira-123, if I super care about it being obvious, I might put it in the message. But also, I might not, as you can see with the other two. You could also pass flags to see more verbose output, if the first line isn't clear enough, but in general, the convention for git as well is to have that short summary that explains your change, so if it's confusing, you probably need to do better on that.

> Specifically, there's a jira cli (well, multiple) as well as a gh cli for github as well as gitlab has one as well.

These systems do require branches in order to open a pull request. In these cases, I use `jj git push -c <change id>`, which will create a branch name for me, and push it up. This is configured to produce a branch name like steveklabnik/push-mrzwmwmvkowx for a change with the id mrzwmwmv, and ultimately, it's still easier to name locally with m or mr depending on if the prefix is ambiguous. That said, from there I do usually just click the button and then "open pull request" on GitHub, but like, all of these tools (gh is the only one I've used, but I can't imagine that the others do not work, since ultimately, it's a git repo) just work if you want to use them.

Other systems do not even require a branch to submit, and so you don't even need to do this. I would say "submit mr" and it would return me the URL for the created change request. Gerrit does this on top of plain old git.

> how does it get the ticket name to submit the code to the system under?

I haven't worked with Jira in a long time, but with GitHub, if I make a change that fixes issue 5, I put "Fixes #5" in my description, and when the PR is created, it updates ticket #5 to link the PR to that change automatically, no other process needed.

Zizizizz 1 hour ago|||
You can name branches in JJ too, they're just called bookmarks.

git checkout main git pull git switch -c jira-234 ... git commit git push -u origin main

jj git fetch jj new main ... jj commit jj b(ookmark) c(reate) jira-234 -r @- jj b(ookmark) t(rack) jira-234@origin jj git push

steveklabnik 1 hour ago||
Right, this is a good point: you can if you want to, or if you're working with a system that requires them.

Just in practice, anonymous branches end up feeling very natural, especially during development, and especially if your code review tooling doesn't require names.

MrJohz 3 hours ago||||
They look identical to people who don't know what to look for, and who don't realise that these two states are different, which is the key thing. You can also distinguish them by running `git status`, but that's kind of the point: there's some magic state living in .git/ that changes how a bunch of commands you run work, and you need to understand how that state works in order to correctly use git. Why not just remove that state entirely, and make all checkouts behave identically to each other, the only difference being which files are present in the filesystem, and what the parent commit was?

What's wrong with unnamed branches? I mean, in git the main issue is that they're not surfaced very clearly (although they exist). But if you can design an interface where unnamed branches are the default, where they're always visible, and where you can clearly see what they're doing, what's wrong with avoiding naming your branches until you really need to?

I think this is the key thing that makes jj so exciting to me: it's consistently a simpler mental model. You don't need to understand the different states a checkout can be in, because there aren't any - a checkout is a checkout is a checkout. You don't need to have a separate concept of a branch, because branches are just chains of commits, and the default jj log commands is very good at showing chains of commits.

fragmede 2 hours ago||
My command looks like either:

    fragmede@laptop:(abranch)~/projects/project-foo$
or fragmede@laptop:(abcdef)~/projects/project-foo$

Depending on if abranch is checked out, or abcdef which may be HEAD of abranch is checked out.

If you're having to run `git status` by hand to figure out which of the two states you're in, something's gone wrong. (That something being your PS1 config.) If people are having trouble with that, I can see why switching to a system that doesn't have that problem, it just that it doesn't seem like it should even be problem to begin with. (It's not that it's not useful to have unnamed branches and to commit to them, just that it's not a intro-to-git level skill. Throwing people into the deep end of the git pool and being surprised when some people sink, isn't a good recipe for getting people to like using git.)

> What's wrong with unnamed branches? As you point out, those commits kinda just go into the ether, and must be dug out via reflog, so operationally, why would you do that to yourself. Separate from that though, do you "cd" into the project directory, and then just randomly start writing code, or is there some idea of what you're working on. Either a (Jira) ticket name/number, or at least some idea of the bug or feature you wanna work on. Or am I crazy (which I am open to the possibilty) and that people do just "cd" into some code and just start writing stuff?

VCS aside, nothing worse than opening Google docs/a document folder and seeing a list of 50 "Untitled document" files an my habit of naming branches comes from that. Even though I'm capable of digging random commits out of reflog, if all of those commits are on unnamed branches, and have helpful commit messages like "wip" or "poop", figuring out the right commit is gonna be an exercise in frustration.

As long as you've got something that works for you though, to each their own. I've been using too long for me to change.

tom_alexander 3 hours ago|||
> any competently set up shell PS1 will tell you that

I certainly hope your shell is not running `git` commands automatically for you. If so, that is a RCE vulnerability since you could extract a tarball/zip that you don't expect to be a git repository but it contains a `.git` folder with a `fsmonitor` configured to execute a malicious script: https://github.com/califio/publications/blob/main/MADBugs/vi...

fragmede 2 hours ago||
Might want to let git know. It's been a part of the git source code since 2006. If there were an RCE vulnerability from using __git_ps1, one would hope it would have been found by now!

https://github.com/git/git/blob/master/contrib/completion/gi...

tom_alexander 1 hour ago||
I was able to reproduce it using that script in my PS1 when `GIT_PS1_SHOWUNTRACKEDFILES=1` which triggers a call to `git ls-files`. Without that, it seems to be just calling `git rev-parse` which does not execute fsmonitor.

I was also able to reproduce it with `GIT_PS1_SHOWDIRTYSTATE=1` which invokes `git diff`.

As far as I am aware, this has been brought to the attention of the git maintainers years ago: https://github.com/justinsteven/advisories/blob/main/2022_gi...

saghm 7 hours ago|||
> In all 3 cases, the command is pointed at a commit and the behavior is the same

    echo "something" >> foo.txt
    git checkout foo.txt
What's the name of the branch this is pointed at? If I have to run another git command to find out, then it's not "pointed" at it.
webstrand 7 hours ago||
If you don't provide it a <tree-ish> it reads from the index (staged files). So you're right its not really pointed anywhere, since the index isn't a ref.
saghm 6 hours ago||
That's my overall point: the argument itself (with respect to the current state of the repo) is what determines the behavior. I don't think this is anywhere close to as intuitive as commands that only ever accept one "type" of argument (and erroring if it's different).
nightski 5 hours ago||
I stand corrected by this one scenario, but I’ve been using git for over a decade and never found that useful. Just don’t use checkout on a file path, there is no need.
saghm 37 minutes ago|||
"Just don't accidentally do things wrong" is also the way to avoid null pointer errors, type mismatches in dynamically typed languages, UB in C/C++. It works, until it doesn't, and in practice that happens pretty quickly. Personally, I like things that have proper safety checks.
sswatson 5 hours ago||||
I find this kind of advice to be a more scathing indictment of an interface than a critic could ever muster: asking users to forego available functionality so that some sense of order can be imposed.
tom_alexander 4 hours ago||
< glances around at all the people telling me to never use `jj edit` >
dwattttt 57 minutes ago|||
That goes in the same bucket as rebase. Until you know what it does, you'll be fine avoiding it.

Since people are sharing their experiences and my recent one is relevant to edit, I'll go:

Working on a feature recently, I ended up making 3 changes ("commits") on top of each other and hopping between them via jj edit.

The first change wasn't feature specific, it was extending the base project in preparation.

The second change just added a doc describing all the changes needed for the feature.

The third change removed the doc as parts were implemented, bit by bit.

As I progressed on the third change & found stuff I'd missed at the start of this process, I jumped back to edit the first change (maybe I had a bug in that base project extension) and the second change (oh hey, I found something else that needed to be done for the feature).

It sounds crazy compared to a git workflow, but at the end of the process I have 3 changes, all tested & working. If I was doing this with git, I'd have to rebase/squash to get the final changes into a neat clear history.

smackmybishop 35 minutes ago||||
I suggested that since you seemed really concerned about editing the commit that you just told it to edit. Use 'edit' all you want if your goal is to edit commits, otherwise 'new' does what it seems like you're expecting...
baq 2 hours ago|||
edit is useful and there are good reasons to use it, 'never use edit' is like 'never use goto' i.e. false - but if you're just starting out, jj new/jj squash is the way to go indeed.

(my particular favorite reasons to use jj edit are git-native tools which expect to work with uncommitted files e.g. autoformatters, linters, etc. which have been scripted in CI/dev workflows such that they cannot accept a list of files as params)

hollowcelery 3 hours ago||||
Interesting - I use git checkout constantly, whenever I have a file in another branch or commit that I want to drag into this one wholesale.
saghm 34 minutes ago||
It's a useful thing to be able to do! It just fundamentally shouldn't be under one command. To its credit, git did add `switch` (with `-c` for creating a new branch and `-d` for specifying detached HEAD), but after two decades I can't imagine they'll ever get rid of checkout entirely because it was so fundamental for so long, and as long as its there, it's a loaded footgun with the safety off.
tom_alexander 4 hours ago|||
If you don't run checkout on file paths, how do you undo changes to specific files that you haven't committed yet? Like you've edited but not committed <foo>, <bar>, and <baz>. You realize your edits to <bar> are a mistake. I'd just run `git checkout <bar>` to revert those changes, what do you do?

It is also really useful when you realize you want <bar> to be the version from a commit two weeks ago. I guess you could always switch to the branch 2 weeks ago, copy the file to /tmp/, switch back, and copy the file into place, but `git checkout c23a99b -- <bar>` is so quick and easy. Or does this example not fall under the "dont run checkout on a path" since it is taking a treeish first before the path?

nomel 8 hours ago|||
> preventing you from accidentally rebasing changes that have been shared.

I think this ruins it for me then. I push my in-progress work, to my in-progress branches (then git-squash or whatever later, if needed). It makes switching between (lab) computers, dead or not, trivial.

Is there some "live remote" feature that could work for me, that just constantly force pushes to enabled branches?

aseipp 8 hours ago|||
Yes, almost all JJ users do this constantly. Just "track" the particular branch. JJ has an idea that only some commits are immutable, the set of "immutable heads", and the default logic is something like "The main branch is always immutable, remote branches are immutable, 'tracked' remote branches are mutable." In other words, tracking a remote branch removes it from the set of immutable heads.

So just run:

    jj bookmark track myname/somecoolfeature --remote origin
and the default settings will Do What You Want. This is intended as a kind of safeguard so that you do not accidentally update someone else's work.

Some people configure the set of immutable heads to be the empty set so they can go wild.

nomel 7 hours ago||
This is all incredible. I even see a great looking GUI [1]!

[1] https://jj-gui.com/

saghm 8 hours ago|||
Nothing stops you from doing the equivalent of `git push --force` in `jj`. The flag is just named differently: `--ignore-immutable`. This is a global flag though, so it's available to all commands, and `jj` requires it whenever you're making changes to immutable commits, even locally. I'd argue that this is one of the killer features of `jj`, since by comparison `git rebase` treats everything the same whether you're squashing your own local commits on a feature branch or messing with the history of `main` in a way that would break things for everyone.
baq 7 hours ago|||
> edits to files are automatically committed

this is a core feature and it makes jj possible - you're supposed to get used to jj new and jj squash into the previous bookmarked commit, which you map to the git branch head/PR.

IOW you're supposed to work on a detached git head and jj makes this easy and pleasant.

saghm 9 hours ago|||
How are you "checking out" the old commit? It sounds like you're using `jj edit`, which I'd argue does what it says on the tin. Switch to using `jj new <branch>` and your problem goes away.
tom_alexander 8 hours ago||
That avoids the problem for the specific workflow of checking out an old revision (and it was what I was describing with checking out a new branch off the old commit and adding a blank commit to that branch), but another way this design bites me: At work I am constantly jumping around numerous repos because I might be working on repo <foo> but then someone on my team will ask for help with repo <bar>. So I'll turn on screen sharing, open up repo <bar> and I'll type out psuedo-code into <bar> as I'm explaining things to them.

So if the last thing I did on <bar> was finish some work by making a new commit, then writing some changes, and then giving it a commit message with `jj desc`, then I am now polluting that commit with the unrelated explanatory psuedo-code. So when switching to a repo I'm not actively working in, I need to defensively remember to check the current `jj status` before typing in any files to make sure I am on an empty commit. With git, I can jump around repos and make explanatory edits willy-nilly, confident that my changes are distinct from real meaningful commits.

I guess one way to describe it is: we want to make it easy to make good commits and hard to make bad commits. jj seems to be prioritizing the former to the detriment of the latter. My personality prioritizes rigorous safety / lack of surprises.

kps 6 hours ago|||
I think you have somehow picked up an overcomplicated workflow, and this is case is actually something that `jj` is much better at.

If I'm in the middle of working on <foo> and someone asks about <bar>: `jj new <bar>`. When I'm done (and do whatever I want with those new changes in <bar>, including deferring deciding what to do), I just `jj edit <foo>` and I'm back exactly where I left off. It's a bit like `git stash` without having to remember to stash in advance, and using regular commit navigation rather than being bolted on the side.

MrJohz 3 hours ago||||
I think the right intuition to have with jj is that `jj st` should show an empty change unless you are actively working on something. `jj commit`, as mentioned below, is a good example of this - it automatically creates a new change and checks it out. The "squash flow" also does this well - you use the branch tip as a staging area and squash work into other changes on the branch as you go along. Either way, once the work is finished, there's an empty change at the tip of the branch.

This is also supported by jj implicitly - whenever you check out a different commit, if the change you were on is empty, has no description, and is the tip of a branch, it's automatically deleted to clean things up for you.

icorbrey 8 hours ago||||
Fwiw I generally solve this by using `jj commit` instead of `jj desc` unless I'm specifically targeting something that isn't my working copy. Technically it violates the "we want commands to be orthogonal" guideline we use to write Jujutsu (otherwise this would indeed be `jj desc; jj new`) but as a habit it's never let me down
tom_alexander 7 hours ago||
Ah, thanks! That's a command I haven't learned yet, so I'll have to check it out. I learned jj from the tutorial that was posted and I don't think it covered `jj commit` at all.
steveklabnik 7 hours ago||
I didn't cover it for various reasons, but I think it's good to teach now that I've had more time to consider this sort of thing, so the next iteration will likely start by beginning with jj commit.
saghm 6 hours ago||
In a pure `jj` model, commit might not even be necessary as it's own subcommand (since you could easily define an alias for `desc` followed by `new`). We're still living in a world where most people who would consider adopting `jj` are git users currently, so I wonder if starting with `commit` and then following it up with an explanation of "here's how you can change the commit message without needing to make a new commit" and "here's how you can make a new commit without changing the name of the current one" would end up fitting people's expectations better.
steveklabnik 5 hours ago||
Yes, I do think that the latter is correct now.

I tend to learn "bottom-up", so I like the new + describe as a way of learning, but people want to jump in and get going with tools, so commit fits that expectation better.

saghm 29 minutes ago||
I'm the same way. I've learned over the years that this ends up being somewhat uncommon though, and one of the harder but more rewarding parts of helping people learn is figuring out where they're coming from and meeting them there. (I'm positive this is something you've been well aware of for a while though, probably longer than me!)
ersatz-matty 8 hours ago|||
From your "polluted" snapshot, you can run `jj commit -i` and use the TUI to select only what you want.
hacker161 7 hours ago||
Just like you can run `git add -p`
baq 7 hours ago||
yes but no
hacker161 7 hours ago||
Explain the difference.
baq 7 hours ago||
git add -p doesn't create a commit.
alunchbox 8 hours ago|||
if you loose an edit jj op log is incredible, I've saved a ton of work more-so now with AI's making mistakes. Also workspaces are super fast compared to git worktree's - same concept, different implementation.

I agree, that was a bit of an interesting approach but more-so than not it's been better in DX even though you have to 'unlearn' long term it's been a benefit IMO, but a soft one, not something you can measure easily.

csmantle 10 hours ago|||
`jj new` works like `git checkout` most by creating an empty revision on the top. `jj edit` on the other hand resembles `git checkout; [edits...]; git add -A; git commit --amend --no-edit`.
arianvanp 10 hours ago|||
You can disable the auto staging of new files since recently which removed the main grype for me
tom_alexander 4 hours ago||
ooo that will be a nice improvement. So many times I've run `jj status`, then saw a file I wanted gitignored, so I'll edit my gitignore, but the file has already been added to the repo so I have to `mv <file> /tmp/ && jj status && mv /tmp/<file> .` to get the file out of the repo.
steveklabnik 4 hours ago|||
You can `jj file untrack` instead of that mv bit.
tom_alexander 3 hours ago||
Oh neat, thanks! I (clearly) did not know that command.
kps 4 hours ago|||

    [snapshot]
    auto-track = '~glob:**/*'
jdkoeck 10 hours ago|||
Wow, that’s a total deal breaker to me. Using git may require a complex mental model, but at least it’s not doing anything I didn’t ask for.
Diggsey 10 hours ago|||
You would have had to run `jj edit` in order for this to happen, so I think it's a stretch to say you didn't ask for the edit?

This is the main difference though: in git files can be `staged`, `unstaged` or `committed`, so at any one time there are 3 entire snapshots of the repo "active".

In `jj` there is only one kind of snapshot (a change) and only one is "active" (the current working directory). When you make changes to the working directory you are modifying that "change".

As others have mentioned, the equivalent to `git checkout` would be `jj new`, which ensures a new empty change exists above the one you are checking out, so that any changes you make go into that new change rather than affecting the existing one.

jdkoeck 1 hour ago||
Thanks for the explanation! I wish I could edit my comment to reflect the truth.
saghm 8 hours ago||||
Using `jj edit` will edit a commit you specify, and `jj new` will make a new empty commit after the one you specify. These work exactly the same whether you specify a commit by branch or by the hash. I'd argue that you're getting exactly what you ask for with these commands, and by comparison, what "checkout" is asking for is much less obvious (and depends on context). We've just internalized the bad behavior of git for so long that it's become normalized.
stouset 7 hours ago||||
`jj edit` is quite literally asking for that.

GP is holding it wrong. If you don’t want to edit a commit, don’t ask to edit it. Use `jj new`.

Jenk 10 hours ago|||
This is literally jj's schtick and reason for existing, so I wouldn't be surprised if you decide it is not the tool for you.
tom_alexander 8 hours ago||
Yeah, that's a very real possibility. On the bright side, jj is git-compatible so at least the two camps can live together in harmony.
et1337 8 hours ago||
Jujutsu has a concept of mutable vs immutable commits to solve this. Usually everything in a remote branch is immutable. To work on a branch, I track it and that makes it mutable.
dgb23 11 hours ago||
The last paragraph might be the most important one:

> There's one other reason you should be interested in giving jj a try: it has a git compatible backend, and so you can use jj on your own, without requiring anyone else you're working with to convert too. This means that there's no real downside to giving it a shot; if it's not for you, you're not giving up all of the history you wrote with it, and can go right back to git with no issues.

verdverm 8 hours ago||
Unless you use LFS, submodules, or hooks at your org.
igor47 6 hours ago||
Submodules work fine but yeah, it's frustrating that lfs is taking so long. But there seems to be some momentum recently https://github.com/jj-vcs/jj/pull/9068
verdverm 4 hours ago||
The git compatibility page states that submodules are not supported

https://docs.jj-vcs.dev/latest/git-compatibility/

steveklabnik 4 hours ago||
What "not supported" means with submodules specifically is that jj doesn't have commands to manage them. You can use git commands to manage them, and it does, in my understanding, work. There's just no native support yet.

This is sort of similar to how you can create lightweight tags with jj tag, but you need to push them with git push --tags.

jeremyjh 9 hours ago|||
But this is not true. They are interoperable but far from seamless. Those features mainly support migration use cases or things like git deployment from an repo managed in jj. Operations git does are not in jj’s log. You have to constantly import them. The project recommends a single primary interface.
miyoji 9 hours ago|||
But it is true. I (and many others) happily use jj on teams that use git without anyone else on the team using jj or knowing (or caring) what I'm using.
ongy 9 hours ago||||
I think you are talking about colocation, which is slightly different than the `jj git push` `jj git fetch` type commands.

Colocation has its uses bit is a bit finicky. The push/pull compatibility works perfectly fine (with some caveats of github being broken that can be worked around).

maleldil 9 hours ago||||
If you constantly switch between the two, you're going to have a hard time, but you can take a git repo, try jj for a while, and if you decide to go back, you don't lose anything.
jeremyjh 9 hours ago||
Right, but that’s different from working in a team environment where everyone else continues using git.
stouset 7 hours ago|||
No?

What problems, exactly, are you suggesting exist? I have used jj extensively on git teams and it has been seamless. The only people who have noticed or cared are the coworkers I’ve gotten to join me.

maleldil 3 hours ago||||
You're confusing mixing git and jj in your local copy of the repo vs what it looks like to other people. You can use jj locally, and it interoperates perfectly with any git remote, and no one has to know you're even using it. From the point of view of other people, it doesn't matter.
saghm 8 hours ago||||
How so? I've used `jj` locally on teams where most (if not all) of the other team members were using git, and they only found out I was using `jj` when I told them.
pyreko 7 hours ago|||
fwiw I don't use it personally but some people on my team use it while the others use git, and nobody complains.
tonfa 5 hours ago||
Yeah same here, have been using jj exclusively, the only reason people notice is because my branch names default to the changeid in my setup so I've had questions about the random looking strings.
asdfasgasdgasdg 9 hours ago||||
Most importantly, submodules are not fully supported, which are used by almost every open source project at least in the space I work in (embedded). So you can't use jj to easily contribute back to those project. It can be done but you always have to be cognizant of whether a submodule has changed between two branches or when you sync, since they don't update automatically the way they do with git.
saghm 8 hours ago|||
It's been over a year since I last used git manually in the CLI, and I've exclusively worked with git remotes. The only time I had any friction was on a team where stale code-gen output was checked into the repo and for whatever reason no one was willing to either add it to the `.gitignore` or commit (pun intended) to keeping it up to date, meaning that I had to manually remove the changes from when I compiled before pushing. I would have argued in favor of adding to .gitignore or keeping it up to date even if I didn't use `jj` though because I think having stale output checked in is just silly.
nchagnet 2 hours ago||
For what it's worth, you can have your own local gitignore by adding patterns to .git/info/exclude. It's quite useful in this exact situation.
saghm 41 minutes ago||
I did try this, but for whatever reason it kept getting added back automatically. I forget the details of exactly why it was happening because it was close to a year ago, and in the compatibility guide it says this is supported, but I'm not sure if it was at the time or I was running into something different. This was a contract gig for me where I knew it would be ending within a month or so, which meant I didn't bother spending a ton of time trying to figure out a long-term solution.
eru 9 hours ago|||
Funnily enough, that's how I used git with CVS and Subversion, too.
IshKebab 9 hours ago||
Big caveat: do not try to use Git and JJ in the same directory. It's probably fine if you only use JJ, but if you mix them you will horribly break things.
chriswarbo 6 hours ago|||
I suppose it depends what you mean by "horribly break things".

The only thing I've noticed is that `jj` will leave the git repo with either a detached HEAD, or with a funny `@` ref checked out.

I don't think that would trouble someone who's experienced with git and knows its "DAG of commits" model.

For someone who's less experienced, or only uses git for a set of branches with mostly linear history (like a sort of "fancy undo"), I could imagine getting a shock when trying to `git commit` and not seeing them on any of the branches!

nailer 1 hour ago||
> I don't think that would trouble someone who's experienced with git and knows its "DAG of commits" model.

I think most people that have git experience don't know what a DAG is and have never used reflog.

surajrmal 7 hours ago|||
This isn't true?
IshKebab 7 hours ago||
It is when I tried it.
stouset 7 hours ago||
Jujutsu uses git as its primary backing store and synthesizes anything else it needs on top on-the-fly. Any incompatibility here is considered a serious bug.

Obviously I can’t argue against your lived experience, but it is neither typical nor common. This is quite literally an explicitly-supported use, and one that many people do daily.

BeetleB 8 hours ago||
One of my favorite jj features is "jj absorb".

For each change you've made in the current revision, it finds the last commit where you made a change near there, and moves your changes to that commit.

Really handy when you forgot to make a change to some config file or .gitignore. You just "jj new", make the changes, and "jj absorb". No need to make a new commit or figure out where to rebase to.

Oh, and not having to deal with merge conflicts now is awesome. My repository still has merge conflicts from months ago. I'll probably go and delete those branches as I have no intention to resolve them.

dnmc 1 hour ago||
And If `jj absorb` gets it wrong, you can run `jj undo`.

This is such a killer feature to me. I'm not scared to start potentially gnarly rebases anymore because I can painlessly undo.

BeetleB 1 hour ago|||
Yes. With "jj undo", I'm not scared to do anything. The brief time I had to go back to using vanilla "git", I didn't enjoy being extra cautious.

Using a version control tool shouldn't require much self discipline.

nchmy 30 minutes ago|||
yeah, i regularly try absorb then undo when it moves it to a commit from 7 years ago, then manually squash where appropriate
xixixao 8 hours ago||
git absorb exists too fyi
bitdivision 7 hours ago||
Not by default: https://github.com/tummychow/git-absorb
steveklabnik 8 hours ago||
Hey folks!

So, I haven't updated the tutorial in a long time. My intent is to upstream it, but I've been very very busy at the startup I'm at, ersc.io, and haven't had the chance. I'm still using jj every day, and loving it.

Happy to answer any questions!

opem 2 hours ago||
I think one major difference between git and jj is how immutable their DAG is, due to the difference in how they refer to their unit of change (i.e. stable change ID with changing commit IDs vs. immutable commit ID). One implication of that is change history in a git repo feels much more immutable to the one in a jj repo. Consequently operations that involves changing the history like, undo/rebase feels much easier/flexible. Is my understanding correct?
steveklabnik 2 hours ago||
Sorta! I think it can feel that way at times, but also the opposite. jj’s changes are immutable in the same way commits are, when you modify a change, it makes a new immutable commit and associates that with the change. So on the literal level, they’re the same.

But it’s true that mutating history is easy and sometimes even automatic with jj, whereas it’s not with git. So that could make it feel more mutable. On the other hand, jj has the concept of mutable vs immutable commits; jj will present you from modifying certain changes unless you pass in a flag to override it. So in some ways, it’s more immutable than git.

Just really depends on your perspective.

klauserc 7 hours ago||
jj automatically hides "uninteresting" changes. Most of the time, this is good.

Occasionally, I need to see more changes. It is not obvious to me how I get jj to show me elided changes. I mean, sure, I can explicitly ask jj to show me the one ancestor of the last visible change, and then show me the ancestor of that one, etc. Is some flag to say: "just show me 15 more changes that you would otherwise elide"?

steveklabnik 6 hours ago|||
I use `jj log -r ..` for that, which is just an open ended range. It's not the "15 more" but it's what's worked for me. I suspect you could do it with some sort of revset stuff, but I like to keep it simple.
nickisnoble 6 hours ago|||
Easy: `jj log -n 25`

(Default is 10 iirc, so if you want 15 more... 25)

If you want everything, ever: `jj log -r ::`

Or every ancestor of your current change: `jj log -r ..@`

steveklabnik 5 hours ago||
IIRC -n only limits the output, not expands it. jj log and jj long -n 25 show the same results for me.
TrysterosEmp 4 hours ago||
Why should you care about jj? Look, ethereal, balaeric Gothenburg indie from the 2010s may not be important in the grand scheme of things, but their strong hip hop influence was genuinely exciting at the time. When the great chill wave summer of 2009 crested, you definitely cared about jj’s otherworldly grooves and lil Wayne samples. Even if they never did reach the euphoric highs of Washed Out or even label mates The Tough Alliance.
erdaniels 4 hours ago|
Preach... thank you
jiggunjer 7 hours ago||
I think the mental model is like C vs python. Git gives you a forensic trace back in time. jj gives you a story with chapters. Look under the hood you'll still see forensic map of state transitions, but this not what we want to navigate most of the time. Sometimes we need to rewrite an early chapter to make the latest chapter make more sense.
baq 7 hours ago|
the fact that almost by definition stuff that jj does is possible in git makes it hard for some folks to let go of the baggage that git has; it's simply hard to imagine a world where you can't git add ('how do you commit what you need committed and not commit the rest?') or not having to resolve conflicts immediately ('why would I want not to?')

...and it turns out when you answer these questions differently ('working tree is a commit', 'conflicts can committed) but still want git compatibility, jj kinda falls out of the design space by necessity.

compiler-guy 7 hours ago||
One of the things that makes jj worth trying out is simply the fact that it is different than git, and having exposure to more than one way of doing things is a good thing.

Even if you don't adopt it (and I didn't), it's easy to think that "this way is the only way", and seeing how systems other than your own preferred one manage workflows and issues is very useful for perspective.

That doesn't mean you should try everything regardless (we all only have so much time), but part of being a good engineer is understanding the options and tradeoffs, even of well loved and totally functional workflows.

beanjuiceII 2 hours ago|
being a good engineer is also understanding when something is a waste of time because the gain is insignificant 99% of the time
zingar 8 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 7 hours ago||
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 7 hours ago|||
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 6 hours ago|||
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 7 hours ago||||
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 6 hours ago||||
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 2 hours ago||||
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 7 hours ago|||
Yeah I legit do not understand the appeal. I’m willing to be wrong but it’s not clicking with me at all
tcoff91 6 hours ago|||
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 6 hours ago|||
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 7 hours ago||||
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 6 hours ago||||
> 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 4 hours ago|||
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 6 hours ago|||
`jj undo` compared to what exactly?
k4rnaj1k 7 hours ago|||
[dead]
latortuga 7 hours ago|||
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 8 hours ago|||
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 8 hours ago|||
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 7 hours ago||
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 8 hours ago||||
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 7 hours ago||||
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 7 hours ago|||
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 7 hours ago|||
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 8 hours ago|||
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 7 hours ago||
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 7 hours ago|||
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 7 hours ago|||
> that the workflows are intuitive

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

WesolyKubeczek 7 hours ago|||
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 7 hours ago|||
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 7 hours ago||
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 7 hours ago|||
> but not seeing is not believing.

Classic denying the antecedent :-)

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

coldtea 7 hours ago||
It's not supposed to be a modus ponens deduction.

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

Rebelgecko 6 hours ago|||
"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 7 hours ago|||
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 7 hours ago|||
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 7 hours ago||
There is no index anymore. I guess that is the "easier" part.
steveklabnik 7 hours ago||
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.

acoustics 6 hours ago|
jj has made me much more comfortable using non-linear DAGs in my trunk-based development workflow. Several changes with the same parent, changes with several different parents, etc.

I used to have a habit of imposing an unnecessary ordering structure on my work product. My stack of changes would look like A -> B -> C -> D, even if the order of B and C was logically interchangeable.

jj makes DAGs easier to work with because of how it handles conflicts and merges. Now I feel empowered to be more expressive and accurate about what a change actually depends on. In turn, this makes review and submission more efficient.

tonfa 5 hours ago|
Do you use a mega-merge + absorb workflow on top of the faned-out changes?
More comments...