LCOV - code coverage report
Current view: top level - src/esys/repo/exe - nested.cpp (source / functions) Hit Total Coverage
Test: esysrepo_coverage.info Lines: 107 150 71.3 %
Date: 2026-11-07 22:18:26 Functions: 10 12 83.3 %

          Line data    Source code
       1             : /*!
       2             :  * \file esys/repo/exe/nested.cpp
       3             :  * \brief Nested workspace helpers (ADR-0043)
       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/exe/nested.h"
      20             : #include "esys/repo/exe/cmdinit.h"
      21             : #include "esys/repo/exe/cmdsync.h"
      22             : #include "esys/repo/configfolder.h"
      23             : #include "esys/repo/manifest/syncrepos.h"
      24             : 
      25             : #include <boost/filesystem.hpp>
      26             : 
      27             : namespace esys::repo::exe
      28             : {
      29             : namespace
      30             : {
      31             : 
      32           4 : std::string normalize_abs(const std::string &path)
      33             : {
      34           8 :     return boost::filesystem::absolute(path).lexically_normal().make_preferred().string();
      35             : }
      36             : 
      37           0 : class NestedHostCmd : public Cmd
      38             : {
      39             : public:
      40           1 :     NestedHostCmd()
      41           1 :         : Cmd("nested-host")
      42             :     {
      43           1 :         set_print_cmd_name_by_base(false);
      44           1 :         set_print_result_summary(false);
      45           1 :     }
      46             : 
      47           1 :     Result open_folder()
      48             :     {
      49           1 :         return open_esysrepo_folder();
      50             :     }
      51             : 
      52           1 :     Result load()
      53             :     {
      54           1 :         return load_manifest();
      55             :     }
      56             : 
      57             : protected:
      58           0 :     Result impl_run() override
      59             :     {
      60           0 :         return ESYSREPO_RESULT(ResultCode::OK);
      61             :     }
      62             : };
      63             : 
      64             : } // namespace
      65             : 
      66          66 : std::vector<std::shared_ptr<manifest::Repository>> collect_nested_repositories(const Manifest &manifest)
      67             : {
      68          66 :     std::vector<std::shared_ptr<manifest::Repository>> out;
      69         155 :     for (auto location : manifest.get_locations())
      70             :     {
      71          89 :         if (location == nullptr) continue;
      72         524 :         for (auto repo : location->get_repos())
      73             :         {
      74         435 :             if (repo != nullptr && repo->is_nested_workspace()) out.push_back(repo);
      75         435 :         }
      76          89 :     }
      77          66 :     return out;
      78           0 : }
      79             : 
      80           2 : std::string nested_child_abs_path(const std::string &parent_workspace, const std::string &repo_path)
      81             : {
      82           2 :     boost::filesystem::path p = parent_workspace;
      83           2 :     p /= repo_path;
      84           2 :     return normalize_abs(p.string());
      85           2 : }
      86             : 
      87           3 : bool nested_workspace_initialized(const std::string &child_path)
      88             : {
      89           3 :     return ConfigFolder::is_config_folder(child_path);
      90             : }
      91             : 
      92           3 : std::string find_nested_local_manifest(const std::string &child_path)
      93             : {
      94           3 :     const char *names[] = {".esysrepo.manifest", ".esysrepo.manifest.xml", ".esysrepo.manifest.json"};
      95           6 :     for (const char *name : names)
      96             :     {
      97          12 :         boost::filesystem::path candidate = boost::filesystem::path(child_path) / name;
      98           9 :         if (boost::filesystem::exists(candidate) && boost::filesystem::is_regular_file(candidate))
      99           6 :             return candidate.lexically_normal().make_preferred().string();
     100           6 :     }
     101           3 :     return {};
     102             : }
     103             : 
     104           3 : void prepare_nested_cmd(Cmd &parent, Cmd &child, const std::string &child_abs_path)
     105             : {
     106           3 :     child.set_workspace_path(child_abs_path);
     107           3 :     child.set_git(parent.get_git());
     108           3 :     child.set_job_count(parent.get_job_count());
     109           3 :     child.set_debug(parent.get_debug());
     110           3 :     child.set_logger_if(parent.get_logger_if());
     111           3 :     if (parent.get_console_os() != nullptr) child.set_console_os(parent.get_console_os());
     112           3 :     child.set_nested_context(parent.get_nested_context());
     113           3 : }
     114             : 
     115           0 : Result sync_nested_parent_path(Cmd &parent, const std::string &repo_path)
     116             : {
     117           0 :     Result result = parent.ensure_manifest_loaded();
     118           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     119             : 
     120           0 :     manifest::SyncRepos sync_repos;
     121           0 :     sync_repos.set_folders_to_sync({repo_path});
     122           0 :     sync_repos.set_job_count(parent.get_job_count());
     123           0 :     sync_repos.set_logger_if(parent.get_logger_if());
     124           0 :     sync_repos.set_config_folder(parent.get_config_folder());
     125           0 :     sync_repos.set_git(parent.get_git());
     126           0 :     sync_repos.set_manifest(parent.get_manifest());
     127           0 :     sync_repos.set_tui(false);
     128             : 
     129           0 :     if (parent.get_git() != nullptr) parent.get_git()->detect_ssh_agent(true);
     130             : 
     131           0 :     result = sync_repos.run();
     132           0 :     return ESYSREPO_RESULT(result);
     133           0 : }
     134             : 
     135           1 : Result ensure_child_workspace(Cmd &parent, const std::string &child_abs_path)
     136             : {
     137           1 :     if (nested_workspace_initialized(child_abs_path)) return ESYSREPO_RESULT(ResultCode::OK);
     138             : 
     139           3 :     if (!boost::filesystem::exists(child_abs_path) || !boost::filesystem::is_directory(child_abs_path))
     140             :     {
     141           0 :         return ESYSREPO_RESULT(ResultCode::PATH_NOT_EXISTING, "nested child path missing: " + child_abs_path);
     142             :     }
     143             : 
     144           1 :     std::string local = find_nested_local_manifest(child_abs_path);
     145           1 :     if (local.empty())
     146             :     {
     147           0 :         return ESYSREPO_RESULT(ResultCode::WORKSPACE_NESTED_NO_LOCAL_MANIFEST,
     148             :                                "nested child has no local native manifest: " + child_abs_path);
     149             :     }
     150             : 
     151           1 :     parent.info("Nested workspace: init-from-local at " + child_abs_path);
     152             : 
     153           1 :     CmdInit child_init;
     154           1 :     prepare_nested_cmd(parent, child_init, child_abs_path);
     155           1 :     child_init.set_manifest_local(child_abs_path);
     156           1 :     child_init.set_bootstrap_nested(false);
     157             : 
     158           1 :     Result result = child_init.run();
     159           1 :     if (result.error())
     160             :     {
     161           0 :         parent.error("Nested init failed for " + child_abs_path);
     162           0 :         return ESYSREPO_RESULT(result);
     163             :     }
     164           1 :     return ESYSREPO_RESULT(ResultCode::OK);
     165           2 : }
     166             : 
     167          31 : Result bootstrap_nested_workspaces(Cmd &parent, NestedContext &ctx)
     168             : {
     169          31 :     if (ctx.depth > ctx.max_depth)
     170             :     {
     171           0 :         parent.warn("Nested workspace max depth reached; stopping further nesting");
     172           0 :         return ESYSREPO_RESULT(ResultCode::OK);
     173             :     }
     174             : 
     175          31 :     Result result = parent.ensure_manifest_loaded();
     176          31 :     if (result.error()) return ESYSREPO_RESULT(result);
     177          62 :     if (parent.get_manifest() == nullptr) return ESYSREPO_RESULT(ResultCode::MANIFEST_IS_NULLPTR);
     178             : 
     179          31 :     auto nested = collect_nested_repositories(*parent.get_manifest());
     180          31 :     if (nested.empty()) return ESYSREPO_RESULT(ResultCode::OK);
     181             : 
     182           1 :     const std::string parent_ws = normalize_abs(parent.get_workspace_path());
     183             : 
     184           2 :     for (auto &repo : nested)
     185             :     {
     186           1 :         const std::string child_abs = nested_child_abs_path(parent_ws, repo->get_path());
     187           1 :         if (!ctx.visited_abs.insert(child_abs).second)
     188             :         {
     189           0 :             parent.warn("Nested workspace cycle skipped: " + child_abs);
     190           0 :             continue;
     191             :         }
     192             : 
     193           1 :         if (!boost::filesystem::exists(child_abs))
     194             :         {
     195           0 :             parent.info("Nested workspace: sync parent path '" + repo->get_path() + "'");
     196           0 :             result = sync_nested_parent_path(parent, repo->get_path());
     197           0 :             if (result.error()) return ESYSREPO_RESULT(result);
     198             :         }
     199             :         else
     200             :         {
     201           2 :             parent.info("Nested workspace: path already present '" + repo->get_path() + "'");
     202             :         }
     203             : 
     204           1 :         result = ensure_child_workspace(parent, child_abs);
     205           1 :         if (result.error()) return ESYSREPO_RESULT(result);
     206             : 
     207           1 :         NestedHostCmd child_host;
     208           1 :         prepare_nested_cmd(parent, child_host, child_abs);
     209           1 :         result = child_host.open_folder();
     210           1 :         if (result.error()) return ESYSREPO_RESULT(result);
     211           1 :         result = child_host.load();
     212           1 :         if (result.error()) return ESYSREPO_RESULT(result);
     213             : 
     214           1 :         NestedContext child_ctx = ctx;
     215           1 :         child_ctx.depth += 1;
     216           1 :         result = bootstrap_nested_workspaces(child_host, child_ctx);
     217           1 :         if (result.error()) return ESYSREPO_RESULT(result);
     218           1 :     }
     219             : 
     220           1 :     return ESYSREPO_RESULT(ResultCode::OK);
     221          31 : }
     222             : 
     223          35 : Result for_each_nested(
     224             :     Cmd &parent, NestedMissingMode mode, NestedContext &ctx,
     225             :     const std::function<Result(const std::string &child_abs, std::shared_ptr<manifest::Repository> repo)> &fn)
     226             : {
     227          35 :     if (ctx.depth > ctx.max_depth)
     228             :     {
     229           0 :         parent.warn("Nested workspace max depth reached; stopping further nesting");
     230           0 :         return ESYSREPO_RESULT(ResultCode::OK);
     231             :     }
     232             : 
     233          35 :     Result result = parent.ensure_manifest_loaded();
     234          35 :     if (result.error()) return ESYSREPO_RESULT(result);
     235          70 :     if (parent.get_manifest() == nullptr) return ESYSREPO_RESULT(ResultCode::MANIFEST_IS_NULLPTR);
     236             : 
     237          35 :     auto nested = collect_nested_repositories(*parent.get_manifest());
     238          35 :     if (nested.empty()) return ESYSREPO_RESULT(ResultCode::OK);
     239             : 
     240           1 :     const std::string parent_ws = normalize_abs(parent.get_workspace_path());
     241           1 :     Result rresult;
     242             : 
     243           2 :     for (auto &repo : nested)
     244             :     {
     245           1 :         const std::string child_abs = nested_child_abs_path(parent_ws, repo->get_path());
     246           1 :         if (!ctx.visited_abs.insert(child_abs).second)
     247             :         {
     248           0 :             parent.warn("Nested workspace cycle skipped: " + child_abs);
     249           0 :             continue;
     250             :         }
     251             : 
     252           1 :         if (!nested_workspace_initialized(child_abs))
     253             :         {
     254           0 :             std::string msg = "Nested workspace not initialized: " + child_abs;
     255           0 :             if (mode == NestedMissingMode::Fail)
     256             :             {
     257           0 :                 parent.error(msg);
     258           0 :                 return ESYSREPO_RESULT(ResultCode::CMD_NO_ESYSREPO_FOLDER_FOUND, msg);
     259             :             }
     260           0 :             parent.warn(msg + " (skipping)");
     261           0 :             continue;
     262           0 :         }
     263             : 
     264           2 :         parent.info("--- nested workspace: " + child_abs + " ---");
     265           1 :         ctx.depth += 1;
     266           2 :         result = fn(child_abs, repo);
     267           1 :         ctx.depth -= 1;
     268           1 :         if (result.error())
     269             :         {
     270           0 :             if (mode == NestedMissingMode::Fail) return ESYSREPO_RESULT(result);
     271           0 :             rresult.add(ESYSREPO_RESULT(result));
     272             :         }
     273             :         // Deeper nesting is handled when ``fn`` runs the child command (e.g. CmdSync::run).
     274           1 :     }
     275             : 
     276           2 :     return ESYSREPO_RESULT(rresult);
     277          36 : }
     278             : 
     279             : } // namespace esys::repo::exe

Generated by: LCOV version 1.14