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