Line data Source code
1 : /*! 2 : * \file esys/repo/sharing/context_sharing.cpp 3 : * \brief Sync-time sharing context + apply (ADR-0039–0042) 4 : * 5 : * \cond 6 : * __legal_b__ 7 : * 8 : * Copyright (c) 2026 Michel Gillet 9 : * Distributed under the MIT License. 10 : * (See accompanying file LICENSE.txt or 11 : * copy at https://opensource.org/licenses/MIT) 12 : * 13 : * __legal_e__ 14 : * \endcond 15 : * 16 : */ 17 : 18 : #include "esys/repo/esysrepo_prec.h" 19 : #include "esys/repo/sharing/context.h" 20 : #include "esys/repo/sharing/discover.h" 21 : #include "esys/repo/sharing/gitmodules.h" 22 : #include "esys/repo/sharing/materializer.h" 23 : #include "esys/repo/configfolder.h" 24 : #include "esys/repo/gitbase.h" 25 : #include "esys/repo/git/remote.h" 26 : #include "esys/repo/manifest.h" 27 : #include "esys/repo/manifest/repository.h" 28 : 29 : #include <esys/log/logger_if.h> 30 : 31 : #include <boost/filesystem.hpp> 32 : 33 : #include <fstream> 34 : #include <map> 35 : #include <sstream> 36 : 37 : #include <nlohmann/json.hpp> 38 : 39 : namespace esys::repo::sharing 40 : { 41 : namespace 42 : { 43 : 44 4 : std::string abs_norm(const std::string &path) 45 : { 46 4 : boost::filesystem::path p(path); 47 4 : if (!p.is_absolute()) p = boost::filesystem::absolute(p); 48 8 : return p.lexically_normal().make_preferred().string(); 49 4 : } 50 : 51 1 : std::string parent_remote_url(GitBase &git) 52 : { 53 1 : std::vector<git::Remote> remotes; 54 1 : if (git.get_remotes(remotes).error() || remotes.empty()) return ""; 55 1 : for (const auto &r : remotes) 56 : { 57 1 : if (r.get_name() == "origin") return r.get_url(); 58 : } 59 0 : return remotes.front().get_url(); 60 1 : } 61 : 62 0 : std::string error_text(const Result &r, const std::string &fallback) 63 : { 64 0 : if (r.get_error_info() && !r.get_error_info()->get_text().empty()) return r.get_error_info()->get_text(); 65 0 : return fallback; 66 : } 67 : 68 : } // namespace 69 : 70 27 : Result Context::build(const Manifest &manifest) 71 : { 72 27 : Result r = m_registry.build(manifest); 73 27 : if (r.error()) return r; 74 27 : m_inventory.clear(); 75 27 : if (has_providers()) m_inventory.set_coverage(Coverage::Partial); 76 27 : return ESYSREPO_RESULT(ResultCode::OK); 77 27 : } 78 : 79 2 : ProviderRegistry &Context::get_registry() 80 : { 81 2 : return m_registry; 82 : } 83 : 84 0 : const ProviderRegistry &Context::get_registry() const 85 : { 86 0 : return m_registry; 87 : } 88 : 89 8 : Inventory &Context::get_inventory() 90 : { 91 8 : return m_inventory; 92 : } 93 : 94 0 : const Inventory &Context::get_inventory() const 95 : { 96 0 : return m_inventory; 97 : } 98 : 99 1 : std::mutex &Context::provider_lock(const std::string &provider_abs_path) 100 : { 101 1 : std::lock_guard<std::mutex> guard(m_locks_mutex); 102 1 : auto &slot = m_provider_locks[provider_abs_path]; 103 2 : if (!slot) slot = std::make_unique<std::mutex>(); 104 1 : return *slot; 105 1 : } 106 : 107 56 : bool Context::has_providers() const 108 : { 109 56 : return !m_registry.get_providers().empty(); 110 : } 111 : 112 1 : Result apply_for_consumer(Context &ctx, const std::string &workspace_abs, const std::string &consumer_rel_path, 113 : const std::string &consumer_abs_path, GitBase::GeneratorType git_generator, 114 : std::shared_ptr<log::Logger_if> logger) 115 : { 116 1 : if (!ctx.has_providers()) return ESYSREPO_RESULT(ResultCode::OK); 117 1 : if (git_generator == nullptr) return ESYSREPO_RESULT(ResultCode::GIT_IS_NULLPTR); 118 : 119 2 : boost::filesystem::path gm_path = boost::filesystem::path(consumer_abs_path) / ".gitmodules"; 120 1 : if (!boost::filesystem::exists(gm_path)) return ESYSREPO_RESULT(ResultCode::OK); 121 : 122 1 : GitmodulesFile gm; 123 1 : Result r = gm.parse_file(gm_path.string()); 124 1 : if (r.error()) return r; 125 1 : if (gm.get_entries().empty()) return ESYSREPO_RESULT(ResultCode::OK); 126 : 127 1 : auto consumer_git = git_generator(); 128 1 : if (!consumer_git) return ESYSREPO_RESULT(ResultCode::GIT_IS_NULLPTR); 129 1 : r = consumer_git->open(consumer_abs_path); 130 1 : if (r.error()) return r; 131 : 132 1 : std::map<std::string, std::string> head_links; 133 1 : std::map<std::string, std::string> index_links; 134 1 : r = consumer_git->list_gitlinks(head_links, "HEAD"); 135 1 : if (r.error()) head_links.clear(); 136 1 : r = consumer_git->list_index_gitlinks(index_links); 137 1 : if (r.error()) index_links.clear(); 138 : 139 : // Prefer index as worktree approximation when clean; discover skips disagreements 140 1 : std::map<std::string, std::string> wt_links = index_links; 141 : 142 1 : std::string parent_remote = parent_remote_url(*consumer_git); 143 1 : std::ifstream gmf(gm_path.string()); 144 1 : std::ostringstream gms; 145 1 : gms << gmf.rdbuf(); 146 1 : consumer_git->close(); 147 : 148 1 : std::vector<Occurrence> occs; 149 1 : r = discover_submodules(consumer_rel_path, parent_remote, gms.str(), head_links, index_links, wt_links, occs); 150 1 : if (r.error()) return r; 151 : 152 1 : ctx.get_inventory().mark_scope_inspected(consumer_rel_path); 153 : 154 1 : const std::string ws = abs_norm(workspace_abs); 155 2 : const std::string esysrepo_path = abs_norm((boost::filesystem::path(ws) / ".esysrepo").string()); 156 : 157 2 : for (auto &occ : occs) 158 : { 159 1 : ctx.get_inventory().upsert(occ); 160 1 : if (occ.get_materialization_status() == MaterializationStatus::Skipped) continue; 161 1 : if (occ.get_normalized_identity().empty()) continue; 162 : 163 1 : auto matched = ctx.get_registry().match(occ.get_normalized_identity()); 164 1 : if (matched.error()) 165 : { 166 0 : if (logger) logger->error(error_text(matched, "ambiguous provider")); 167 0 : return ESYSREPO_RESULT(matched); 168 : } 169 3 : if (!matched.get()) continue; // unmatched → submodule update path 170 : 171 1 : auto provider = matched.get(); 172 1 : auto repo = provider->get_repository(); 173 1 : if (!repo) continue; 174 : 175 2 : boost::filesystem::path provider_path = boost::filesystem::path(ws) / repo->get_path(); 176 1 : if (repo->get_path() == ".") provider_path = ws; 177 1 : const std::string provider_abs = abs_norm(provider_path.string()); 178 : 179 2 : boost::filesystem::path dest = boost::filesystem::path(consumer_abs_path) / occ.get_dependency_path(); 180 1 : const std::string dest_abs = abs_norm(dest.string()); 181 : 182 1 : std::lock_guard<std::mutex> lock(ctx.provider_lock(provider_abs)); 183 : 184 1 : auto provider_git = git_generator(); 185 1 : if (!provider_git) return ESYSREPO_RESULT(ResultCode::GIT_IS_NULLPTR); 186 1 : r = provider_git->open(provider_abs); 187 1 : if (r.error()) 188 : { 189 0 : if (logger) 190 0 : logger->error("sharing: cannot open provider '" + repo->get_path() + "' for " + occ.get_dependency_path()); 191 0 : return r; 192 : } 193 : 194 1 : Materializer mat; 195 2 : mat.set_git(provider_git); 196 1 : if (logger) mat.set_logger_if(logger); 197 1 : mat.set_consumer_root(consumer_abs_path); 198 1 : mat.set_provider_path(provider_abs); 199 3 : mat.set_forbidden_roots({esysrepo_path, provider_abs}); 200 : 201 1 : const std::string commit = occ.get_required_commit(); 202 1 : r = mat.materialize(dest_abs, commit); 203 1 : provider_git->close(); 204 : 205 1 : if (r.error()) 206 : { 207 0 : occ.set_materialization_status(MaterializationStatus::Error); 208 0 : occ.set_error(error_text(r, "materialize failed")); 209 0 : ctx.get_inventory().upsert(occ); 210 0 : if (logger) logger->error("sharing: materialize failed for " + occ.get_dependency_path()); 211 0 : return r; 212 : } 213 : 214 1 : occ.set_matched_provider_path(repo->get_path()); 215 1 : occ.set_materialization_status(MaterializationStatus::Worktree); 216 1 : ctx.get_inventory().upsert(occ); 217 1 : if (!mat.get_last_mismatch_warning().empty() && logger) logger->warn(mat.get_last_mismatch_warning()); 218 4 : } 219 : 220 1 : ctx.get_inventory().drop_stale_in_scope(consumer_rel_path); 221 1 : return ESYSREPO_RESULT(ResultCode::OK); 222 6 : } 223 : 224 1 : Result finalize_inventory(Context &ctx, ConfigFolder &config_folder, std::shared_ptr<log::Logger_if> logger) 225 : { 226 1 : if (!ctx.has_providers() && ctx.get_inventory().get_occurrences().empty()) 227 0 : return ESYSREPO_RESULT(ResultCode::OK); 228 : 229 1 : auto registered = ctx.get_registry().registered_identities(); 230 1 : auto dups = ctx.get_inventory().unregistered_duplicates(registered); 231 1 : for (const auto &pair : dups) 232 : { 233 0 : std::ostringstream msg; 234 0 : msg << "unregistered duplicate dependency identity '" << pair.first << "' (" << pair.second.size() 235 0 : << " occurrences):"; 236 0 : for (const auto &occ : pair.second) 237 : { 238 0 : msg << " [" << occ.get_consumer_path() << "/" << occ.get_dependency_path(); 239 0 : if (!occ.get_required_commit().empty()) msg << " @" << occ.get_required_commit().substr(0, 8); 240 0 : msg << "]"; 241 : } 242 0 : if (logger) logger->warn(msg.str()); 243 0 : } 244 : 245 : // Private inventory under .esysrepo (tool-private; ADR-0006 / 0042) 246 2 : boost::filesystem::path out = boost::filesystem::path(config_folder.get_path()) / "sharing-inventory.json"; 247 1 : nlohmann::json root; 248 1 : root["coverage"] = static_cast<int>(ctx.get_inventory().get_coverage()); 249 1 : root["occurrences"] = nlohmann::json::array(); 250 2 : for (const auto &occ : ctx.get_inventory().get_occurrences()) 251 : { 252 1 : nlohmann::json j; 253 2 : j["consumer"] = occ.get_consumer_path(); 254 2 : j["path"] = occ.get_dependency_path(); 255 2 : j["identity"] = occ.get_normalized_identity(); 256 2 : j["commit"] = occ.get_required_commit(); 257 2 : j["provider"] = occ.get_matched_provider_path(); 258 1 : j["status"] = static_cast<int>(occ.get_materialization_status()); 259 2 : j["error"] = occ.get_error(); 260 1 : root["occurrences"].push_back(j); 261 1 : } 262 1 : std::ofstream ofs(out.string()); 263 1 : if (!ofs) return ESYSREPO_RESULT(ResultCode::ERROR_OPENING_FILE, out.string()); 264 2 : ofs << root.dump(2) << "\n"; 265 1 : return ESYSREPO_RESULT(ResultCode::OK); 266 2 : } 267 : 268 : } // namespace esys::repo::sharing