"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Yann Lorwyn <r00tth3w0r1d@gmail.com>
Subject:
Got publishes local unversioned bytes during merge and rebase
To:
gameoftrees@openbsd.org
Date:
Fri, 18 Sep 2026 10:40:23 +0800

Download raw body.

Thread
Hello,

I am writing to report a potential bug int Got 0.128.

Got 0.128 can publish a local unversioned path when an incoming merge or
rebase adds the same pathname. The operation succeeds and can leave a clean
work tree, but the resulting tree contains the local file, ignored file, or
symlink rather than the incoming entry. The ignored-file case can publish
local generated or sensitive bytes without an explicit add or conflict
resolution.

The attached report contains the source analysis and Git-core comparison.
attachments.zip contains a shell reproducer that starts from fresh Got
repositories and builds the pinned 0.128 source.

Best regards,
Yann
# Got publishes local unversioned bytes during merge and rebase

## Summary

Got 0.128 can publish a local unversioned path when an incoming merge or
rebase adds the same path. The operation reports success, advances or rewrites
the relevant ref, and leaves the work tree clean, but the resulting tree
contains the local file, ignored file, or symlink rather than the incoming
tree entry.

The strongest case uses an ignored file. `got status` is empty before and after
the merge, while the merge commit contains the ignored file's local bytes. The
behavior is present in the 0.128 source at commit
[`d24a860213932756025038bcc8b9c1ed820778e5`](https://github.com/gameoftrees/got-portable/commit/d24a860213932756025038bcc8b9c1ed820778e5).

## Impact

A user or automation process can merge a remote branch into a work tree that
already contains generated, ignored, or otherwise unversioned data. If the
remote branch adds that pathname, Got can advance the branch to a merge commit
containing bytes that came only from the local work tree. Pushing that ref can
then publish a local deployment file, credential, generated source, or policy
file as repository history without an explicit add or conflict resolution.

The reproducer demonstrates the full state transition: the incoming branch
contains `target\n`, the local path contains `local-ignored\n`, the merge returns
zero, ordinary status remains empty, and the merge tree contains
`local-ignored\n`. The regular-file and symlink variants show the same
publication issue for other path kinds. Rebase reaches the same file-install
and commit path and records the local bytes in the rewritten commit.

## Reproduction

The attachment follows Got's upstream reporting convention: the shell runner
starts from fresh repositories, builds the pinned release, and records the
command output, return codes, refs, tree entries, and bytes. The target image
contains no Git executable. A separate control runs Git 2.56.0-rc1.

From the directory containing this report and `attachments.zip`:

```sh
unzip -q attachments.zip
cd attachments
docker build --memory=4g \
  --build-arg GOT_COMMIT=d24a860213932756025038bcc8b9c1ed820778e5 \
  --build-arg GOT_SHA256=036746d0117fb03622d3bfa4865651afc7403b630f5a135c21afe925ac3e8f02 \
  -t game-of-trees-merge:0.128 .
docker build --memory=4g \
  --build-arg GIT_COMMIT=12cb6293d6288865c1a133cf22accbaf99d13eb6 \
  --build-arg GIT_SOURCE_SHA256=36309cfd13b1589df75805ee1e6ec7551c2b09b75dc57a26cc222d7950826ffd \
  -f git-core-control.Dockerfile \
  -t git-research/git-core:main-12cb6293d628 .
GOT_IMAGE=game-of-trees-merge:0.128 ./reproduce.sh result-got
./native-control.sh result-git-core
```

The standalone instructions and exact source identity are in
[`attachments/README.md`](attachments/README.md) and
[`attachments/source-identity.txt`](attachments/source-identity.txt).

## Observed behavior

The merge fixture has a common base, a `main` branch, and a `feature` branch.
The feature branch adds `collision.txt` with `target\n`; on `main`, the runner
creates the corresponding local path before running `got merge feature`.

The current release produced these results:

| Case | Got result | Published result |
| --- | --- | --- |
| regular untracked file | success | local untracked bytes |
| ignored file | success; ordinary status empty | local ignored bytes |
| symlink | success; ordinary status empty | mode 120000 symlink entry |
| rebase with untracked file | success | local bytes in rewritten commit |

The Git-core control rejects the regular-file and symlink collisions before
moving `HEAD`. For the ignored-file control, Git completes the merge but writes
the incoming `target\n` bytes into the work tree and merge tree. The compact
captured summaries are in
[`attachments/evidence/current-main-summary.log`](attachments/evidence/current-main-summary.log)
and
[`attachments/evidence/git-core-control-summary.log`](attachments/evidence/git-core-control-summary.log).

## Source analysis

The target-only merge path in
[`lib/worktree.c#L3323-L3348`](https://github.com/gameoftrees/got-portable/blob/d24a860213932756025038bcc8b9c1ed820778e5/lib/worktree.c#L3323-L3348)
calls `add_file()` with `path_is_unversioned=1`. The file installation code
treats `EEXIST` as a request to preserve the existing path at
[`lib/worktree.c#L1458-L1465`](https://github.com/gameoftrees/got-portable/blob/d24a860213932756025038bcc8b9c1ed820778e5/lib/worktree.c#L1458-L1465).
That preservation is then followed by the `ie == NULL` branch in
[`lib/worktree.c#L3001-L3013`](https://github.com/gameoftrees/got-portable/blob/d24a860213932756025038bcc8b9c1ed820778e5/lib/worktree.c#L3001-L3013),
which inserts a file-index entry for the existing on-disk path.

The commit path collects that entry and serializes the bytes currently on disk
at [`lib/worktree.c#L8918-L8928`](https://github.com/gameoftrees/got-portable/blob/d24a860213932756025038bcc8b9c1ed820778e5/lib/worktree.c#L8918-L8928).
The rebase entry point invokes the same `merge_files()` machinery through
[`lib/worktree.c#L7369-L7392`](https://github.com/gameoftrees/got-portable/blob/d24a860213932756025038bcc8b9c1ed820778e5/lib/worktree.c#L7369-L7392),
so the rewritten commit has the same source of bytes.

Got's 0.128 documentation describes an unversioned destination as a merge
condition that should interrupt the operation before incomplete changes can be
committed. The documented status and merge contract is at
[`got/got.1#L3501-L3509`](https://github.com/gameoftrees/got-portable/blob/d24a860213932756025038bcc8b9c1ed820778e5/got/got.1#L3501-L3509).
Git core's corresponding untracked-path guard is at
[`unpack-trees.c#L2475-L2544`](https://github.com/git/git/blob/12cb6293d6288865c1a133cf22accbaf99d13eb6/unpack-trees.c#L2475-L2544).

## Expected behavior

When a merge or rebase would write an incoming path over a local unversioned
path, Got should interrupt before publication and preserve the local path for
the user to resolve. If a merge policy explicitly chooses the incoming entry,
the committed tree should contain the incoming bytes rather than silently
serializing the preserved local path.