Fossil Forum
Post: bundle import leaves a file as a phantom when its delta basis is already in the repository
Problem
fossil bundle import can end with exit code 0 and still leave a file of an imported check-in as a phantom. This happens when the bundle contains an artifact that the receiving repository already has, and another artifact in the bundle is stored as a delta from it. A file that goes back to an earlier content inside the range of the bundle is enough. --standalone does not avoid it.
I found this while restoring a repository from a full backup plus a bundle of the later check-ins: one file in 102 check-ins was missing.
To reproduce
#!/bin/sh
# FOSSIL=/path/to/fossil sh repro.sh
set -e
F=${FOSSIL:-fossil}
USER=tester; export USER
rm -rf bundle_bug; mkdir -p bundle_bug/co; cd bundle_bug
$F init -A tester repo.fossil >/dev/null
cd co; $F open ../repo.fossil >/dev/null
i=1; while [ $i -le 100 ]; do echo "line $i of the test file"; i=$((i+1)); done > f.txt
$F add f.txt >/dev/null
$F commit -m A --no-warnings >/dev/null # version A
cd ..; $F backup -R repo.fossil base.fossil; cd co # base.fossil has A
sed 's/^line 50 /changed line 50 /' f.txt > t; mv t f.txt
$F commit -m B --no-warnings >/dev/null # version B
C2=$($F info | sed -n 's/^checkout: *\([0-9a-f]*\).*/\1/p')
sed 's/^changed line 50 /line 50 /' f.txt > t; mv t f.txt
$F commit -m A-again --no-warnings >/dev/null # back to A: B is now stored as a delta from A
C3=$($F info | sed -n 's/^checkout: *\([0-9a-f]*\).*/\1/p')
cd ..
$F bundle export b.bundle -R repo.fossil --from $C2 --to $C3
$F bundle import b.bundle -R base.fossil --publish
echo "bundle import exit code: $?"
echo "phantoms: $($F sql -R base.fossil 'SELECT count(*) FROM phantom')"
$F cat f.txt -r $C2 -R base.fossil >/dev/null && echo "f.txt of check-in B: OK"
With 2.28 [52445a27f1] and trunk [76742655f0]:
Imported content:
328d85a78e3a6a89 check-in to trunk by tester on 2026-09-28 10:19
bc1382e81ff14d6d check-in to trunk by tester on 2026-09-28 10:19
bundle import exit code: 0
phantoms: 1
missing artifact 41d5a684a7dd2896e5aad4d0d55352761fcfca78af307a29c46509a3444310bf for file f.txt in check-in 328d85a78e3a6a8917c46a1dd1693de052531ac1
The file B (41d5a684…) is in the bundle but is not imported.
Cause
- When f.txt goes back to A,
content_deltify()stores B as a delta from A. Both A and B are used by check-ins in the bundle, sobundle exportcopies B's stored delta as is, andbblob.deltaof B is the blobid of A. bundle importleaves A out ofbixbecause the repository already has it.bundle_import_elements()starts from the entries withbix.delta=0and follows the deltas from there. B hasbix.delta= A, and A is not inbix, so B is never reached. The check-ins are still imported, so B becomes a phantom.
Suggested fix
Start from such entries as well, and take their basis from the repository, in the same way as a delta whose basis is given by hash (the first branch in bundle_import_elements()):
--- src/bundle.c
+++ src/bundle.c
@@ -460,6 +460,17 @@
}else if( pBasis ){
blob_delta_apply(pBasis, &c1, &c2);
blob_reset(&c1);
+ }else if( db_column_type(&q,2)==SQLITE_INTEGER ){
+ /* A delta against another bundle entry that was not imported
+ ** because the repository already has it. Use the repository copy. */
+ Blob basis;
+ rid = db_int(0,"SELECT rid FROM blob WHERE uuid="
+ "(SELECT uuid FROM bblob WHERE blobid=%d)",
+ db_column_int(&q,2));
+ content_get(rid, &basis);
+ blob_delta_apply(&basis, &c1, &c2);
+ blob_reset(&basis);
+ blob_reset(&c1);
}else{
c2 = c1;
}
@@ -614,6 +625,8 @@
" THEN delta ELSE 0 END"
" FROM bblob"
" WHERE NOT EXISTS(SELECT 1 FROM blob WHERE uuid=bblob.uuid AND size>=0);"
+ "UPDATE bix SET delta=0"
+ " WHERE delta>0 AND delta NOT IN (SELECT blobid FROM bix);"
"CREATE TEMP TABLE got(rid INTEGER PRIMARY KEY ON CONFLICT IGNORE);"
);
manifest_crosslink_begin();
The new branch is reached only by the entries changed by the UPDATE; before the patch no entry with an integer bblob.delta had bix.delta=0. The imported content is still checked by hname_verify_hash(), so a wrong basis fails loudly.
Testing (Windows only)
- The script above with trunk + patch: B is imported, no phantom. The same with
bundle export --standalone. - Importing the same bundle a second time, and importing it into a copy of the full repository: exit code 0, nothing changes.
- Five of my own repositories, each restored from a full backup plus one bundle of the later check-ins (9 to 105 check-ins, up to 58,000 files): with the patch, the set of artifacts after the import is exactly the backup plus the bundle, with no phantoms. Without the patch, one repository had the phantom described above. Z