Skip to content

Conversation

@matts1
Copy link
Contributor

@matts1 matts1 commented Oct 7, 2025

As has been discussed in several places, jj squash is a very complicated command that can do many things.

jj movediff is much simpler to explain ("moves diffs from one commit to another").

It is implemented in terms of squash because the difference between the two is more semantic than programatic.

In the future, we may consider removing the ability for squash to operate on individual files, but that is still hotly debated.

This works towards #7465

Checklist

If applicable:

  • I have updated CHANGELOG.md
  • I have updated the documentation (README.md, docs/, demos/)
  • I have updated the config schema (cli/src/config-schema.json)
  • I have added/updated tests to cover my changes

@matts1 matts1 requested a review from a team as a code owner October 7, 2025 06:02
@matts1 matts1 force-pushed the push-znvlsuwynovn branch from a32c9f5 to 23aec32 Compare October 7, 2025 06:27
@PhilipMetzger
Copy link
Contributor

I know this is bikeshedding but can the command be named diffmove to be consistent with diffedit. Also this is probably still up for discussion.

@glehmann
Copy link
Contributor

glehmann commented Oct 7, 2025

According to #7465, you probably want to remove more options from diffmove, like -r or -A/-B/-d. And maybe make --from allow a single revision.

Also I'm not sure why you haven't kept --message and --use-destination-message in diffmove.

@joyously
Copy link

joyously commented Oct 7, 2025

jj movediff is much simpler to explain ("moves diffs from one commit to another").

I beg to differ about this. Maybe from a perspective of knowing implementation details, it makes more sense. But for me (who doesn't know the code), "moving a diff" makes no sense at all.

@matts1 matts1 force-pushed the push-znvlsuwynovn branch 2 times, most recently from dd8e02e to fe82e9a Compare October 7, 2025 23:19
@matts1
Copy link
Contributor Author

matts1 commented Oct 7, 2025

According to #7465, you probably want to remove more options from diffmove, like -r or -A/-B/-d. And maybe make --from allow a single revision.

Done

Also I'm not sure why you haven't kept --message and --use-destination-message in diffmove.

My rationale was that diffmove works on diffs, not on revisions. In retrospect, --use-destination-message doesn't make much sense but -m could be useful, so I've added that back.

I beg to differ about this. Maybe from a perspective of knowing implementation details, it makes more sense. But for me (who doesn't know the code), "moving a diff" makes no sense at all.

Moving a diff is simply the first step of squashing. It "squashes" the changes, but not any metadata.

git checkout was a multimodal command that was changed to two commands - git switch and git restore. jj squash presents a similar (but not as severe) problem. Attempt to explain squash to someone who's never used jj before - it's actually really complex.

Roughly speaking, squash is a four stage process:

  1. Move diffs from one revision to another
  2. Decide whether or not the source commit needs to be abandoned
  3. (optional) Abandon the source commit
  4. (optional) Decide the destination commit description

jj diffmove is far simpler by comparison. It simply performs the first step. It basically is squash, but doesn't touch bookmarks, descriptions, change IDs, etc.

@joyously
Copy link

joyously commented Oct 8, 2025

Roughly speaking, squash is a four stage process

Again, it sounds more like implementation details. I think it's better to hide all that from the user, and just do what needs to be done. The user shouldn't have to run multiple commands to accomplish the squash.

@matts1
Copy link
Contributor Author

matts1 commented Oct 8, 2025

Again, it sounds more like implementation details.

They aren't implementation details though, because it affects how squash behaves in a meaningful way to a user. jj squash abandons a commit if it becomes empty, and combines metadata to the new commit. There are several different edge cases with squash, and the options interact in a rather confusing way.

  • --use-destination-message only works with a full squash
  • --keep-emptied makes a full squash behave like a partial squash
  • Specifying filesets or -i creates a partial squash
    • Unless you happened to specify every file, in which case it does a full squash

It's not an implementation detail because, for example, I frequently don't want jj to abandon a commit that I was squashing from, or to combine the descriptions.

The user shouldn't have to run multiple commands to accomplish the squash.

No-one is suggesting having to run multiple commands to accomplish the squash. For now, this splits squash into two commands:

  • squash does the same thing it previously did. If you want to just keep using squash as is, you can
  • diffmove only does partial squashes. It doesn't touch metadata.

There has been discussions about removing the partial squash functionality from squash. This is still under debate, so I'm not implemmenting this. However, even if this were implemented, this would also not require you to use multiple commands to accomplish the squash. You would simply choose between squash for squashing whole revisions (including content) and diffmove for squashing only revision content (read: diffs)

@joyously
Copy link

joyously commented Oct 8, 2025

It's not an implementation detail because, for example, I frequently don't want jj to abandon a commit that I was squashing from, or to combine the descriptions.

I seems like implementation detail to me, since I think squash is a command only some workflows use. If your workflow doesn't quite fit it, maybe you should examine that instead.

You would simply choose between squash for squashing whole revisions (including content) and diffmove for squashing only revision content (read: diffs)

Again, this doesn't actually make sense to me since I don't know what is in a revision (and I shouldn't have to know to use the tool).

@matts1
Copy link
Contributor Author

matts1 commented Oct 8, 2025

It's not an implementation detail because, for example, I frequently don't want jj to abandon a commit that I was squashing from, or to combine the descriptions.

I seems like implementation detail to me, since I think squash is a command only some workflows use. If your workflow doesn't quite fit it, maybe you should examine that instead.

By that definition, every part of squash is an implementation detail. The definition of an implementation detail is a piece of information that only someone writing the feature needs to know, and not someone using it. Whether all workflows use it is completely irrelevant (except if you can achieve it with other commands, which you cannot). The only thing that matters is whether it makes a meaningful difference to the user, which in this case, it does. To some users it might be an implementation detail, but discussions have clearly shown that it isn't an implementation detail to all users, and some people care a great deal about this (see #7465).

You would simply choose between squash for squashing whole revisions (including content) and diffmove for squashing only revision content (read: diffs)

Again, this doesn't actually make sense to me since I don't know what is in a revision (and I shouldn't have to know to use the tool).

You don't need to know what's in a revision to use this tool - that's what -i is for. Also, the exact same argument applies to squash, so the argument doesn't seem particularly relevant anyway.

Copy link
Contributor

@PhilipMetzger PhilipMetzger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nits

@joyously
Copy link

joyously commented Oct 8, 2025

You would simply choose between squash for squashing whole revisions (including content) and diffmove for squashing only revision content (read: diffs)

Again, this doesn't actually make sense to me since I don't know what is in a revision (and I shouldn't have to know to use the tool).

You don't need to know what's in a revision to use this tool - that's what -i is for. Also, the exact same argument applies to squash, so the argument doesn't seem particularly relevant anyway.

It is very relevant, when you refer to choosing which command to use by knowing what is in the revision! How can you say it's not relevant that your statement made no sense? I don't know how to choose between the commands because I don't know what's in a revision. When you "explain" the command in terms that don't make sense, how is the user supposed to learn the tool? I know it's difficult, but you have to design as if you don't know what you know. I don't look at the details, so I can tell you that this makes no sense to a novice.

@matts1 matts1 force-pushed the push-znvlsuwynovn branch from fe82e9a to 48e8f62 Compare October 8, 2025 20:59
@matts1 matts1 changed the title cli: Split jj squash into jj squash and jj movediff cli: Split jj squash into jj squash and jj diffmove Oct 8, 2025
@matts1
Copy link
Contributor Author

matts1 commented Oct 8, 2025

It is very relevant, when you refer to choosing which command to use by knowing what is in the revision! How can you say it's not relevant that your statement made no sense? I don't know how to choose between the commands because I don't know what's in a revision. When you "explain" the command in terms that don't make sense, how is the user supposed to learn the tool? I know it's difficult, but you have to design as if you don't know what you know. I don't look at the details, so I can tell you that this makes no sense to a novice.

There are two questions at play here:

  • Should the command exist?
  • How should we document it in a way that's easy for users to understand?

Your previous commands seemed to be mostly about whether the command should exist (perhaps because you don't think it's easy to understand for a user?), while your latest seems to be complaining that it's not clear what it does. I personally think the name explains what it does, but I will admit that I (and probably most other people commenting on jj PRs) understand that a revision is just a set of diffs + some metadata, and that to "squash" is to move those diffs from one revision to another.

Do you have a better name for it, or a way to better explain it?

@joyously
Copy link

joyously commented Oct 8, 2025

Whether all workflows use it is completely irrelevant (except if you can achieve it with other commands, which you cannot).

and

Your previous commands seemed to be mostly about whether the command should exist (perhaps because you don't think it's easy to understand for a user?), while your latest seems to be complaining that it's not clear what it does.

These both have the insider knowledge built-in, and you can't even see it. If there are two commands, you have to figure out which to use, and the basis to decide is what you explain is implementation details of "revision is just a diff with metadata".

When I use version control, it's "changes to files". I don't think in terms of diff or metadata. I might have to update something based on a review, but it's still all about how a file's content is changed (and it is confusing when you use the word "content" differently).
To me, a "diff" is what changed between two files. It is not something you move (or edit or "interdiff"). When describing this, it sounds similar to jj absorb. Words that come to mind are "combine", "squash", "mix", "amend", "remedy".
What I'm wondering is, isn't this just a different flag for "squash"? Why does it have to be a different command?

@matts1
Copy link
Contributor Author

matts1 commented Oct 9, 2025

These both have the insider knowledge built-in, and you can't even see it. If there are two commands, you have to figure out which to use, and the basis to decide is what you explain is implementation details of "revision is just a diff with metadata".

I don't think "revision is just a diff with metadata" is fair to call an implementation detail - I think that's basic knowledge that everyone should have.

The literal meaning of a diff is "changes to files", and while metadata can be understood to a varying degree, at the bare minimum users should understand that a revision can have a description, and most should understand that bookmarks, for example, are metadata associated with a revision (semantically - as an implementation detail I have no clue where they're stored). So movediff literally means what it says in the first line of the description of the command - "move changes from a revision into another revision". jj is fairly consistent about calling "changes to files" diffs, so I think it's pretty reasonable to call it diffmove.

I might have to update something based on a review, but it's still all about how a file's content is changed (and it is confusing when you use the word "content" differently).

Fair, but note that the term "content" doesn't exist in the documentation, I only used it in this discussion

When describing this, it sounds similar to jj absorb. Words that come to mind are "combine", "squash", "mix", "amend", "remedy". What I'm wondering is, isn't this just a different flag for "squash"? Why does it have to be a different command?

That's a good point, and a very philosophical question. To phrase it in the terminology you're more familiar with then, jj squash is overloaded to do two things:

  • Combine two commits into one similar to git rebase --squash or hg fold
  • Amend a commit, similar to git commit --amend or hg amend

I think that the pros and cons are:

  • amend is more familiar to users coming from other version control systems, and probably provides a better understanding to people who are less familiar with version control
  • For the casual user, amend tells you when to use it in the name itself
  • diffmove, on the other hand, tells you precisely what it does, but may require a more formal understanding of version control systems.
  • Calling it diffmove may be better for more advanced use cases (eg. in the event of jj amend --from x --to y), "amend" may not make so much semantic sense? Or maybe it does?)

I called it diffmove because that was the conclusion that most of the jj devs came to. I think a reasonable argument could be made, however, that they are biased because they know too much about jj, and that jj amend is a better name.

Personally though, I do agree that jj amend is a better choice, now that you've brought it up as an option. In fact, similar to git checkout, I'd be in favor of splitting jj squash into jj amend and jj combine/join (I searched google for "opposite of split", since I think split is excellently named).

I personally feel that jj squash is a cool "hey, did you know that you can use the same mechanism to combine commits and to amend commits because of the fact that we have @ as a commit", but in reality, squash is overcomplicated to use and has too many flags, due to it wanting to do everything.

Also, this way jj combine could take revsets instead of files as positional arguments.

@joyously
Copy link

joyously commented Oct 9, 2025

The literal meaning of a diff is "changes to files", and while metadata can be understood to a varying degree, at the bare minimum users should understand that a revision can have a description, and most should understand that bookmarks, for example, are metadata associated with a revision

...according to you... The trick is that my concept of diff is that it's a transient thing, and what the VCS is storing is my files. So talking about moving a diff makes no sense when all I have is files. That's why it's an implementation detail.
And having two commands, I still have to figure out which to use, just as figuring out which flags to put on a single command. The difference with 'single command and flags' is that you can make the most common use case the default (no flags), and then advanced users can add the flags when they need them. But you do have a good point about being able to use positional parameters for file paths or revsets, although I have seen comments that indicate a standardization of always requiring -r for the revset.

@matts1 matts1 force-pushed the push-znvlsuwynovn branch 2 times, most recently from b566ead to baab557 Compare October 18, 2025 01:38
As has been discussed in several places, `jj squash` is a very
complicated command that can do many things.

`jj movediff` is much simpler to explain ("moves diffs from one commit
to another").

It is implemented in terms of squash because the difference between the
two is more semantic than programatic.

In the future, we *may* consider removing the ability for squash to
operate on individual files, but that is still hotly debated.
@matts1
Copy link
Contributor Author

matts1 commented Oct 18, 2025

It's very clear to me now from talking to coworkers that while diffmove is popular name amongst jj developers for being technically correct, it's an absolutely terrible name for your average dev.

I've renamed it to jj amend, since everyone immediately knew what it did when I called it that, and it's consistent with both hg amend and git commit --amend.

@matts1 matts1 force-pushed the push-znvlsuwynovn branch from baab557 to efdb809 Compare October 18, 2025 01:44
@matts1 matts1 changed the title cli: Split jj squash into jj squash and jj diffmove cli: Split jj squash into jj squash and jj amend Oct 18, 2025
@matts1 matts1 requested a review from PhilipMetzger October 18, 2025 01:45
@joyously
Copy link

diffmove [...] an absolutely terrible name for your average dev.

Maybe you can tackle diffedit and interdiff.

@matts1
Copy link
Contributor Author

matts1 commented Oct 18, 2025

Maybe you can tackle diffedit and interdiff.

Definitely outside the scope of this PR. That'd need to be a whole seperate discussion.

@martinvonz
Copy link
Member

jj amend used to be an alias for jj squash :)

Just to be sure, when you talked to your coworkers, did you explain that the command moves changes from one commit to another? I want to make sure that people don't think of it just as the more limited hg amend and git commit --amend (which always move changes from the working copy into its parent).

@PhilipMetzger
Copy link
Contributor

I'm still not in favor of doing such a thing even without considering the amend name which was removed as an alias not to long ago. I'd personally rather have a command which is the combination of split and squash since they do the same thing, just in opposite directions (although this compromises the learnability of the CLI as whole again) but I think it should be tried out.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: in the commit message you still talk about movediff/diffmove when the actual command name now is amend. I still am unconvinced on it though.

@matts1
Copy link
Contributor Author

matts1 commented Oct 20, 2025

Just to be sure, when you talked to your coworkers, did you explain that the command moves changes from one commit to another? I want to make sure that people don't think of it just as the more limited hg amend and git commit --amend (which always move changes from the working copy into its parent).

That's a good point. My personal opinion is that amend means "fix something up with something else". Thus, I think that jj amend --to <revision> makes just as much sense as jj diffmove --to <revision> (and same for other flags).

My survey of my teammates (different teammates from before, so they wouldn't be primed):

  • "what does jj squash do?": It combines the current commit with its parent
    • "And what other things can it do with options?"
      • "it can combine any commit with it's parent" (they understood the -r flag)
      • When I explained it to them, they said that the --from and --to flags were unintuitive, and so was the concept of a partial squash
  • "what should jj amend do if it were to exist?": They understood this immediately
  • "and what other things can it do with options?":
    • They came up with -m and partial amend
    • They thought the -r was unintiuitive
    • They also thought the --from and --to was unintiutive, but they also felt the same way about squash here.
  • "What if we were to call it diffmove? Would it make it more clear that it could move diffs from an arbitrary commit to another?"
    • I received a look that was a mix of incredulous and confused. It appeared to be roughly a combination of "what the hell is that" and "why on earth would that make it easier"
    • After discussions, consensus was "I just really don't like that name. I have no idea what it does. I just wouldn't use it". Very strong protests against this. I suspect they feel the same way about this as I do about jj diffedit, for example. I have no idea what it does, and I won't touch it because I can get by without it even if it's a little more difficult.

My conclusion:

  • They don't understand the concept of moving things between arbitrary revisions. I think that's perfectly ok to be not understood properly, since they didn't understand it with squash either.
  • You're right that the full power of jj amend was not fully understood by them. However, jj diffmove was even less understood. My personal opinion is that it's ok to not fully understand that it supports these flags if you can look it up in the docs, assuming the flags sense. And if they don't fully understand that it supports this use case, they can always use jj squash instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants