1. We're rewriting some commits. Let's say a chain of commits A through G. We want to make some change to commits A and D.
2. As we're editing commit D, we realized that we need to make some changes to B to match the updated A.
3. Also while editing D we realized that we want to take a look at the state in A to see how something worked there.
With jj, here's what I would do:
1. Run `jj new A`, make the changes, then `jj squash` to squash the changes in to A and propagate them through the chain.
2. Run `jj new D` to make changes. We now notice that we wanted some changes to go into B. We can make the changes in the working copy and run `jj squash --into B -i` to interactively select the changes to squash into B.
3. Run `jj new A` to create a new working-copy commit on top of A, look around in your IDE or whatever, then run `jj edit <old commit on top of D>`. Then run `jj squash` to squash the remaining changes in the working copy into D.
I think you know the steps to do with Git so I won't bother writing them down. I find them less intuitive anyway.
Ah, I see, so you avoid interactive rebase and instead make all changes in the working copy and use `git commit --fixup` and `git rebase --autosquash` . Makes sense, but doesn't it break down when there are conflicts between the changes you're making in the working copy and the target commit? How do you adjust the steps if there were conflicts between the changes we wanted to make to A and the changes already present in B?
> Ah, I see, so you avoid interactive rebase and instead make all changes in the working copy and use `git commit --fixup` and `git rebase -i .
I wouldn't say I avoid this, I also run `git rebase -i` several times per day, and I also often use `git commit --fixup` during a rebase.
> Makes sense, but doesn't it break down when there are conflicts between the changes you're making in the working copy and the target commit?
Yes, but wouldn't this be the same in JJ, when you do your changes on top of A, and later squash them into D? If you don't want to have the changes, you can also checkout D and do the changes there. Then you have two options:
- `git commit --fixup`, later do `git rebase`
- `git commit --amend`, and `git rebase --onto`
Most times I do the thing described earlier and just solve the conflicts, because that's just a single command. Also when its only a single case, I use `git stash`. (The workflow then is do random fix, git stash, then figure out where these should go, git rebase)
> How do you adjust the steps if there were conflicts between the changes we wanted to make to A and the changes already present in B?
I just resolve them? I think I don't understand this question.
> I just resolve them? I think I don't understand this question.
In order to make changes to commit A when there are conflicting changes in B, I was thinking that you would have to use interactive rebase instead because you can no longer make those changes in the working copy and use `git commit --fixup`, right? And because there will now be conflicts in commit B, you will be in this "interrupted rebase" state where you have conflicts in the staging area and it's a bit tricky (IMO) to leave those and look around somewhere else and then come back and resolve the conflicts and continue the rebase later.
> Yes, but wouldn't this be the same in JJ, when you do your changes on top of A, and later squash them into D?
The difference is that we don't end up in an interrupted rebase. If we squashed some changes into A and that resulted in conflicts in B, then we would then create a new working-copy commit on top of the conflicted B (I call all of the related commits B even if they've been rewritten - I hope that's not too confusing). We then resolve the conflicts and squash the resolution into B and the resolution gets propagated to the descendants. We are free at any time to check out any other commit etc.; there's no interrupted rebase or unfinished conflicts we need to take care of first. I hope that clarifies.
> I was thinking that you would have to use interactive rebase instead because you can no longer make those changes in the working copy and use `git commit --fixup`, right? And because there will now be conflicts in commit B, you will be in this "interrupted rebase" state
Yes.
I don't see the drawback honestly. Invoking git rebase, means I want to resolve conflicts now, when I want to do that later, I can just call git rebase later. When you want to work on top of the B with conflicts, the code wouldn't even compile, so I expect JJ, to just give you the code before the squash, right? How is this different from in Git?
I don’t know about you, but I am tired of having to remember the dozens of simple, one-off workarounds to every single thing I want to actually accomplish.
A few months back I had to sanitize the commit history of a repo that needed certain files removed from the entire history. I could have read the manpage for `git filter-branch`, but in jj editing commits is just a normal, sane part of your workflow. It was a blindingly obvious one-liner that just used all the commands I use every day already.
Even better, it was fast. Because “edit the contents of a bunch of commits” is a first-class operation in jj, there’s no need to repeatedly check out and re-commit. Everything can be done directly against the backing repository.
I don't remember anything really, I just derive it on demand from first principles and by using autocomplete in the shell.
I don't consider `commit --fixup` to be some arcane workaround, that is basically the default to record a change to some older commit.
Editing commits is also a normal, sane part of my workflow, what else is a version control system supposed to do? I consider modifying every commit in a repo not to be that frequent, but nice if JJ supports that easily. Do you want to educate us of the command?
Git also does certain modifications entirely in memory, but when I edit some file obviously my editor needs to access it. Also I want to rerun the tests on some modified commit anyway, so to me checking it out is not some extra cost.
Not sure that they had in mind but you can do `jj squash --from <oldest commit with unwanted file>:: --destination 'root()' <path to unwanted file>`. That will take the changes to the unwanted file from all those commits and move them into a new commit based on the root commit (the root commit is virtual commit that's the ancestor of every other commit).
1. We're rewriting some commits. Let's say a chain of commits A through G. We want to make some change to commits A and D.
2. As we're editing commit D, we realized that we need to make some changes to B to match the updated A.
3. Also while editing D we realized that we want to take a look at the state in A to see how something worked there.
With jj, here's what I would do:
1. Run `jj new A`, make the changes, then `jj squash` to squash the changes in to A and propagate them through the chain.
2. Run `jj new D` to make changes. We now notice that we wanted some changes to go into B. We can make the changes in the working copy and run `jj squash --into B -i` to interactively select the changes to squash into B.
3. Run `jj new A` to create a new working-copy commit on top of A, look around in your IDE or whatever, then run `jj edit <old commit on top of D>`. Then run `jj squash` to squash the remaining changes in the working copy into D.
I think you know the steps to do with Git so I won't bother writing them down. I find them less intuitive anyway.