Fossil Forum

ryo_horii 2 weeks, 1 day ago

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

  1. 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, so bundle export copies B's stored delta as is, and bblob.delta of B is the blobid of A.
  2. bundle import leaves A out of bix because the repository already has it. bundle_import_elements() starts from the entries with bix.delta=0 and follows the deltas from there. B has bix.delta = A, and A is not in bix, 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
stephan 2 weeks ago

fossil bundle import can end with exit code 0 and still leave a file of an imported check-in as a phantom.

Thank you for the interesting failure, reproducer, and concise patch :). A very slight variation of this is now checked in .

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button