// Copyright 2022 Huawei Cloud Computing Technology Co., Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "src/other_tools/root_maps/fpath_git_map.hpp" #include #include // std::move #include #include "fmt/core.h" #include "src/buildtool/common/artifact_digest_factory.hpp" #include "src/buildtool/crypto/hash_function.hpp" #include "src/buildtool/file_system/file_root.hpp" #include "src/buildtool/file_system/file_system_manager.hpp" #include "src/buildtool/file_system/git_cas.hpp" #include "src/buildtool/multithreading/task_system.hpp" #include "src/buildtool/storage/fs_utils.hpp" #include "src/other_tools/git_operations/git_ops_types.hpp" #include "src/other_tools/git_operations/git_repo_remote.hpp" #include "src/utils/cpp/expected.hpp" #include "src/utils/cpp/tmp_dir.hpp" namespace { /// \brief Does the serve endpoint checks and sets the workspace root. /// It guarantees the logger is called exactly once with fatal on failure, and /// the setter on success. void CheckServeAndSetRoot(std::string const& tree_id, std::string const& repo_root, bool absent, ServeApi const* serve, FilePathGitMap::SetterPtr const& ws_setter, FilePathGitMap::LoggerPtr const& logger) { // if serve endpoint is given, try to ensure it has this tree available to // be able to build against it. If root is not absent, do not fail if we // don't have a suitable remote endpoint, but warn user nonetheless. if (serve != nullptr) { auto const has_tree = serve->CheckRootTree(tree_id); if (not has_tree) { (*logger)(fmt::format("Checking that the serve endpoint knows tree " "{} failed.", tree_id), /*fatal=*/true); return; } if (not *has_tree) { auto digest = ArtifactDigestFactory::Create(HashFunction::Type::GitSHA1, tree_id, /*size_unknown=*/0, /*is_tree=*/true); if (not digest) { (*logger)(std::move(digest).error(), /*fatal=*/true); return; } auto uploaded = serve->UploadTree(*digest, repo_root); // only enforce root setup on the serve endpoint if root is absent if (not uploaded.has_value()) { bool const fatal = absent or not uploaded.error().IsSyncError(); (*logger)(std::move(uploaded).error().Message(), fatal); if (fatal) { return; } } } } else { if (absent) { // give warning (*logger)(fmt::format("Workspace root {} marked absent but no " "suitable serve endpoint provided.", tree_id), /*fatal=*/false); } } // set the workspace root auto root = nlohmann::json::array({FileRoot::kGitTreeMarker, tree_id}); if (not absent) { root.emplace_back(repo_root); } (*ws_setter)(std::move(root)); } void ResolveFilePathTree( std::string const& repo_root, std::string const& target_path, std::string const& tree_hash, std::optional const& pragma_special, GitCASPtr const& source_cas, GitCASPtr const& target_cas, bool absent, gsl::not_null const& critical_git_op_map, gsl::not_null const& resolve_symlinks_map, ServeApi const* serve, gsl::not_null const& native_storage_config, gsl::not_null const& ts, FilePathGitMap::SetterPtr const& ws_setter, FilePathGitMap::LoggerPtr const& logger) { if (pragma_special) { // get the resolved tree auto tree_id_file = StorageUtils::GetResolvedTreeIDFile( *native_storage_config, tree_hash, *pragma_special); if (FileSystemManager::Exists(tree_id_file)) { // read resolved tree id auto resolved_tree_id = FileSystemManager::ReadFile(tree_id_file); if (not resolved_tree_id) { (*logger)( fmt::format("Failed to read resolved tree id from file {}", tree_id_file.string()), /*fatal=*/true); return; } // if serve endpoint is given, try to ensure it has this tree // available to be able to build against it; the tree is resolved, // so it is in our Git cache CheckServeAndSetRoot(*resolved_tree_id, native_storage_config->GitRoot().string(), absent, serve, ws_setter, logger); } else { // resolve tree resolve_symlinks_map->ConsumeAfterKeysReady( ts, {GitObjectToResolve(tree_hash, ".", *pragma_special, /*known_info=*/std::nullopt, source_cas, target_cas)}, [critical_git_op_map, tree_hash, tree_id_file, absent, serve, native_storage_config, ts, ws_setter, logger](auto const& hashes) { auto const& resolved_tree_id = hashes[0]->id; // keep tree alive in Git cache via a tagged commit GitOpKey op_key = { .params = { native_storage_config ->GitRoot(), // target_path resolved_tree_id, // git_hash "Keep referenced tree alive" // message }, .op_type = GitOpType::KEEP_TREE}; critical_git_op_map->ConsumeAfterKeysReady( ts, {std::move(op_key)}, [resolved_tree_id, tree_id_file, absent, serve, native_storage_config, ws_setter, logger](auto const& values) { GitOpValue op_result = *values[0]; // check flag if (not op_result.result) { (*logger)("Keep tree failed", /*fatal=*/true); return; } // cache the resolved tree in the CAS map if (not StorageUtils::WriteTreeIDFile( tree_id_file, resolved_tree_id)) { (*logger)( fmt::format("Failed to write resolved tree " "id to file {}", tree_id_file.string()), /*fatal=*/true); return; } // if serve endpoint is given, try to ensure it has // this tree available to be able to build against // it; the resolved tree is in the Git cache CheckServeAndSetRoot( resolved_tree_id, native_storage_config->GitRoot().string(), absent, serve, ws_setter, logger); }, [logger, target_path = native_storage_config->GitRoot()]( auto const& msg, bool fatal) { (*logger)( fmt::format("While running critical Git op " "KEEP_TREE for target {}:\n{}", target_path.string(), msg), fatal); }); }, [logger, target_path](auto const& msg, bool fatal) { (*logger)(fmt::format( "While resolving symlinks for target {}:\n{}", target_path, msg), fatal); }); } } else { // tree needs no further processing; // if serve endpoint is given, try to ensure it has this tree available // to be able to build against it CheckServeAndSetRoot( tree_hash, repo_root, absent, serve, ws_setter, logger); } } } // namespace auto CreateFilePathGitMap( std::optional const& current_subcmd, gsl::not_null const& critical_git_op_map, gsl::not_null const& import_to_git_map, gsl::not_null const& resolve_symlinks_map, ServeApi const* serve, gsl::not_null const& native_storage_config, std::size_t jobs, std::string const& multi_repo_tool_name, std::string const& build_tool_name) -> FilePathGitMap { auto dir_to_git = [current_subcmd, critical_git_op_map, import_to_git_map, resolve_symlinks_map, serve, native_storage_config, multi_repo_tool_name, build_tool_name](auto ts, auto setter, auto logger, auto /*unused*/, auto const& key) { // setup wrapped logger auto wrapped_logger = std::make_shared( [logger](auto const& msg, bool fatal) { (*logger)( fmt::format("While getting repo root from path:\n{}", msg), fatal); }); // check if path is a part of a git repo auto repo_root = GitRepoRemote::GetRepoRootFromPath( key.fpath, wrapped_logger); // static function if (not repo_root) { return; } if (not repo_root->empty()) { // if repo root found // get head commit GitOpKey op_key = {.params = { *repo_root, // target_path "", // git_hash }, .op_type = GitOpType::GET_HEAD_ID}; critical_git_op_map->ConsumeAfterKeysReady( ts, {std::move(op_key)}, [fpath = key.fpath, pragma_special = key.pragma_special, absent = key.absent, repo_root = std::move(*repo_root), critical_git_op_map, resolve_symlinks_map, serve, native_storage_config, ts, setter, logger](auto const& values) { GitOpValue op_result = *values[0]; // check flag if (not op_result.result) { (*logger)("Get Git head id failed", /*fatal=*/true); return; } auto git_repo = GitRepoRemote::Open( op_result.git_cas); // link fake repo to odb if (not git_repo) { (*logger)(fmt::format("Could not open repository {}", repo_root.string()), /*fatal=*/true); return; } // setup wrapped logger auto wrapped_logger = std::make_shared( [logger](auto const& msg, bool fatal) { (*logger)( fmt::format("While getting subtree from " "path:\n{}", msg), fatal); }); // get tree id auto tree_hash = git_repo->GetSubtreeFromPath( fpath, *op_result.result, wrapped_logger); if (not tree_hash) { return; } // resolve tree and set workspace root; tree gets resolved // from source repo into the Git cache, which we first need // to ensure is initialized GitOpKey op_key = {.params = { native_storage_config ->GitRoot(), // target_path "", // git_hash std::nullopt, // message std::nullopt, // source_path true // init_bare }, .op_type = GitOpType::ENSURE_INIT}; critical_git_op_map->ConsumeAfterKeysReady( ts, {std::move(op_key)}, [repo_root, fpath, tree_hash, pragma_special, source_cas = op_result.git_cas, absent, critical_git_op_map, resolve_symlinks_map, serve, native_storage_config, ts, setter, logger](auto const& values) { GitOpValue op_result = *values[0]; // check flag if (not op_result.result) { (*logger)("Git init failed", /*fatal=*/true); return; } ResolveFilePathTree( repo_root.string(), fpath.string(), *tree_hash, pragma_special, source_cas, op_result.git_cas, /*just_git_cas*/ absent, critical_git_op_map, resolve_symlinks_map, serve, native_storage_config, ts, setter, logger); }, [logger, target_path = native_storage_config->GitRoot()]( auto const& msg, bool fatal) { (*logger)( fmt::format("While running critical Git op " "ENSURE_INIT for target {}:\n{}", target_path.string(), msg), fatal); }); }, [logger, target_path = *repo_root](auto const& msg, bool fatal) { (*logger)(fmt::format("While running critical Git op " "GET_HEAD_ID for target {}:\n{}", target_path.string(), msg), fatal); }); } else { // warn if import to git is inefficient if (current_subcmd) { (*logger)(fmt::format("Inefficient Git import of file " "path \'{}\'.\nPlease consider using " "\'{} setup\' and \'{} {}\' " "separately to cache the output.", key.fpath.string(), multi_repo_tool_name, build_tool_name, *current_subcmd), /*fatal=*/false); } // it's not a git repo, so import it to git cache auto tmp_dir = native_storage_config->CreateTypedTmpDir("file"); if (not tmp_dir) { (*logger)("Failed to create import-to-git tmp directory!", /*fatal=*/true); return; } // copy folder content to tmp dir if (not FileSystemManager::CopyDirectoryImpl(key.fpath, tmp_dir->GetPath())) { (*logger)( fmt::format("Failed to copy content from directory {}", key.fpath.string()), /*fatal=*/true); return; } // do import to git CommitInfo c_info{tmp_dir->GetPath(), "file", key.fpath}; import_to_git_map->ConsumeAfterKeysReady( ts, {std::move(c_info)}, // tmp_dir passed, to ensure folder is not removed until import // to git is done [tmp_dir, fpath = key.fpath, pragma_special = key.pragma_special, absent = key.absent, critical_git_op_map, resolve_symlinks_map, serve, native_storage_config, ts, setter, logger](auto const& values) { // check for errors if (not values[0]->second) { (*logger)("Importing to git failed", /*fatal=*/true); return; } // we only need the tree std::string tree = values[0]->first; // resolve tree and set workspace root; // we work on the Git CAS directly ResolveFilePathTree( native_storage_config->GitRoot().string(), fpath.string(), tree, pragma_special, values[0]->second, /*source_cas*/ values[0]->second, /*target_cas*/ absent, critical_git_op_map, resolve_symlinks_map, serve, native_storage_config, ts, setter, logger); }, [logger, target_path = key.fpath](auto const& msg, bool fatal) { (*logger)( fmt::format("While importing target {} to git:\n{}", target_path.string(), msg), fatal); }); } }; return AsyncMapConsumer(dir_to_git, jobs); }