LCOV - code coverage report
Current view: top level - src/esys/repo/manifest - merge_manifest.cpp (source / functions) Hit Total Coverage
Test: esysrepo_coverage.info Lines: 194 309 62.8 %
Date: 2026-11-07 22:18:26 Functions: 24 33 72.7 %

          Line data    Source code
       1             : /*!
       2             :  * \file esys/repo/manifest/merge_manifest.cpp
       3             :  * \brief Flatten-merge manifests (ADR-0038)
       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/manifest/merge.h"
      20             : #include "esys/repo/manifest/depends.h"
      21             : #include "esys/repo/manifest/group.h"
      22             : #include "esys/repo/manifest/location.h"
      23             : #include "esys/repo/manifest/repository.h"
      24             : #include "esys/repo/manifest/revisionset.h"
      25             : #include "esys/repo/manifest/xmlfile.h"
      26             : #include "esys/repo/grepo/manifest.h"
      27             : #include "esys/repo/resultcode.h"
      28             : 
      29             : #include <boost/filesystem.hpp>
      30             : 
      31             : #include <fstream>
      32             : #include <functional>
      33             : 
      34             : namespace esys::repo::manifest
      35             : {
      36             : 
      37             : namespace
      38             : {
      39             : 
      40           2 : bool path_has_dir_sep_suffix(const std::string &path)
      41             : {
      42           2 :     if (path.empty()) return false;
      43           2 :     const char last = path.back();
      44           2 :     return last == '/' || last == '\\';
      45             : }
      46             : 
      47           8 : bool file_looks_native(const std::string &path)
      48             : {
      49           8 :     std::ifstream ifs(path);
      50           8 :     if (!ifs) return false;
      51           8 :     std::string chunk(4096, '\0');
      52           8 :     ifs.read(chunk.data(), static_cast<std::streamsize>(chunk.size()));
      53           8 :     chunk.resize(static_cast<std::size_t>(ifs.gcount()));
      54           8 :     return chunk.find("esysrepo_manifest") != std::string::npos;
      55           8 : }
      56             : 
      57             : } // namespace
      58             : 
      59           3 : const std::string &Merge::default_native_filename()
      60             : {
      61           3 :     static const std::string name = ".esysrepo.manifest.xml";
      62           3 :     return name;
      63             : }
      64             : 
      65           0 : const std::string &Merge::default_google_filename()
      66             : {
      67           0 :     static const std::string name = "default.xml";
      68           0 :     return name;
      69             : }
      70             : 
      71           3 : Result Merge::resolve_output_path(const std::string &output, OutputFormat format, std::string &resolved)
      72             : {
      73           3 :     resolved.clear();
      74           3 :     if (output.empty()) return ESYSREPO_RESULT(ResultCode::OK);
      75             : 
      76           2 :     const std::string &def_name =
      77           2 :         (format == OutputFormat::Google) ? default_google_filename() : default_native_filename();
      78             : 
      79           2 :     boost::system::error_code ec;
      80           2 :     const bool as_dir = path_has_dir_sep_suffix(output);
      81           2 :     boost::filesystem::path out(output);
      82             : 
      83           2 :     if (as_dir)
      84             :     {
      85           0 :         boost::filesystem::create_directories(out, ec);
      86           0 :         if (ec) return ESYSREPO_RESULT(ResultCode::FOLDER_CREATION_ERROR, output);
      87           0 :         resolved = (out / def_name).lexically_normal().string();
      88           0 :         return ESYSREPO_RESULT(ResultCode::OK);
      89             :     }
      90             : 
      91           3 :     if (boost::filesystem::exists(out) && boost::filesystem::is_directory(out))
      92             :     {
      93           3 :         resolved = (out / def_name).lexically_normal().string();
      94           1 :         return ESYSREPO_RESULT(ResultCode::OK);
      95             :     }
      96             : 
      97           2 :     resolved = out.lexically_normal().string();
      98           1 :     return ESYSREPO_RESULT(ResultCode::OK);
      99           3 : }
     100             : 
     101           4 : Merge::Merge() = default;
     102             : 
     103           8 : Merge::~Merge() = default;
     104             : 
     105           8 : void Merge::add_input(const std::string &path)
     106             : {
     107           8 :     m_inputs.push_back(path);
     108           8 : }
     109             : 
     110           0 : void Merge::set_inputs(const std::vector<std::string> &paths)
     111             : {
     112           0 :     m_inputs = paths;
     113           0 : }
     114             : 
     115           0 : const std::vector<std::string> &Merge::get_inputs() const
     116             : {
     117           0 :     return m_inputs;
     118             : }
     119             : 
     120           2 : void Merge::set_output(const std::string &path)
     121             : {
     122           2 :     m_output = path;
     123           2 : }
     124             : 
     125           0 : const std::string &Merge::get_output() const
     126             : {
     127           0 :     return m_output;
     128             : }
     129             : 
     130           1 : const std::string &Merge::get_resolved_output() const
     131             : {
     132           1 :     return m_resolved_output;
     133             : }
     134             : 
     135           1 : void Merge::set_on_conflict(OnConflict on_conflict)
     136             : {
     137           1 :     m_on_conflict = on_conflict;
     138           1 : }
     139             : 
     140           0 : Merge::OnConflict Merge::get_on_conflict() const
     141             : {
     142           0 :     return m_on_conflict;
     143             : }
     144             : 
     145           0 : void Merge::set_output_format(OutputFormat format)
     146             : {
     147           0 :     m_output_format = format;
     148           0 : }
     149             : 
     150           0 : Merge::OutputFormat Merge::get_output_format() const
     151             : {
     152           0 :     return m_output_format;
     153             : }
     154             : 
     155           0 : void Merge::set_parse_mode(ParseMode mode)
     156             : {
     157           0 :     m_parse_mode = mode;
     158           0 : }
     159             : 
     160           0 : ParseMode Merge::get_parse_mode() const
     161             : {
     162           0 :     return m_parse_mode;
     163             : }
     164             : 
     165           5 : std::shared_ptr<Manifest> Merge::get_result() const
     166             : {
     167           5 :     return m_result;
     168             : }
     169             : 
     170           1 : Result Merge::conflict(const std::string &msg)
     171             : {
     172           1 :     return ESYSREPO_RESULT(ResultCode::MANIFEST_MERGE_CONFLICT, msg);
     173             : }
     174             : 
     175           4 : bool Merge::same_location_identity(const Location &a, const Location &b)
     176             : {
     177           8 :     return a.get_name() == b.get_name() && a.get_address() == b.get_address()
     178           8 :         && a.get_alt_address() == b.get_alt_address();
     179             : }
     180             : 
     181           2 : bool Merge::same_repo_identity(const Repository &a, const Repository &b)
     182             : {
     183           2 :     if (generic_repo_path(a.get_path()) != generic_repo_path(b.get_path())) return false;
     184           2 :     if (a.get_name() != b.get_name()) return false;
     185           2 :     if (a.get_revision() != b.get_revision()) return false;
     186           0 :     if (a.get_location_str() != b.get_location_str()) return false;
     187           0 :     if (a.get_url() != b.get_url()) return false;
     188           2 :     if (a.get_sync_s() != b.get_sync_s()) return false;
     189           0 :     if (a.get_sharing() != b.get_sharing()) return false;
     190           0 :     if (a.get_workspace() != b.get_workspace()) return false;
     191           0 :     if (a.get_depends().size() != b.get_depends().size()) return false;
     192           0 :     for (std::size_t idx = 0; idx < a.get_depends().size(); ++idx)
     193             :     {
     194           0 :         if (*a.get_depends()[idx] != *b.get_depends()[idx]) return false;
     195             :     }
     196             :     return true;
     197             : }
     198             : 
     199           6 : std::shared_ptr<Repository> Merge::clone_repo(const Repository &src)
     200             : {
     201           6 :     auto copy = std::make_shared<Repository>(src.get_name(), src.get_path());
     202           6 :     copy->set_revision(src.get_revision());
     203           6 :     copy->set_location(src.get_location_str());
     204           6 :     copy->set_sync_s(src.get_sync_s());
     205           6 :     copy->set_sharing(src.get_sharing());
     206           6 :     copy->set_workspace(src.get_workspace());
     207           6 :     for (const auto &dep : src.get_depends())
     208             :     {
     209           0 :         if (dep == nullptr) continue;
     210           0 :         auto d = std::make_shared<Depends>();
     211           0 :         d->set_selector(dep->get_selector());
     212           0 :         d->set_value(dep->get_value());
     213           0 :         d->set_line_number(dep->get_line_number());
     214           0 :         copy->add_depends(d);
     215           0 :     }
     216           6 :     return copy;
     217           0 : }
     218             : 
     219           8 : Result Merge::load_file(const std::string &path, std::shared_ptr<Manifest> &out)
     220             : {
     221           8 :     if (!boost::filesystem::exists(path)) return ESYSREPO_RESULT(ResultCode::FILE_NOT_EXISTING, path);
     222             : 
     223          16 :     out = std::make_shared<Manifest>();
     224           8 :     const bool native = file_looks_native(path);
     225           8 :     if (native)
     226             :     {
     227           8 :         XMLFile xml;
     228          16 :         xml.set_data(out);
     229           8 :         xml.set_parse_mode(m_parse_mode);
     230           8 :         Result result = xml.read(path);
     231           8 :         if (result.error()) return ESYSREPO_RESULT(result);
     232           8 :         out->set_type(Type::ESYSREPO_MANIFEST);
     233           8 :         out->set_format(Format::XML);
     234           8 :         return ESYSREPO_RESULT(ResultCode::OK);
     235           8 :     }
     236             : 
     237           0 :     grepo::Manifest grepo;
     238           0 :     grepo.set_data(out);
     239           0 :     grepo.set_parse_mode(m_parse_mode);
     240           0 :     Result result = grepo.read(path);
     241           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     242           0 :     out->set_type(Type::GOOGLE_MANIFEST);
     243           0 :     out->set_format(Format::XML);
     244           0 :     return ESYSREPO_RESULT(ResultCode::OK);
     245           0 : }
     246             : 
     247           4 : Result Merge::run()
     248             : {
     249           4 :     m_result = std::make_shared<Manifest>();
     250           4 :     m_result->set_format(Format::XML);
     251           4 :     m_all_inputs_google = true;
     252           4 :     m_resolved_output.clear();
     253             : 
     254           4 :     if (m_inputs.size() < 2)
     255           0 :         return ESYSREPO_RESULT(ResultCode::CMD_MISSING_PARAMETERS, "manifest merge needs at least two input files");
     256             : 
     257          11 :     for (const auto &path : m_inputs)
     258             :     {
     259           8 :         std::shared_ptr<Manifest> src;
     260           8 :         Result result = load_file(path, src);
     261           8 :         if (result.error()) return ESYSREPO_RESULT(result);
     262           8 :         if (src->get_type() != Type::GOOGLE_MANIFEST) m_all_inputs_google = false;
     263           8 :         if (!src->get_includes().empty())
     264           0 :             return conflict("Google <include> is not flattened: '" + path + "'");
     265           8 :         result = absorb(*src, path);
     266           9 :         if (result.error()) return ESYSREPO_RESULT(result);
     267          16 :     }
     268             : 
     269           3 :     if (m_output_format == OutputFormat::Google && !m_all_inputs_google)
     270           0 :         return ESYSREPO_RESULT(ResultCode::CMD_INCORRECT_PARAMETERS_IN_CONTEXT,
     271             :                                "--format google requires every input to be Google XML");
     272             : 
     273           3 :     if (m_output_format == OutputFormat::Google)
     274           0 :         m_result->set_type(Type::GOOGLE_MANIFEST);
     275             :     else
     276           3 :         m_result->set_type(Type::ESYSREPO_MANIFEST);
     277             : 
     278           3 :     Result result = m_result->validate_depends();
     279           3 :     if (result.error()) return ESYSREPO_RESULT(result);
     280             : 
     281           3 :     if (m_result->get_locations().empty())
     282           0 :         return ESYSREPO_RESULT(ResultCode::MANIFEST_NO_LOCATIONS_DEFINED);
     283             : 
     284           3 :     result = write_result();
     285           3 :     if (result.error()) return ESYSREPO_RESULT(result);
     286           3 :     return ESYSREPO_RESULT(ResultCode::OK);
     287           3 : }
     288             : 
     289           3 : Result Merge::write_result()
     290             : {
     291           3 :     Result result = resolve_output_path(m_output, m_output_format, m_resolved_output);
     292           3 :     if (result.error()) return ESYSREPO_RESULT(result);
     293           3 :     if (m_resolved_output.empty()) return ESYSREPO_RESULT(ResultCode::OK);
     294             : 
     295           2 :     std::shared_ptr<FileBase> file;
     296           2 :     if (m_output_format == OutputFormat::Google)
     297           0 :         file = std::make_shared<grepo::Manifest>();
     298             :     else
     299           4 :         file = std::make_shared<XMLFile>();
     300           4 :     file->set_data(m_result);
     301           2 :     return file->write(m_resolved_output);
     302           3 : }
     303             : 
     304           8 : Result Merge::absorb(const Manifest &src, const std::string &src_path)
     305             : {
     306           8 :     Result result = absorb_defaults(src, src_path);
     307           8 :     if (result.error()) return ESYSREPO_RESULT(result);
     308             : 
     309          15 :     for (const auto &loc : src.get_locations())
     310             :     {
     311           8 :         if (loc == nullptr) continue;
     312          16 :         result = absorb_location(loc, src_path);
     313           9 :         if (result.error()) return ESYSREPO_RESULT(result);
     314             :     }
     315             : 
     316           7 :     result = absorb_groups(src);
     317           7 :     if (result.error()) return ESYSREPO_RESULT(result);
     318           7 :     return absorb_revision_sets(src, src_path);
     319           8 : }
     320             : 
     321           8 : Result Merge::absorb_defaults(const Manifest &src, const std::string &src_path)
     322             : {
     323          32 :     auto take_or_conflict = [this, &src_path](const std::string &field, const std::string &cur, const std::string &inc,
     324           0 :                                               const std::function<void(const std::string &)> &set) -> Result {
     325          24 :         if (inc.empty()) return ESYSREPO_RESULT(ResultCode::OK);
     326           0 :         if (cur.empty())
     327             :         {
     328           0 :             set(inc);
     329           0 :             return ESYSREPO_RESULT(ResultCode::OK);
     330             :         }
     331           0 :         if (cur == inc) return ESYSREPO_RESULT(ResultCode::OK);
     332           0 :         if (m_on_conflict == OnConflict::PreferFirst) return ESYSREPO_RESULT(ResultCode::OK);
     333           0 :         if (m_on_conflict == OnConflict::PreferLast)
     334             :         {
     335           0 :             set(inc);
     336           0 :             return ESYSREPO_RESULT(ResultCode::OK);
     337             :         }
     338           0 :         return conflict(field + " differs in '" + src_path + "'");
     339           8 :     };
     340             : 
     341           8 :     if (src.get_kind() != Kind::NOT_SET && src.get_kind() != Kind::UNKNOWN)
     342             :     {
     343           8 :         if (m_result->get_kind() == Kind::NOT_SET || m_result->get_kind() == Kind::UNKNOWN)
     344           4 :             m_result->set_kind(src.get_kind());
     345           4 :         else if (m_result->get_kind() != src.get_kind())
     346             :         {
     347           0 :             if (m_on_conflict == OnConflict::Fail) return conflict("kind differs in '" + src_path + "'");
     348           0 :             if (m_on_conflict == OnConflict::PreferLast) m_result->set_kind(src.get_kind());
     349             :         }
     350             :     }
     351             : 
     352           8 :     const std::string dest_rev = (m_result->get_default_revision() == "master") ? "" : m_result->get_default_revision();
     353           8 :     const std::string src_rev = (src.get_default_revision() == "master") ? "" : src.get_default_revision();
     354           8 :     Result result = take_or_conflict(
     355          16 :         "default revision", dest_rev, src_rev, [this](const std::string &v) { m_result->set_default_revision(v); });
     356           8 :     if (result.error()) return ESYSREPO_RESULT(result);
     357             : 
     358           8 :     if (src.get_default_job_count() != 1)
     359             :     {
     360           0 :         if (m_result->get_default_job_count() == 1)
     361           0 :             m_result->set_default_job_count(src.get_default_job_count());
     362           0 :         else if (m_result->get_default_job_count() != src.get_default_job_count())
     363             :         {
     364           0 :             if (m_on_conflict == OnConflict::Fail) return conflict("default sync-j differs in '" + src_path + "'");
     365           0 :             if (m_on_conflict == OnConflict::PreferLast) m_result->set_default_job_count(src.get_default_job_count());
     366             :         }
     367             :     }
     368             : 
     369           8 :     if (src.get_default_sync_s().has_value())
     370             :     {
     371           0 :         if (!m_result->get_default_sync_s().has_value())
     372           0 :             m_result->set_default_sync_s(src.get_default_sync_s());
     373           0 :         else if (m_result->get_default_sync_s() != src.get_default_sync_s())
     374             :         {
     375           0 :             if (m_on_conflict == OnConflict::Fail) return conflict("default sync-s differs in '" + src_path + "'");
     376           0 :             if (m_on_conflict == OnConflict::PreferLast) m_result->set_default_sync_s(src.get_default_sync_s());
     377             :         }
     378             :     }
     379             : 
     380          16 :     result = take_or_conflict("default location", m_result->get_default_location_str(), src.get_default_location_str(),
     381           8 :                               [this](const std::string &v) { m_result->set_default_location(v); });
     382           8 :     if (result.error()) return ESYSREPO_RESULT(result);
     383             : 
     384          16 :     result = take_or_conflict("default revision-set", m_result->get_default_revision_set(),
     385             :                               src.get_default_revision_set(),
     386           8 :                               [this](const std::string &v) { m_result->set_default_revision_set(v); });
     387           8 :     if (result.error()) return ESYSREPO_RESULT(result);
     388             : 
     389           8 :     return ESYSREPO_RESULT(ResultCode::OK);
     390          16 : }
     391             : 
     392           8 : Result Merge::absorb_location(std::shared_ptr<Location> src_loc, const std::string &src_path)
     393             : {
     394           8 :     auto dest_loc = m_result->find_location(src_loc->get_name());
     395           8 :     if (dest_loc == nullptr)
     396             :     {
     397           4 :         dest_loc = std::make_shared<Location>(src_loc->get_name(), src_loc->get_address());
     398           4 :         dest_loc->set_alt_address(src_loc->get_alt_address());
     399          12 :         if (m_result->add_location(dest_loc) < 0)
     400           0 :             return ESYSREPO_RESULT(ResultCode::INTERNAL_ERROR, src_loc->get_name());
     401             :     }
     402           4 :     else if (!same_location_identity(*dest_loc, *src_loc))
     403             :     {
     404           0 :         if (m_on_conflict == OnConflict::Fail)
     405           0 :             return conflict("location '" + src_loc->get_name() + "' differs in '" + src_path + "'");
     406           0 :         if (m_on_conflict == OnConflict::PreferLast)
     407             :         {
     408           0 :             dest_loc->set_address(src_loc->get_address());
     409           0 :             dest_loc->set_alt_address(src_loc->get_alt_address());
     410             :         }
     411             :     }
     412             : 
     413          15 :     for (const auto &repo : src_loc->get_repos())
     414             :     {
     415           8 :         if (repo == nullptr) continue;
     416          32 :         Result result = absorb_repo(dest_loc, repo, src_path);
     417           9 :         if (result.error()) return ESYSREPO_RESULT(result);
     418           8 :     }
     419           7 :     return ESYSREPO_RESULT(ResultCode::OK);
     420           8 : }
     421             : 
     422           8 : Result Merge::absorb_repo(std::shared_ptr<Location> dest_loc, std::shared_ptr<Repository> src_repo,
     423             :                           const std::string &src_path)
     424             : {
     425           8 :     auto existing = m_result->find_repo_by_path(src_repo->get_path());
     426           8 :     if (existing == nullptr)
     427             :     {
     428           6 :         dest_loc->add_repo(clone_repo(*src_repo));
     429           6 :         return ESYSREPO_RESULT(ResultCode::OK);
     430             :     }
     431             : 
     432           2 :     if (existing->get_location() != nullptr && existing->get_location()->get_name() != dest_loc->get_name())
     433           0 :         return conflict("path '" + generic_repo_path(src_repo->get_path()) + "' uses different locations in '"
     434           0 :                         + src_path + "'");
     435             : 
     436           2 :     if (same_repo_identity(*existing, *src_repo)) return ESYSREPO_RESULT(ResultCode::OK);
     437             : 
     438           2 :     if (m_on_conflict == OnConflict::PreferFirst) return ESYSREPO_RESULT(ResultCode::OK);
     439           2 :     if (m_on_conflict == OnConflict::PreferLast)
     440             :     {
     441           1 :         existing->set_name(src_repo->get_name());
     442           1 :         existing->set_revision(src_repo->get_revision());
     443           1 :         existing->set_location(src_repo->get_location_str());
     444           1 :         existing->set_sync_s(src_repo->get_sync_s());
     445           1 :         existing->clear_depends();
     446           1 :         for (const auto &dep : src_repo->get_depends())
     447             :         {
     448           0 :             if (dep == nullptr) continue;
     449           0 :             auto d = std::make_shared<Depends>();
     450           0 :             d->set_selector(dep->get_selector());
     451           0 :             d->set_value(dep->get_value());
     452           0 :             d->set_line_number(dep->get_line_number());
     453           0 :             existing->add_depends(d);
     454           0 :         }
     455           1 :         return ESYSREPO_RESULT(ResultCode::OK);
     456             :     }
     457             : 
     458           2 :     return conflict("repository path '" + generic_repo_path(src_repo->get_path()) + "' differs in '" + src_path + "'");
     459           8 : }
     460             : 
     461           7 : Result Merge::absorb_groups(const Manifest &src)
     462             : {
     463           9 :     for (const auto &group : src.get_groups().get_groups())
     464             :     {
     465           2 :         if (group == nullptr) continue;
     466           2 :         auto dest_group = m_result->get_groups().find_or_new_group_by_name(group->get_name());
     467           4 :         for (const auto &repo : group->get_repos())
     468             :         {
     469           2 :             if (repo == nullptr) continue;
     470           2 :             auto dest_repo = m_result->find_repo_by_path(repo->get_path());
     471           2 :             if (dest_repo == nullptr) continue;
     472           6 :             if (dest_group->find_repo_by_path(dest_repo->get_path()) == nullptr) dest_group->add_repo(dest_repo);
     473           2 :         }
     474           2 :     }
     475           7 :     return ESYSREPO_RESULT(ResultCode::OK);
     476             : }
     477             : 
     478           7 : Result Merge::absorb_revision_sets(const Manifest &src, const std::string &src_path)
     479             : {
     480           7 :     for (const auto &rs : src.get_revision_sets().get_revision_sets())
     481             :     {
     482           0 :         if (rs == nullptr) continue;
     483           0 :         auto dest = m_result->get_revision_sets().find_by_name(rs->get_name());
     484           0 :         if (dest == nullptr)
     485             :         {
     486           0 :             auto copy = std::make_shared<RevisionSet>(rs->get_name());
     487           0 :             copy->set_default_revision(rs->get_default_revision());
     488           0 :             for (const auto &kv : rs->get_repo_revisions()) copy->set_repo_revision(kv.first, kv.second);
     489           0 :             m_result->get_revision_sets().add_revision_set(copy);
     490           0 :             continue;
     491           0 :         }
     492           0 :         const bool same = dest->get_default_revision() == rs->get_default_revision()
     493           0 :             && dest->get_repo_revisions() == rs->get_repo_revisions();
     494           0 :         if (same) continue;
     495           0 :         if (m_on_conflict == OnConflict::PreferFirst) continue;
     496           0 :         if (m_on_conflict == OnConflict::PreferLast)
     497             :         {
     498           0 :             dest->clear();
     499           0 :             dest->set_name(rs->get_name());
     500           0 :             dest->set_default_revision(rs->get_default_revision());
     501           0 :             for (const auto &kv : rs->get_repo_revisions()) dest->set_repo_revision(kv.first, kv.second);
     502           0 :             continue;
     503           0 :         }
     504           0 :         return conflict("revision-set '" + rs->get_name() + "' differs in '" + src_path + "'");
     505           0 :     }
     506           7 :     return ESYSREPO_RESULT(ResultCode::OK);
     507             : }
     508             : 
     509             : } // namespace esys::repo::manifest

Generated by: LCOV version 1.14