stacked-git / stgit

Stacked Git
https://stacked-git.github.io/
GNU General Public License v2.0
521 stars 60 forks source link

FR: `swap` command for reordering patches - keeping tree contents the same at each #449

Closed hjfreyer closed 3 months ago

hjfreyer commented 4 months ago

Sometimes I'm working on a change B, when I realize that it'd be more sensible to first have a change A, and then have B on top of that. When I'm lucky, A and B operate on disjoint files, but I'm pretty frequently unlucky.

My normal workflow in this situation is the following:

  1. stg new A . Now my stack is main > B > A
  2. Revert all the stuff from B that doesn't belong in A, using the diff between main and B as a reference.
  3. stg refresh.
  4. stg new B2. Now my stack is main > B > A > B2.
  5. git checkout HEAD~~ ., restoring everything in B2 to the way it was as of B.
  6. stg refresh.
  7. stg squash B A. Fix up all the commit messages to remove traces of B.
  8. Go digging through my git history to recover the commit message from B so I can reuse it in B2.

I'd love it if there was a stg swap A B command that I could use at step 4. The state of the tree would be exactly the same as it was at both A and B, but the order would be reversed (and therefore the diff would be inverted).

I've done this procedure enough times that I probably ought to write a script to automate it, but native support would be even better!

hjfreyer commented 4 months ago

Here's something I whipped up quickly:

#!/bin/bash -e

TEMPDIR=$(mktemp -d)

A=$(stg top)
A_ID=$(stg id $A)
stg pop

B=$(stg top)
B_ID=$(stg id $B)
stg pop

echo $A
echo $A_ID
echo $B
echo $B_ID

stg rename $A swap-temp-a  # Hold onto these in case something goes wrong...
stg rename $B swap-temp-b

git log --format=%B -n 1 $A_ID > $TEMPDIR/msg-a
git log --format=%B -n 1 $B_ID > $TEMPDIR/msg-b

stg new -n $A -f $TEMPDIR/msg-a
git checkout $A_ID .
stg refresh

stg new -n $B -f $TEMPDIR/msg-b
git checkout $B_ID .
stg refresh

stg delete swap-temp-a
stg delete swap-temp-b
jpgrayson commented 4 months ago

Hi @hjfreyer. Thanks for posting this feature request.

I'm wondering if you are aware of stg spill and stg pop --spill?

Also stg push --set-tree?

For the workflow enumerated with the starting state main > B, I might suggest:

  1. stg pop --spill && git reset. Patch B is not altered; the worktree contains all the modifications from patch B.
  2. git add -i. Stage the hunks that belong in the to-be-created patch A.
  3. stg new --index --refresh --name A. Create patch A with the relevant content that has been staged in the index.
  4. git restore .. We had modifications remaining in the worktree. If the right stuff was staged, then what remained corresponded to what we want in patch B, but we don't need these changes because the existing patch B already points to a tree that contains all these changes. So we reset the worktree in preparation for our next magic step...
  5. stg push --set-tree. This special form of push says "push the next patch, but instead of treating it like a diff and applying it, just use the tree that this patch/commit already points at". So after this step, the worktree will be in the same state as before step 1, but the diff captured by patch B will no longer include those changes that we've captured in patch A. The final stack state is main > A > B.

There are certainly valid variations on this workflow that would achieve the same ends. For example, after step 1, you could immediately stg new --name A (create an empty patch A) and then iteratively stage hunks/files and capture them with stg refresh --index.

And after step 3, instead of doing the git restore . (which could feel scary), you could instead capture those changes in a patch stg new --refresh --name pre-B, then do a regular stg push of the existing patch B, which should net you an empty patch B, and then finally do a stg squash pre-B B to get to one B patch and use the original B's message.

So with spill and stg push --set-tree in your arsenal, I'm wondering if you still see a need for a swap command?

hjfreyer commented 3 months ago

--set-tree is exactly what I needed! Thanks for the tip!