upvote
> 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.

reply
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()'
reply
> 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!

reply
I think calling them "commits" is doing it a disservice because it's not the same as git commits, and the differences confuse people coming from git. I'd say "jj changes are like git commits, except they're mutable, so you can freely move edits between them. They only become immutable when you push/share them with people"..

It's a mouthful, but it's more accurate and may be less confusing.

reply
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!
reply
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.
reply
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.

reply
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.
reply
Yes, that’s idiomatic in JJ. Honestly, I’ve been recommending the same workflow (first commit, then make changes) with Git too since years

https://arialdomartini.github.io/pre-emptive-commit-comments

If you want to have a workflow similar to Git with index, check out the Squash Workflow: basically, you would edit your files in a disposable commit having the same purpose of Git’s index.

reply
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.

reply
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.

reply
> 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.

reply
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.

reply
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`

reply
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).
reply
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.
reply
> 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.

reply
> 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`.

reply
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.
reply
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.

reply
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.

reply
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.

reply
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.

reply
> 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. =)

reply
git promises "version control", this clearly implies that the versions predate the control: in this picture the git workflow is not backwards.
reply
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.

reply
Not necessarily, I often make changes on unrelated commits. You can always use jj split to extract the change and put it somewhere else.
reply
That totally works and it's how I use jj. jj commit -i does what you would want
reply
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.

reply
I am dumb. why is that better than a git branch or a git worktree ?
reply
In sort of the same way juggling apples is better than juggling hand grenades: it's mostly the same in the simple cases, but once you start doing the really fancy stuff, one of the two will get you a lot fewer messy explosions.

(Your question is not dumb, BTW. The pithy answer is: UX matters, but it does so in ways that can be hard to convey since it's about the amount of cognition you need to put in a given thing to get a desired outcome, and that's tricky to express in text. Also there will always be some people for whom a new mental model just doesn't work. That doesn't make them dumb either, at least provided they have the wisdom not to petulantly piss in the cornflakes of those who get a kick out of how much better the new thing works for them.)

reply
You have to put a lot of effort to mess up a git repo. So I'm not seeing the allusion to hand grenades.
reply
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.

reply
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.

reply