From: Yann Lorwyn Subject: Got publishes local unversioned bytes during merge and rebase To: gameoftrees@openbsd.org Date: Fri, 18 Sep 2026 10:40:23 +0800 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.