// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® import std; import Crafter.Build; namespace fs = std::filesystem; using namespace Crafter; // A fetched dependency's submodules have to match the revision it was fetched // at. They used to come from the remote's default branch: a fresh clone was // `git clone --recursive`, which checks out the default branch's submodules, // and the `switch` or `checkout` to the requested branch or commit that came // after it left them where they were. A branch that was pulled forward kept // the submodules of its old tip as well. So pinning msquic to a release built // that release against the main branch's quictls. // // The test makes a repository whose submodule moves between two of its // commits, then fetches it four ways through GitFetch: // - pinned to the older commit; // - by a branch at the older commit; // - by a branch that is then moved to the newer commit and fetched again; // - pinned again after the cached clone's submodule was checked out // elsewhere, as an older crafter-build left it. // Everything is local; XDG_CACHE_HOME keeps the clones out of the real cache. extern "C" int setenv(const char* name, const char* value, int overwrite); namespace { std::int32_t failures = 0; void Check(bool cond, std::string_view msg) { if (!cond) { std::println(std::cerr, "FAIL: {}", msg); ++failures; } } // Runs git in `dir`; the output, or an exception naming the command. std::string Git(const fs::path& dir, std::string_view args) { const std::string command = std::format("git -C {} {}", ShellQuote(dir.string()), args); auto result = RunCommandWithTimeout(command, std::chrono::seconds(60)); if (result.exitCode != 0) throw std::runtime_error(std::format("{} failed: {}", command, result.output)); while (!result.output.empty() && (result.output.back() == '\n' || result.output.back() == '\r')) result.output.pop_back(); return result.output; } void Write(const fs::path& file, std::string_view text) { std::ofstream(file, std::ios::binary | std::ios::trunc) << text; } std::string Read(const fs::path& file) { std::ifstream stream(file, std::ios::binary); return std::string(std::istreambuf_iterator(stream), std::istreambuf_iterator()); } } int main() { const fs::path scratch = fs::temp_directory_path() / "crafter-build-git-submodules"; fs::remove_all(scratch); fs::create_directories(scratch); setenv("XDG_CACHE_HOME", (scratch / "cache").c_str(), 1); // A known git: no user or system configuration, an identity for the // commits, and file:// allowed for submodules (git refuses it by default), // also for the git that GitFetch runs. setenv("GIT_CONFIG_NOSYSTEM", "1", 1); setenv("GIT_CONFIG_GLOBAL", "/dev/null", 1); setenv("GIT_CONFIG_COUNT", "1", 1); setenv("GIT_CONFIG_KEY_0", "protocol.file.allow", 1); setenv("GIT_CONFIG_VALUE_0", "always", 1); for (const char* name : { "GIT_AUTHOR_NAME", "GIT_COMMITTER_NAME" }) setenv(name, "crafter-build test", 1); for (const char* name : { "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_EMAIL" }) setenv(name, "test@example.invalid", 1); // The submodule: "one", then "two". const fs::path sub = scratch / "sub"; fs::create_directories(sub); Git(sub, "init -q -b main"); Write(sub / "value.txt", "one"); Git(sub, "add value.txt"); Git(sub, "commit -q -m one"); const std::string one = Git(sub, "rev-parse HEAD"); Write(sub / "value.txt", "two"); Git(sub, "commit -q -am two"); const std::string two = Git(sub, "rev-parse HEAD"); // The superproject: `older` has the submodule at "one", main at "two". const fs::path super = scratch / "super"; fs::create_directories(super); Git(super, "init -q -b main"); Git(super, std::format("submodule --quiet add {} lib", ShellQuote(std::format("file://{}", sub.string())))); Git(super / "lib", std::format("checkout -q {}", one)); Git(super, "add lib"); Git(super, "commit -q -m older"); const std::string older = Git(super, "rev-parse HEAD"); Git(super, "branch older"); Git(super, "branch track"); Git(super / "lib", std::format("checkout -q {}", two)); Git(super, "add lib"); Git(super, "commit -q -m newer"); const std::string url = std::format("file://{}", super.string()); // ── Pinned to the older commit ─────────────────────────────────────────── const fs::path pinned = GitFetch({ .url = url, .commit = older }); Check(Read(pinned / "lib" / "value.txt") == "one", std::format("a commit pin gets that commit's submodule (lib says {})", Read(pinned / "lib" / "value.txt"))); // ── By a branch at the older commit ────────────────────────────────────── const fs::path branch = GitFetch({ .url = url, .branch = "older" }); Check(Read(branch / "lib" / "value.txt") == "one", std::format("a branch gets its own tip's submodule (lib says {})", Read(branch / "lib" / "value.txt"))); // ── A tracked branch moving forward ────────────────────────────────────── const fs::path tracked = GitFetch({ .url = url, .branch = "track" }); Check(Read(tracked / "lib" / "value.txt") == "one", "a tracked branch starts with its tip's submodule"); Git(super, "branch -f track main"); const fs::path pulled = GitFetch({ .url = url, .branch = "track" }); Check(pulled == tracked && Read(pulled / "lib" / "value.txt") == "two", std::format("pulling it forward moves the submodule along (lib says {})", Read(pulled / "lib" / "value.txt"))); // ── A cached clone with its submodule out of step ──────────────────────── Git(pinned / "lib", std::format("checkout -q {}", two)); const fs::path repaired = GitFetch({ .url = url, .commit = older }); Check(repaired == pinned && Read(repaired / "lib" / "value.txt") == "one", std::format("a cached clone at the pin gets its submodule put back (lib says {})", Read(repaired / "lib" / "value.txt"))); std::error_code ec; fs::remove_all(scratch, ec); if (failures > 0) { std::println(std::cerr, "{} assertions failed", failures); return 1; } return 0; }