#include "nix/store/store-open.hh" #include "nix/store/globals.hh" #include "nix/store/dummy-store.hh" #include "nix/fetchers/fetch-settings.hh" #include "nix/fetchers/fetchers.hh" #include "nix/fetchers/git-utils.hh" #include #include #include namespace { template struct Deleter { void operator()(T * p) const { if (p) F(p); } }; template using Handle = std::unique_ptr>; using RepoHandle = Handle; using IndexHandle = Handle; using TreeHandle = Handle; using SigHandle = Handle; using RefHandle = Handle; using CommitHandle = Handle; using SubmoduleHandle = Handle; #define CHECK_LIBGIT(expr) ASSERT_TRUE((expr) >= 0) << git_error_last() static void commitAll(git_repository * repo, const char * msg) { IndexHandle idx; { git_index * raw = nullptr; CHECK_LIBGIT(git_repository_index(&raw, repo)); idx.reset(raw); } CHECK_LIBGIT(git_index_add_all(idx.get(), nullptr, 0, nullptr, nullptr)); CHECK_LIBGIT(git_index_write(idx.get())); git_oid treeId{}; CHECK_LIBGIT(git_index_write_tree(&treeId, idx.get())); TreeHandle tree; { git_tree * raw = nullptr; CHECK_LIBGIT(git_tree_lookup(&raw, repo, &treeId)); tree.reset(raw); } SigHandle sig; { git_signature * raw = nullptr; CHECK_LIBGIT(git_signature_now(&raw, "you", "you@example.com")); sig.reset(raw); } git_oid commitId{}; if (git_repository_is_empty(repo) == 1) { CHECK_LIBGIT(git_commit_create_v(&commitId, repo, "HEAD", sig.get(), sig.get(), nullptr, msg, tree.get(), 0)); CHECK_LIBGIT(git_reference_create(nullptr, repo, "refs/heads/main", &commitId, true, nullptr)); CHECK_LIBGIT(git_repository_set_head(repo, "refs/heads/main")); } else { RefHandle head; { git_reference * raw = nullptr; CHECK_LIBGIT(git_repository_head(&raw, repo)); head.reset(raw); } CommitHandle parent; { git_commit * raw = nullptr; CHECK_LIBGIT(git_commit_lookup(&raw, repo, git_reference_target(head.get()))); parent.reset(raw); } const git_commit * parents[] = {parent.get()}; CHECK_LIBGIT(git_commit_create( &commitId, repo, "HEAD", sig.get(), sig.get(), /*message_encoding=*/nullptr, msg, tree.get(), /*parent_count=*/1, &parents[0])); } } } // namespace using namespace nix; class GitTest : public ::testing::Test { std::unique_ptr delTmpDir; protected: std::filesystem::path tmpDir; void SetUp() override { tmpDir = createTempDir(); delTmpDir = std::make_unique(tmpDir, /*recursive=*/true); nix::initLibStore(/*loadConfig=*/false); git_libgit2_init(); } void TearDown() override { delTmpDir.reset(); } }; // Regression test for https://github.com/NixOS/nix/issues/13215 TEST_F(GitTest, submodulePeriodSupport) { auto storePath = tmpDir / "store"; auto repoPath = tmpDir / "repo"; auto submodulePath = tmpDir / "submodule"; // Set up our git directories: one top level and a submodule // the submodule in the .gitmodules has the branch listed as '.' // 1) Create sub repo { git_repository * raw = nullptr; CHECK_LIBGIT(git_repository_init(&raw, submodulePath.string().c_str(), /*is_bare=*/0)); RepoHandle sub(raw); writeFile(submodulePath / "lib.txt", "hello from submodule\n"); commitAll(sub.get(), "init sub"); } // 2) Create super repo RepoHandle super; { git_repository * raw = nullptr; CHECK_LIBGIT(git_repository_init(&raw, repoPath.string().c_str(), /*is_bare=*/0)); super.reset(raw); } writeFile(repoPath / "README.md", "# super\n"); commitAll(super.get(), "init super"); // 3) Add submodule at deps/sub { git_repository * raw = nullptr; git_clone_options cloneOpts = GIT_CLONE_OPTIONS_INIT; // clone from local subPath into superPath/deps/sub CHECK_LIBGIT( git_clone(&raw, submodulePath.string().c_str(), (repoPath / "deps" / "sub").string().c_str(), &cloneOpts)); RepoHandle sub(raw); } // 4) Add submodule and set branch="." SubmoduleHandle sm; { git_submodule * raw = nullptr; CHECK_LIBGIT(git_submodule_add_setup( &raw, super.get(), "../submodule", "deps/sub", /*use_gitlink=*/1)); sm.reset(raw); } CHECK_LIBGIT(git_submodule_set_branch(super.get(), git_submodule_name(sm.get()), /*branch=*/".")); CHECK_LIBGIT(git_submodule_sync(sm.get())); // 5) Finalize now that the worktree exists; libgit2 can read its HEAD OID CHECK_LIBGIT(git_submodule_add_finalize(sm.get())); // 6) Commit the addition in super commitAll(super.get(), "Add submodule with branch='.'"); auto store = [] { auto cfg = make_ref(StoreReference::Params{}); cfg->readOnly = false; return cfg->openStore(); }(); auto settings = fetchers::Settings{}; auto input = fetchers::Input::fromAttrs( settings, { {"url", "file://" + repoPath.string()}, {"submodules", Explicit{true}}, {"type", "git"}, {"ref", "main"}, }); auto [accessor, i] = input.getAccessor(settings, *store); ASSERT_EQ(accessor->readFile(CanonPath("deps/sub/lib.txt")), "hello from submodule\n"); }