FR: Make `jj squash -r` take multiple revisions
Is your feature request related to a problem? Please describe.
I think it's natural to expect that jj squash -r <revset> squashes all the revisions in the given revset, but it currently accepts only one revision and squashes it into its parent.
Describe the solution you'd like
Allow more than one revision to jj squash -r and squash them all together. Passing a single revision would become a no-op. We should make that a warning for a while so existing users learn about the new behavior.
I think we can also allow non-contiguous revisions, similar to how jj parallelize allows non-contiguous revisions. Consider this history:
○ I
│ ○ H
├─╯
○ G
○ F
├─╮
│ ○ E
│ ○ D
○ │ C
○ │ B
├─╯
○ A
jj squash -r 'B|C|D|E' would separately squash {B and C} and {D and E}.
jj squash -r 'E|F' would squash E and F since they're directly related. The resulting commit would have C and D as parents, not just D. That's because F depends on the changes in both C and E before squashing, so the result should still depend on C. Squashing can therefore be seen as happening towards the children. That might also mean that the squashed revision's change id should be inherited from F.
If we allow jj squash -r 'G|H|I' and make it squash all three commits, it would mean that jj squash -r could result in new conflicts (because we would combine the changes from H and I). It's also unclear which change id to use in this case. We would presumably either use G's change id or pick and arbitrary one between H and I. An alternative is to make that invocation squash G into both H and I (duplicating the changes from G), but that seems less useful, and probably not what the user intended.
Describe alternatives you've considered
Keep the current behavior, or extend it to allow multiple commits but squash them all into their parent(s). However, it seems surprising if jj squash -r A::C squashes the commits into A's parent(s).
Additional context
I think the reason I made jj squash -r originally take only a single revision and made it squash the changes into the parent is that it makes the default behavior of jj squash to be jj squash -r @. We'll have to change it to mean jj squash --from @ --to @-.
I could also imagine some workflows where you want to duplicate the input revisions rather then rewrite them. It would probably be fine to require you to run jj duplicate followed by jj squash, except that there's no straightforward way to automatically get the resulting duplicated commits and pipe them into jj squash. This is tracked by https://github.com/jj-vcs/jj/issues/3814.
I think we can also allow non-contiguous revisions, similar to how
jj parallelizeallows non-contiguous revisions.
[aside] Given that characterization, I added "squash" as a topology to my categorization in https://github.com/jj-vcs/jj/issues/4708 (where jj parallelize is described as a "horizontal" topology).
I came looking for this feature specifically for the "duplicate then squash" behaviour I mention in #7044 ;
I'd love to be able to collapse a feature branch into a single commit while keeping the original.
I think that's a different request. Feel free to create a FR for something like jj squash --keep. I don't know if it should be an option on squash or if it should be its own command.
Some thoughts:
It's not immediately obvious to me what "squash" means. To me, I know what it means with --from and --into, but I hadn't even realized it accepts just -r, nor do I have any intuition about what that means. (The existing behavior makes some amount of sense, at least.)
From your description here, it seems the new squash would mean "take this set of commits and squash them down to one" -- except not quite, because of the B|C|D|E doing two independent operations. So it's more like "find all contiguous groupings of commits and squash each group into a single new commit"?
In a way, I kind of want squash to work on the edges between nodes. I could imagine using -r B|C -r D|E to do the operation you're doing with -r B|C|D|E. squash -r B|C -r D|E -r F|G would then merge B into C, D into E, and F into G -- whereas yours would merge them all into G.
Or thinking of it slightly differently, perhaps -r could work in conjunction with --into: with -r <revset>, --into would default to heads(<revset>). So -r B|C|D|E|F|G --into C|E|G would merge B into C, D into E, and F into G, but -r B|C|D|E|F|G would use an implicit --into G and merge them all into G. The general rule would be: for each revision R in --into, collect all of the nearest ancestors before hitting either something else in --into or something not in -r. Call those Ds. Squash all of Ds into R, setting its parents to parents(Ds).
If there are leftovers in -r at that point, well, that's where the "squash down to one" meaning confuses me. It could be an error. Or --into could pretend that it has an implicit |heads(leftovers) (equivalent, I think, to |heads(r)).
That still leaves the weirdness of -r G|H|I. I don't have any intuition as to what that should mean. I think the most natural thing, using the default --into of H|I, would be the thing you suggested where G gets squashed into H and I separately. Is that unlikely to be what the user wants? I don't know what the user wants in this case. It seems like a weird operation no matter what.
If you are going to doing the cross-squash where all 3 end up as 1, then you could at least use -r G|H|I --into H vs -r G|H|I --into I to specify what the resulting change id is. (But -r G|H|I --into H|I would still be ambiguous.)
I almost want an --into newchild(H|I) so you can be specific about where the changes end up. But... ugh. (And even then, you'd need --into newchild(H|I, H) to say that you want the result to have the change id H, or perhaps that's --into reparent(H, H|I)? More ugh.)
I've drafted two versions, to see what would make more sense to me, between squashing to the root of the revset (#7398) or squashing to the head of the revset (#7400).
I initially thought that squashing to the root would feel more natural, because of the tree structure that more easily leads to a single root than a single head.
But squashing multiple heads also leads to more conflicts, and squashing to the head allows for not moving the bookmark, which is more likely to be associated with the head.
Squashing to the head is the opposite of jj split with #6466. If we have run jj split -r x, running jj squash -r 'x|x- returns to the same state as before the split (modulo the revision ids).
Note that these are simpler versions than what is discussed here: they only work with a single head or root.
My git workflow is leaving a trail of dozens of small commits, then coming back with git rebase -i, reordering lines to group commits by semantics and putting fixes after what they fix, and using "fixup" or "squash" (depending on whether the commit message is useful) to combine them into a handful of logically meaningful commits.
I expected jj squash to help me do that, but it seems right now I need to repeat jj squash -r@- or such several times to combine multiple commits. (@- because at that point for me @ is typically an empty commit, the squashing of which is pointless.)
@tv42: jj squash --from accepts a revset that can resolve to many commits. Will that help you?
It seems like I was looking for something like jj squash -f bottom::top -t bottom- or perhaps more succinctly jj squash -f good..noise -t good, with change IDs instead of those names.
If I didn't have to repeat the destination, that'd be great. I'll need to play with this to gain confidence.
@tv42 you might be interested in this kind of thing:
[aliases]
# second arg optional. if left out does all descendants
collapse = ["util", "exec", "--", "zsh", "-c", 'jj squash -f "$0"::"$1" -t "$0"']
This lets you do
jj collapse x y
Or just
jj collapse x
if you want to collapse all of x’s descendants into it.
Thanks! After playing with it, I ended up with this:
[aliases]
# `squash-into INTO [TOP]`
# collapse commits that are on top of `INTO` into it (up to `TOP` if given).
# <https://github.com/jj-vcs/jj/issues/5301#issuecomment-3534935735>
squash-into = ["util", "exec", "--", "sh", "-c", '''
jj squash -f "$1"::"$2" -t "$1"
''',
# this becomes `$0`
"jj squash-into"]
My biggest remaining annoyance with that is that it's not a real command, and has poorer input validation, error reporting, completion, and how it doesn't follow the usual jj foo -r abc::xyz calling style.
What I guess I'd really like is for jj squash -f default behavior to be -t parent_from_dash_f. No idea if that breaks some other use case. I personally don't think squashing from history "upward" into @ makes any sense as a default; the word squash sounds to be downward, right?-)
If that changed, then my use would be just jj squash -f a::b.
What I guess I'd really like is for jj squash -f default behavior to be -t parent_from_dash_f. No idea if that breaks some other use case. I personally don't think squashing from history "upward" into @ makes any sense as a default; the word squash sounds to be downward, right?-)
I agree about the name, but it's not at all uncommon that I squash from a commit on a parallel branch.
This FR seems to conflict with FR #7619, i.e. I don't think we want to support both. Another option is to implement neither and even remove the current -r support. Perhaps -r isn't that useful anyway? A problem with -r is that it's not obvious which change id to keep. If we force users to use -f and -t instead, we make them decide which change id to use.
Unsure if you're looking for feedback @martinvonz, but I'm personally in favor of removing -r and keeping -f and -t, with the caveat that jj squash --into REV should squash from the working copy to REV.
Sometimes I only want to squash a set of commits, and don't care much about what change-id is kept. I'm happy to do jj squash -r x::y in that case.
I'm not sure -r and -f conflicts—I used both in #7400—but they make jj squash more complex
I'm not sure -r and -f conflicts—I used both in #7400—but they make
jj squashmore complex
I just meant that I don't think we should support both jj squash -f source -t destination and jj squash -r source -t destination (as I think #7619 suggests).