diff --git a/Taskfile.pkl b/Taskfile.pkl index 6950aad4..afc51c77 100644 --- a/Taskfile.pkl +++ b/Taskfile.pkl @@ -313,7 +313,7 @@ local testUpdate: Task = new { local testJsBuild: Task = new { name = "test-js-build" description = "Node-side smoke tests for the published JS lib + npm wrapper" - cmd = "node --test tools/js-build.test.mjs tools/npm-lib.test.mjs tools/npm-cli.test.mjs tools/demo-relay.test.mjs tools/demo-editor-link.test.mjs tools/playground-commands.test.mjs tools/playground-view.test.mjs tools/flaker-affected-rules.test.mjs tools/select-affected-tests.test.mjs" + cmd = "node --test tools/js-build.test.mjs tools/npm-lib.test.mjs tools/shallow-push.test.mjs tools/npm-cli.test.mjs tools/demo-relay.test.mjs tools/demo-editor-link.test.mjs tools/playground-commands.test.mjs tools/playground-view.test.mjs tools/flaker-affected-rules.test.mjs tools/select-affected-tests.test.mjs" deps { syncNpmLibRaw syncNpmBitMjs diff --git a/Test.pkl b/Test.pkl index 3a99235a..4e91263d 100644 --- a/Test.pkl +++ b/Test.pkl @@ -66,6 +66,6 @@ tests { } new { name = "js-build" - cmd = "node --test tools/js-build.test.mjs tools/npm-lib.test.mjs tools/npm-cli.test.mjs tools/demo-relay.test.mjs tools/demo-editor-link.test.mjs tools/playground-commands.test.mjs tools/playground-view.test.mjs" + cmd = "node --test tools/js-build.test.mjs tools/npm-lib.test.mjs tools/shallow-push.test.mjs tools/npm-cli.test.mjs tools/demo-relay.test.mjs tools/demo-editor-link.test.mjs tools/playground-commands.test.mjs tools/playground-view.test.mjs" } } diff --git a/modules/bit_lib/src/js_api_exports.mbt b/modules/bit_lib/src/js_api_exports.mbt index fa136286..c0309a0a 100644 --- a/modules/bit_lib/src/js_api_exports.mbt +++ b/modules/bit_lib/src/js_api_exports.mbt @@ -2579,7 +2579,7 @@ pub async fn js_clone_remote( } Some((refname, commit_id)) => { if wants.length() > 0 { - let pack = @bitnative.fetch_pack_with_http( + let result = @bitnative.fetch_pack_with_http_result( remote_url, wants, prefer_v2, @@ -2599,6 +2599,7 @@ pub async fn js_clone_remote( js_transport_post(transport_id, url, body, headers) }, ) + let pack = result.pack let objects = @pack.parse_packfile(pack) let git_dir = join_path(root, ".git") @bit.FileSystem::mkdir_p(fs, join_path(git_dir, "objects/pack")) @@ -2607,6 +2608,16 @@ pub async fn js_clone_remote( @bit.materialize_clone_to_fs( store, commit_id, refname, remote_url, fs, root, fs, ) + // Record the history boundary so later walks (push in particular) + // know the parents of these commits were never fetched. The server + // reports it for a `depth` request and for an already-shallow repo. + @repo.apply_shallow_updates( + fs, + fs, + js_resolve_git_dir(fs, root), + result.shallow, + result.unshallow, + ) } else { init_repo( fs, @@ -2680,7 +2691,7 @@ pub async fn js_fetch_remote( match js_select_fetch_target(refs, symrefs, refspec) { None => { status: "empty", commit_id: "", refname: "", remote_ref: "" } Some((refname, commit_id)) => { - let pack = @bitnative.fetch_pack_with_http( + let result = @bitnative.fetch_pack_with_http_result( remote_url, [commit_id], prefer_v2, @@ -2699,10 +2710,19 @@ pub async fn js_fetch_remote( ) -> (@bit.HttpResponse, Bytes) raise @bit.GitError { js_transport_post(transport_id, url, body, headers) }, + shallow=@repo.read_shallow_boundaries(fs, git_dir), ) + let pack = result.pack let objects = @pack.parse_packfile(pack) @bit.FileSystem::mkdir_p(fs, join_path(git_dir, "objects/pack")) @pack.write_packfile_with_index(fs, git_dir, pack, objects) + @repo.apply_shallow_updates( + fs, + fs, + git_dir, + result.shallow, + result.unshallow, + ) let remote_ref = js_remote_tracking_refname(refname) let remote_ref_path = join_path(git_dir, remote_ref) @bit.FileSystem::mkdir_p(fs, rebase_parent_dir(remote_ref_path)) @@ -2769,9 +2789,11 @@ pub async fn js_push_remote( js_normalize_push_refname(refname) } let db = ObjectDb::load(fs, git_dir) - let objects = collect_reachable_objects(db, fs, head_id) - let pack = @pack.create_packfile(objects) let remote = @protocol.Remote::new(remote_url) + // Discover before collecting: the refs the receiver advertises bound the + // walk. Packing HEAD's full history first both sends objects the remote + // already has and fails outright when this clone (or the remote itself) + // is shallow, because the advertised tip's parents are not local. let (refs, _caps) = @protocol.discover_refs_with_http(remote, async fn( url : String, headers : Map[String, String], @@ -2786,6 +2808,14 @@ pub async fn js_push_remote( ) } } + let objects = collect_reachable_objects_excluding_commits( + db, + fs, + [head_id], + advertised_have_ids(refs), + shallow=@repo.read_shallow_boundaries(fs, git_dir), + ) + let pack = @pack.create_packfile(objects) let req = @protocol.PushRequest::new(old_id, head_id, target_ref, pack) @protocol.push_with_http( remote, diff --git a/modules/bit_lib/src/native/sync_http.mbt b/modules/bit_lib/src/native/sync_http.mbt index fdd5b30b..62cc1204 100644 --- a/modules/bit_lib/src/native/sync_http.mbt +++ b/modules/bit_lib/src/native/sync_http.mbt @@ -160,6 +160,7 @@ pub async fn push_http( rfs, [head_id], sync_remote_have_ids(refs), + shallow=@repo.read_shallow_boundaries(rfs, actual_git_dir), ) let pack = @pack.create_packfile(objects) sync_raise_lfs_upload_errors( @@ -204,6 +205,7 @@ pub async fn push_http_from_commit( rfs, [commit_id], sync_remote_have_ids(refs), + shallow=@repo.read_shallow_boundaries(rfs, actual_git_dir), ) let pack = @pack.create_packfile(objects) sync_raise_lfs_upload_errors( @@ -274,18 +276,7 @@ fn sync_find_ref( fn sync_remote_have_ids( refs : Array[(@bit.ObjectId, String)], ) -> Array[@bit.ObjectId] { - let zero = @bit.ObjectId::zero() - let seen : Map[String, Bool] = Map([]) - let haves : Array[@bit.ObjectId] = [] - for item in refs { - let (id, _) = item - let hex = id.to_hex() - if id != zero && !seen.contains(hex) { - seen[hex] = true - haves.push(id) - } - } - haves + @bitlib.advertised_have_ids(refs) } ///| diff --git a/modules/bit_lib/src/pack_collect.mbt b/modules/bit_lib/src/pack_collect.mbt index 71ccbd2d..67a4e669 100644 --- a/modules/bit_lib/src/pack_collect.mbt +++ b/modules/bit_lib/src/pack_collect.mbt @@ -54,6 +54,7 @@ fn collect_object_ids_only( fs : &@bit.RepoFileSystem, commit_id : @bit.ObjectId, seen : Map[String, Bool], + shallow? : Map[String, Bool] = Map([]), ) -> Unit raise @bit.GitError { let hex = commit_id.to_hex() if seen.contains(hex) { @@ -67,8 +68,12 @@ fn collect_object_ids_only( if o.obj_type == @bit.ObjectType::Commit { let info = @bit.parse_commit(o.data) collect_tree_ids_only(db, fs, info.tree, seen) + if shallow.contains(hex) { + // Shallow boundary: the parents are absent by construction. + return + } for p in info.parents { - collect_object_ids_only(db, fs, p, seen) + collect_object_ids_only(db, fs, p, seen, shallow~) } } } @@ -202,10 +207,12 @@ pub fn collect_reachable_objects( db : ObjectDb, fs : &@bit.RepoFileSystem, commit_id : @bit.ObjectId, + shallow? : Array[@bit.ObjectId] = [], ) -> Array[@bit.PackObject] raise @bit.GitError { let seen : Map[String, Bool] = Map([]) let out : Array[@bit.PackObject] = [] - collect_commit_objects(db, fs, commit_id, seen, out) + let boundaries = shallow_boundary_set(shallow) + collect_commit_objects(db, fs, commit_id, seen, out, shallow=boundaries) out } @@ -215,11 +222,13 @@ pub fn collect_reachable_objects_from_commits( db : ObjectDb, fs : &@bit.RepoFileSystem, commits : Array[@bit.ObjectId], + shallow? : Array[@bit.ObjectId] = [], ) -> Array[@bit.PackObject] raise @bit.GitError { let seen : Map[String, Bool] = Map([]) let out : Array[@bit.PackObject] = [] + let boundaries = shallow_boundary_set(shallow) for commit_id in commits { - collect_commit_objects(db, fs, commit_id, seen, out) + collect_commit_objects(db, fs, commit_id, seen, out, shallow=boundaries) } out } @@ -234,17 +243,52 @@ pub fn collect_reachable_objects_excluding_commits( fs : &@bit.RepoFileSystem, commits : Array[@bit.ObjectId], excluded_commits : Array[@bit.ObjectId], + shallow? : Array[@bit.ObjectId] = [], ) -> Array[@bit.PackObject] raise @bit.GitError { // Mark the complete object closure already owned by the receiver. Merely // stopping the commit walk at a have commit is insufficient: the wanted // commit's new tree can still reference unchanged blobs from that commit. let seen : Map[String, Bool] = Map([]) + let boundaries = shallow_boundary_set(shallow) for id in excluded_commits { - collect_object_ids_only(db, fs, id, seen) + collect_object_ids_only(db, fs, id, seen, shallow=boundaries) } let out : Array[@bit.PackObject] = [] for commit_id in commits { - collect_commit_objects(db, fs, commit_id, seen, out) + collect_commit_objects(db, fs, commit_id, seen, out, shallow=boundaries) + } + out +} + +///| +/// The distinct non-zero commit ids a peer advertises, usable as the exclusion +/// set of a push walk: everything reachable from them is already on the other +/// side, so it must not be packed again. +pub fn advertised_have_ids( + refs : Array[(@bit.ObjectId, String)], +) -> Array[@bit.ObjectId] { + let zero = @bit.ObjectId::zero() + let seen : Map[String, Bool] = Map([]) + let haves : Array[@bit.ObjectId] = [] + for item in refs { + let (id, _) = item + let hex = id.to_hex() + if id != zero && !seen.contains(hex) { + seen[hex] = true + haves.push(id) + } + } + haves +} + +///| +/// Index shallow boundary commits by hex id for O(1) lookups during a walk. +fn shallow_boundary_set( + shallow : Array[@bit.ObjectId], +) -> Map[String, Bool] { + let out : Map[String, Bool] = Map([]) + for id in shallow { + out[id.to_hex()] = true } out } @@ -256,6 +300,7 @@ fn collect_commit_objects( commit_id : @bit.ObjectId, seen : Map[String, Bool], out : Array[@bit.PackObject], + shallow? : Map[String, Bool] = Map([]), ) -> Unit raise @bit.GitError { let hex = commit_id.to_hex() if seen.contains(hex) { @@ -272,8 +317,13 @@ fn collect_commit_objects( out.push(o) let info = @bit.parse_commit(o.data) collect_tree_objects(db, fs, info.tree, seen, out) + if shallow.contains(hex) { + // Shallow boundary: this commit's parents were never fetched, so the + // history stops here just as it does for git's grafted walk. + return + } for p in info.parents { - collect_commit_objects(db, fs, p, seen, out) + collect_commit_objects(db, fs, p, seen, out, shallow~) } } } diff --git a/modules/bit_lib/src/pack_collect_test.mbt b/modules/bit_lib/src/pack_collect_test.mbt index ba80873a..94b9fa9a 100644 --- a/modules/bit_lib/src/pack_collect_test.mbt +++ b/modules/bit_lib/src/pack_collect_test.mbt @@ -397,3 +397,105 @@ test "collect_reachable_objects_excluding_commits: stops at have commits" { assert_eq(nested_shared_blob_count, 0) assert_eq(new_blob_count, 1) } + +///| +/// Drop a loose object so the walk sees the truncated history a shallow clone +/// actually has on disk. +fn drop_loose_object( + fs : @bit.TestFs, + git_dir : String, + id : @bit.ObjectId, +) -> Unit raise @bit.GitError { + let hex = id.to_hex() + let prefix = String::unsafe_substring(hex, start=0, end=2) + let suffix = String::unsafe_substring(hex, start=2, end=hex.length()) + fs.remove_file(git_dir + "/objects/" + prefix + "/" + suffix) +} + +///| +test "collect_reachable_objects_excluding_commits: shallow tip whose parent is absent" { + let fs = @bit.TestFs::new() + init_repo(fs, "/repo") catch { + _ => () + } + fs.write_file("/repo/base.txt", b"base") + add_paths(fs, fs, "/repo", ["."]) + let base = commit(fs, fs, "/repo", "base", "test ", 1000L) + fs.write_file("/repo/boundary.txt", b"boundary") + add_paths(fs, fs, "/repo", ["."]) + let boundary = commit( + fs, fs, "/repo", "boundary", "test ", 2000L, + ) + fs.write_file("/repo/new.txt", b"new") + add_paths(fs, fs, "/repo", ["."]) + let tip = commit(fs, fs, "/repo", "tip", "test ", 3000L) + // A depth-1 clone (or a clone of an already shallow remote) holds `boundary` + // but never received its parent. + drop_loose_object(fs, "/repo/.git", base) + let db = ObjectDb::load(fs, "/repo/.git") + + // The receiver advertises `boundary`, so the walk must stop there instead of + // trying to load the absent parent. + let objects = collect_reachable_objects_excluding_commits(db, fs, [tip], [ + boundary, + ]) + let ids : Map[String, Bool] = Map([]) + for obj in objects { + ids[obj.id.to_hex()] = true + } + assert_true(ids.contains(tip.to_hex())) + assert_true(!ids.contains(boundary.to_hex())) +} + +///| +test "collect_reachable_objects: stops at a shallow boundary commit" { + let fs = @bit.TestFs::new() + init_repo(fs, "/repo") catch { + _ => () + } + fs.write_file("/repo/base.txt", b"base") + add_paths(fs, fs, "/repo", ["."]) + let base = commit(fs, fs, "/repo", "base", "test ", 1000L) + fs.write_file("/repo/boundary.txt", b"boundary") + add_paths(fs, fs, "/repo", ["."]) + let boundary = commit( + fs, fs, "/repo", "boundary", "test ", 2000L, + ) + drop_loose_object(fs, "/repo/.git", base) + let db = ObjectDb::load(fs, "/repo/.git") + + // Without the boundary the walk reaches the absent parent and fails. + let mut failed = false + (collect_reachable_objects(db, fs, boundary) |> ignore) catch { + _ => failed = true + } + assert_true(failed) + + // Declaring the boundary grafts the history closed, as git's shallow file does. + let objects = collect_reachable_objects(db, fs, boundary, shallow=[boundary]) + let ids : Map[String, Bool] = Map([]) + for obj in objects { + ids[obj.id.to_hex()] = true + } + assert_true(ids.contains(boundary.to_hex())) + assert_true(!ids.contains(base.to_hex())) +} + +///| +test "advertised_have_ids: drops zero and duplicate ids" { + let first = @bit.ObjectId::from_hex( + "1111111111111111111111111111111111111111", + ) + let second = @bit.ObjectId::from_hex( + "2222222222222222222222222222222222222222", + ) + let haves = advertised_have_ids([ + (first, "HEAD"), + (first, "refs/heads/main"), + (@bit.ObjectId::zero(), "refs/heads/empty"), + (second, "refs/heads/topic"), + ]) + assert_eq(haves.length(), 2) + assert_eq(haves[0].to_hex(), first.to_hex()) + assert_eq(haves[1].to_hex(), second.to_hex()) +} diff --git a/modules/bit_lib/src/pkg.generated.mbti b/modules/bit_lib/src/pkg.generated.mbti index b0942102..bdc9c58f 100644 --- a/modules/bit_lib/src/pkg.generated.mbti +++ b/modules/bit_lib/src/pkg.generated.mbti @@ -13,6 +13,8 @@ pub fn add_paths(&@bit_types.FileSystem, &@bit_types.RepoFileSystem, String, Arr pub async fn add_paths_async(&@bit_types.FileSystem, &@bit_types.RepoFileSystem, String, Array[String], run_filter_cmd? : FilterCmd, warnings? : Array[String]) -> Unit raise @bit_object.GitError +pub fn advertised_have_ids(Array[(@bit_object.ObjectId, String)]) -> Array[@bit_object.ObjectId] + pub fn append_reflog(&@bit_types.FileSystem, &@bit_types.RepoFileSystem, String, String, @bit_object.ObjectId, @bit_object.ObjectId, String, String, Int64, String, String) -> Unit raise @bit_object.GitError pub fn apply_config_overrides(Map[String, RemoteConfig], Map[String, BranchConfig], Array[(String, String)]) -> Unit @@ -61,11 +63,11 @@ pub fn collect_loose_ref_ids(&@bit_types.RepoFileSystem, String, String, Map[Str pub fn collect_packed_ref_ids(&@bit_types.RepoFileSystem, String, Map[String, @bit_object.ObjectId], String?) -> Unit -pub fn collect_reachable_objects(ObjectDb, &@bit_types.RepoFileSystem, @bit_object.ObjectId) -> Array[@bit_object.PackObject] raise @bit_object.GitError +pub fn collect_reachable_objects(ObjectDb, &@bit_types.RepoFileSystem, @bit_object.ObjectId, shallow? : Array[@bit_object.ObjectId]) -> Array[@bit_object.PackObject] raise @bit_object.GitError -pub fn collect_reachable_objects_excluding_commits(ObjectDb, &@bit_types.RepoFileSystem, Array[@bit_object.ObjectId], Array[@bit_object.ObjectId]) -> Array[@bit_object.PackObject] raise @bit_object.GitError +pub fn collect_reachable_objects_excluding_commits(ObjectDb, &@bit_types.RepoFileSystem, Array[@bit_object.ObjectId], Array[@bit_object.ObjectId], shallow? : Array[@bit_object.ObjectId]) -> Array[@bit_object.PackObject] raise @bit_object.GitError -pub fn collect_reachable_objects_from_commits(ObjectDb, &@bit_types.RepoFileSystem, Array[@bit_object.ObjectId]) -> Array[@bit_object.PackObject] raise @bit_object.GitError +pub fn collect_reachable_objects_from_commits(ObjectDb, &@bit_types.RepoFileSystem, Array[@bit_object.ObjectId], shallow? : Array[@bit_object.ObjectId]) -> Array[@bit_object.PackObject] raise @bit_object.GitError pub fn collect_tree_blobs(ObjectDb, &@bit_types.RepoFileSystem, @bit_object.ObjectId) -> Array[@bit_object.ObjectId] raise @bit_object.GitError diff --git a/tools/shallow-push.test.mjs b/tools/shallow-push.test.mjs new file mode 100644 index 00000000..81cf7c06 --- /dev/null +++ b/tools/shallow-push.test.mjs @@ -0,0 +1,162 @@ +// Regression guard for pushing when the local clone or the remote is shallow. +// +// A repo imported with `depth: 1` (Cloudflare Artifacts does this) advertises a +// tip whose parents it does not have. Packing HEAD's whole history for a push +// used to walk past that boundary into objects nobody has, failing with +// "Missing commit object" even though real git pushes to the same remote fine. +import assert from "node:assert/strict"; +import test from "node:test"; +import { execFileSync, spawn } from "node:child_process"; +import { createServer } from "node:http"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import * as bit from "@mizchi/bit"; + +const AUTHOR = "bit "; + +function hasGit() { + try { + execFileSync("git", ["--version"], { stdio: "ignore" }); + execFileSync("git", ["http-backend", "--help"], { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +function git(cwd, ...args) { + return execFileSync( + "git", + [ + "-c", "user.email=dev@example.com", + "-c", "user.name=dev", + "-c", "commit.gpgsign=false", + ...args, + ], + { cwd, encoding: "utf8" }, + ).trim(); +} + +// Serve GIT_PROJECT_ROOT over smart HTTP by piping requests through git's own CGI. +function startHttpBackend(projectRoot) { + const server = createServer((req, res) => { + const url = new URL(req.url, "http://placeholder"); + const chunks = []; + req.on("data", (chunk) => chunks.push(chunk)); + req.on("end", () => { + const body = Buffer.concat(chunks); + const cgi = spawn("git", ["http-backend"], { + env: { + ...process.env, + GIT_PROJECT_ROOT: projectRoot, + GIT_HTTP_EXPORT_ALL: "1", + REQUEST_METHOD: req.method, + PATH_INFO: url.pathname, + QUERY_STRING: url.search.slice(1), + CONTENT_TYPE: req.headers["content-type"] ?? "", + CONTENT_LENGTH: String(body.length), + REMOTE_USER: "test", + REMOTE_ADDR: "127.0.0.1", + }, + }); + const out = []; + cgi.stdout.on("data", (chunk) => out.push(chunk)); + cgi.on("close", () => { + const all = Buffer.concat(out); + const separator = all.indexOf("\r\n\r\n"); + const headers = Object.fromEntries( + all.subarray(0, separator).toString() + .split("\r\n") + .filter((line) => line.includes(":")) + .map((line) => line.split(/:\s*/)), + ); + res.writeHead(Number(headers.Status?.split(" ")[0] ?? 200), headers); + res.end(all.subarray(separator + 4)); + }); + cgi.stdin.end(body); + }); + }); + return new Promise((resolve) => { + server.listen(0, "127.0.0.1", () => resolve(server)); + }); +} + +const fetchTransport = { + async get(url, headers) { + const response = await fetch(url, { headers }); + return { status: response.status, body: new Uint8Array(await response.arrayBuffer()) }; + }, + async post(url, body, headers) { + const response = await fetch(url, { method: "POST", headers, body }); + return { status: response.status, body: new Uint8Array(await response.arrayBuffer()) }; + }, +}; + +async function commitAndPush(remoteUrl, cloneOptions, fileName) { + const backend = bit.createMemoryBackend(); + try { + await bit.clone(backend, "/repo", remoteUrl, fetchTransport, cloneOptions); + bit.writeString(backend, `/repo/${fileName}`, "hello\n"); + bit.add(backend, "/repo", ["."]); + const commitId = bit.commit( + backend, "/repo", `from bit (${fileName})`, AUTHOR, 1700002000, + ); + await bit.push(backend, "/repo", remoteUrl, fetchTransport, { + refname: "refs/heads/main", + force: false, + }); + return commitId; + } finally { + bit.destroyBackend(backend); + } +} + +test("push succeeds when the clone or the remote is shallow", { skip: !hasGit() && "git with http-backend is unavailable" }, async () => { + const dir = mkdtempSync(join(tmpdir(), "bit-shallow-push-")); + let server; + try { + // A three-commit origin, plus a depth-1 bare copy of it: the shallow remote + // holds only c3, so c3's parents exist nowhere the client can reach. + const origin = join(dir, "origin.git"); + const work = join(dir, "work"); + git(dir, "init", "-q", "--bare", origin); + git(origin, "config", "http.receivepack", "true"); + git(dir, "clone", "-q", origin, work); + for (const n of [1, 2, 3]) { + writeFileSync(join(work, `f${n}.txt`), `${n}\n`); + git(work, "add", "."); + git(work, "commit", "-q", "-m", `c${n}`); + } + git(work, "push", "-q", "origin", "HEAD:refs/heads/main"); + git(origin, "symbolic-ref", "HEAD", "refs/heads/main"); + + const shallow = join(dir, "shallow.git"); + git(dir, "clone", "-q", "--bare", "--depth", "1", `file://${origin}`, shallow); + git(shallow, "config", "http.receivepack", "true"); + git(shallow, "symbolic-ref", "HEAD", "refs/heads/main"); + assert.equal(git(shallow, "rev-list", "--count", "main"), "1"); + + server = await startHttpBackend(dir); + const base = `http://127.0.0.1:${server.address().port}`; + + for (const [label, repo, depth] of [ + ["full remote, full clone", origin, undefined], + ["full remote, depth 1", origin, 1], + ["shallow remote, full clone", shallow, undefined], + ["shallow remote, depth 1", shallow, 1], + ]) { + const remoteUrl = `${base}/${repo === origin ? "origin" : "shallow"}.git`; + const fileName = `${label.replace(/[^a-z0-9]+/gi, "-")}.txt`; + const commitId = await commitAndPush(remoteUrl, depth ? { depth } : {}, fileName); + assert.equal(commitId.length, 40, label); + // The receiver accepted the pack and moved its branch to our commit. + assert.equal(git(repo, "rev-parse", "main"), commitId, label); + assert.equal(git(repo, "cat-file", "-e", `${commitId}^{tree}`), "", label); + } + } finally { + if (server) server.close(); + rmSync(dir, { recursive: true, force: true }); + } +});