diff options
-rw-r--r-- | src/buildtool/file_system/git_repo.cpp | 123 | ||||
-rw-r--r-- | src/buildtool/file_system/git_repo.hpp | 9 | ||||
-rw-r--r-- | test/buildtool/file_system/TARGETS | 1 | ||||
-rw-r--r-- | test/buildtool/file_system/git_repo.test.cpp | 30 |
4 files changed, 163 insertions, 0 deletions
diff --git a/src/buildtool/file_system/git_repo.cpp b/src/buildtool/file_system/git_repo.cpp index c7ecddda..4fefc76b 100644 --- a/src/buildtool/file_system/git_repo.cpp +++ b/src/buildtool/file_system/git_repo.cpp @@ -934,6 +934,129 @@ auto GitRepo::FetchFromPath(std::shared_ptr<git_config> cfg, #endif // BOOTSTRAP_BUILD_TOOL } +auto GitRepo::KeepTree(std::string const& tree_id, + std::string const& message, + anon_logger_ptr const& logger) noexcept -> bool { +#ifdef BOOTSTRAP_BUILD_TOOL + return false; +#else + try { + // only possible for real repository! + if (IsRepoFake()) { + (*logger)("cannot commit and tag a tree using a fake repository!", + true /*fatal*/); + return false; + } + // share the odb lock + std::shared_lock lock{GetGitCAS()->mutex_}; + + // get tree oid + git_oid tree_oid; + if (git_oid_fromstr(&tree_oid, tree_id.c_str()) != 0) { + (*logger)(fmt::format("tree ID parsing in git repository {} failed " + "with:\n{}", + GetGitCAS()->git_path_.string(), + GitLastError()), + true /*fatal*/); + return false; + } + // get tree object from oid + git_object* target_ptr{nullptr}; + if (git_object_lookup( + &target_ptr, repo_->Ptr(), &tree_oid, GIT_OBJECT_TREE) != 0) { + (*logger)(fmt::format("object lookup for tree {} in repository " + "{} failed with:\n{}", + tree_id, + GetGitCAS()->git_path_.string(), + GitLastError()), + true /*fatal*/); + git_object_free(target_ptr); + return false; + } + auto target = std::unique_ptr<git_object, decltype(&object_closer)>( + target_ptr, object_closer); + + // set signature + git_signature* signature_ptr{nullptr}; + if (git_signature_new( + &signature_ptr, "Nobody", "nobody@example.org", 0, 0) != 0) { + (*logger)( + fmt::format("creating signature in git repository {} failed " + "with:\n{}", + GetGitCAS()->git_path_.string(), + GitLastError()), + true /*fatal*/); + // cleanup resources + git_signature_free(signature_ptr); + return false; + } + auto signature = + std::unique_ptr<git_signature, decltype(&signature_closer)>( + signature_ptr, signature_closer); + + // create tag + git_oid oid; + auto name = fmt::format("keep-{}", tree_id); + git_strarray tag_names{}; + + // check if tag hasn't already been added by another process + if (git_tag_list_match(&tag_names, name.c_str(), repo_->Ptr()) == 0 and + tag_names.count > 0) { + git_strarray_dispose(&tag_names); + return true; // success! + } + git_strarray_dispose(&tag_names); // free any allocated unused space + + size_t max_attempts = kGitLockNumTries; // number of tries + int err = 0; + std::string err_mess{}; + while (max_attempts > 0) { + --max_attempts; + err = git_tag_create(&oid, + repo_->Ptr(), + name.c_str(), + target.get(), /*tree*/ + signature.get(), /*tagger*/ + message.c_str(), + 1 /*force*/); + if (err == 0) { + return true; // success! + } + err_mess = GitLastError(); // store last error message + // only retry if failure is due to locking + if (err != GIT_ELOCKED) { + break; + } + // check if tag hasn't already been added by another process + if (git_tag_list_match(&tag_names, name.c_str(), repo_->Ptr()) == + 0 and + tag_names.count > 0) { + git_strarray_dispose(&tag_names); + return true; // success! + } + git_strarray_dispose( + &tag_names); // free any allocated unused space + // tag still not in, so sleep and try again + std::this_thread::sleep_for( + std::chrono::milliseconds(kGitLockWaitTime)); + } + (*logger)(fmt::format("tag creation for tree {} in git repository {} " + "failed with:\n{}", + tree_id, + GetGitCAS()->git_path_.string(), + err_mess), + true /*fatal*/); + return false; + } catch (std::exception const& ex) { + Logger::Log(LogLevel::Error, + "keep tree {} failed with:\n{}", + tree_id, + ex.what()); + return false; + } +#endif // BOOTSTRAP_BUILD_TOOL +} + auto GitRepo::GetSubtreeFromCommit(std::string const& commit, std::string const& subdir, anon_logger_ptr const& logger) noexcept diff --git a/src/buildtool/file_system/git_repo.hpp b/src/buildtool/file_system/git_repo.hpp index 60353a00..54706e41 100644 --- a/src/buildtool/file_system/git_repo.hpp +++ b/src/buildtool/file_system/git_repo.hpp @@ -183,6 +183,15 @@ class GitRepo { anon_logger_ptr const& logger) noexcept -> bool; + /// \brief Ensure given tree is kept alive via a tag. It is expected that + /// the tree is part of the repository already. + /// Only possible with real repository and thus non-thread-safe. + /// Returns success flag. + /// It guarantees the logger is called exactly once with fatal if failure. + [[nodiscard]] auto KeepTree(std::string const& tree_id, + std::string const& message, + anon_logger_ptr const& logger) noexcept -> bool; + /// \brief Get the tree id of a subtree given the root commit /// Calling it from a fake repository allows thread-safe use. /// Returns an error + data union, where at index 0 is a flag stating the diff --git a/test/buildtool/file_system/TARGETS b/test/buildtool/file_system/TARGETS index 8121b39a..e63bb06d 100644 --- a/test/buildtool/file_system/TARGETS +++ b/test/buildtool/file_system/TARGETS @@ -123,6 +123,7 @@ , ["@", "src", "src/buildtool/logging", "log_level"] , ["@", "src", "src/buildtool/logging", "logging"] , ["@", "src", "src/utils/cpp", "atomic"] + , ["@", "src", "src/utils/cpp", "hex_string"] , ["utils", "shell_quoting"] ] , "stage": ["test", "buildtool", "file_system"] diff --git a/test/buildtool/file_system/git_repo.test.cpp b/test/buildtool/file_system/git_repo.test.cpp index a6edfc8a..67ad19cd 100644 --- a/test/buildtool/file_system/git_repo.test.cpp +++ b/test/buildtool/file_system/git_repo.test.cpp @@ -31,6 +31,7 @@ #include "src/buildtool/logging/log_level.hpp" #include "src/buildtool/logging/logger.hpp" #include "src/utils/cpp/atomic.hpp" +#include "src/utils/cpp/hex_string.hpp" #include "test/utils/shell_quoting.hpp" namespace { @@ -44,6 +45,9 @@ auto const kBazId = std::string{"1868f82682c290f0b1db3cacd092727eef1fa57f"}; auto const kFooId = std::string{"19102815663d23f8b75a47e7a01965dcdc96468c"}; auto const kBarId = std::string{"ba0e162e1c47469e3fe4b393a8bf8c569f302116"}; +auto const kFooBarTreeId = + std::string{"27b32561185c2825150893774953906c6daa6798"}; + } // namespace class TestUtils { @@ -240,6 +244,32 @@ TEST_CASE("Single-threaded real repository local operations", "[git_repo]") { CHECK(repo_fetch_branch->FetchFromPath( nullptr, *path_fetch_branch, "master", logger)); } + + SECTION("Tag tree") { + auto repo_tag_path = TestUtils::CreateTestRepo(true); + REQUIRE(repo_tag_path); + auto repo_tag = GitRepo::Open(*repo_tag_path); + REQUIRE(repo_tag); + CHECK_FALSE(repo_tag->IsRepoFake()); + + // tag tree already root of a commit + CHECK(repo_tag->KeepTree(kRootId, "test tag 1", logger)); + + // tag tree part of another commit + CHECK(repo_tag->KeepTree(kBazId, "test tag 2", logger)); + + // tag uncommitted tree + auto foo_bar = GitRepo::tree_entries_t{ + {FromHexString(kFooId).value_or<std::string>({}), + {GitRepo::tree_entry_t{"foo", ObjectType::File}}}, + {FromHexString(kBarId).value_or<std::string>({}), + {GitRepo::tree_entry_t{"bar", ObjectType::Executable}}}}; + auto foo_bar_id = repo_tag->CreateTree(foo_bar); + REQUIRE(foo_bar_id); + auto tree_id = ToHexString(*foo_bar_id); + CHECK(tree_id == kFooBarTreeId); + CHECK(repo_tag->KeepTree(tree_id, "test tag 3", logger)); + } } // NOTE: "fake" repo ops tests split into two batches as workaround for |