Hacker News new | ask | show | jobs
by steveklabnik 496 days ago
> So jj calls commits "changes", and this is less confusing?

Sort of. It has both changes and commits, actually. (and sometimes commits are called revisions.)

     jj log
    @  muzrswxs steve@steveklabnik.com 2025-02-12 10:23:11 85b41b31
    │  (empty) (no description set)
    ○  wotxrwpp steve@steveklabnik.com 2025-02-12 10:23:09 24ce0a16
    │  (empty) (no description set)
    ○  ztxxskuu steve@steveklabnik.com 2025-02-11 18:20:56 1b3e12ac
    │  run sqlx migrate as part of deploy process
    ◆  qwovsnvt steve@steveklabnik.com 2025-02-11 17:45:24 trunk b224ca8b
    │  <redacted>
Okay, so muzrswxs is a change ID. It's true that we're connecting changes in a graph, and that that forms history. So in that sense, it's like a git commit. But because changes are mutable (well, the ○ ones and @ are, the ◆ there is not), they are implemented as a sequence of commits. So if you look on the far right there, you'll see 85b41b31 and then below it, 24ce0a16. Below that, 1b3e12ac. These are commit IDs.

The first two changes are empty, so what happens if we modify a file?

     jj log
    @  muzrswxs steve@steveklabnik.com 2025-02-12 10:28:18 404a73b1
    │  (no description set)
    ○  wotxrwpp steve@steveklabnik.com 2025-02-12 10:23:09 24ce0a16
    │  (empty) (no description set)
    ○  ztxxskuu steve@steveklabnik.com 2025-02-11 18:20:56 1b3e12ac
    │  run sqlx migrate as part of deploy process
    ◆  qwovsnvt steve@steveklabnik.com 2025-02-11 17:45:24 trunk b224ca8b
    │  <redacted>
Note that (empty) went away on that head change there, and its change ID is still muzrswxs. But the commit ID has changed from 85b41b31 to 404a73b1. None of the parents changed, of course.

We can even take a look at this history:

     jj evolog --summary
    @  muzrswxs steve@steveklabnik.com 2025-02-12 10:28:18 404a73b1
    │  (no description set)
    │  M README.md
    ○  muzrswxs hidden steve@steveklabnik.com 2025-02-12 10:23:11 85b41b31
       (empty) (no description set)
The evolution log will show us how our change has evolved over time: first we had 85b41b31, then we modified README.d and now we're at 404a73b1.

> I find when you dig into people's understanding of git (or version control in general), a lot of them understand it as storing a sequence of diffs. This small thing breaks their understanding of the whole system.

I agree with you in some sense, but also, kinda don't. That is, I agree that thinking git stores diffs is not correct, but I'm not fully sold on how big of a deal it is to be incorrect here. And once you really get into things, like, how packfiles are implemented, diffs are present.

> Calling them "changes" seems like it would reinforce this belief. Or is that the idea? Does jj embrace this perhaps more intuitive "sequence of diffs" view, but more successfully hide the "sequence of commits" reality?

I can assure you that these names are a heated kind of debate internally. I actually said two days ago "hey, so we have changes, commits, and then revision as a synonym for commit. shouldn't commit be a synonym for revision? because 'revision' is kind of an abstract idea, but 'commit' is git-specific, so like, I think it should be "we have changes, and changes have revisions, but the git backend implements revisions as commits" and that thread is still going this morning, with links to many previous discussions. Someone even wrote a blog post a year ago https://blog.waleedkhan.name/patch-terminology/

jj is still figuring out how best to present its ideas. I really like "change and revision" to describe these two things, but a lot of folks are concerned that "change" is too generic and is hard to figure out, that is, when I said this above

> its change ID is still muzrswxs. But the commit ID has changed

This is two different uses of the word "change". Is that confusing? Maybe. Is it confusing enough to find another word? Not sure.

1 comments

> I agree with you in some sense, but also, kinda don't. That is, I agree that thinking git stores diffs is not correct, but I'm not fully sold on how big of a deal it is to be incorrect here.

It's not so much about what actually happens underneath, that should be irrelevant and git just does what it does for practical reasons ultimately (as you point out, with packfiles, but this is definitely not a detail any git user needs to be aware of).

The problem I see is that git actually exposes both views of things. A seasoned git user will be used to the "duality" of commits vs diffs (ie. they are two different views of the same thing). Git exposes diffs directly when cherry-picking or rebasing, but at most other times you are working with commits. You don't push/pull diffs, you push/pull commits. It seems like a small thing, but every time I've dug into why somebody is having trouble with git it seems to be they view the world only as diffs.

So my question really was whether jj attempts to expose only one or the other. Looking at your explanation I would say it doesn't. It seems to me like changes are very similar to branches in git. At least this is how I think of branches in git, but I tend to be the "git guy" in every place I've worked. I mutate branches all day long by doing git commit amend etc.

It seems like the real point here is to get rid of "branch" as that is an overloaded concept and split it into two things: change and bookmark. In many ways it just seems like a reinforcement of the way I (and I guess other "git guys") use git anyway. Interesting!

We were actually talking about this for quite a while on Discord yesterday. The duality is visible to the user in jj as least as much as it is in git.

Some command arguments treat commits as snapshots (e.g. `jj restore --from/--into`, `jj diff --from/--to`, `jj file list -r`) and some commands arguments instead inspect the diffs (e.g. `jj rebase -r/-b/-s`, `jj diff -r`, `jj squash --from`, `jj log <path>`).

The first-class conflicts (https://jj-vcs.github.io/jj/latest/conflicts/) allow jj to be much better at treating commits as diffs than git is. In particular, there's no real difference between merge commits and other commits; any commit can introduce conflicts and any commit can resolve conflicts. We define the changes in a commit as relative to the auto-merged parents. That means that the diff-centric command arguments work in a consistent way for merges too. For example, if you create a new merge commit (`jj new A B ...`), it might have conflicts, but we still consider it empty/unchanged. If you resolve the conflicts, then `jj diff` will show you the conflict resolutions, and `jj rebase` will rebase the conflict resolutions (a bit like git rerere, but it also works on hunks outside of the conflict areas).