Line data Source code
1 : /*!
2 : * \file esys/repo/exe/cmddeps.cpp
3 : * \brief CmdDeps for esysrepo (ADR-0022)
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/cmddeps.h"
20 : #include "esys/repo/manifest/repository.h"
21 :
22 : #include <nlohmann/json.hpp>
23 :
24 : #include <functional>
25 : #include <iostream>
26 : #include <map>
27 : #include <set>
28 : #include <sstream>
29 :
30 : namespace esys::repo::exe
31 : {
32 :
33 20 : CmdDeps::CmdDeps()
34 40 : : Cmd("Deps")
35 : {
36 20 : }
37 :
38 20 : CmdDeps::~CmdDeps() = default;
39 :
40 0 : void CmdDeps::set_output_format(DepsOutputFormat format)
41 : {
42 0 : m_output_format = format;
43 0 : }
44 :
45 3 : DepsOutputFormat CmdDeps::get_output_format() const
46 : {
47 3 : return m_output_format;
48 : }
49 :
50 1 : void CmdDeps::set_json(bool json)
51 : {
52 1 : m_json = json;
53 1 : if (json)
54 : {
55 1 : m_output_format = DepsOutputFormat::JSON;
56 1 : set_print_cmd_name_by_base(false);
57 1 : set_print_result_summary(false);
58 : }
59 1 : }
60 :
61 1 : bool CmdDeps::get_json() const
62 : {
63 1 : return m_json;
64 : }
65 :
66 1 : void CmdDeps::set_dot(bool dot)
67 : {
68 1 : m_dot = dot;
69 1 : if (dot)
70 : {
71 1 : m_output_format = DepsOutputFormat::DOT;
72 1 : set_print_cmd_name_by_base(false);
73 1 : set_print_result_summary(false);
74 : }
75 1 : }
76 :
77 1 : bool CmdDeps::get_dot() const
78 : {
79 1 : return m_dot;
80 : }
81 :
82 1 : void CmdDeps::set_mermaid(bool mermaid)
83 : {
84 1 : m_mermaid = mermaid;
85 1 : if (mermaid) m_output_format = DepsOutputFormat::MERMAID;
86 1 : }
87 :
88 1 : bool CmdDeps::get_mermaid() const
89 : {
90 1 : return m_mermaid;
91 : }
92 :
93 1 : void CmdDeps::set_link(bool link)
94 : {
95 1 : m_link = link;
96 1 : }
97 :
98 1 : bool CmdDeps::get_link() const
99 : {
100 1 : return m_link;
101 : }
102 :
103 1 : void CmdDeps::set_paths(const std::vector<std::string> &paths)
104 : {
105 1 : m_paths = paths;
106 1 : }
107 :
108 3 : const std::vector<std::string> &CmdDeps::get_paths() const
109 : {
110 3 : return m_paths;
111 : }
112 :
113 0 : void CmdDeps::set_names(const std::vector<std::string> &names)
114 : {
115 0 : m_names = names;
116 0 : }
117 :
118 0 : const std::vector<std::string> &CmdDeps::get_names() const
119 : {
120 0 : return m_names;
121 : }
122 :
123 0 : void CmdDeps::set_projects(const std::vector<std::string> &projects)
124 : {
125 0 : m_projects = projects;
126 0 : }
127 :
128 0 : const std::vector<std::string> &CmdDeps::get_projects() const
129 : {
130 0 : return m_projects;
131 : }
132 :
133 0 : void CmdDeps::set_urls(const std::vector<std::string> &urls)
134 : {
135 0 : m_urls = urls;
136 0 : }
137 :
138 0 : const std::vector<std::string> &CmdDeps::get_urls() const
139 : {
140 0 : return m_urls;
141 : }
142 :
143 0 : const std::vector<std::pair<std::string, std::string>> &CmdDeps::get_session_links() const
144 : {
145 0 : return m_session_links;
146 : }
147 :
148 0 : Result CmdDeps::impl_run()
149 : {
150 0 : const int format_count = (get_json() ? 1 : 0) + (get_dot() ? 1 : 0) + (get_mermaid() ? 1 : 0);
151 0 : if (format_count > 1)
152 : {
153 0 : error("Specify at most one of --json, --dot, --mermaid");
154 0 : return ESYSREPO_RESULT(ResultCode::CMD_GENERIC_RAW_ERROR, -1);
155 : }
156 0 : if (format_count == 0) m_output_format = DepsOutputFormat::MERMAID;
157 :
158 0 : Result result = default_handling_folder_workspace();
159 0 : if (result.error()) return ESYSREPO_RESULT(result);
160 :
161 0 : result = open_esysrepo_folder();
162 0 : if (result.error()) return ESYSREPO_RESULT(result);
163 :
164 0 : result = load_manifest();
165 0 : if (result.error()) return ESYSREPO_RESULT(result);
166 :
167 0 : if (get_link())
168 : {
169 0 : result = apply_session_link();
170 0 : if (result.error()) return ESYSREPO_RESULT(result);
171 : }
172 :
173 0 : auto edges = collect_edges();
174 0 : result = check_no_cycle(edges);
175 0 : if (result.error()) return ESYSREPO_RESULT(result);
176 :
177 0 : std::ostream &os = (get_console_os() != nullptr) ? *get_console_os() : std::cout;
178 0 : switch (get_output_format())
179 : {
180 0 : case DepsOutputFormat::JSON: print_json(os, edges); break;
181 0 : case DepsOutputFormat::DOT: print_dot(os, edges); break;
182 0 : case DepsOutputFormat::MERMAID:
183 0 : default: print_mermaid(os, edges); break;
184 : }
185 0 : return ESYSREPO_RESULT(ResultCode::OK);
186 0 : }
187 :
188 0 : Result CmdDeps::resolve_endpoint(const std::string &selector_kind, const std::string &value, std::string &out_path)
189 : {
190 0 : if (get_manifest() == nullptr) return ESYSREPO_RESULT(ResultCode::MANIFEST_IS_NULLPTR);
191 :
192 0 : std::shared_ptr<manifest::Repository> target;
193 0 : if (selector_kind == "path")
194 : {
195 0 : target = get_manifest()->find_repo_by_path(value);
196 0 : if (target == nullptr)
197 0 : return ESYSREPO_RESULT(ResultCode::MANIFEST_DEPENDS_UNRESOLVED, "path=" + value);
198 : }
199 0 : else if (selector_kind == "name")
200 : {
201 0 : auto matches = get_manifest()->find_repos_by_name(value);
202 0 : if (matches.empty()) return ESYSREPO_RESULT(ResultCode::MANIFEST_DEPENDS_UNRESOLVED, "name=" + value);
203 0 : if (matches.size() > 1) return ESYSREPO_RESULT(ResultCode::MANIFEST_DEPENDS_AMBIGUOUS, "name=" + value);
204 0 : target = matches.front();
205 0 : }
206 0 : else if (selector_kind == "project")
207 : {
208 0 : auto matches = get_manifest()->find_repos_by_project(value);
209 0 : if (matches.empty()) return ESYSREPO_RESULT(ResultCode::MANIFEST_DEPENDS_UNRESOLVED, "project=" + value);
210 0 : if (matches.size() > 1) return ESYSREPO_RESULT(ResultCode::MANIFEST_DEPENDS_AMBIGUOUS, "project=" + value);
211 0 : target = matches.front();
212 0 : }
213 0 : else if (selector_kind == "url")
214 : {
215 0 : target = get_manifest()->find_repo_by_url(value);
216 0 : if (target == nullptr) return ESYSREPO_RESULT(ResultCode::MANIFEST_DEPENDS_UNRESOLVED, "url=" + value);
217 : }
218 : else
219 0 : return ESYSREPO_RESULT(ResultCode::INTERNAL_ERROR);
220 :
221 0 : out_path = target->get_path();
222 0 : return ESYSREPO_RESULT(ResultCode::OK);
223 0 : }
224 :
225 0 : Result CmdDeps::apply_session_link()
226 : {
227 : // Prefer two --path values (ADR example). Otherwise accept two values of one
228 : // identity kind (--name/--project/--url), in order from → to.
229 0 : std::string from_path;
230 0 : std::string to_path;
231 0 : Result result = ESYSREPO_RESULT(ResultCode::OK);
232 :
233 0 : if (get_paths().size() == 2 && get_names().empty() && get_projects().empty() && get_urls().empty())
234 : {
235 0 : result = resolve_endpoint("path", get_paths()[0], from_path);
236 0 : if (result.error()) return result;
237 0 : result = resolve_endpoint("path", get_paths()[1], to_path);
238 0 : if (result.error()) return result;
239 : }
240 0 : else if (get_names().size() == 2 && get_paths().empty() && get_projects().empty() && get_urls().empty())
241 : {
242 0 : result = resolve_endpoint("name", get_names()[0], from_path);
243 0 : if (result.error()) return result;
244 0 : result = resolve_endpoint("name", get_names()[1], to_path);
245 0 : if (result.error()) return result;
246 : }
247 0 : else if (get_projects().size() == 2 && get_paths().empty() && get_names().empty() && get_urls().empty())
248 : {
249 0 : result = resolve_endpoint("project", get_projects()[0], from_path);
250 0 : if (result.error()) return result;
251 0 : result = resolve_endpoint("project", get_projects()[1], to_path);
252 0 : if (result.error()) return result;
253 : }
254 0 : else if (get_urls().size() == 2 && get_paths().empty() && get_names().empty() && get_projects().empty())
255 : {
256 0 : result = resolve_endpoint("url", get_urls()[0], from_path);
257 0 : if (result.error()) return result;
258 0 : result = resolve_endpoint("url", get_urls()[1], to_path);
259 0 : if (result.error()) return result;
260 : }
261 : else
262 : {
263 0 : error("--link requires exactly two endpoints of the same identity kind "
264 : "(--path A --path B, or --name / --project / --url)");
265 0 : return ESYSREPO_RESULT(ResultCode::CMD_GENERIC_RAW_ERROR, -1);
266 : }
267 :
268 0 : m_session_links.emplace_back(from_path, to_path);
269 0 : return ESYSREPO_RESULT(ResultCode::OK);
270 0 : }
271 :
272 0 : std::vector<std::pair<std::string, std::string>> CmdDeps::collect_edges() const
273 : {
274 0 : std::vector<std::pair<std::string, std::string>> edges;
275 0 : if (get_manifest() != nullptr) edges = get_manifest()->get_depends_edges();
276 0 : edges.insert(edges.end(), m_session_links.begin(), m_session_links.end());
277 0 : return edges;
278 0 : }
279 :
280 0 : Result CmdDeps::check_no_cycle(const std::vector<std::pair<std::string, std::string>> &edges) const
281 : {
282 0 : enum class Color
283 : {
284 : White,
285 : Gray,
286 : Black
287 : };
288 0 : std::map<std::string, Color> color;
289 0 : std::map<std::string, std::vector<std::string>> adj;
290 0 : for (const auto &e : edges)
291 : {
292 0 : color[e.first] = Color::White;
293 0 : color[e.second] = Color::White;
294 0 : adj[e.first].push_back(e.second);
295 : }
296 :
297 0 : std::function<Result(const std::string &)> dfs = [&](const std::string &path) -> Result {
298 0 : color[path] = Color::Gray;
299 0 : for (const auto &dep : adj[path])
300 : {
301 0 : if (color[dep] == Color::Gray)
302 0 : return ESYSREPO_RESULT(ResultCode::MANIFEST_DEPENDS_CYCLE, path + " -> " + dep);
303 0 : if (color[dep] == Color::White)
304 : {
305 0 : Result nested = dfs(dep);
306 0 : if (nested.error()) return nested;
307 0 : }
308 : }
309 0 : color[path] = Color::Black;
310 0 : return ESYSREPO_RESULT(ResultCode::OK);
311 0 : };
312 :
313 0 : for (auto &kv : color)
314 : {
315 0 : if (kv.second != Color::White) continue;
316 0 : Result result = dfs(kv.first);
317 0 : if (result.error()) return result;
318 0 : }
319 0 : return ESYSREPO_RESULT(ResultCode::OK);
320 0 : }
321 :
322 0 : void CmdDeps::print_mermaid(std::ostream &os, const std::vector<std::pair<std::string, std::string>> &edges) const
323 : {
324 0 : os << "flowchart LR" << std::endl;
325 0 : if (edges.empty())
326 : {
327 0 : os << " %% no depends edges" << std::endl;
328 0 : return;
329 : }
330 0 : for (const auto &e : edges)
331 0 : os << " \"" << e.first << "\" --> \"" << e.second << "\"" << std::endl;
332 : }
333 :
334 0 : void CmdDeps::print_dot(std::ostream &os, const std::vector<std::pair<std::string, std::string>> &edges) const
335 : {
336 0 : os << "digraph depends {" << std::endl;
337 0 : for (const auto &e : edges)
338 0 : os << " \"" << e.first << "\" -> \"" << e.second << "\";" << std::endl;
339 0 : os << "}" << std::endl;
340 0 : }
341 :
342 0 : void CmdDeps::print_json(std::ostream &os, const std::vector<std::pair<std::string, std::string>> &edges) const
343 : {
344 0 : nlohmann::json root;
345 0 : root["schema"] = "esysrepo.deps";
346 0 : root["schema_version"] = 1;
347 0 : nlohmann::json arr = nlohmann::json::array();
348 0 : for (const auto &e : edges)
349 : {
350 0 : nlohmann::json item;
351 0 : item["from"] = e.first;
352 0 : item["to"] = e.second;
353 0 : arr.push_back(item);
354 0 : }
355 0 : root["edges"] = arr;
356 0 : if (!m_session_links.empty())
357 : {
358 0 : nlohmann::json session = nlohmann::json::array();
359 0 : for (const auto &e : m_session_links)
360 : {
361 0 : nlohmann::json item;
362 0 : item["from"] = e.first;
363 0 : item["to"] = e.second;
364 0 : session.push_back(item);
365 0 : }
366 0 : root["session_links"] = session;
367 0 : }
368 0 : os << root.dump(2) << std::endl;
369 0 : }
370 :
371 : } // namespace esys::repo::exe
|