Thread (11 messages) 11 messages, 4 authors, 2026-02-18

Re: [Bug] Git subtree regression

From: <hidden>
Date: 2026-01-04 14:28:08

---

Ahh, yes. It seems you also need to add `clock` as a remote and fetch it:
$ git clone git@github.com:athena-framework/athena.git
$ cd athena
$ git remote add clock git@github.com:athena-framework/clock.git
$ git fetch clock
$ git subtree split --prefix="src/components/clock"
0efb3d9858e3bfee65165508aeeacc50417c9a99
I wasn't able to use _exactly_ 2.43.7, but I was able to use 2.43.0 which would still be before that other change.
It also produced the expected commit hash, unlike 2.52.0.
It, also was significantly faster, took ~9s vs 2.51.1 which was ~26s.
2.52.0 was better at ~14s, but of course produces the wrong hash.

This reproduces the issue quite well, and what the root cause likely.
It does seem one component was added differently, as a non-merge commit, which seems break things.
Looking at the Athena monorepo, this can somewhat be confirmed via https://github.com/athena-framework/athena/commits/master/?after=ee21a41e9dfc969e759b532d45c0c0faa21876d6+0.
How the first two commits show up as verified, unlike the other times when I normally do `git subtree add --squash` and push directly to main, they show up as unverified.
#!/bin/bash
#
# THE BUG:
# When a commit's direct parent is a squash commit for a DIFFERENT subtree,
# and that squash commit's ancestry includes OUR subtree's squash commit,
# the split breaks.
#
# Old code: `git log -1 --grep` searches ancestry, finds our marker → don't ignore
# New code: only checks parent's own trailers → ignores → breaks parent chain
#
# This pattern occurs when subtree squash commits are cherry-picked or rebased
# into a linear history (instead of the normal merge structure).

set -e

KEEP_TMPDIR="${KEEP_TMPDIR:-}"

TMPDIR=$(mktemp -d)
echo "Working directory: $TMPDIR"

cleanup() {
    if [ -n "$KEEP_TMPDIR" ]; then
        echo "Preserving temp directory: $TMPDIR"
    else
        rm -rf "$TMPDIR"
    fi
}
trap cleanup EXIT

create_repo() {
    local repo="$1"
    git init -b main "$repo"
    git -C "$repo" config user.email "test@test.com"
    git -C "$repo" config user.name "Test User"
    git -C "$repo" config log.date relative
}

create_commit() {
    local repo="$1"
    local name="$2"
    (
        cd "$repo"
        mkdir -p "$(dirname "$name")"
        echo "$name" > "$name"
        git add "$name"
        git commit -m "$name"
    )
}

cd "$TMPDIR"

echo "=== Creating repositories ==="

create_repo monorepo
create_repo subA
create_repo subB

echo "=== Creating upstream commits ==="

create_commit subA subA1
create_commit subA subA2
create_commit subB subB1
create_commit subB subB2

echo "=== Setting up monorepo with linear squash structure ==="

# Initial commit
create_commit monorepo main1

# Add subA with --squash (normal way - creates merge)
git -C monorepo fetch ../subA HEAD
git -C monorepo subtree add --prefix=subA --squash FETCH_HEAD

# Make a change in subA
create_commit monorepo subA/change1

# Now we simulate cherry-picking JUST the squash commit for subB
# (This is what seems to have happened in the athena repo)
# First, get subB ready
git -C monorepo fetch ../subB HEAD

# Create a LINEAR squash commit for subB (simulating cherry-pick of just the squash commit)
# This is the key pattern that triggers the bug - a squash commit as a regular linear commit
(
    cd monorepo
    mkdir -p subB
    git -C ../subB archive HEAD | tar -x -C subB
    git add subB
    # Create a squash-style commit with subtree trailers but as a LINEAR commit
    # Trailers must be in the last paragraph, separated by blank line
    subB_short=$(git -C ../subB rev-parse --short HEAD)
    subB_full=$(git -C ../subB rev-parse HEAD)
    git commit -F - <<EOF
Squashed 'subB/' content from commit $subB_short
git-subtree-dir: subB
git-subtree-split: $subB_full
EOF
)

echo ""
echo "=== Key structure: subB squash is a LINEAR commit, not a merge ==="
git -C monorepo log -1 --format='%H %s' HEAD
echo "Parent count: $(git -C monorepo cat-file -p HEAD | grep -c '^parent')"

# Now make a commit that touches subA
# This commit's parent is the subB squash commit (linear)
create_commit monorepo subA/change2

echo ""
echo "=== Repository structure ==="
git -C monorepo log --oneline --graph

# Verify the squash commit's ancestry includes subA's marker
subB_squash=$(git -C monorepo rev-parse HEAD^)
echo ""
echo "=== Checking ancestry of subB squash commit ($subB_squash) ==="
echo "Looking for subA marker in ancestry..."
if git -C monorepo log -1 --grep="git-subtree-dir: subA" "$subB_squash" --oneline 2>/dev/null; then
    echo "  FOUND - old code would search this and NOT ignore"
else
    echo "  NOT FOUND - test setup may be incomplete"
fi

echo ""
echo "=== Running subtree split on subA ==="

split_hash=$(git -C monorepo subtree split --prefix=subA 2>/dev/null)
echo "Split hash: $split_hash"

split_count=$(git -C monorepo rev-list --count "$split_hash")
echo "Commits in split: $split_count"

echo ""
echo "=== Split history ==="
git -C monorepo log --oneline "$split_hash"

echo ""
echo "=== Result ==="

# Expected: 4 commits (2 upstream + 2 local changes)
if [ "$split_count" -ge 4 ]; then
    echo "PASS: Split produced connected history ($split_count commits)"
    exit 0
else
    echo "FAIL: Split produced disconnected history (only $split_count commits, expected >= 4)"
    exit 1
fi
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help