LCOV - code coverage report
Current view: top level - src/esys/repo/exe - cmddeps.cpp (source / functions) Hit Total Coverage
Test: esysrepo_coverage.info Lines: 40 211 19.0 %
Date: 2026-09-13 13:32:21 Functions: 13 31 41.9 %

          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

Generated by: LCOV version 1.14