# Git Said It Succeeded. The State Said Otherwise.

> Source: <https://dev.to/kenielzep97/git-said-it-succeeded-the-state-said-otherwise-34ka>
> Published: 2026-08-30 01:44:59+00:00

I keep finding git behaviours the same way: set up the case, run the command, then check the state I actually cared about instead of trusting the success message. Guides pair `rerere.enabled`

with `rerere.autoupdate`

when the second one removes the checkpoint the first one earns you. GitHub reports mergeable while the reviewer is still reading stale bytes. `--autosquash`

prints `Successfully rebased and updated`

and leaves a `fixup!`

commit sitting in history.

Copy the config. The reasoning is why you would leave one of them off.

This started as a comment on [Sylwia Laskowska's git list](https://dev.to/sylwia-lask/10-git-commands-youll-wish-you-knew-earlier-4fcp). She asked me to turn it into a post. The first four items are from ordinary rebasing. The rest I measured this week on a stacked-PR repo.

`rerere`

does not learn from `git add`

alone

```
git config rerere.enabled true
```

`rerere`

is reuse recorded resolution. It remembers how you resolved a conflict and replays it when the same one appears. On a long rebase that is resolving something once instead of five times.

```
CONFLICT (content): Merge conflict in f.txt
Staged 'f.txt' using previous resolution.
```

**Resolve, git add, then abort immediately, and it recorded nothing.** The next attempt conflicts identically. That bit me twice before I understood it.

Two things are going on and my test could not separate them, so here are both. `git add`

does not record the postimage — `git commit`

, `git rebase --continue`

, or an explicit `git rerere`

do. And `git rebase --abort`

runs `rerere clear`

, which wipes the in-flight metadata anyway.

What I could measure, on git 2.39.5: resolve + `git add`

+ abort, and the next attempt conflicts identically. Resolve + `git add`

+ explicit `git rerere`

+ abort, and it replays — **before the rebase goes anywhere.** So "it learns when the rebase finishes" is the wrong model either way.

I taught it a resolution in `one.txt`

. Then I created the same conflicting hunk in a completely different file, `two.txt`

, on a different branch. **It replayed the one.txt answer into two.txt.**

On a rebase that is usually what you want. It is not always what you want. If the right answer differs between those two places, it will quietly give you the first one.

`rerere.autoupdate`

I kept seeing them paired. I checked the index in both modes:

``` php
autoupdate off  ->  UU two.txt    still conflicted, you must read it and git add
autoupdate on   ->  M  two.txt    fully staged, nothing asks you to look
```

**Autoupdate off is the checkpoint that catches a replay you did not want.** Leave it off and `rerere`

still writes the resolution into the file. You just have to look at it before adding.

If it got it wrong:

```
git rerere forget <path>   # drop what it learned
git checkout -m <path>     # bring the conflict markers back
```

`--autosquash`

can succeed and still leave the fixup in history

```
git commit --fixup abc1234
git rebase -i --autosquash abc1234~1
```

First marks it. Second reorders and squashes it. The todo list opens already correct.

Two sharp edges.

Bare `git rebase -i --autosquash`

with no range **fails outright** on a branch with no upstream, so you need the base.

And if the range does not reach far enough back to include the target, **it does not warn you.** It prints `Successfully rebased and updated`

and leaves the `fixup!`

commit sitting in your history.

```
git log --oneline | grep fixup!
```

Worth running after. **The rebase step rewrites history** — `git commit --fixup`

just adds a commit — so the usual rule about rebasing shared commits applies to the second line, not the first.

This one I did not set up on purpose. I had a stack: `main ← #3 ← #4 ← #2`

. After #3 and #4 merged, I retargeted #2 onto `main`

. GitHub immediately showed `base: main`

, mergeable, clean.

**The new base commit was still not an ancestor of my head.**

I measured it. Head `b1cf109`

versus `main`

at `e6dc136`

: GitHub compare status `diverged`

, **behind by 8**. After `git merge origin/main`

(commit `b4ac271`

): **behind by 0**.

```
git fetch origin main
git merge-base --is-ancestor origin/main HEAD && echo "head contains main" || echo "STALE — merge first"
```

Exit 1 is the stale case. GitHub's "Change base" button is not "Update branch." Change base retargets what the PR is compared against. Update branch, or `git merge origin/main`

, is what actually brings the commits in.

Those SHAs are from a private stack, so this one is a field note rather than something you can clone and rerun. The check itself is two lines and works anywhere.

That head was what the automated reviewer had to work from. It carried an old implementation I had already replaced on `main`

, and I burned a cycle chasing a finding about code that only still existed on that stale branch — before I thought to check whether the branch contained what I thought it contained.

The fix is one merge, then the ancestor check. `MERGEABLE`

does not mean "this head contains current main."

`main`

A link to `/blob/main/path/file`

shows whatever `main`

says today, and 404s outright if the path later moves. A link to `/blob/<sha>/path/file`

is immutable. Both may return 200 right now. Only one of them still means the same thing next month.

I hit this the same week. `main`

still carried an old write-up while the corrected file only existed on a branch. I published the SHA-pinned URL so a reader could open the bytes I was citing, not whatever landed on default later.

If you are citing evidence, cite the sha.

`git worktree`

instead of stashing, if you have untracked files you cannot lose
Stash does not save untracked files unless you remember `-u`

. I had an untracked scratch file I needed to keep through a week of branch hops. Worktrees solved it without the dance: each branch in its own directory, one object store, untracked files stay put.

```
git worktree add -b some/branch /tmp/scratch origin/main
```

When you are done: `git worktree remove`

. If you have ever needed to fix one branch while another is mid-edit, this is the command.

Same disease as the rest of the list, incidentally. `git stash`

reports success and your untracked file is simply not in it.

`--autosquash`

prints success while leaving a `fixup!`

behind. A retargeted PR reports `MERGEABLE`

while its head is eight commits behind the branch it now claims as base. `rerere.autoupdate`

stages the replay cleanly and removes the unmerged path you would otherwise have been forced to look at.

**In each case the message was true and the state was not what I assumed.** And each one needed a different thing checked: history for the leftover `fixup!`

, ancestry for the stale head, the index for `UU`

versus `M`

.

Which is the actual lesson, and it is not "check the index":

**A success message tells you the command completed according to its own contract. It does not tell you the repository now satisfies the condition you actually cared about.** Those are different sentences. Go check the one you cared about.

*Measured on git 2.39.5 locally; the GitHub compare behaviour is as of this week. Items 1–4 are reproducible on any repo in about two minutes. Item 5 is a field note from a private stack.*

*This post exists because Sylwia Laskowska wrote a git list good enough that I went and tested the rebase section against it, and then told me the comment should be its own post. She was right, and I would not have written it otherwise. Go read hers.*
