Repository: git-town/git-town

Issue:
sync: support for amended branches

I have a branch `feature-1`, which has one commit. 

I create another branch `feature-2` on top of `feature-1` using `append` . Now my branch lineage looks like this.

```
  main
    feature1
*     feature2
```



I do some change in `feature-1`, but instead of adding as a new commit, I amend the existing commit using 

```
git add .
git commit --amend --no-edit
```

When I try to sync my stack it checks out `feature-2` and rebase with `feature-1`. Since I amended the commit in `feature-1` , this will result in a merge conflict and I need to resolve.

I usually don't change `feature-1` commits while working in `feature-2`. Is it possible to add a flag to tweak this behaviour ? I mean when I sync the stack, can we remove the already existing parent commits in the child branch and perform a rebase ? 

In this case, when I sync the stack 

```
checkout feature 2
remove all the existing feature 1 commits
rebase feature 1
```

Kindly let me know if you need more info..  I have created a [repo](https://github.com/pradeepmurugesan/git-town-rebase-issue-demo) to demo this. 

just make sure the lineage is still present and the strategy is rebase. Perform the following

```
git-town switch feature2
git-town sync -s
```


Discussion:
@kevgo: Steps to reproduce the problem:

```sh
# initialize repo
git init test
cd test
git commit --allow-empty -m "Initial commit"

# create branch feature1
git town hack feature1
echo "one" > file1
git add .
git commit -m "commit 1a"

# create child branch feature2
git town append feature2
echo "two" > file2
git add .
git commit -m "commit 2"

# amend the commit on branch1
git checkout feature1
echo "another one" > file1
git add .
git commit --amend -m "commit 1b"

# rebase branch feature2
git checkout feature2
git rebase feature1
```

This does indeed result in a merge conflict for `file1` on `feature2`:

```diff
<<<<<<< HEAD
another one
=======
one
>>>>>>> 0a250f0 (commit 1a)
```

Here is the proposed alternative to rebasing, which avoids the merge conflict:

```sh
# create a temporary branch that will become the new "feature2" branch
git checkout -b feature2-new feature1

# move the commits from the old feature2 branch over to the new feature2 branch 
git cherry-pick [SHA of "commit2" on feature2]

# point the feature2 branch to the new commits on the temporary branch
git checkout feature2
git reset --hard feature2-new

# push the new commits to the tracking branch
git push --force-with-lease --force-if-includes

# delete the temporary branch
git branch -d feature2-new
```
@kevgo: It's strange that this is necessary, given that a normal Git rebase is [defined](https://git-scm.com/docs/git-rebase) as "Reapply commits on top of another base tip", which is exactly what we do manually here. @pradeepmurugesan: > Git rebase is [defined](https://git-scm.com/docs/git-rebase) as "Reapply commits on top of another base tip", which is exactly what we do manually here.

`amend` changes the SHA of the commit on the feature1 right ? Not sure if git rebase can identify the amended commit.

> Here is the proposed alternative to rebasing

@kevgo  Do you think `git-town sync` with rebase should adopt this as the default behaviour of rebase or controlled by any flag? 

Happy to contribute if you think we can add this work around into git-town.. @stephenwade: > It's strange that this is necessary, given that a normal Git rebase is [defined](https://git-scm.com/docs/git-rebase) as "Reapply commits on top of another base tip", which is exactly what we do manually here.

`git rebase <upstream>` will apply "commits in the current branch but that are not in `<upstream>` … the same set of commits that would be shown by `git log <upstream>..HEAD`" (see [git-rebase docs](https://git-scm.com/docs/git-rebase#:~:text=All%20changes%20made%20by%20commits%20in%20the%20current%20branch%20but%20that%20are%20not%20in%20%3Cupstream%3E%20are%20saved%20to%20a%20temporary%20area.%20This%20is%20the%20same%20set%20of%20commits%20that%20would%20be%20shown%20by%20git%20log%20%3Cupstream%3E..HEAD)). After replacing "commit 1a" with the appended "commit 1b" in `feature1`, the list of commits in `feature2` but that are not in `feature1` are "commit 1a" and "commit 2".

We can "cherry-pick" just the commits we want with `git rebase` by using `git rebase --onto <newbase> <upstream>`. By passing `feature1` as the `<newbase>` and where `feature1` used to be as `<upstream>`, we apply only the desired commits.

Steps reproducing the solution (diff from https://github.com/git-town/git-town/issues/4586#issuecomment-2752121727):

```diff
 # initialize repo
 git init test
 cd test
 git commit --allow-empty -m "Initial commit"
 
 # create branch feature1
 git town hack feature1
 echo "one" > file1
 git add .
 git commit -m "commit 1a"
 
 # create child branch feature2
 git town append feature2
 echo "two" > file2
 git add .
 git commit -m "commit 2"
 
 # amend the commit on branch1
+FEATURE1_OLD=$(git rev-parse feature1)
 git checkout feature1
 echo "another one" > file1
 git add .
 git commit --amend -m "commit 1b"
 
 # rebase branch feature2
 git checkout feature2
-git rebase feature1
+git rebase --onto feature1 $FEATURE1_OLD
```@pradeepmurugesan: I tried with few more commits in feature1 and amended the commits using interactive rebase. The solution works. 

However I am not able to understand how will this work with `git-town`  as amending the commit happens outside git-town. I mean how can `git-town` know about  the FEATURE1_OLD ?@stephenwade: For each branch, Git Town should track the parent's HEAD as well. That way, Git Town knows which commits are considered part of this branch, even if the parent's HEAD changes. I don't know enough about the internals to know whether this is already happening, but it should be possible.@pradeepmurugesan: @kevgo  do you think this can be to accommodated into `git-town` ? 

I am happy to contribute if that is the case. Kindly let me know. @kevgo: This addresses the most widely requested open issue: https://github.com/git-town/git-town/issues/2223. It does so in a better way than earlier ideas, like https://github.com/git-town/git-town/issues/3179. Thanks @stephenwade for figuring out how to implement this idiomatically! 🙏 We finally have a solution good enough to add to Git Town, so let's build this.

Git Town already tracks the last known SHA for each local and remote branch in its [runstate file](https://github.com/git-town/git-town/blob/main/internal/vm/runstate/runstate.go). You can see the content of the runstate file by executing `git town status show`. Currently, this data is used to undo the most recent Git Town command. It makes perfect sense to also use it to sync better.

All we have to do is modify the `rebase` sync strategy to run the commands outlined in this ticket if there is a useful runstate file. Otherwise keep doing the existing behavior. Hopefully, our pretty comprehensive end-to-end test suite will reveal edge cases where this doesn't work as expected.

@pradeepmurugesan any help with Git Town would be greatly appreciated. Unfortunately, the sync algorithm is the most complex part of Git Town, so this ticket is the pretty much the polar opposite of a "good first issue". If you haven't already, please take a look at the [developer documentation](https://github.com/git-town/git-town/blob/main/docs/DEVELOPMENT.md). The [architecture](https://github.com/git-town/git-town/blob/main/docs/ARCHITECTURE.md) document gives some background on the codebase. A good starting point is https://github.com/git-town/git-town/blob/main/internal/cmd/sync/sync_branch.go, which implements the logic to sync a Git branch. Please don't hesitate to reach out with any questions, no matter how big or small!@kevgo: I added an end-to-end test that documents the problematic behavior of the existing codebase: https://github.com/git-town/git-town/pull/4631@pradeepmurugesan: Thank you for the pointers. I will take a look and get back to you with questions. 👍 @pradeepmurugesan: @kevgo  I have a naive question regarding the tests.. I tried to run the end to end test to understand how the setup works.. I see the `test_runner.go` runs the command configured in the feature file.  

I added a print statement to the sync logic and tried running the test `adding additional branches while resolving conflicts` in `features/sync/current_branch/feature_branch/change_repo_in_between/create_branch.feature`.

I don't see the logs I added getting printed, When I tried to debug the debug points were not hit. I tired following the `test_runner.go` and I see it executes these commands via `exec` , `subProcess.Run()` ..  I doubt whether this uses the local installation instead of the code. 

Am I understanding this correctly ? If so is there anyway i can make these end to end tests execute the changes I made in the code ? Kindly clarify 🙏 @kevgo: Great questions—you're definitely on the right track! `test_runner.go` does indeed invoke the locally installed Git Town binary (the one in `~/go/bin/`) via a subshell. By default, the output of that subshell is captured, which is why you don’t see it directly in the terminal.

A few more details to round out the picture:

- The tests are written in [Cucumber](https://cucumber.io), using the [Go implementation of Cucumber](https://github.com/cucumber/godog).
- Each line in a `.feature` file represents a _Step_, which maps to some Go test code. That test logic is run with data parsed from the step.
- All step implementations live in `steps.go`. For example, here is the implementation for the `And I run "git town sync"` step: https://github.com/git-town/git-town/blob/397ce0c6aa4a17467c0ff04fce07ef259b15982b/test/cucumber/steps.go#L713-L715
  
  This step calls `runCommand(ctx, command)`, which in turn calls `devRepo.MustQueryStringCode(command)`. The latter is a method on `TestRunner`.

To run an end-to-end test and view your debug logs, please follow these steps:

1. Add the tags `@this` and `@debug` to the Cucumber scenario you want to debug. In your case, you'd update `features/sync/current_branch/feature_branch/change_repo_in_between/create_branch.feature` around line 32 like so:

   ```diff
   + @debug
   + @this
   Scenario: result
     Then Git Town runs the commands
       | BRANCH    | COMMAND  |
       | feature-1 | git push |
   ```

2. Run `make cukethis`. This builds the Git Town binary and executes only the scenario tagged with `@this`. The `@debug` tag causes a detailed trace of all activity: How it runs various Git commands to prepare a Git repo for the test, then runs Git Town, and then runs more Git commands to inspect the repo after the test ran. Scroll through the logs to find the Git Town command output, including any log statements you have added. 

That's the easiest way to debug. If you prefer to debug interactively in VSCode, please follow the instructions in https://github.com/git-town/git-town/blob/main/docs/DEVELOPMENT.md#debug-end-to-end-tests.

And of course, if you have ideas for how we can make this process smoother or more intuitive, I’d love to hear them!@erik-rw: If I have to amend an existing commit as shown in the example, I usually do manual `rebase -i` s to get rid of the outdated commit from the child branches.

Using the data from the runstate file and solving this automatically sounds perfect. But there is the caveat that running manual `rebase` in between git town commands will get the runstate file out of sync. And the next `git town sync` will have unforeseeable side effects.
Users should, therefore, avoid any non git town commands .. which is a bit of a pitty
@pradeepmurugesan: Hello @kevgo  

Thank you for responding. 

> does indeed invoke the locally installed Git Town binary (the one in ~/go/bin/) via a subshell

Yes.. I tried uninstalling the local installation and ran `make cukethis` . I can see the test failing with 
```
iagnostic information of failed command

COMMAND: git-town sync
ERROR: exec: "git-town": executable file not found in $PATH
OUTPUT START

OUTPUT END
```
> Run make cukethis. This builds the Git Town binary and executes only the scenario tagged with @this

I am not sure if this is actually building the binary from the code base as I can see the tests are failing without a local installation. Am I missing something here ?


I did a `go install` before running the test. This made sure the binary is built from the source.  

But I still cannot get the logs I added. I use the `print` statement to print something out.  I tried to do a `panic` when the sync runs. Even then the test fails but the panic stacktrace / message is not printed when I run `go install && make cukethis`

> That's the easiest way to debug. If you prefer to debug interactively in VSCode, please follow the instructions in https://github.com/git-town/git-town/blob/main/docs/DEVELOPMENT.md#debug-end-to-end-tests.

I will try this.. The link says `change the path of the test to execute` what is the path here.. is it `options.Paths = pflag.Args()` Can you kindly share an example 🙏@pradeepmurugesan: Interestingly when I try to run the binary manually I can see the print statements I added

```
git-town sync
2025/04/09 22:40:14 Sync command called

[rebase-with-ammend] git fetch --prune --tags
--Pradeep is Syncing branch
``` 

`Sync command called` , `Pradeep is Syncing branch`

But when I execute an end to end test the above logs are not present in the command output. 🤔 

``` 
DEVELOPER@feature-1 > git-town sync


[feature-1] git fetch --prune --tags

[feature-1] git merge --no-edit --ff main
Already up to date.
```


I tried to double check if the test is using the same binary by providing `when I run which git-town` step. can confirm it points to the same one. @kevgo: The error message is correct. You need to add `~/go/bin` (the directory that `go install` puts executables in on your machine) to your $PATH environment variable. Otherwise the end-to-end tests cannot find the `git-town` executable and cannot execute it. This setup step was missing in DEVELOPMENT.md. Apologies for the troubles. I have added this in https://github.com/git-town/git-town/pull/4694. Thanks for helping identify this problem! 🙏 @pradeepmurugesan: 
> But I still cannot get the logs I added. I use the print statement to print something out.

Using `fmt.Printf` instead of `print` , I can see the logs when we execute the end to test 👍 

@kevgo: Yes, the correct way to print in Go is `fmt.Println("text")` or to print variables `fmt.Printf("text %s\n", variable)`. Here is an overview of the formatting verbs you can use with `fmt.Printf`: https://pkg.go.dev/fmt@kevgo: The `print` function is some weird internal thing for the people developing Go itself. Normal users of Go should use the functions in the `fmt` package. See https://stackoverflow.com/questions/14680255/difference-between-fmt-println-and-println-in-go for some background.@pradeepmurugesan: Thanks for all the pointers.. I tried debugging using the launch config in vs code.. I am able to get the debugging points only up until the subprocess execution starts and after the command is completed..  

The debugger is gone when the actual command is invoked.. We cannot debug the binary execution in anyway is it ?  

I mean when the test_runner.go says `err = subProcess.Run()` this will call the installed binary..  We cannot have a break point in the `sync_branch.go` for example and debug that ?@kevgo: Yes, the debugging setup is primarily intended for debugging the end-to-end tests themselves. They contain some complexity to speed up the suite. Debugging the Git Town executable that's invoked by the tests is probably a bit more involved. I haven’t really gone down that path, since I rarely use the debugger. The Git Town CLI already produces quite detailed output (especially with `--verbose`), and I usually supplement that with some extra log statements. Combined with small, incremental changes via TDD, that approach tends to be the most efficient for me when working in this codebase. Most of the logic is single-threaded and fairly straightforward.

That said, one possible way to debug the Git Town executable as part of an end-to-end test could be:

1. Add the step `And inspect the repo` once the Git test repo is fully set up (just before `And I run "git-town ..."`). This step outputs the test repo path and pauses the test.
2. Then you can manually run the Git Town binary in debug mode in that directory.

That approach should work, but requires a fair bit of manual setup per debug session.

Pradeep – thanks again for all your work on this. This ticket touches some of the most complex parts of Git Town, and will require updating around 100 end-to-end tests, which is no small task, and not a good first issue for working on this codebase. I’ve got some bandwidth now. Would it be alright with you if I took a stab at implementing this? If you're close to wrapping this up, I'm also happy to pick up something else from the backlog. Just let me know what you want to do here. Thanks! 🙂 @pradeepmurugesan: > If you're close to wrapping this up

ha ha. No not close at all.  I am just scratching the surface :-) . Please feel free to implement this if you have bandwidth.. 

I will find something else to work on, may be a proper `good first issue`..  

Thank you so much for being so considerate and helpful.. 🙏 @kevgo: Please keep scratching! 😃 Git Town can use some help, and the codebase is clean and fun to work on (at least that's one of the goals, haha). I have just tagged some [good first issues](https://github.com/git-town/git-town/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22).@kevgo: Update: This works correctly, even if the branches contain multiple commits and only the last commit gets amended. This test script works properly:

```
# initialize repo
git init test
cd test
git commit --allow-empty -m "Initial commit"

# create branch feature1
git town hack feature1
touch file1a
git add .
git commit -m "commit 1a"
touch file1b
git add .
git commit -m "commit 1b1"

# create child branch feature2
git town append feature2
touch file2
git add .
git commit -m "commit 2"

# amend the commit on branch1
echo "CHECKPOINT A (before update)"
git log --format="%h %s"  
FEATURE1_OLD=$(git rev-parse feature1)
echo "Old branch: $FEATURE1_OLD"
git checkout feature1
echo "new content" > file1a
git add .
git commit --amend -m "commit 1b2"

# rebase branch feature2
git checkout feature2
git rebase --onto feature1 $FEATURE1_OLD

echo "CHECKPOINT B (after update)"
git log --format="%h %s"
```

Output:

```
CHECKPOINT A (before update)
b87e22c commit 2
48b6742 commit 1b1
014c08b commit 1a
98b4fc4 Initial commit

CHECKPOINT B (after update)
422a98c commit 2
04b41a2 commit 1b2
014c08b commit 1a
98b4fc4 Initial commit
```

`branch-2` gets correctly updated and contains the new `commit 1b2`. The old version of that commit (1b1) is gone. The unchanged commit (1a) still exists.@kevgo: This also works correctly when changing a different commit than the last one in the branch, via interactive rebase. This test script works:

```
# initialize repo
git init test
cd test
git commit --allow-empty -m "Initial commit"

# create branch branch-1
git town hack branch-1
touch file_1a
git add .
git commit -m "commit-1a"
touch file_1b
git add .
git commit -m "commit-1b1"
touch file_1c
git add .
git commit -m "commit-1c"

# create child branch branch-2
git town append branch-2
touch file_2
git add .
git commit -m "commit-2"

echo "CHECKPOINT 1: branch-2 before interactive rebase"
git log --format="%h %s"  

# change commit-1b via interactive rebase on branch-1
FEATURE1_OLD=$(git rev-parse branch-1)
echo "Old branch: $FEATURE1_OLD"
git checkout branch-1
INITIAL_SHA=$(git log --grep=Initial --format="%h")
git rebase -i $INITIAL_SHA

# you see this:
#
# pick ddd36bb commit-1a
# pick 8d11ec1 commit-1b1
# pick 14c2572 commit-1c
#
# change it to:
# 
# pick ddd36bb commit-1a
# r 8d11ec1 commit-1b1
# pick 14c2572 commit-1c
#
# In the new editor that pops up, change the commit message 
# from "commit-1b1" to "commit-1b2".

echo "CHECKPOINT 2: branch-1 after interactive rebase"
git log --format="%h %s"  

git checkout branch-2

echo "CHECKPOINT 3: branch-2 before sync"
git log --format="%h %s"  

# sync branch branch-2
git rebase --onto branch-1 $FEATURE1_OLD

echo "CHECKPOINT 4: branch-2 after sync"
git log --format="%h %s"
```

What this test script does is change the commit in the middle of `branch-1` (with message `commit-1b1`). Git also gives `commit-1c` a new SHA.

Output:

```
CHECKPOINT 1: branch-2 before interactive rebase
bfb971d commit-2
14c2572 commit-1c
8d11ec1 commit-1b1
ddd36bb commit-1a
488e770 Initial commit

CHECKPOINT 2: branch-1 after interactive rebase
8586447 commit-1c
9c5a2e5 commit-1b2
ddd36bb commit-1a
488e770 Initial commit

CHECKPOINT 3: branch-2 before sync
bfb971d commit-2
14c2572 commit-1c
8d11ec1 commit-1b1
ddd36bb commit-1a
488e770 Initial commit

CHECKPOINT 4: branch-2 after sync
600e84e commit-2
8586447 commit-1c
9c5a2e5 commit-1b2
ddd36bb commit-1a
488e770 Initial commit
```

At checkpoint 3, `branch-2` contains the old commits from before the interactive rebase.
At checkpoint 4, `branch-2` now contains the new commits from after the interactive rebase.@kevgo: This also works correctly when squashing commits together via interactive rebase.
This test script works:

```
# initialize repo
git init test
cd test
git commit --allow-empty -m "Initial commit"

# create branch branch-1
git town hack branch-1
touch file_1a
git add .
git commit -m "commit-1a"
touch file_1b
git add .
git commit -m "commit-1b1"
touch file_1c
git add .
git commit -m "commit-1c"

# create child branch branch-2
git town append branch-2
touch file_2
git add .
git commit -m "commit-2"

echo "CHECKPOINT 1: branch-2 before interactive rebase"
git log --format="%h %s"  

# change commit-1b via interactive rebase on branch-1
FEATURE1_OLD=$(git rev-parse branch-1)
git checkout branch-1
INITIAL_SHA=$(git log --grep=Initial --format="%h")
git rebase -i $INITIAL_SHA

# you see this:
#
# pick 1bfcd1b commit-1a
# pick d35bb45 commit-1b1
# pick 3bcddc5 commit-1c
#
# change it to:
# 
# pick 1bfcd1b commit-1a
# f d35bb45 commit-1b1
# f 3bcddc5 commit-1c

echo "CHECKPOINT 2: branch-1 after interactive rebase"
git log --format="%h %s"  

git checkout branch-2

echo "CHECKPOINT 3: branch-2 before sync"
git log --format="%h %s"  

# sync branch branch-2
git rebase --onto branch-1 $FEATURE1_OLD

echo "CHECKPOINT 4: branch-2 after sync"
git log --format="%h %s"
```

Output:

```
CHECKPOINT 1: branch-2 before interactive rebase
11fd166 commit-2
3bcddc5 commit-1c
d35bb45 commit-1b1
1bfcd1b commit-1a
e947e18 Initial commit

CHECKPOINT 2: branch-1 after interactive rebase
ef9c032 commit-1a
e947e18 Initial commit

CHECKPOINT 3: branch-2 before sync
11fd166 commit-2
3bcddc5 commit-1c
d35bb45 commit-1b1
1bfcd1b commit-1a
e947e18 Initial commit

CHECKPOINT 4: branch-2 after sync
44c47e8 commit-2
ef9c032 commit-1a
e947e18 Initial commit
```

That's all the possible edge cases I can think of right now. This new way of syncing seems pretty robust and dependable.@kevgo: @pradeepmurugesan @stephenwade This is implemented but won't ship for a few more days. If you want, you can use this already by compiling the Git Town binary to use from source:

- run `make install`
- add `$HOME/go/bin` to your $PATH

This would provide some real-world user testing before rolling this out to the tens of thousands of Git Town users worldwide. I don't use the rebase sync strategy, so I can't test this myself. Thanks!

Solution patch:
diff --git a/features/prepend/beam/rebase.feature b/features/prepend/beam/rebase.feature
index 076823a91..b97afef48 100644
--- a/features/prepend/beam/rebase.feature
+++ b/features/prepend/beam/rebase.feature
@@ -42,10 +42,10 @@ Feature: prepend a branch to a feature branch using the "rebase" sync strategy
       | parent | main   |
     When I run "git town sync"
     Then Git Town runs the commands
-      | BRANCH | COMMAND                                    |
-      | parent | git fetch --prune --tags                   |
-      |        | git -c rebase.updateRefs=false rebase main |
-      |        | git push -u origin parent                  |
+      | BRANCH | COMMAND                                                                      |
+      | parent | git fetch --prune --tags                                                     |
+      |        | git -c rebase.updateRefs=false rebase --onto main {{ sha 'initial commit' }} |
+      |        | git push -u origin parent                                                    |
     And these commits exist now
       | BRANCH | LOCATION      | MESSAGE  |
       | old    | local, origin | commit 1 |
diff --git a/features/sync/current_branch/feature_branch/mixed_sync_strategies/two_collaborators_different_sync_strategies.feature b/features/sync/current_branch/feature_branch/mixed_sync_strategies/two_collaborators_different_sync_strategies.feature
index 8643a77d9..491cfcfe1 100644
--- a/features/sync/current_branch/feature_branch/mixed_sync_strategies/two_collaborators_different_sync_strategies.feature
+++ b/features/sync/current_branch/feature_branch/mixed_sync_strategies/two_collaborators_different_sync_strategies.feature
@@ -64,11 +64,11 @@ Feature: compatibility between different sync-feature-strategy settings
       | my second commit | file.txt  | my new content |
     When I run "git-town sync"
     Then Git Town runs the commands
-      | BRANCH  | COMMAND                                              |
-      | feature | git fetch --prune --tags                             |
-      |         | git -c rebase.updateRefs=false rebase main           |
-      |         | git push --force-with-lease --force-if-includes      |
-      |         | git -c rebase.updateRefs=false rebase origin/feature |
+      | BRANCH  | COMMAND                                                                      |
+      | feature | git fetch --prune --tags                                                     |
+      |         | git -c rebase.updateRefs=false rebase --onto main {{ sha 'initial commit' }} |
+      |         | git push --force-with-lease --force-if-includes                              |
+      |         | git -c rebase.updateRefs=false rebase origin/feature                         |
     And Git Town prints the error:
       """
       CONFLICT (content): Merge conflict in file.txt
@@ -88,9 +88,12 @@ Feature: compatibility between different sync-feature-strategy settings
     When I resolve the conflict in "file.txt" with "my new and coworker content"
     And I run "git town continue" and close the editor
     Then Git Town runs the commands
-      | BRANCH  | COMMAND                                    |
-      | feature | git -c core.editor=true rebase --continue  |
-      |         | git -c rebase.updateRefs=false rebase main |
+      | BRANCH  | COMMAND                                                                      |
+      | feature | git -c core.editor=true rebase --continue                                    |
+      |         | git -c rebase.updateRefs=false rebase --onto main {{ sha 'initial commit' }} |
+      |         | git checkout --theirs file.txt                                               |
+      |         | git add file.txt                                                             |
+      |         | git -c core.editor=true rebase --continue                                    |
     And Git Town prints the error:
       """
       CONFLICT (add/add): Merge conflict in file.txt
@@ -99,8 +102,16 @@ Feature: compatibility between different sync-feature-strategy settings
       """
       Could not apply \S+ my first commit
       """
+    And file "file.txt" now has content:
+      """
+      <<<<<<< HEAD
+      my content
+      =======
+      my new and coworker content
+      >>>>>>> {{ sha-short 'my second commit' }} (my second commit)
+      """
     And a rebase is now in progress
-    When I resolve the conflict in "file.txt" with "my and coworker content"
+    When I resolve the conflict in "file.txt" with "my new and coworker content"
     And I run "git town continue" and close the editor
     Then Git Town runs the commands
       | BRANCH  | COMMAND                                         |
@@ -111,7 +122,7 @@ Feature: compatibility between different sync-feature-strategy settings
     And these commits exist now
       | BRANCH  | LOCATION                | MESSAGE                                                    | FILE NAME | FILE CONTENT                |
       | feature | local, coworker, origin | coworker first commit                                      | file.txt  | coworker content            |
-      |         | local, origin           | my first commit                                            | file.txt  | my and coworker content     |
+      |         | local, origin           | my first commit                                            | file.txt  | my content                  |
       |         |                         | my second commit                                           | file.txt  | my new and coworker content |
       |         | coworker                | my first commit                                            | file.txt  | my content                  |
       |         |                         | Merge remote-tracking branch 'origin/feature' into feature | file.txt  | my and coworker content     |
diff --git a/features/sync/current_branch/feature_branch/rebase_sync_strategy/modified_commits/amended_commit.feature b/features/sync/current_branch/feature_branch/rebase_sync_strategy/modified_commits/amended_commit.feature
index bbbb3159a..93c646fb9 100644
--- a/features/sync/current_branch/feature_branch/rebase_sync_strategy/modified_commits/amended_commit.feature
+++ b/features/sync/current_branch/feature_branch/rebase_sync_strategy/modified_commits/amended_commit.feature
@@ -1,24 +1,18 @@
 Feature: rebase a branch that contains amended commits
 
-# This test demonstrates how Git Town currently does not sync branches that contain amended commits correctly.
-# This will be fixed by implementing https://github.com/git-town/git-town/issues/4586.
-
   Background:
     Given a Git repo with origin
-    And the branches
-      | NAME      | TYPE    | PARENT | LOCATIONS     |
-      | feature-1 | feature | main   | local, origin |
-    And the commits
-      | BRANCH    | LOCATION      | MESSAGE   | FILE NAME | FILE CONTENT |
-      | feature-1 | local, origin | commit 1a | file_1    | one          |
     And the branches
       | NAME      | TYPE    | PARENT    | LOCATIONS     |
+      | feature-1 | feature | main      | local, origin |
       | feature-2 | feature | feature-1 | local, origin |
     And the commits
-      | BRANCH    | LOCATION      | MESSAGE  | FILE NAME | FILE CONTENT |
-      | feature-2 | local, origin | commit 2 | file_2    | two          |
+      | BRANCH    | LOCATION      | MESSAGE   | FILE NAME | FILE CONTENT |
+      | feature-1 | local, origin | commit 1a | file_1    | one          |
+      | feature-2 | local, origin | commit 2  | file_2    | two          |
     And Git setting "git-town.sync-feature-strategy" is "rebase"
     And the current branch is "feature-2"
+    And I ran "git-town sync"
     And I amend this commit
       | BRANCH    | LOCATION | MESSAGE   | FILE NAME | FILE CONTENT |
       | feature-1 | local    | commit 1b | file_1    | another one  |
@@ -27,28 +21,16 @@ Feature: rebase a branch that contains amended commits
 
   Scenario: result
     Then Git Town runs the commands
-      | BRANCH    | COMMAND                                         |
-      | feature-2 | git fetch --prune --tags                        |
-      |           | git checkout feature-1                          |
-      | feature-1 | git -c rebase.updateRefs=false rebase main      |
-      |           | git push --force-with-lease --force-if-includes |
-      |           | git -c rebase.updateRefs=false rebase main      |
-      |           | git checkout feature-2                          |
-      | feature-2 | git -c rebase.updateRefs=false rebase feature-1 |
-    And Git Town prints the error:
-      """
-      CONFLICT (add/add): Merge conflict in file_1
-      """
-    And a rebase is now in progress
-
-  Scenario: resolve and continue
-    When I resolve the conflict in "file_1" with "another one"
-    And I run "git-town continue" and close the editor
-    Then Git Town runs the commands
-      | BRANCH    | COMMAND                                         |
-      | feature-2 | git -c core.editor=true rebase --continue       |
-      |           | git push --force-with-lease --force-if-includes |
-      |           | git -c rebase.updateRefs=false rebase feature-1 |
+      | BRANCH    | COMMAND                                                                                 |
+      | feature-2 | git fetch --prune --tags                                                                |
+      |           | git checkout feature-1                                                                  |
+      | feature-1 | git -c rebase.updateRefs=false rebase --onto main {{ sha 'initial commit' }}            |
+      |           | git push --force-with-lease --force-if-includes                                         |
+      |           | git -c rebase.updateRefs=false rebase --onto main {{ sha 'initial commit' }}            |
+      |           | git checkout feature-2                                                                  |
+      | feature-2 | git -c rebase.updateRefs=false rebase --onto feature-1 {{ sha-before-run 'commit 1a' }} |
+      |           | git push --force-with-lease --force-if-includes                                         |
+      |           | git -c rebase.updateRefs=false rebase --onto feature-1 {{ sha-before-run 'commit 1a' }} |
     And these commits exist now
       | BRANCH    | LOCATION      | MESSAGE   | FILE NAME | FILE CONTENT |
       | feature-1 | local, origin | commit 1b | file_1    | another one  |
@@ -57,9 +39,10 @@ Feature: rebase a branch that contains amended commits
   Scenario: undo
     When I run "git-town undo"
     Then Git Town runs the commands
-      | BRANCH    | COMMAND                                                            |
-      | feature-2 | git rebase --abort                                                 |
-      |           | git push --force-with-lease origin {{ sha 'commit 1a' }}:feature-1 |
+      | BRANCH    | COMMAND                                                                                 |
+      | feature-2 | git reset --hard {{ sha 'commit 2' }}                                                   |
+      |           | git push --force-with-lease --force-if-includes                                         |
+      |           | git push --force-with-lease origin {{ sha-in-origin-before-run 'commit 1a' }}:feature-1 |
     And these commits exist now
       | BRANCH    | LOCATION      | MESSAGE   |
       | feature-1 | local         | commit 1b |
diff --git a/features/sync/current_branch/feature_branch/rebase_sync_strategy/multiple_commands/multiple_commands.feature b/features/sync/current_branch/feature_branch/rebase_sync_strategy/multiple_commands/multiple_commands.feature
index 100f768ae..09ac87793 100644
--- a/features/sync/current_branch/feature_branch/rebase_sync_strategy/multiple_commands/multiple_commands.feature
+++ b/features/sync/current_branch/feature_branch/rebase_sync_strategy/multiple_commands/multiple_commands.feature
@@ -23,16 +23,16 @@ Feature: running a sync after running another Git Town command
 
   Scenario: result
     Then Git Town runs the commands
-      | BRANCH | COMMAND                                         |
-      | child  | git fetch --prune --tags                        |
-      |        | git checkout parent                             |
-      | parent | git -c rebase.updateRefs=false rebase main      |
-      |        | git push --force-with-lease --force-if-includes |
-      |        | git -c rebase.updateRefs=false rebase main      |
-      |        | git checkout child                              |
-      | child  | git -c rebase.updateRefs=false rebase parent    |
-      |        | git push --force-with-lease --force-if-includes |
-      |        | git -c rebase.updateRefs=false rebase parent    |
+      | BRANCH | COMMAND                                                                             |
+      | child  | git fetch --prune --tags                                                            |
+      |        | git checkout parent                                                                 |
+      | parent | git -c rebase.updateRefs=false rebase --onto main {{ sha 'local main commit' }}     |
+      |        | git push --force-with-lease --force-if-includes                                     |
+      |        | git -c rebase.updateRefs=false rebase --onto main {{ sha 'local main commit' }}     |
+      |        | git checkout child                                                                  |
+      | child  | git -c rebase.updateRefs=false rebase --onto parent {{ sha 'local parent commit' }} |
+      |        | git push --force-with-lease --force-if-includes                                     |
+      |        | git -c rebase.updateRefs=false rebase --onto parent {{ sha 'local parent commit' }} |
     And these commits exist now
       | BRANCH | LOCATION      | MESSAGE              |
       | main   | local, origin | origin main commit   |
diff --git a/features/sync/current_branch/feature_branch/rebase_sync_strategy/two_collaborators/conflicting_commits.feature b/features/sync/current_branch/feature_branch/rebase_sync_strategy/two_collaborators/conflicting_commits.feature
index 03ccf41a5..ec555d5ba 100644
--- a/features/sync/current_branch/feature_branch/rebase_sync_strategy/two_collaborators/conflicting_commits.feature
+++ b/features/sync/current_branch/feature_branch/rebase_sync_strategy/two_collaborators/conflicting_commits.feature
@@ -70,11 +70,11 @@ Feature: two people using rebase make conflicting changes to a branch
       | my second commit | file.txt  | my new content |
     When I run "git-town sync"
     Then Git Town runs the commands
-      | BRANCH  | COMMAND                                              |
-      | feature | git fetch --prune --tags                             |
-      |         | git -c rebase.updateRefs=false rebase main           |
-      |         | git push --force-with-lease --force-if-includes      |
-      |         | git -c rebase.updateRefs=false rebase origin/feature |
+      | BRANCH  | COMMAND                                                                             |
+      | feature | git fetch --prune --tags                                                            |
+      |         | git -c rebase.updateRefs=false rebase --onto main {{ sha 'persisted config file' }} |
+      |         | git push --force-with-lease --force-if-includes                                     |
+      |         | git -c rebase.updateRefs=false rebase origin/feature                                |
     And Git Town prints the error:
       """
       To continue after having resolved conflicts, run "git town continue".
@@ -82,10 +82,10 @@ Feature: two people using rebase make conflicting changes to a branch
     When I resolve the conflict in "file.txt" with "my new and coworker content"
     And I run "git town continue" and close the editor
     Then Git Town runs the commands
-      | BRANCH  | COMMAND                                         |
-      | feature | git -c core.editor=true rebase --continue       |
-      |         | git -c rebase.updateRefs=false rebase main      |
-      |         | git push --force-with-lease --force-if-includes |
+      | BRANCH  | COMMAND                                                                             |
+      | feature | git -c core.editor=true rebase --continue                                           |
+      |         | git -c rebase.updateRefs=false rebase --onto main {{ sha 'persisted config file' }} |
+      |         | git push --force-with-lease --force-if-includes                                     |
     And all branches are now synchronized
     And these commits exist now
       | BRANCH  | LOCATION                | MESSAGE               | FILE NAME | FILE CONTENT                |
diff --git a/features/sync/current_branch/feature_branch/rebase_sync_strategy/two_collaborators/happy_path.feature b/features/sync/current_branch/feature_branch/rebase_sync_strategy/two_collaborators/happy_path.feature
index e2c58927c..1b1c94b17 100644
--- a/features/sync/current_branch/feature_branch/rebase_sync_strategy/two_collaborators/happy_path.feature
+++ b/features/sync/current_branch/feature_branch/rebase_sync_strategy/two_collaborators/happy_path.feature
@@ -54,12 +54,12 @@ Feature: two people with rebase strategy sync changes made by them
     Given the current branch is "feature"
     When I run "git-town sync"
     Then Git Town runs the commands
-      | BRANCH  | COMMAND                                              |
-      | feature | git fetch --prune --tags                             |
-      |         | git -c rebase.updateRefs=false rebase main           |
-      |         | git push --force-with-lease --force-if-includes      |
-      |         | git -c rebase.updateRefs=false rebase origin/feature |
-      |         | git -c rebase.updateRefs=false rebase main           |
+      | BRANCH  | COMMAND                                                                             |
+      | feature | git fetch --prune --tags                                                            |
+      |         | git -c rebase.updateRefs=false rebase --onto main {{ sha 'persisted config file' }} |
+      |         | git push --force-with-lease --force-if-includes                                     |
+      |         | git -c rebase.updateRefs=false rebase origin/feature                                |
+      |         | git -c rebase.updateRefs=false rebase --onto main {{ sha 'persisted config file' }} |
     And no rebase is now in progress
     And all branches are now synchronized
     And these commits exist now
diff --git a/internal/cmd/append.go b/internal/cmd/append.go
index 31f0ce862..48cff0038 100644
--- a/internal/cmd/append.go
+++ b/internal/cmd/append.go
@@ -150,6 +150,7 @@ func executeAppend(arg string, beam configdomain.Beam, commit configdomain.Commi
 		EndBranchesSnapshot:   None[gitdomain.BranchesSnapshot](),
 		EndConfigSnapshot:     None[undoconfig.ConfigSnapshot](),
 		EndStashSize:          None[gitdomain.StashSize](),
+		BranchInfosLastRun:    data.branchInfosLastRun,
 		RunProgram:            runProgram,
 		TouchedBranches:       runProgram.TouchedBranches(),
 		UndoAPIProgram:        program.Program{},
@@ -177,6 +178,7 @@ func executeAppend(arg string, beam configdomain.Beam, commit configdomain.Commi
 type appendFeatureData struct {
 	beam                      configdomain.Beam
 	branchInfos               gitdomain.BranchInfos
+	branchInfosLastRun        Option[gitdomain.BranchInfos]
 	branchesSnapshot          gitdomain.BranchesSnapshot
 	branchesToSync            configdomain.BranchesToSync
 	commit                    configdomain.Commit
@@ -211,7 +213,7 @@ func determineAppendData(targetBranch gitdomain.LocalBranchName, beam configdoma
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, stashSize, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, stashSize, branchInfosLastRun, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
@@ -295,6 +297,7 @@ func determineAppendData(targetBranch gitdomain.LocalBranchName, beam configdoma
 	return appendFeatureData{
 		beam:                      beam,
 		branchInfos:               branchesSnapshot.Branches,
+		branchInfosLastRun:        branchInfosLastRun,
 		branchesSnapshot:          branchesSnapshot,
 		branchesToSync:            branchesToSync,
 		commit:                    commit,
@@ -326,6 +329,7 @@ func appendProgram(data appendFeatureData, finalMessages stringslice.Collector,
 		branchesToDelete := set.New[gitdomain.LocalBranchName]()
 		sync.BranchesProgram(data.branchesToSync, sync.BranchProgramArgs{
 			BranchInfos:         data.branchInfos,
+			BranchInfosLastRun:  data.branchInfosLastRun,
 			BranchesToDelete:    NewMutable(&branchesToDelete),
 			Config:              data.config,
 			InitialBranch:       data.initialBranch,
diff --git a/internal/cmd/branch.go b/internal/cmd/branch.go
index 5a0a78719..d43ad1e3e 100644
--- a/internal/cmd/branch.go
+++ b/internal/cmd/branch.go
@@ -70,7 +70,7 @@ func determineBranchData(repo execute.OpenRepoResult, verbose configdomain.Verbo
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, _, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, _, _, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
diff --git a/internal/cmd/compress.go b/internal/cmd/compress.go
index 8dfe701fe..dfda97f41 100644
--- a/internal/cmd/compress.go
+++ b/internal/cmd/compress.go
@@ -122,6 +122,7 @@ func executeCompress(dryRun configdomain.DryRun, verbose configdomain.Verbose, m
 		EndBranchesSnapshot:   None[gitdomain.BranchesSnapshot](),
 		EndConfigSnapshot:     None[undoconfig.ConfigSnapshot](),
 		EndStashSize:          None[gitdomain.StashSize](),
+		BranchInfosLastRun:    data.branchInfosLastRun,
 		RunProgram:            runProgram,
 		TouchedBranches:       runProgram.TouchedBranches(),
 		UndoAPIProgram:        program.Program{},
@@ -147,6 +148,7 @@ func executeCompress(dryRun configdomain.DryRun, verbose configdomain.Verbose, m
 }
 
 type compressBranchesData struct {
+	branchInfosLastRun Option[gitdomain.BranchInfos]
 	branchesSnapshot   gitdomain.BranchesSnapshot
 	branchesToCompress []compressBranchData
 	config             config.ValidatedConfig
@@ -174,7 +176,7 @@ func determineCompressBranchesData(repo execute.OpenRepoResult, dryRun configdom
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, stashSize, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, stashSize, branchInfosLastRun, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
@@ -280,6 +282,7 @@ func determineCompressBranchesData(repo execute.OpenRepoResult, dryRun configdom
 		return data, exit, fmt.Errorf(messages.CompressNoCommits, branchNamesToCompress[0])
 	}
 	return compressBranchesData{
+		branchInfosLastRun: branchInfosLastRun,
 		branchesSnapshot:   branchesSnapshot,
 		branchesToCompress: branchesToCompress,
 		config:             validatedConfig,
diff --git a/internal/cmd/config/setup.go b/internal/cmd/config/setup.go
index 3dd789b4b..8a4c57caf 100644
--- a/internal/cmd/config/setup.go
+++ b/internal/cmd/config/setup.go
@@ -247,7 +247,7 @@ func loadSetupData(repo execute.OpenRepoResult, verbose configdomain.Verbose) (d
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, _, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, _, _, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
diff --git a/internal/cmd/continue.go b/internal/cmd/continue.go
index a61bb5940..b6a53fd5e 100644
--- a/internal/cmd/continue.go
+++ b/internal/cmd/continue.go
@@ -93,7 +93,7 @@ func determineContinueData(repo execute.OpenRepoResult, verbose configdomain.Ver
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, stashSize, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, stashSize, _, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
diff --git a/internal/cmd/delete.go b/internal/cmd/delete.go
index 43dcdc2ec..4c0677527 100644
--- a/internal/cmd/delete.go
+++ b/internal/cmd/delete.go
@@ -119,6 +119,7 @@ func executeDelete(args []string, dryRun configdomain.DryRun, verbose configdoma
 		EndConfigSnapshot:     None[undoconfig.ConfigSnapshot](),
 		EndStashSize:          None[gitdomain.StashSize](),
 		FinalUndoProgram:      finalUndoProgram,
+		BranchInfosLastRun:    data.branchInfosLastRun,
 		RunProgram:            runProgram,
 		TouchedBranches:       runProgram.TouchedBranches(),
 		UndoAPIProgram:        program.Program{},
@@ -144,6 +145,7 @@ func executeDelete(args []string, dryRun configdomain.DryRun, verbose configdoma
 }
 
 type deleteData struct {
+	branchInfosLastRun       Option[gitdomain.BranchInfos]
 	branchToDeleteInfo       gitdomain.BranchInfo
 	branchToDeleteType       configdomain.BranchType
 	branchWhenDone           gitdomain.LocalBranchName
@@ -166,7 +168,7 @@ func determineDeleteData(args []string, repo execute.OpenRepoResult, dryRun conf
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, stashSize, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, stashSize, branchInfosLastRun, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
@@ -240,6 +242,7 @@ func determineDeleteData(args []string, repo execute.OpenRepoResult, dryRun conf
 	lineageBranches := validatedConfig.NormalConfig.Lineage.BranchNames()
 	_, nonExistingBranches := branchesSnapshot.Branches.Select(repo.UnvalidatedConfig.NormalConfig.DevRemote, lineageBranches...)
 	return deleteData{
+		branchInfosLastRun:       branchInfosLastRun,
 		branchToDeleteInfo:       *branchToDelete,
 		branchToDeleteType:       branchTypeToDelete,
 		branchWhenDone:           branchWhenDone,
diff --git a/internal/cmd/detach.go b/internal/cmd/detach.go
index 5f1b0c758..9faeedd19 100644
--- a/internal/cmd/detach.go
+++ b/internal/cmd/detach.go
@@ -122,6 +122,7 @@ func executeDetach(args []string, dryRun configdomain.DryRun, verbose configdoma
 		EndBranchesSnapshot:   None[gitdomain.BranchesSnapshot](),
 		EndConfigSnapshot:     None[undoconfig.ConfigSnapshot](),
 		EndStashSize:          None[gitdomain.StashSize](),
+		BranchInfosLastRun:    data.branchInfosLastRun,
 		RunProgram:            runProgram,
 		TouchedBranches:       runProgram.TouchedBranches(),
 		UndoAPIProgram:        program.Program{},
@@ -147,6 +148,7 @@ func executeDetach(args []string, dryRun configdomain.DryRun, verbose configdoma
 }
 
 type detachData struct {
+	branchInfosLastRun  Option[gitdomain.BranchInfos]
 	branchToDetachInfo  gitdomain.BranchInfo
 	branchToDetachName  gitdomain.LocalBranchName
 	branchToDetachType  configdomain.BranchType
@@ -177,7 +179,7 @@ func determineDetachData(args []string, repo execute.OpenRepoResult, dryRun conf
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, stashSize, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, stashSize, branchInfosLastRun, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
@@ -283,6 +285,7 @@ func determineDetachData(args []string, repo execute.OpenRepoResult, dryRun conf
 	lineageBranches := validatedConfig.NormalConfig.Lineage.BranchNames()
 	_, nonExistingBranches := branchesSnapshot.Branches.Select(repo.UnvalidatedConfig.NormalConfig.DevRemote, lineageBranches...)
 	return detachData{
+		branchInfosLastRun:  branchInfosLastRun,
 		branchToDetachInfo:  *branchToDetachInfo,
 		branchToDetachName:  branchNameToDetach,
 		branchToDetachType:  branchTypeToDetach,
diff --git a/internal/cmd/diff_parent.go b/internal/cmd/diff_parent.go
index cb8008cf5..2339baa50 100644
--- a/internal/cmd/diff_parent.go
+++ b/internal/cmd/diff_parent.go
@@ -84,7 +84,7 @@ func determineDiffParentData(args []string, repo execute.OpenRepoResult, verbose
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, _, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, _, _, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
diff --git a/internal/cmd/hack.go b/internal/cmd/hack.go
index ed354a9fd..e06140163 100644
--- a/internal/cmd/hack.go
+++ b/internal/cmd/hack.go
@@ -150,6 +150,7 @@ func executeHack(args []string, beam configdomain.Beam, commit configdomain.Comm
 			beginBranchesSnapshot: createNewFeatureBranchData.branchesSnapshot,
 			beginConfigSnapshot:   repo.ConfigSnapshot,
 			beginStashSize:        createNewFeatureBranchData.stashSize,
+			branchInfosLastRun:    createNewFeatureBranchData.branchInfosLastRun,
 			commandsCounter:       repo.CommandsCounter,
 			dryRun:                dryRun,
 			finalMessages:         repo.FinalMessages,
@@ -188,6 +189,7 @@ func createFeatureBranch(args createFeatureBranchArgs) error {
 		BeginBranchesSnapshot: args.beginBranchesSnapshot,
 		BeginConfigSnapshot:   args.beginConfigSnapshot,
 		BeginStashSize:        args.beginStashSize,
+		BranchInfosLastRun:    args.branchInfosLastRun,
 		Command:               "hack",
 		DryRun:                args.dryRun,
 		EndBranchesSnapshot:   None[gitdomain.BranchesSnapshot](),
@@ -223,6 +225,7 @@ type createFeatureBranchArgs struct {
 	beginBranchesSnapshot gitdomain.BranchesSnapshot
 	beginConfigSnapshot   undoconfig.ConfigSnapshot
 	beginStashSize        gitdomain.StashSize
+	branchInfosLastRun    Option[gitdomain.BranchInfos]
 	commandsCounter       Mutable[gohacks.Counter]
 	dryRun                configdomain.DryRun
 	finalMessages         stringslice.Collector
@@ -245,7 +248,7 @@ func determineHackData(args []string, repo execute.OpenRepoResult, beam configdo
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, stashSize, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, stashSize, branchInfosLastRun, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
@@ -353,6 +356,7 @@ func determineHackData(args []string, repo execute.OpenRepoResult, beam configdo
 	data = Left[appendFeatureData, convertToFeatureData](appendFeatureData{
 		beam:                      beam,
 		branchInfos:               branchesSnapshot.Branches,
+		branchInfosLastRun:        branchInfosLastRun,
 		branchesSnapshot:          branchesSnapshot,
 		branchesToSync:            branchesToSync,
 		commit:                    commit,
diff --git a/internal/cmd/merge.go b/internal/cmd/merge.go
index fb40ca0f2..eeea8ea41 100644
--- a/internal/cmd/merge.go
+++ b/internal/cmd/merge.go
@@ -111,6 +111,7 @@ func executeMerge(dryRun configdomain.DryRun, verbose configdomain.Verbose) erro
 		BeginBranchesSnapshot: data.branchesSnapshot,
 		BeginConfigSnapshot:   repo.ConfigSnapshot,
 		BeginStashSize:        data.stashSize,
+		BranchInfosLastRun:    data.branchInfosLastRun,
 		Command:               mergeCmd,
 		DryRun:                dryRun,
 		EndBranchesSnapshot:   None[gitdomain.BranchesSnapshot](),
@@ -141,6 +142,7 @@ func executeMerge(dryRun configdomain.DryRun, verbose configdomain.Verbose) erro
 }
 
 type mergeData struct {
+	branchInfosLastRun              Option[gitdomain.BranchInfos]
 	branchesSnapshot                gitdomain.BranchesSnapshot
 	config                          config.ValidatedConfig
 	connector                       Option[forgedomain.Connector]
@@ -174,7 +176,7 @@ func determineMergeData(repo execute.OpenRepoResult, verbose configdomain.Verbos
 	if err != nil {
 		return mergeData{}, false, err
 	}
-	branchesSnapshot, stashSize, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, stashSize, branchInfosLastRun, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
@@ -267,6 +269,7 @@ func determineMergeData(repo execute.OpenRepoResult, verbose configdomain.Verbos
 		}
 	}
 	return mergeData{
+		branchInfosLastRun:              branchInfosLastRun,
 		branchesSnapshot:                branchesSnapshot,
 		config:                          validatedConfig,
 		connector:                       connectorOpt,
diff --git a/internal/cmd/prepend.go b/internal/cmd/prepend.go
index d36004579..31d5b8082 100644
--- a/internal/cmd/prepend.go
+++ b/internal/cmd/prepend.go
@@ -164,6 +164,7 @@ func executePrepend(args []string, beam configdomain.Beam, proposalBody gitdomai
 		BeginBranchesSnapshot: data.branchesSnapshot,
 		BeginConfigSnapshot:   repo.ConfigSnapshot,
 		BeginStashSize:        data.stashSize,
+		BranchInfosLastRun:    data.branchInfosLastRun,
 		Command:               "prepend",
 		DryRun:                dryRun,
 		EndBranchesSnapshot:   None[gitdomain.BranchesSnapshot](),
@@ -196,6 +197,7 @@ func executePrepend(args []string, beam configdomain.Beam, proposalBody gitdomai
 type prependData struct {
 	beam                configdomain.Beam
 	branchInfos         gitdomain.BranchInfos
+	branchInfosLastRun  Option[gitdomain.BranchInfos]
 	branchesSnapshot    gitdomain.BranchesSnapshot
 	branchesToSync      configdomain.BranchesToSync
 	commit              configdomain.Commit
@@ -234,7 +236,7 @@ func determinePrependData(args []string, repo execute.OpenRepoResult, beam confi
 		return data, false, err
 	}
 	fc := execute.FailureCollector{}
-	branchesSnapshot, stashSize, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, stashSize, branchInfosLastRun, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
@@ -328,6 +330,7 @@ func determinePrependData(args []string, repo execute.OpenRepoResult, beam confi
 	return prependData{
 		beam:                beam,
 		branchInfos:         branchesSnapshot.Branches,
+		branchInfosLastRun:  branchInfosLastRun,
 		branchesSnapshot:    branchesSnapshot,
 		branchesToSync:      branchesToSync,
 		commit:              commit,
@@ -363,6 +366,7 @@ func prependProgram(data prependData, finalMessages stringslice.Collector) progr
 		branchesToDelete := set.New[gitdomain.LocalBranchName]()
 		sync.BranchesProgram(data.branchesToSync, sync.BranchProgramArgs{
 			BranchInfos:         data.branchInfos,
+			BranchInfosLastRun:  data.branchInfosLastRun,
 			BranchesToDelete:    NewMutable(&branchesToDelete),
 			Config:              data.config,
 			InitialBranch:       data.initialBranch,
diff --git a/internal/cmd/propose.go b/internal/cmd/propose.go
index 4cba4e624..8f9a37bc3 100644
--- a/internal/cmd/propose.go
+++ b/internal/cmd/propose.go
@@ -123,6 +123,7 @@ func executePropose(dryRun configdomain.DryRun, verbose configdomain.Verbose, ti
 		BeginBranchesSnapshot: data.branchesSnapshot,
 		BeginConfigSnapshot:   repo.ConfigSnapshot,
 		BeginStashSize:        data.stashSize,
+		BranchInfosLastRun:    data.branchInfosLastRun,
 		Command:               proposeCmd,
 		DryRun:                dryRun,
 		EndBranchesSnapshot:   None[gitdomain.BranchesSnapshot](),
@@ -154,6 +155,7 @@ func executePropose(dryRun configdomain.DryRun, verbose configdomain.Verbose, ti
 
 type proposeData struct {
 	branchInfos         gitdomain.BranchInfos
+	branchInfosLastRun  Option[gitdomain.BranchInfos]
 	branchesSnapshot    gitdomain.BranchesSnapshot
 	branchesToPropose   []branchToProposeData
 	branchesToSync      configdomain.BranchesToSync
@@ -189,7 +191,7 @@ func determineProposeData(repo execute.OpenRepoResult, dryRun configdomain.DryRu
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, stashSize, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, stashSize, branchInfosLastRun, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
@@ -317,6 +319,7 @@ func determineProposeData(repo execute.OpenRepoResult, dryRun configdomain.DryRu
 	}
 	return proposeData{
 		branchInfos:         branchesSnapshot.Branches,
+		branchInfosLastRun:  branchInfosLastRun,
 		branchesSnapshot:    branchesSnapshot,
 		branchesToPropose:   branchesToPropose,
 		branchesToSync:      branchesToSync,
@@ -342,6 +345,7 @@ func proposeProgram(repo execute.OpenRepoResult, data proposeData) program.Progr
 	branchesToDelete := set.New[gitdomain.LocalBranchName]()
 	sync.BranchesProgram(data.branchesToSync, sync.BranchProgramArgs{
 		BranchInfos:         data.branchInfos,
+		BranchInfosLastRun:  data.branchInfosLastRun,
 		BranchesToDelete:    NewMutable(&branchesToDelete),
 		Config:              data.config,
 		InitialBranch:       data.initialBranch,
diff --git a/internal/cmd/rename.go b/internal/cmd/rename.go
index 1896434ac..9be3bf970 100644
--- a/internal/cmd/rename.go
+++ b/internal/cmd/rename.go
@@ -90,6 +90,7 @@ func executeRename(args []string, dryRun configdomain.DryRun, force configdomain
 		BeginBranchesSnapshot: data.branchesSnapshot,
 		BeginConfigSnapshot:   repo.ConfigSnapshot,
 		BeginStashSize:        data.stashSize,
+		BranchInfosLastRun:    data.branchInfosLastRun,
 		Command:               "rename",
 		DryRun:                dryRun,
 		EndBranchesSnapshot:   None[gitdomain.BranchesSnapshot](),
@@ -120,6 +121,7 @@ func executeRename(args []string, dryRun configdomain.DryRun, force configdomain
 }
 
 type renameData struct {
+	branchInfosLastRun       Option[gitdomain.BranchInfos]
 	branchesSnapshot         gitdomain.BranchesSnapshot
 	config                   config.ValidatedConfig
 	connector                Option[forgedomain.Connector]
@@ -143,7 +145,7 @@ func determineRenameData(args []string, force configdomain.Force, repo execute.O
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, stashSize, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, stashSize, branchInfosLastRun, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
@@ -238,6 +240,7 @@ func determineRenameData(args []string, force configdomain.Force, repo execute.O
 		OldBranchHasTrackingBranch: oldBranch.HasTrackingBranch(),
 	})
 	return renameData{
+		branchInfosLastRun:       branchInfosLastRun,
 		branchesSnapshot:         branchesSnapshot,
 		config:                   validatedConfig,
 		connector:                connectorOpt,
diff --git a/internal/cmd/set_parent.go b/internal/cmd/set_parent.go
index 549ba22be..83636df56 100644
--- a/internal/cmd/set_parent.go
+++ b/internal/cmd/set_parent.go
@@ -125,6 +125,7 @@ func executeSetParent(args []string, verbose configdomain.Verbose) error {
 		BeginBranchesSnapshot: data.branchesSnapshot,
 		BeginConfigSnapshot:   repo.ConfigSnapshot,
 		BeginStashSize:        data.stashSize,
+		BranchInfosLastRun:    data.branchInfosLastRun,
 		Command:               "set-parent",
 		DryRun:                false,
 		EndBranchesSnapshot:   None[gitdomain.BranchesSnapshot](),
@@ -155,16 +156,17 @@ func executeSetParent(args []string, verbose configdomain.Verbose) error {
 }
 
 type setParentData struct {
-	branchesSnapshot gitdomain.BranchesSnapshot
-	config           config.ValidatedConfig
-	connector        Option[forgedomain.Connector]
-	defaultChoice    gitdomain.LocalBranchName
-	dialogTestInputs components.TestInputs
-	hasOpenChanges   bool
-	initialBranch    gitdomain.LocalBranchName
-	mainBranch       gitdomain.LocalBranchName
-	proposal         Option[forgedomain.Proposal]
-	stashSize        gitdomain.StashSize
+	branchInfosLastRun Option[gitdomain.BranchInfos]
+	branchesSnapshot   gitdomain.BranchesSnapshot
+	config             config.ValidatedConfig
+	connector          Option[forgedomain.Connector]
+	defaultChoice      gitdomain.LocalBranchName
+	dialogTestInputs   components.TestInputs
+	hasOpenChanges     bool
+	initialBranch      gitdomain.LocalBranchName
+	mainBranch         gitdomain.LocalBranchName
+	proposal           Option[forgedomain.Proposal]
+	stashSize          gitdomain.StashSize
 }
 
 func determineSetParentData(repo execute.OpenRepoResult, verbose configdomain.Verbose) (data setParentData, exit bool, err error) {
@@ -173,7 +175,7 @@ func determineSetParentData(repo execute.OpenRepoResult, verbose configdomain.Ve
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, stashSize, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, stashSize, branchInfosLastRun, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
@@ -234,16 +236,17 @@ func determineSetParentData(repo execute.OpenRepoResult, verbose configdomain.Ve
 		proposalOpt = ship.FindProposal(connectorOpt, initialBranch, parentOpt)
 	}
 	return setParentData{
-		branchesSnapshot: branchesSnapshot,
-		config:           validatedConfig,
-		connector:        connectorOpt,
-		defaultChoice:    defaultChoice,
-		dialogTestInputs: dialogTestInputs,
-		hasOpenChanges:   repoStatus.OpenChanges,
-		initialBranch:    initialBranch,
-		mainBranch:       mainBranch,
-		proposal:         proposalOpt,
-		stashSize:        stashSize,
+		branchInfosLastRun: branchInfosLastRun,
+		branchesSnapshot:   branchesSnapshot,
+		config:             validatedConfig,
+		connector:          connectorOpt,
+		defaultChoice:      defaultChoice,
+		dialogTestInputs:   dialogTestInputs,
+		hasOpenChanges:     repoStatus.OpenChanges,
+		initialBranch:      initialBranch,
+		mainBranch:         mainBranch,
+		proposal:           proposalOpt,
+		stashSize:          stashSize,
 	}, false, nil
 }
 
@@ -289,7 +292,7 @@ func setParentProgram(dialogOutcome dialog.ParentOutcome, selectedBranch gitdoma
 			if parent, hasParent := parentOpt.Get(); hasParent {
 				prog.Add(
 					&opcodes.RebaseOntoKeepDeleted{
-						BranchToRebaseOnto: selectedBranch,
+						BranchToRebaseOnto: selectedBranch.BranchName(),
 						CommitsToRemove:    parent.Location(),
 						Upstream:           None[gitdomain.LocalBranchName](),
 					},
diff --git a/internal/cmd/ship/cmd.go b/internal/cmd/ship/cmd.go
index acaf53c43..0a33d69a4 100644
--- a/internal/cmd/ship/cmd.go
+++ b/internal/cmd/ship/cmd.go
@@ -145,6 +145,7 @@ func executeShip(args []string, message Option[gitdomain.CommitMessage], dryRun
 		EndBranchesSnapshot:   None[gitdomain.BranchesSnapshot](),
 		EndConfigSnapshot:     None[undoconfig.ConfigSnapshot](),
 		EndStashSize:          None[gitdomain.StashSize](),
+		BranchInfosLastRun:    sharedData.previousBranchInfos,
 		RunProgram:            optimizedProgram,
 		TouchedBranches:       optimizedProgram.TouchedBranches(),
 		UndoAPIProgram:        program.Program{},
diff --git a/internal/cmd/ship/shared.go b/internal/cmd/ship/shared.go
index 30793bbf0..f9fcfbee0 100644
--- a/internal/cmd/ship/shared.go
+++ b/internal/cmd/ship/shared.go
@@ -33,6 +33,7 @@ type sharedShipData struct {
 	initialBranch            gitdomain.LocalBranchName
 	isShippingInitialBranch  bool
 	previousBranch           Option[gitdomain.LocalBranchName]
+	previousBranchInfos      Option[gitdomain.BranchInfos]
 	proposalsOfChildBranches []forgedomain.Proposal
 	stashSize                gitdomain.StashSize
 	targetBranch             gitdomain.BranchInfo
@@ -45,7 +46,7 @@ func determineSharedShipData(args []string, repo execute.OpenRepoResult, dryRun
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, stashSize, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, stashSize, previousBranchInfos, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
@@ -148,6 +149,7 @@ func determineSharedShipData(args []string, repo execute.OpenRepoResult, dryRun
 		initialBranch:            initialBranch,
 		isShippingInitialBranch:  isShippingInitialBranch,
 		previousBranch:           previousBranch,
+		previousBranchInfos:      previousBranchInfos,
 		proposalsOfChildBranches: proposalsOfChildBranches,
 		stashSize:                stashSize,
 		targetBranch:             *targetBranch,
diff --git a/internal/cmd/skip.go b/internal/cmd/skip.go
index fdce51f57..a99faceae 100644
--- a/internal/cmd/skip.go
+++ b/internal/cmd/skip.go
@@ -59,7 +59,7 @@ func executeSkip(verbose configdomain.Verbose) error {
 	if err != nil {
 		return err
 	}
-	branchesSnapshot, _, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, _, _, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
diff --git a/internal/cmd/swap.go b/internal/cmd/swap.go
index 4c53662f5..1d2c1934c 100644
--- a/internal/cmd/swap.go
+++ b/internal/cmd/swap.go
@@ -112,6 +112,7 @@ func executeSwap(args []string, dryRun configdomain.DryRun, verbose configdomain
 		BeginBranchesSnapshot: data.branchesSnapshot,
 		BeginConfigSnapshot:   repo.ConfigSnapshot,
 		BeginStashSize:        data.stashSize,
+		BranchInfosLastRun:    data.branchInfosLastRun,
 		Command:               swapCommandName,
 		DryRun:                dryRun,
 		EndBranchesSnapshot:   None[gitdomain.BranchesSnapshot](),
@@ -142,6 +143,7 @@ func executeSwap(args []string, dryRun configdomain.DryRun, verbose configdomain
 }
 
 type swapData struct {
+	branchInfosLastRun  Option[gitdomain.BranchInfos]
 	branchToSwapInfo    gitdomain.BranchInfo
 	branchToSwapName    gitdomain.LocalBranchName
 	branchToSwapType    configdomain.BranchType
@@ -174,7 +176,7 @@ func determineSwapData(args []string, repo execute.OpenRepoResult, dryRun config
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, stashSize, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, stashSize, branchInfosLastRun, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
@@ -283,6 +285,7 @@ func determineSwapData(args []string, repo execute.OpenRepoResult, dryRun config
 	lineageBranches := validatedConfig.NormalConfig.Lineage.BranchNames()
 	_, nonExistingBranches := branchesSnapshot.Branches.Select(repo.UnvalidatedConfig.NormalConfig.DevRemote, lineageBranches...)
 	return swapData{
+		branchInfosLastRun:  branchInfosLastRun,
 		branchToSwapInfo:    *branchToSwapInfo,
 		branchToSwapName:    branchNameToSwap,
 		branchToSwapType:    branchTypeToSwap,
@@ -309,7 +312,7 @@ func swapProgram(data swapData, finalMessages stringslice.Collector) program.Pro
 	data.config.CleanupLineage(data.branchesSnapshot.Branches, data.nonExistingBranches, finalMessages)
 	prog.Value.Add(
 		&opcodes.RebaseOntoKeepDeleted{
-			BranchToRebaseOnto: data.grandParentBranch,
+			BranchToRebaseOnto: data.grandParentBranch.BranchName(),
 			CommitsToRemove:    data.parentBranch.Location(),
 			Upstream:           None[gitdomain.LocalBranchName](),
 		},
@@ -324,7 +327,7 @@ func swapProgram(data swapData, finalMessages stringslice.Collector) program.Pro
 			Branch: data.parentBranch,
 		},
 		&opcodes.RebaseOntoKeepDeleted{
-			BranchToRebaseOnto: data.branchToSwapName,
+			BranchToRebaseOnto: data.branchToSwapName.BranchName(),
 			CommitsToRemove:    data.grandParentBranch.Location(),
 			Upstream:           None[gitdomain.LocalBranchName](),
 		},
@@ -346,7 +349,7 @@ func swapProgram(data swapData, finalMessages stringslice.Collector) program.Pro
 		}
 		prog.Value.Add(
 			&opcodes.RebaseOntoKeepDeleted{
-				BranchToRebaseOnto: data.parentBranch,
+				BranchToRebaseOnto: data.parentBranch.BranchName(),
 				CommitsToRemove:    oldBranchSHA.Location(),
 				Upstream:           None[gitdomain.LocalBranchName](),
 			},
diff --git a/internal/cmd/switch.go b/internal/cmd/switch.go
index 15c5df734..e2759f765 100644
--- a/internal/cmd/switch.go
+++ b/internal/cmd/switch.go
@@ -125,7 +125,7 @@ func determineSwitchData(args []string, repo execute.OpenRepoResult, verbose con
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, _, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, _, _, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
diff --git a/internal/cmd/sync/cmd.go b/internal/cmd/sync/cmd.go
index e598dd261..d03a80d21 100644
--- a/internal/cmd/sync/cmd.go
+++ b/internal/cmd/sync/cmd.go
@@ -129,6 +129,7 @@ func executeSync(syncAllBranches configdomain.AllBranches, syncStack configdomai
 	branchesToDelete := set.New[gitdomain.LocalBranchName]()
 	BranchesProgram(data.branchesToSync, BranchProgramArgs{
 		BranchInfos:         data.branchInfos,
+		BranchInfosLastRun:  data.previousBranchInfos,
 		BranchesToDelete:    NewMutable(&branchesToDelete),
 		Config:              data.config,
 		InitialBranch:       data.initialBranch,
@@ -166,6 +167,7 @@ func executeSync(syncAllBranches configdomain.AllBranches, syncStack configdomai
 		EndBranchesSnapshot:   None[gitdomain.BranchesSnapshot](),
 		EndConfigSnapshot:     None[undoconfig.ConfigSnapshot](),
 		EndStashSize:          None[gitdomain.StashSize](),
+		BranchInfosLastRun:    data.previousBranchInfos,
 		RunProgram:            optimizedProgram,
 		TouchedBranches:       optimizedProgram.TouchedBranches(),
 		UndoAPIProgram:        program.Program{},
@@ -202,6 +204,7 @@ type syncData struct {
 	nonExistingBranches      gitdomain.LocalBranchNames
 	prefetchBranchesSnapshot gitdomain.BranchesSnapshot
 	previousBranch           Option[gitdomain.LocalBranchName]
+	previousBranchInfos      Option[gitdomain.BranchInfos]
 	remotes                  gitdomain.Remotes
 	shouldPushTags           bool
 	stashSize                gitdomain.StashSize
@@ -217,7 +220,7 @@ func determineSyncData(syncAllBranches configdomain.AllBranches, syncStack confi
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, stashSize, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, stashSize, previousBranchInfos, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
@@ -348,6 +351,7 @@ func determineSyncData(syncAllBranches configdomain.AllBranches, syncStack confi
 		nonExistingBranches:      nonExistingBranches,
 		prefetchBranchesSnapshot: preFetchBranchesSnapshot,
 		previousBranch:           previousBranchOpt,
+		previousBranchInfos:      previousBranchInfos,
 		remotes:                  remotes,
 		shouldPushTags:           shouldPushTags,
 		stashSize:                stashSize,
diff --git a/internal/cmd/sync/sync_branch.go b/internal/cmd/sync/sync_branch.go
index 5c791111d..1360631db 100644
--- a/internal/cmd/sync/sync_branch.go
+++ b/internal/cmd/sync/sync_branch.go
@@ -33,6 +33,14 @@ func BranchProgram(localName gitdomain.LocalBranchName, branchInfo gitdomain.Bra
 			RebaseOnto:        args.Config.ValidatedConfigData.MainBranch, // TODO: RebaseOnto the latest existing parent, which isn't always main
 		})
 	}
+	parentLastRunSHA := None[gitdomain.SHA]()
+	if parent, has := originalParentName.Get(); has {
+		if branchInfosLastRun, has := args.BranchInfosLastRun.Get(); has {
+			if parentInfoLastRun, has := branchInfosLastRun.FindByLocalName(parent).Get(); has {
+				parentLastRunSHA = Some(parentInfoLastRun.GetLocalOrRemoteSHA())
+			}
+		}
+	}
 	switch {
 	case hasParentToRemove && parentToRemove == parentName && trackingBranchGone && hasDescendents:
 		args.BranchesToDelete.Value.Add(localName)
@@ -41,17 +49,18 @@ func BranchProgram(localName gitdomain.LocalBranchName, branchInfo gitdomain.Bra
 	case rebaseSyncStrategy && trackingBranchGone && hasDescendents:
 		args.BranchesToDelete.Value.Add(localName)
 	case trackingBranchGone:
-		deletedBranchProgram(args.Program, localName, originalParentName, originalParentSHA, args)
+		deletedBranchProgram(args.Program, localName, originalParentName, originalParentSHA, parentLastRunSHA, args)
 	case branchInfo.SyncStatus == gitdomain.SyncStatusOtherWorktree:
 		// cannot sync branches that are active in another worktree
 	default:
-		LocalBranchProgram(localName, branchInfo, originalParentName, originalParentSHA, firstCommitMessage, args)
+		LocalBranchProgram(localName, branchInfo, originalParentName, originalParentSHA, parentLastRunSHA, firstCommitMessage, args)
 	}
 	args.Program.Value.Add(&opcodes.ProgramEndOfBranch{})
 }
 
 type BranchProgramArgs struct {
 	BranchInfos         gitdomain.BranchInfos                       // the initial BranchInfos, after "git fetch" ran
+	BranchInfosLastRun  Option[gitdomain.BranchInfos]               // the BranchInfos at the end of the previous Git Town command
 	BranchesToDelete    Mutable[set.Set[gitdomain.LocalBranchName]] // branches that should be deleted after the branches are all synced
 	Config              config.ValidatedConfig
 	InitialBranch       gitdomain.LocalBranchName
@@ -63,7 +72,7 @@ type BranchProgramArgs struct {
 }
 
 // LocalBranchProgram provides the program to sync a local branch.
-func LocalBranchProgram(localName gitdomain.LocalBranchName, branchInfo gitdomain.BranchInfo, originalParentName Option[gitdomain.LocalBranchName], originalParentSHA Option[gitdomain.SHA], firstCommitMessage Option[gitdomain.CommitMessage], args BranchProgramArgs) {
+func LocalBranchProgram(localName gitdomain.LocalBranchName, branchInfo gitdomain.BranchInfo, originalParentName Option[gitdomain.LocalBranchName], originalParentSHA, parentLastRunSHA Option[gitdomain.SHA], firstCommitMessage Option[gitdomain.CommitMessage], args BranchProgramArgs) {
 	branchType := args.Config.BranchType(localName)
 	isMainOrPerennialBranch := branchType == configdomain.BranchTypeMainBranch || branchType == configdomain.BranchTypePerennialBranch
 	if isMainOrPerennialBranch && !args.Remotes.HasRemote(args.Config.NormalConfig.DevRemote) {
@@ -79,6 +88,7 @@ func LocalBranchProgram(localName gitdomain.LocalBranchName, branchInfo gitdomai
 			offline:            args.Config.NormalConfig.Offline,
 			originalParentName: originalParentName,
 			originalParentSHA:  originalParentSHA,
+			parentLastRunSHA:   parentLastRunSHA,
 			program:            args.Program,
 			prune:              args.Prune,
 			pushBranches:       args.PushBranches,
@@ -93,6 +103,7 @@ func LocalBranchProgram(localName gitdomain.LocalBranchName, branchInfo gitdomai
 			offline:            args.Config.NormalConfig.Offline,
 			originalParentName: originalParentName,
 			originalParentSHA:  originalParentSHA,
+			parentLastRunSHA:   parentLastRunSHA,
 			program:            args.Program,
 			prune:              args.Prune,
 			pushBranches:       args.PushBranches,
@@ -109,6 +120,7 @@ func LocalBranchProgram(localName gitdomain.LocalBranchName, branchInfo gitdomai
 			offline:            args.Config.NormalConfig.Offline,
 			originalParentName: originalParentName,
 			originalParentSHA:  originalParentSHA,
+			parentLastRunSHA:   parentLastRunSHA,
 			program:            args.Program,
 			prune:              args.Prune,
 			pushBranches:       configdomain.PushBranches(branchInfo.HasTrackingBranch()),
@@ -143,7 +155,8 @@ func pullParentBranchOfCurrentFeatureBranchOpcode(args pullParentBranchOfCurrent
 		})
 	case configdomain.SyncFeatureStrategyRebase:
 		args.program.Value.Add(&opcodes.RebaseParentIfNeeded{
-			Branch: args.branch,
+			Branch:      args.branch,
+			PreviousSHA: args.previousParentSHA,
 		})
 	case configdomain.SyncFeatureStrategyCompress:
 		args.program.Value.Add(&opcodes.MergeParentIfNeeded{
@@ -158,6 +171,7 @@ type pullParentBranchOfCurrentFeatureBranchOpcodeArgs struct {
 	branch             gitdomain.LocalBranchName
 	originalParentName Option[gitdomain.LocalBranchName]
 	originalParentSHA  Option[gitdomain.SHA]
+	previousParentSHA  Option[gitdomain.SHA]
 	program            Mutable[program.Program]
 	syncStrategy       configdomain.SyncFeatureStrategy
 }
diff --git a/internal/cmd/sync/sync_deleted_branch.go b/internal/cmd/sync/sync_deleted_branch.go
index fdef72c57..ed4ad82ae 100644
--- a/internal/cmd/sync/sync_deleted_branch.go
+++ b/internal/cmd/sync/sync_deleted_branch.go
@@ -9,10 +9,10 @@ import (
 )
 
 // deletedBranchProgram adds opcodes that sync a branch that was deleted at origin to the given program.
-func deletedBranchProgram(prog Mutable[program.Program], branch gitdomain.LocalBranchName, originalParentName Option[gitdomain.LocalBranchName], originalParentSHA Option[gitdomain.SHA], args BranchProgramArgs) {
+func deletedBranchProgram(prog Mutable[program.Program], branch gitdomain.LocalBranchName, originalParentName Option[gitdomain.LocalBranchName], originalParentSHA, parentLastRunSHA Option[gitdomain.SHA], args BranchProgramArgs) {
 	switch args.Config.BranchType(branch) {
 	case configdomain.BranchTypeFeatureBranch:
-		syncDeletedFeatureBranchProgram(prog, branch, originalParentName, originalParentSHA, args)
+		syncDeletedFeatureBranchProgram(prog, branch, originalParentName, originalParentSHA, parentLastRunSHA, args)
 	case
 		configdomain.BranchTypePerennialBranch,
 		configdomain.BranchTypeMainBranch,
@@ -26,7 +26,7 @@ func deletedBranchProgram(prog Mutable[program.Program], branch gitdomain.LocalB
 
 // syncDeletedFeatureBranchProgram syncs a feare branch whose remote has been deleted.
 // The parent branch must have been fully synced before calling this function.
-func syncDeletedFeatureBranchProgram(prog Mutable[program.Program], branch gitdomain.LocalBranchName, originalParentName Option[gitdomain.LocalBranchName], originalParentSHA Option[gitdomain.SHA], args BranchProgramArgs) {
+func syncDeletedFeatureBranchProgram(prog Mutable[program.Program], branch gitdomain.LocalBranchName, originalParentName Option[gitdomain.LocalBranchName], originalParentSHA, parentLastRunSHA Option[gitdomain.SHA], args BranchProgramArgs) {
 	var syncStatus gitdomain.SyncStatus
 	if preFetchBranchInfo, has := args.PrefetchBranchInfos.FindByLocalName(branch).Get(); has {
 		syncStatus = preFetchBranchInfo.SyncStatus
@@ -52,6 +52,7 @@ func syncDeletedFeatureBranchProgram(prog Mutable[program.Program], branch gitdo
 			branch:             branch,
 			originalParentName: originalParentName,
 			originalParentSHA:  originalParentSHA,
+			previousParentSHA:  parentLastRunSHA,
 			program:            prog,
 			syncStrategy:       args.Config.NormalConfig.SyncFeatureStrategy,
 		})
diff --git a/internal/cmd/sync/sync_feature_branch.go b/internal/cmd/sync/sync_feature_branch.go
index 47a835ebd..92055d441 100644
--- a/internal/cmd/sync/sync_feature_branch.go
+++ b/internal/cmd/sync/sync_feature_branch.go
@@ -14,6 +14,7 @@ func FeatureBranchProgram(syncStrategy configdomain.SyncStrategy, args featureBr
 	if trackingBranch, hasTrackingBranch := args.trackingBranchName.Get(); hasTrackingBranch {
 		FeatureTrackingBranchProgram(trackingBranch, syncStrategy, FeatureTrackingArgs{
 			FirstCommitMessage: args.firstCommitMessage,
+			LastRunParentSHA:   args.parentLastRunSHA,
 			LocalName:          args.localName,
 			Offline:            args.offline,
 			Program:            args.program,
@@ -31,6 +32,7 @@ type featureBranchArgs struct {
 	offline            configdomain.Offline              // whether offline mode is enabled
 	originalParentName Option[gitdomain.LocalBranchName] // the parent when Git Town started
 	originalParentSHA  Option[gitdomain.SHA]             // the parent when Git Town started
+	parentLastRunSHA   Option[gitdomain.SHA]             // the parent at the end of the last Git Town command
 	program            Mutable[program.Program]          // the program to update
 	prune              configdomain.Prune
 	pushBranches       configdomain.PushBranches
@@ -50,7 +52,8 @@ func syncFeatureParentBranch(syncStrategy configdomain.SyncStrategy, args featur
 	case configdomain.SyncStrategyRebase:
 		args.program.Value.Add(
 			&opcodes.RebaseParentIfNeeded{
-				Branch: args.localName,
+				Branch:      args.localName,
+				PreviousSHA: args.parentLastRunSHA,
 			},
 		)
 	case configdomain.SyncStrategyCompress:
@@ -92,7 +95,8 @@ func FeatureTrackingBranchProgram(trackingBranch gitdomain.RemoteBranchName, syn
 					PushBranches: args.PushBranches,
 				},
 				&opcodes.RebaseParentIfNeeded{
-					Branch: args.LocalName,
+					Branch:      args.LocalName,
+					PreviousSHA: args.LastRunParentSHA,
 				},
 				&opcodes.PushCurrentBranchForceIfNeeded{
 					ForceIfIncludes: true,
@@ -108,6 +112,7 @@ func FeatureTrackingBranchProgram(trackingBranch gitdomain.RemoteBranchName, syn
 
 type FeatureTrackingArgs struct {
 	FirstCommitMessage Option[gitdomain.CommitMessage]
+	LastRunParentSHA   Option[gitdomain.SHA]
 	LocalName          gitdomain.LocalBranchName
 	Offline            configdomain.Offline     // whether offline mode is enabled
 	Program            Mutable[program.Program] // the program to update
diff --git a/internal/cmd/undo.go b/internal/cmd/undo.go
index 2f5e29891..6f8234b2a 100644
--- a/internal/cmd/undo.go
+++ b/internal/cmd/undo.go
@@ -105,7 +105,7 @@ func determineUndoData(repo execute.OpenRepoResult, verbose configdomain.Verbose
 	if err != nil {
 		return data, false, err
 	}
-	branchesSnapshot, stashSize, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
+	branchesSnapshot, stashSize, _, exit, err := execute.LoadRepoSnapshot(execute.LoadRepoSnapshotArgs{
 		Backend:               repo.Backend,
 		CommandsCounter:       repo.CommandsCounter,
 		ConfigSnapshot:        repo.ConfigSnapshot,
diff --git a/internal/execute/load_repo_snapshot.go b/internal/execute/load_repo_snapshot.go
index 1ca8a8b9d..67f238e6b 100644
--- a/internal/execute/load_repo_snapshot.go
+++ b/internal/execute/load_repo_snapshot.go
@@ -16,10 +16,17 @@ import (
 )
 
 // LoadRepoSnapshot loads the initial snapshot of the Git repo.
-func LoadRepoSnapshot(args LoadRepoSnapshotArgs) (gitdomain.BranchesSnapshot, gitdomain.StashSize, bool, error) {
+func LoadRepoSnapshot(args LoadRepoSnapshotArgs) (gitdomain.BranchesSnapshot, gitdomain.StashSize, Option[gitdomain.BranchInfos], bool, error) {
 	runStateOpt, err := statefile.Load(args.RootDir)
 	if err != nil {
-		return gitdomain.EmptyBranchesSnapshot(), 0, false, err
+		return gitdomain.EmptyBranchesSnapshot(), 0, None[gitdomain.BranchInfos](), false, err
+	}
+	previousBranchInfos := None[gitdomain.BranchInfos]()
+	if runstate, hasRunstate := runStateOpt.Get(); hasRunstate {
+		if endSnapshot, hasEndSnapshot := runstate.EndBranchesSnapshot.Get(); hasEndSnapshot {
+			runstate.BranchInfosLastRun = Some(endSnapshot.Branches)
+		}
+		previousBranchInfos = runstate.BranchInfosLastRun
 	}
 	if args.HandleUnfinishedState {
 		exit, err := validate.HandleUnfinishedState(validate.UnfinishedStateArgs{
@@ -39,34 +46,34 @@ func LoadRepoSnapshot(args LoadRepoSnapshotArgs) (gitdomain.BranchesSnapshot, gi
 			Verbose:           args.Verbose,
 		})
 		if err != nil || exit {
-			return gitdomain.EmptyBranchesSnapshot(), 0, exit, err
+			return gitdomain.EmptyBranchesSnapshot(), 0, previousBranchInfos, exit, err
 		}
 	}
 	if args.ValidateNoOpenChanges {
 		err = validate.NoOpenChanges(args.RepoStatus.OpenChanges)
 		if err != nil {
-			return gitdomain.EmptyBranchesSnapshot(), 0, false, err
+			return gitdomain.EmptyBranchesSnapshot(), 0, previousBranchInfos, false, err
 		}
 	}
 	if args.Fetch {
 		var remotes gitdomain.Remotes
 		remotes, err := args.Git.Remotes(args.Repo.Backend)
 		if err != nil {
-			return gitdomain.EmptyBranchesSnapshot(), 0, false, err
+			return gitdomain.EmptyBranchesSnapshot(), 0, previousBranchInfos, false, err
 		}
 		if remotes.HasRemote(args.UnvalidatedConfig.NormalConfig.DevRemote) && args.Repo.IsOffline.IsFalse() {
 			err = args.Git.Fetch(args.Frontend, args.UnvalidatedConfig.NormalConfig.SyncTags)
 			if err != nil {
-				return gitdomain.EmptyBranchesSnapshot(), 0, false, err
+				return gitdomain.EmptyBranchesSnapshot(), 0, previousBranchInfos, false, err
 			}
 		}
 	}
 	stashSize, err := args.Repo.Git.StashSize(args.Repo.Backend)
 	if err != nil {
-		return gitdomain.EmptyBranchesSnapshot(), stashSize, false, err
+		return gitdomain.EmptyBranchesSnapshot(), stashSize, previousBranchInfos, false, err
 	}
 	branchesSnapshot, err := args.Repo.Git.BranchesSnapshot(args.Repo.Backend)
-	return branchesSnapshot, stashSize, false, err
+	return branchesSnapshot, stashSize, previousBranchInfos, false, err
 }
 
 type LoadRepoSnapshotArgs struct {
diff --git a/internal/git/gitdomain/branch_info.go b/internal/git/gitdomain/branch_info.go
index bceb41435..e2413cdfb 100644
--- a/internal/git/gitdomain/branch_info.go
+++ b/internal/git/gitdomain/branch_info.go
@@ -41,6 +41,11 @@ func (self BranchInfo) GetLocalOrRemoteName() BranchName {
 	panic("BranchInfo has neither a local nor remote name")
 }
 
+func (self BranchInfo) GetLocalOrRemoteSHA() SHA {
+	// this can never panic because there is always either a local or remote SHA
+	return self.LocalSHA.GetOrElse(self.RemoteSHA.GetOrPanic())
+}
+
 // provides both the name and SHA of the remote branch
 func (self BranchInfo) GetRemoteBranch() (bool, RemoteBranchName, SHA) {
 	name, hasName := self.RemoteName.Get()
diff --git a/internal/vm/interpreter/config/finished.go b/internal/vm/interpreter/config/finished.go
index 14f969dc1..964a038f8 100644
--- a/internal/vm/interpreter/config/finished.go
+++ b/internal/vm/interpreter/config/finished.go
@@ -49,6 +49,7 @@ func Finished(args FinishedArgs) error {
 		EndConfigSnapshot:        Some(configSnapshot),
 		EndStashSize:             None[gitdomain.StashSize](),
 		FinalUndoProgram:         program.Program{},
+		BranchInfosLastRun:       None[gitdomain.BranchInfos](),
 		RunProgram:               program.Program{},
 		TouchedBranches:          args.TouchedBranches,
 		UndoablePerennialCommits: gitdomain.SHAs{},
diff --git a/internal/vm/opcodes/rebase_onto_keep_deleted.go b/internal/vm/opcodes/rebase_onto_keep_deleted.go
index 041a8df3f..f95d556d1 100644
--- a/internal/vm/opcodes/rebase_onto_keep_deleted.go
+++ b/internal/vm/opcodes/rebase_onto_keep_deleted.go
@@ -10,7 +10,7 @@ import (
 
 // rebases the current branch against the target branch while executing "git town swap", while moving the target branch onto the Onto branch.
 type RebaseOntoKeepDeleted struct {
-	BranchToRebaseOnto      gitdomain.LocalBranchName
+	BranchToRebaseOnto      gitdomain.BranchName
 	CommitsToRemove         gitdomain.Location
 	Upstream                Option[gitdomain.LocalBranchName]
 	undeclaredOpcodeMethods `exhaustruct:"optional"`
@@ -43,7 +43,10 @@ func (self *RebaseOntoKeepDeleted) Run(args shared.RunArgs) error {
 				_ = args.Git.StageFiles(args.Frontend, baseChange.FilePath)
 			}
 		}
-		_ = args.Git.ContinueRebase(args.Frontend)
+		err = args.Git.ContinueRebase(args.Frontend)
+		if err != nil {
+			return fmt.Errorf("cannot continue rebase: %w", err)
+		}
 	}
 	return nil
 }
diff --git a/internal/vm/opcodes/rebase_parent_if_needed.go b/internal/vm/opcodes/rebase_parent_if_needed.go
index db50eaf69..3713650a1 100644
--- a/internal/vm/opcodes/rebase_parent_if_needed.go
+++ b/internal/vm/opcodes/rebase_parent_if_needed.go
@@ -4,10 +4,12 @@ import (
 	"github.com/git-town/git-town/v19/internal/git/gitdomain"
 	"github.com/git-town/git-town/v19/internal/messages"
 	"github.com/git-town/git-town/v19/internal/vm/shared"
+	. "github.com/git-town/git-town/v19/pkg/prelude"
 )
 
 type RebaseParentIfNeeded struct {
 	Branch                  gitdomain.LocalBranchName
+	PreviousSHA             Option[gitdomain.SHA]
 	undeclaredOpcodeMethods `exhaustruct:"optional"`
 }
 
@@ -40,9 +42,19 @@ func (self *RebaseParentIfNeeded) Run(args shared.RunArgs) error {
 		} else {
 			branchToRebase = parent.BranchName()
 		}
-		program = append(program, &RebaseBranch{
-			Branch: branchToRebase,
-		})
+		var opcode shared.Opcode
+		if previousParentSHA, hasPreviousParentSHA := self.PreviousSHA.Get(); hasPreviousParentSHA {
+			opcode = &RebaseOntoKeepDeleted{
+				BranchToRebaseOnto: branchToRebase,
+				CommitsToRemove:    previousParentSHA.Location(),
+				Upstream:           None[gitdomain.LocalBranchName](),
+			}
+		} else {
+			opcode = &RebaseBranch{
+				Branch: branchToRebase,
+			}
+		}
+		program = append(program, opcode)
 		break
 	}
 	args.PrependOpcodes(program...)
diff --git a/internal/vm/runstate/runstate.go b/internal/vm/runstate/runstate.go
index e2e97873c..521b96d37 100644
--- a/internal/vm/runstate/runstate.go
+++ b/internal/vm/runstate/runstate.go
@@ -22,6 +22,7 @@ type RunState struct {
 	BeginBranchesSnapshot    gitdomain.BranchesSnapshot                 // snapshot of the Git branches before the Git Town command that this RunState is for ran
 	BeginConfigSnapshot      undoconfig.ConfigSnapshot                  // snapshot of the Git configuration before the Git Town command that this RunState is for ran
 	BeginStashSize           gitdomain.StashSize                        // size of the Git stash before the Git Town command that this RunState is for ran
+	BranchInfosLastRun       Option[gitdomain.BranchInfos]              // branch infos when the last Git Town command ended
 	Command                  string                                     // name of the Git Town command that this RunState is for
 	DryRun                   configdomain.DryRun                        // whether the Git Town command that this RunState is for operated in dry-run mode
 	EndBranchesSnapshot      Option[gitdomain.BranchesSnapshot]         // snapshot of the Git branches after the Git Town command that this RunState is for ran



Test patch:
diff --git a/internal/vm/runstate/runstate_test.go b/internal/vm/runstate/runstate_test.go
index f88ba9103..1968abf6d 100644
--- a/internal/vm/runstate/runstate_test.go
+++ b/internal/vm/runstate/runstate_test.go
@@ -19,6 +19,15 @@ func TestRunState(t *testing.T) {
 	t.Run("Marshal and Unmarshal", func(t *testing.T) {
 		t.Parallel()
 		runState := &runstate.RunState{
+			BranchInfosLastRun: Some(gitdomain.BranchInfos{
+				{
+					LocalName:  Some(gitdomain.NewLocalBranchName("branch")),
+					LocalSHA:   Some(gitdomain.NewSHA("111111")),
+					RemoteName: Some(gitdomain.NewRemoteBranchName("origin/branch")),
+					RemoteSHA:  Some(gitdomain.NewSHA("222222")),
+					SyncStatus: gitdomain.SyncStatusAhead,
+				},
+			}),
 			Command: "sync",
 			DryRun:  true,
 			AbortProgram: program.Program{
@@ -85,6 +94,15 @@ func TestRunState(t *testing.T) {
     "Local": {}
   },
   "BeginStashSize": 0,
+  "BranchInfosLastRun": [
+    {
+      "LocalName": "branch",
+      "LocalSHA": "111111",
+      "RemoteName": "origin/branch",
+      "RemoteSHA": "222222",
+      "SyncStatus": "ahead"
+    }
+  ],
   "Command": "sync",
   "DryRun": true,
   "EndBranchesSnapshot": {
diff --git a/internal/vm/statefile/save_load_test.go b/internal/vm/statefile/save_load_test.go
index 254119ebb..d34044782 100644
--- a/internal/vm/statefile/save_load_test.go
+++ b/internal/vm/statefile/save_load_test.go
@@ -40,6 +40,7 @@ func TestLoadSave(t *testing.T) {
 			BeginBranchesSnapshot: gitdomain.EmptyBranchesSnapshot(),
 			BeginConfigSnapshot:   undoconfig.EmptyConfigSnapshot(),
 			BeginStashSize:        0,
+			BranchInfosLastRun:    None[gitdomain.BranchInfos](),
 			Command:               "command",
 			DryRun:                true,
 			EndBranchesSnapshot:   None[gitdomain.BranchesSnapshot](),
@@ -119,7 +120,7 @@ func TestLoadSave(t *testing.T) {
 				&opcodes.RebaseContinueIfNeeded{},
 				&opcodes.RebaseOntoKeepDeleted{BranchToRebaseOnto: "branch-2", CommitsToRemove: "branch-1"},
 				&opcodes.RebaseOntoRemoveDeleted{BranchToRebaseOnto: "branch-2", CommitsToRemove: "branch-1", Upstream: Some(gitdomain.NewLocalBranchName("upstream"))},
-				&opcodes.RebaseParentIfNeeded{Branch: "branch"},
+				&opcodes.RebaseParentIfNeeded{Branch: "branch", PreviousSHA: Some(gitdomain.SHA("123456"))},
 				&opcodes.RebaseTrackingBranch{RemoteBranch: "origin/branch", PushBranches: true},
 				&opcodes.RegisterUndoablePerennialCommit{Parent: "parent"},
 				&opcodes.SnapshotInitialUpdateLocalSHA{Branch: "branch", SHA: "111111"},
@@ -152,6 +153,7 @@ func TestLoadSave(t *testing.T) {
     "Local": {}
   },
   "BeginStashSize": 0,
+  "BranchInfosLastRun": null,
   "Command": "command",
   "DryRun": true,
   "EndBranchesSnapshot": null,
@@ -630,7 +632,8 @@ func TestLoadSave(t *testing.T) {
     },
     {
       "data": {
-        "Branch": "branch"
+        "Branch": "branch",
+        "PreviousSHA": "123456"
       },
       "type": "RebaseParentIfNeeded"
     },