LCOV - code coverage report
Current view: top level - src/esys/repo/sharing - context_sharing.cpp (source / functions) Hit Total Coverage
Test: esysrepo_coverage.info Lines: 124 152 81.6 %
Date: 2026-11-07 22:18:26 Functions: 9 12 75.0 %

          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

Generated by: LCOV version 1.14