| #!/bin/sh | |
| test_description='connectivity check (transfer.connectivityCheck)' | |
| GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main | |
| export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME | |
| . ./test-lib.sh | |
| test_oid_cache <<-\EOF | |
| missing sha1:0000000000000000000000000000000000000001 | |
| missing sha256:0000000000000000000000000000000000000000000000000000000000000001 | |
| EOF | |
| set_connectivity_check () { | |
| git -C "$1" config transfer.connectivityCheck "$2" | |
| } | |
| # Run a connectivity check via rev-list directly. Uses $mode | |
| # (set by the enclosing for-loop) to choose full or incremental. | |
| check_connected () { | |
| printf '%s\n' "$@" | | |
| git rev-list --connectivity-check="$mode" \ | |
| --objects --stdin --not --all --quiet \ | |
| --exclude-promisor-objects | |
| } | |
| # Run git with a temporary index, leaving the real index untouched. | |
| tmpgit () { | |
| GIT_INDEX_FILE=.git/tmp-idx git "$@" | |
| } | |
| # Create a commit with one file changed, without modifying HEAD, | |
| # index, or worktree. Prints the new commit OID on stdout. | |
| # Usage: commit_with_change <parent> <path> <content> | |
| commit_with_change () { | |
| new_blob=$(echo "$3" | git hash-object -w --stdin) && | |
| tmpgit read-tree "$1" && | |
| tmpgit update-index --replace \ | |
| --cacheinfo "100644,$new_blob,$2" && | |
| new_tree=$(tmpgit write-tree) && | |
| rm -f .git/tmp-idx && | |
| git commit-tree "$new_tree" -p "$1" -m "modify $2" | |
| } | |
| # Check OIDs and optionally verify trace2 counts. | |
| # Usage: check_connected_trace <trace-file> <trees> <blobs> <oid>... | |
| # An empty string for <trees> or <blobs> skips that assertion. | |
| check_connected_trace () { | |
| trace_file=$1 trees=$2 blobs=$3 && | |
| shift 3 && | |
| test_env GIT_TRACE2_EVENT="$(pwd)/$trace_file" \ | |
| check_connected "$@" && | |
| if test "$mode" != incremental | |
| then | |
| return | |
| fi && | |
| if test -n "$trees" | |
| then | |
| test_trace2_data_singular connectivity trees_loaded "$trees" \ | |
| <"$trace_file" | |
| fi && | |
| if test -n "$blobs" | |
| then | |
| test_trace2_data_singular connectivity blobs_checked "$blobs" \ | |
| <"$trace_file" | |
| fi | |
| } | |
| # Shared setup: a repo with several root-level files and nested dirs. | |
| # The unchanged/ subtree (10 dirs x 10 files = 100 blobs, 11 trees) | |
| # acts as a canary: any test asserting small tree/blob counts would | |
| # fail dramatically if incremental accidentally walked into it. | |
| test_expect_success 'setup main repo' ' | |
| git init main-repo && | |
| ( | |
| cd main-repo && | |
| for i in $(test_seq 1 5) | |
| do | |
| echo "file $i" >"file-$i.txt" || return 1 | |
| done && | |
| git add file-*.txt && | |
| git commit -m "initial" && | |
| mkdir -p a/b/c && | |
| echo deep >a/b/c/deep.txt && | |
| echo other >a/other.txt && | |
| git add a/b/c/deep.txt a/other.txt && | |
| git commit -m "add nested dirs" && | |
| for i in $(test_seq 1 10) | |
| do | |
| d="unchanged/dir-$i" && | |
| mkdir -p "$d" && | |
| for j in $(test_seq 1 10) | |
| do | |
| echo "$i $j" >"$d/file-$j.txt" || return 1 | |
| done | |
| done && | |
| git add unchanged/ && | |
| git commit -m "add unchanged canary subtree" | |
| ) | |
| ' | |
| test_expect_success 'setup replacement object repo' ' | |
| git init replace-test && | |
| ( | |
| cd replace-test && | |
| test_commit --no-tag original file.txt && | |
| original=$(git rev-parse HEAD) && | |
| orig_blob=$(git rev-parse HEAD:file.txt) && | |
| # Orphan replacement commit with a different tree | |
| replacement_tree=$(echo replaced | git hash-object -w --stdin | | |
| xargs -I{} git mktree <<-EOF | |
| 100644 blob {} file.txt | |
| EOF | |
| ) && | |
| replacement=$(git commit-tree -m "replacement" \ | |
| "$replacement_tree") && | |
| git replace "$original" "$replacement" && | |
| # Remove the original blob so only the replacement | |
| # tree is complete. | |
| rm .git/objects/$(test_oid_to_path "$orig_blob") && | |
| # Drop branch and HEAD so --not --all does not | |
| # exclude the original commit. | |
| git update-ref -d refs/heads/main && | |
| git update-ref -d HEAD && | |
| echo "$original" >.git/test-oid && | |
| echo "$orig_blob" >.git/test-blob | |
| ) | |
| ' | |
| missing_oid=$(test_oid missing) | |
| original_oid=$(cat replace-test/.git/test-oid) | |
| orig_blob_oid=$(cat replace-test/.git/test-blob) | |
| for mode in full incremental | |
| do | |
| # The full traversal reports a missing tree as "bad tree object". | |
| case "$mode" in | |
| full) | |
| missing_tree="bad tree object" | |
| ;; | |
| incremental) | |
| missing_tree="missing tree object" | |
| ;; | |
| esac | |
| # Corruption detection: craft broken object graphs and verify detection. | |
| # All tests use main-repo without modifying its refs or worktree. | |
| test_expect_success "$mode: rejects commit with missing blob" ' | |
| ( | |
| cd main-repo && | |
| bad_tree=$(printf "100644 blob ${missing_oid}\tfile.txt\n" | | |
| git mktree --missing) && | |
| bad_commit=$(git commit-tree "$bad_tree" -p HEAD -m "bad") && | |
| test_expect_code 128 check_connected "$bad_commit" 2>err && | |
| test_grep "missing blob object.*$missing_oid" err | |
| ) | |
| ' | |
| test_expect_success "$mode: rejects commit with missing subtree" ' | |
| ( | |
| cd main-repo && | |
| bad_tree=$(printf "40000 tree ${missing_oid}\tdir\n" | | |
| git mktree --missing) && | |
| bad_commit=$(git commit-tree "$bad_tree" -p HEAD -m "bad") && | |
| test_expect_code 128 check_connected "$bad_commit" 2>err && | |
| test_grep "$missing_tree $missing_oid" err | |
| ) | |
| ' | |
| test_expect_success "$mode: verifies direct tree tip" ' | |
| ( | |
| cd main-repo && | |
| bad_tree=$(printf "100644 blob ${missing_oid}\tfile.txt\n" | | |
| git mktree --missing) && | |
| test_expect_code 128 check_connected "$bad_tree" 2>err && | |
| test_grep "missing blob object.*$missing_oid" err | |
| ) | |
| ' | |
| test_expect_success "$mode: verifies direct blob tip" ' | |
| ( | |
| cd main-repo && | |
| blob_oid=$(echo "hello" | git hash-object -w --stdin) && | |
| check_connected "$blob_oid" | |
| ) | |
| ' | |
| test_expect_success "$mode: rejects missing direct blob tip" ' | |
| ( | |
| cd main-repo && | |
| test_expect_code 128 check_connected \ | |
| "$missing_oid" 2>err | |
| ) | |
| ' | |
| test_expect_success PERL_TEST_HELPERS \ | |
| "$mode: rejects blob OID reused as tree entry" ' | |
| ( | |
| cd main-repo && | |
| blob_oid=$(git rev-parse HEAD:file-1.txt) && | |
| bin_oid=$(echo "$blob_oid" | hex2oct) && | |
| bad_tree=$(printf "40000 subdir\0$bin_oid" | | |
| git hash-object -t tree -w --stdin) && | |
| bad_commit=$(git commit-tree -p HEAD -m "child" "$bad_tree") && | |
| test_expect_code 128 check_connected "$bad_commit" 2>err && | |
| test_grep "not a tree" err | |
| ) | |
| ' | |
| test_expect_success PERL_TEST_HELPERS \ | |
| "$mode: rejects tree OID reused as blob entry" ' | |
| ( | |
| cd main-repo && | |
| tree_oid=$(git rev-parse HEAD:a) && | |
| bin_oid=$(echo "$tree_oid" | hex2oct) && | |
| bad_tree=$(printf "100644 fakefile\0$bin_oid" | | |
| git hash-object -t tree -w --stdin) && | |
| bad_commit=$(git commit-tree -p HEAD -m "child" "$bad_tree") && | |
| test_expect_code 128 check_connected "$bad_commit" 2>err && | |
| test_grep "not a blob" err | |
| ) | |
| ' | |
| test_expect_success "$mode: checks multiple tips" ' | |
| ( | |
| cd main-repo && | |
| c1=$(commit_with_change HEAD file-1.txt "tip-a") && | |
| c2=$(commit_with_change HEAD file-2.txt "tip-b") && | |
| git tag -a -m "tagged" multi-tag "$c1" && | |
| tag_oid=$(git rev-parse multi-tag) && | |
| git tag -d multi-tag && | |
| check_connected "$c2" "$tag_oid" | |
| ) | |
| ' | |
| # Tree-diff optimization: verify trace2 counts. | |
| test_expect_success "$mode: handles root commit (no parents)" ' | |
| ( | |
| cd main-repo && | |
| blob_oid=$(echo "root-content" | git hash-object -w --stdin) && | |
| tree_oid=$(printf "100644 blob %s\tfile.txt\n" \ | |
| "$blob_oid" | git mktree) && | |
| root_oid=$(git commit-tree "$tree_oid" -m "root") && | |
| # No parent trees, so the full tree is verified. | |
| # 1 tree loaded (root), 1 blob checked. | |
| check_connected_trace trace-root.txt 1 1 "$root_oid" | |
| ) | |
| ' | |
| test_expect_success "$mode: handles single file change" ' | |
| ( | |
| cd main-repo && | |
| oid=$(commit_with_change HEAD file-1.txt "changed") && | |
| # 2 trees loaded (new root + parent root), 1 blob checked. | |
| check_connected_trace trace-flat.txt 2 1 "$oid" | |
| ) | |
| ' | |
| test_expect_success "$mode: handles nested change" ' | |
| ( | |
| cd main-repo && | |
| oid=$(commit_with_change HEAD a/b/c/deep.txt "deep-changed") && | |
| # 4 new trees + 4 parent trees = 8 loaded, 1 blob checked. | |
| check_connected_trace trace-nested.txt 8 1 "$oid" | |
| ) | |
| ' | |
| test_expect_success "$mode: handles change-then-revert" ' | |
| ( | |
| cd main-repo && | |
| c1=$(commit_with_change HEAD file-1.txt "revert-tmp") && | |
| c2=$(commit_with_change "$c1" file-1.txt "file 1") && | |
| c3=$(commit_with_change "$c2" file-1.txt "revert-final") && | |
| # c1: new root + parent root = 2 loads. c2: root matches | |
| # HEAD (already trusted), skipped. c3: new root + parent | |
| # already expanded = 1 load. Total: 3 trees, 2 blobs. | |
| check_connected_trace trace-revert.txt 3 2 "$c3" | |
| ) | |
| ' | |
| test_expect_success "$mode: handles subtree moved to another path" ' | |
| ( | |
| cd main-repo && | |
| moved_tree=$(git ls-tree HEAD | | |
| sed "s/ a$/ moved/" | | |
| git mktree) && | |
| moved=$(git commit-tree "$moved_tree" -p HEAD -m move) && | |
| # New root + parent root scanned, but the moved subtree | |
| # (same OID) is trusted and not descended into. | |
| check_connected_trace trace-move.txt 2 0 "$moved" | |
| ) | |
| ' | |
| test_expect_success "$mode: handles multi-parent merge" ' | |
| ( | |
| cd main-repo && | |
| left=$(commit_with_change HEAD file-1.txt left) && | |
| right=$(commit_with_change HEAD file-2.txt right) && | |
| git update-ref refs/heads/left "$left" && | |
| git update-ref refs/heads/right "$right" && | |
| left_blob=$(git rev-parse "$left:file-1.txt") && | |
| right_blob=$(git rev-parse "$right:file-2.txt") && | |
| tmpgit read-tree HEAD && | |
| tmpgit update-index --replace \ | |
| --cacheinfo "100644,$left_blob,file-1.txt" && | |
| tmpgit update-index --replace \ | |
| --cacheinfo "100644,$right_blob,file-2.txt" && | |
| merge_tree=$(tmpgit write-tree) && | |
| rm -f .git/tmp-idx && | |
| merge=$(git commit-tree "$merge_tree" \ | |
| -p "$left" -p "$right" -m merge) && | |
| # Both parent root trees are scanned as bases, so | |
| # blobs from each parent are trusted without ODB checks. | |
| check_connected_trace trace-merge.txt 3 0 "$merge" && | |
| git update-ref -d refs/heads/left && | |
| git update-ref -d refs/heads/right | |
| ) | |
| ' | |
| test_expect_success "$mode: handles merge with incoming and boundary parents" ' | |
| ( | |
| cd main-repo && | |
| # parent1 is incoming (not a ref), HEAD is boundary. | |
| parent1=$(commit_with_change HEAD file-1.txt merge-inc) && | |
| merge=$(git commit-tree \ | |
| "$(git rev-parse "$parent1^{tree}")" \ | |
| -p "$parent1" -p HEAD -m merge-mixed) && | |
| # parent1 verified first (topo order): 2 trees, 1 blob. | |
| # merge tree = parent1 tree (already trusted): 0 extra. | |
| check_connected_trace trace-merge-inc.txt 2 1 "$merge" | |
| ) | |
| ' | |
| test_expect_success "$mode: handles gitlink entries (submodules)" ' | |
| ( | |
| cd main-repo && | |
| tmpgit read-tree HEAD && | |
| tmpgit update-index --add \ | |
| --cacheinfo "160000,$missing_oid,my-submodule" && | |
| gitlink_tree=$(tmpgit write-tree) && | |
| rm -f .git/tmp-idx && | |
| gitlink_commit=$(git commit-tree "$gitlink_tree" -p HEAD \ | |
| -m "add gitlink") && | |
| # Gitlink entries are skipped -- the missing submodule | |
| # commit OID does not cause a failure. | |
| check_connected_trace trace-gitlink.txt 2 0 "$gitlink_commit" | |
| ) | |
| ' | |
| test_expect_success "$mode: handles file-to-directory transition" ' | |
| ( | |
| cd main-repo && | |
| # Parent: "foo" is a blob at root. | |
| blob_a=$(echo "file-content" | git hash-object -w --stdin) && | |
| tmpgit read-tree HEAD && | |
| tmpgit update-index --add \ | |
| --cacheinfo "100644,$blob_a,foo" && | |
| parent_tree=$(tmpgit write-tree) && | |
| rm -f .git/tmp-idx && | |
| parent=$(git commit-tree "$parent_tree" -p HEAD \ | |
| -m "add foo as file") && | |
| # Child: "foo" becomes a directory (foo/bar.txt). | |
| blob_b=$(echo "dir-content" | git hash-object -w --stdin) && | |
| tmpgit read-tree "$parent" && | |
| tmpgit update-index --remove foo && | |
| tmpgit update-index --add \ | |
| --cacheinfo "100644,$blob_b,foo/bar.txt" && | |
| child_tree=$(tmpgit write-tree) && | |
| rm -f .git/tmp-idx && | |
| child=$(git commit-tree "$child_tree" -p "$parent" \ | |
| -m "foo: file to directory") && | |
| check_connected_trace trace-f2d.txt "" "" "$child" | |
| ) | |
| ' | |
| test_expect_success "$mode: handles directory-to-file transition" ' | |
| ( | |
| cd main-repo && | |
| # Parent: "bar/baz.txt" exists (bar is a directory). | |
| blob_a=$(echo "nested" | git hash-object -w --stdin) && | |
| tmpgit read-tree HEAD && | |
| tmpgit update-index --add \ | |
| --cacheinfo "100644,$blob_a,bar/baz.txt" && | |
| parent_tree=$(tmpgit write-tree) && | |
| rm -f .git/tmp-idx && | |
| parent=$(git commit-tree "$parent_tree" -p HEAD \ | |
| -m "add bar as directory") && | |
| # Child: "bar" becomes a plain file. | |
| blob_b=$(echo "flat" | git hash-object -w --stdin) && | |
| tmpgit read-tree "$parent" && | |
| tmpgit update-index --remove bar/baz.txt && | |
| tmpgit update-index --add \ | |
| --cacheinfo "100644,$blob_b,bar" && | |
| child_tree=$(tmpgit write-tree) && | |
| rm -f .git/tmp-idx && | |
| child=$(git commit-tree "$child_tree" -p "$parent" \ | |
| -m "bar: directory to file") && | |
| check_connected_trace trace-d2f.txt "" "" "$child" | |
| ) | |
| ' | |
| # Replacement objects. | |
| test_expect_success "$mode: accepts with replacement objects" ' | |
| ( | |
| cd replace-test && | |
| check_connected "$original_oid" | |
| ) | |
| ' | |
| test_expect_success "$mode: rejects without replacement objects" ' | |
| ( | |
| cd replace-test && | |
| GIT_NO_REPLACE_OBJECTS=1 && | |
| export GIT_NO_REPLACE_OBJECTS && | |
| test_expect_code 128 check_connected \ | |
| "$original_oid" 2>err && | |
| test_grep "missing blob object.*$orig_blob_oid" err | |
| ) | |
| ' | |
| test_expect_success "$mode: accepts missing promised blob" ' | |
| test_when_finished "rm -rf prom-src prom-server.git prom-client" && | |
| git init prom-src && | |
| test_commit -C prom-src --no-tag base file.txt original && | |
| test_commit -C prom-src --no-tag "add file2" file2.txt extra && | |
| git clone --bare prom-src prom-server.git && | |
| git -C prom-server.git config uploadpack.allowfilter true && | |
| git -C prom-server.git config uploadpack.allowanysha1inwant true && | |
| git clone --no-checkout --filter=blob:none \ | |
| "file://$(pwd)/prom-server.git" prom-client && | |
| ( | |
| cd prom-client && | |
| promised_blob=$(git rev-parse HEAD:file2.txt) && | |
| test_must_fail env GIT_NO_LAZY_FETCH=1 \ | |
| git cat-file -e "$promised_blob" && | |
| new_tree=$(printf "100644 blob %s\tnewname.txt\n" \ | |
| "$promised_blob" | | |
| git mktree --missing) && | |
| new_commit=$(git commit-tree "$new_tree" \ | |
| -p HEAD -m "reuse promised blob") && | |
| check_connected "$new_commit" && | |
| test_must_fail env GIT_NO_LAZY_FETCH=1 \ | |
| git cat-file -e "$promised_blob" | |
| ) | |
| ' | |
| test_expect_success "$mode: does not lazy-fetch promised blob" ' | |
| test_when_finished "rm -rf nofetch-src nofetch-server.git nofetch-client" && | |
| git init nofetch-src && | |
| test_commit -C nofetch-src --no-tag base file.txt content && | |
| git clone --bare nofetch-src nofetch-server.git && | |
| git -C nofetch-server.git config uploadpack.allowfilter true && | |
| git -C nofetch-server.git config uploadpack.allowanysha1inwant true && | |
| git clone --no-checkout --filter=blob:none \ | |
| "file://$(pwd)/nofetch-server.git" nofetch-client && | |
| ( | |
| cd nofetch-client && | |
| promised_blob=$(git rev-parse HEAD:file.txt) && | |
| test_must_fail env GIT_NO_LAZY_FETCH=1 \ | |
| git cat-file -e "$promised_blob" && | |
| new_tree=$(printf "100644 blob %s\tfile.txt\n" \ | |
| "$promised_blob" | | |
| git mktree --missing) && | |
| new_commit=$(git commit-tree "$new_tree" \ | |
| -m "root commit with promised blob") && | |
| check_connected "$new_commit" && | |
| test_must_fail env GIT_NO_LAZY_FETCH=1 \ | |
| git cat-file -e "$promised_blob" | |
| ) | |
| ' | |
| test_expect_success "$mode: accepts missing promised tree" ' | |
| test_when_finished "rm -rf prom-tree-src prom-tree-server.git prom-tree-client" && | |
| git init prom-tree-src && | |
| mkdir -p prom-tree-src/a/b && | |
| test_commit -C prom-tree-src --no-tag "nested dirs" a/b/file.txt deep && | |
| git clone --bare prom-tree-src prom-tree-server.git && | |
| git -C prom-tree-server.git config uploadpack.allowfilter true && | |
| git -C prom-tree-server.git config uploadpack.allowanysha1inwant true && | |
| git clone --no-checkout --filter=tree:1 \ | |
| "file://$(pwd)/prom-tree-server.git" prom-tree-client && | |
| ( | |
| cd prom-tree-client && | |
| promised_tree=$(git ls-tree HEAD -- a | | |
| awk "{print \$3}") && | |
| test_must_fail env GIT_NO_LAZY_FETCH=1 \ | |
| git cat-file -e "$promised_tree" && | |
| new_tree=$(printf "40000 tree %s\trenamed\n" \ | |
| "$promised_tree" | | |
| git mktree --missing) && | |
| new_commit=$(git commit-tree "$new_tree" \ | |
| -p HEAD -m "reuse promised tree") && | |
| check_connected "$new_commit" && | |
| test_must_fail env GIT_NO_LAZY_FETCH=1 \ | |
| git cat-file -e "$promised_tree" | |
| ) | |
| ' | |
| test_expect_success "$mode: verifies new subtree when parent subtree is promised" ' | |
| test_when_finished "rm -rf prom-base-src prom-base-server.git prom-base-client" && | |
| git init prom-base-src && | |
| mkdir -p prom-base-src/a && | |
| test_commit -C prom-base-src --no-tag "base" a/file.txt deep && | |
| git clone --bare prom-base-src prom-base-server.git && | |
| git -C prom-base-server.git config uploadpack.allowfilter true && | |
| git -C prom-base-server.git config uploadpack.allowanysha1inwant true && | |
| git clone --no-checkout --filter=tree:1 \ | |
| "file://$(pwd)/prom-base-server.git" prom-base-client && | |
| ( | |
| cd prom-base-client && | |
| parent_subtree=$(git ls-tree HEAD -- a | | |
| awk "{print \$3}") && | |
| test_must_fail env GIT_NO_LAZY_FETCH=1 \ | |
| git cat-file -e "$parent_subtree" && | |
| new_blob=$(echo "local-content" | git hash-object -w --stdin) && | |
| new_subtree=$(printf "100644 blob %s\tfile.txt\n" \ | |
| "$new_blob" | git mktree) && | |
| new_root=$(printf "40000 tree %s\ta\n" \ | |
| "$new_subtree" | git mktree) && | |
| new_commit=$(git commit-tree "$new_root" \ | |
| -p HEAD -m "replace promised subtree") && | |
| check_connected "$new_commit" && | |
| test_must_fail env GIT_NO_LAZY_FETCH=1 \ | |
| git cat-file -e "$parent_subtree" | |
| ) | |
| ' | |
| test_expect_success "$mode: verifies local commit in partial clone" ' | |
| test_when_finished "rm -rf pc-src pc-server.git pc-client" && | |
| git init pc-src && | |
| test_commit -C pc-src --no-tag base file.txt && | |
| git clone --bare pc-src pc-server.git && | |
| git -C pc-server.git config uploadpack.allowfilter true && | |
| git -C pc-server.git config uploadpack.allowanysha1inwant true && | |
| git clone --filter=blob:none \ | |
| "file://$(pwd)/pc-server.git" pc-client && | |
| ( | |
| cd pc-client && | |
| local_commit=$(commit_with_change HEAD file.txt local-content) && | |
| check_connected "$local_commit" | |
| ) | |
| ' | |
| test_expect_success "$mode: tolerates missing promisor parent commit" ' | |
| test_when_finished "rm -rf mp-src mp-server.git mp-client" && | |
| git init mp-src && | |
| test_commit -C mp-src --no-tag base file.txt && | |
| git clone --bare mp-src mp-server.git && | |
| git -C mp-server.git config uploadpack.allowfilter true && | |
| git -C mp-server.git config uploadpack.allowanysha1inwant true && | |
| git clone --filter=blob:none \ | |
| "file://$(pwd)/mp-server.git" mp-client && | |
| test_commit -C mp-src --no-tag c1 file.txt && | |
| test_commit -C mp-src --no-tag c2 file.txt && | |
| c1=$(git -C mp-src rev-parse HEAD~1) && | |
| c2=$(git -C mp-src rev-parse HEAD) && | |
| ( | |
| cd mp-client && | |
| # Add a promisor pack holding only c2, so its parent c1 | |
| # is missing but promised (as after a lazy fetch that | |
| # omits dependents). | |
| echo "$c2" | | |
| git -C ../mp-src pack-objects --stdout >c2.pack && | |
| git index-pack --promisor --stdin <c2.pack && | |
| test_must_fail env GIT_NO_LAZY_FETCH=1 \ | |
| git cat-file -e "$c1" && | |
| tree=$(git rev-parse HEAD^{tree}) && | |
| local_commit=$(git hash-object -t commit -w --stdin <<-EOF | |
| tree $tree | |
| parent $c1 | |
| author A U Thor <author@example.com> 1112912053 -0700 | |
| committer C O Mitter <committer@example.com> 1112912053 -0700 | |
| on top of a missing promisor commit | |
| EOF | |
| ) && | |
| check_connected "$local_commit" | |
| ) | |
| ' | |
| test_expect_success "$mode: respects shallow boundary" ' | |
| test_when_finished "rm -rf shallow-src shallow" && | |
| git init shallow-src && | |
| test_commit -C shallow-src --no-tag base file content-1 && | |
| mkdir shallow-src/sub && | |
| test_commit -C shallow-src --no-tag change sub/other content-2 && | |
| git clone --depth=1 "file://$(pwd)/shallow-src" shallow && | |
| ( | |
| cd shallow && | |
| tip=$(git rev-parse HEAD) && | |
| git for-each-ref --format="delete %(refname)" | | |
| git update-ref --no-deref --stdin && | |
| check_connected "$tip" | |
| ) | |
| ' | |
| test_expect_success "$mode: deepening fetch succeeds" ' | |
| test_when_finished "rm -rf deepen-src deepen-server.git deepen-client" && | |
| git init deepen-src && | |
| test_commit -C deepen-src --no-tag c1 file.txt && | |
| test_commit -C deepen-src --no-tag c2 file.txt && | |
| test_commit -C deepen-src --no-tag c3 file.txt && | |
| git clone --bare deepen-src deepen-server.git && | |
| git clone --depth=1 "file://$(pwd)/deepen-server.git" deepen-client && | |
| set_connectivity_check deepen-client $mode && | |
| test -f deepen-client/.git/shallow && | |
| GIT_TRACE2_EVENT="$(pwd)/deepen-trace.txt" \ | |
| git -C deepen-client fetch --deepen=2 origin main && | |
| # Incremental falls back to full for deepening fetches, | |
| # so the trees_loaded event should not appear. | |
| test_grep ! trees_loaded deepen-trace.txt | |
| ' | |
| test_expect_success "$mode: malformed tree detected" ' | |
| ( | |
| cd main-repo && | |
| echo abc >malformed-tree && | |
| malformed_tree=$(git hash-object --literally -t tree -w \ | |
| malformed-tree) && | |
| malformed_commit=$(git commit-tree "$malformed_tree" \ | |
| -p HEAD -m malformed) && | |
| test_expect_code 128 check_connected \ | |
| "$malformed_commit" 2>err | |
| ) | |
| ' | |
| test_expect_success PERL_TEST_HELPERS \ | |
| "$mode: mid-tree corruption detected" ' | |
| ( | |
| cd main-repo && | |
| # Build a tree with one valid entry followed by garbage. | |
| blob_oid=$(echo "valid" | git hash-object -w --stdin) && | |
| bin_oid=$(echo "$blob_oid" | hex2oct) && | |
| printf "100644 good\0${bin_oid}GARBAGE" >corrupt-mid-tree && | |
| corrupt_tree=$(git hash-object --literally -t tree -w \ | |
| corrupt-mid-tree) && | |
| corrupt_commit=$(git commit-tree "$corrupt_tree" \ | |
| -p HEAD -m "mid-tree corruption") && | |
| test_expect_code 128 check_connected \ | |
| "$corrupt_commit" 2>err && | |
| test_grep "too-short tree object" err | |
| ) | |
| ' | |
| done | |
| # The full check only checks that blobs exist, but the incremental | |
| # check reads their headers and reports a corrupt blob. | |
| test_expect_success 'incremental: rejects corrupt blob' ' | |
| test_when_finished "rm -rf corrupt-repo" && | |
| git init corrupt-repo && | |
| ( | |
| cd corrupt-repo && | |
| test_commit base && | |
| blob=$(echo corrupt | git hash-object -w --stdin) && | |
| tree=$(printf "100644 blob $blob\tfile\n" | git mktree) && | |
| commit=$(git commit-tree "$tree" -p HEAD -m corrupt) && | |
| path=.git/objects/$(test_oid_to_path "$blob") && | |
| chmod +w "$path" && | |
| echo garbage >"$path" && | |
| mode=incremental && | |
| test_expect_code 128 check_connected "$commit" 2>err && | |
| test_grep "unable to read blob object $blob" err | |
| ) | |
| ' | |
| # Algorithm selection. | |
| # Run a command with its trace2 events in "trace" and its stderr | |
| # in "err". | |
| run_traced () { | |
| rm -f trace && | |
| GIT_TRACE2_EVENT="$(pwd)/trace" "$@" 2>err | |
| } | |
| # Check which connectivity check the last run_traced used. | |
| expect_mode () { | |
| case "$1" in | |
| full) | |
| test_grep ! trees_loaded trace | |
| ;; | |
| incremental) | |
| test_grep trees_loaded trace | |
| ;; | |
| *) | |
| BUG "unknown mode: $1" | |
| ;; | |
| esac | |
| } | |
| test_expect_success 'setup source repository' ' | |
| git init src && | |
| test_commit -C src base file.txt && | |
| test_commit -C src --no-tag update file.txt updated | |
| ' | |
| test_expect_success 'unknown transfer.connectivityCheck falls back to full' ' | |
| test_when_finished "rm -rf dst" && | |
| git init --bare dst && | |
| git -C dst fetch ../src base:refs/heads/main && | |
| for value in bogus auto= auto=x | |
| do | |
| set_connectivity_check dst "$value" && | |
| run_traced git -C dst fetch ../src main && | |
| expect_mode full && | |
| test_grep "unknown transfer.connectivityCheck algorithm .$value., using .full." err || | |
| return 1 | |
| done | |
| ' | |
| test_expect_success 'push uses incremental when configured' ' | |
| test_when_finished "rm -rf dst" && | |
| git init --bare dst && | |
| git -C src push ../dst base:refs/heads/main && | |
| set_connectivity_check dst incremental && | |
| run_traced git -C src push ../dst main && | |
| expect_mode incremental | |
| ' | |
| test_expect_success 'fetch uses incremental when configured' ' | |
| test_when_finished "rm -rf dst" && | |
| git init --bare dst && | |
| git -C dst fetch ../src base:refs/heads/main && | |
| set_connectivity_check dst incremental && | |
| run_traced git -C dst fetch ../src main && | |
| expect_mode incremental | |
| ' | |
| for mode in incremental auto | |
| do | |
| test_expect_success "push with $mode into an empty repository uses full" ' | |
| test_when_finished "rm -rf dst" && | |
| git init --bare dst && | |
| set_connectivity_check dst $mode && | |
| run_traced git -C src push ../dst main && | |
| expect_mode full | |
| ' | |
| test_expect_success "clone with $mode uses full" ' | |
| test_when_finished "rm -rf dst" && | |
| run_traced git -c transfer.connectivityCheck=$mode \ | |
| clone --no-local src dst && | |
| expect_mode full | |
| ' | |
| done | |
| for value in auto auto=1 | |
| do | |
| test_expect_success "push with $value uses incremental up to the cutoff" ' | |
| test_when_finished "rm -rf dst" && | |
| git init --bare dst && | |
| git -C src push ../dst base:refs/heads/main && | |
| set_connectivity_check dst $value && | |
| run_traced git -C src push ../dst main && | |
| expect_mode incremental | |
| ' | |
| done | |
| test_expect_success 'push with auto uses full above the cutoff' ' | |
| test_when_finished "rm -rf dst" && | |
| git init --bare dst && | |
| git -C src push ../dst base:refs/heads/main && | |
| set_connectivity_check dst auto=0 && | |
| run_traced git -C src push ../dst main && | |
| expect_mode full | |
| ' | |
| test_done |