LCOV - code coverage report
Current view: top level - src/esys/repo/exe - cmdmulti.cpp (source / functions) Hit Total Coverage
Test: esysrepo_coverage.info Lines: 317 583 54.4 %
Date: 2026-09-26 15:38:18 Functions: 25 38 65.8 %

          Line data    Source code
       1             : /*!
       2             :  * \file esys/repo/exe/cmdmulti.cpp
       3             :  * \brief Multi command (ADR-0023)
       4             :  *
       5             :  * \cond
       6             :  * __legal_b__
       7             :  *
       8             :  * Copyright (c) 2023-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/cmdmulti.h"
      20             : #include "esys/repo/manifest/capture.h"
      21             : #include "esys/repo/manifest/multifilexml.h"
      22             : #include "esys/repo/manifest/repository.h"
      23             : #include "esys/repo/tui/is_tty.h"
      24             : #include "esys/repo/workspace.h"
      25             : 
      26             : #include <boost/filesystem.hpp>
      27             : 
      28             : #include <algorithm>
      29             : #include <fstream>
      30             : #include <iomanip>
      31             : #include <iostream>
      32             : #include <map>
      33             : #include <queue>
      34             : #include <set>
      35             : #include <sstream>
      36             : 
      37             : namespace esys::repo::exe
      38             : {
      39             : 
      40             : namespace
      41             : {
      42             : 
      43             : constexpr const char *k_notes_ref = "esysrepo";
      44             : constexpr const char *k_push_state_file = "multi-push.state";
      45             : 
      46           2 : void set_esysrepo_author(GitBase *git)
      47             : {
      48           2 :     auto person = std::make_shared<git::Person>();
      49           2 :     person->set_email("esysrepo@libesys.org");
      50           2 :     person->set_name("ESysRepo");
      51           6 :     git->set_author(person);
      52           2 : }
      53             : 
      54             : } // namespace
      55             : 
      56          18 : CmdMulti::CmdMulti()
      57          18 :     : Cmd("Multi")
      58             : {
      59          18 : }
      60             : 
      61          18 : CmdMulti::~CmdMulti() = default;
      62             : 
      63           8 : void CmdMulti::set_task(Task task)
      64             : {
      65           8 :     m_task = task;
      66           8 : }
      67             : 
      68           9 : CmdMulti::Task CmdMulti::get_task() const
      69             : {
      70           9 :     return m_task;
      71             : }
      72             : 
      73           7 : void CmdMulti::set_dry_run(bool dry_run)
      74             : {
      75           7 :     m_dry_run = dry_run;
      76           7 : }
      77             : 
      78          10 : bool CmdMulti::get_dry_run() const
      79             : {
      80          10 :     return m_dry_run;
      81             : }
      82             : 
      83           0 : void CmdMulti::set_interactive(bool interactive)
      84             : {
      85           0 :     m_interactive = interactive;
      86           0 : }
      87             : 
      88           0 : bool CmdMulti::get_interactive() const
      89             : {
      90           0 :     return m_interactive;
      91             : }
      92             : 
      93           2 : void CmdMulti::set_silent(bool silent)
      94             : {
      95           2 :     m_silent = silent;
      96           2 : }
      97             : 
      98           0 : bool CmdMulti::get_silent() const
      99             : {
     100           0 :     return m_silent;
     101             : }
     102             : 
     103           0 : void CmdMulti::set_capture_scope(CaptureScope scope)
     104             : {
     105           0 :     m_capture_scope = scope;
     106           0 : }
     107             : 
     108          28 : CmdMulti::CaptureScope CmdMulti::get_capture_scope() const
     109             : {
     110          28 :     return m_capture_scope;
     111             : }
     112             : 
     113           9 : Result CmdMulti::impl_run()
     114             : {
     115           9 :     switch (get_task())
     116             :     {
     117           3 :         case Task::CAPTURE: return do_task_capture();
     118           0 :         case Task::DELETE: return do_task_delete();
     119           2 :         case Task::PUSH: return do_task_push();
     120           0 :         case Task::SHOW: return do_task_show();
     121           2 :         case Task::ABORT: return do_task_abort();
     122           2 :         case Task::CONTINUE: return do_task_continue();
     123           0 :         default: return ResultCode::GENERIC_ERROR;
     124             :     }
     125             : }
     126             : 
     127          13 : Result CmdMulti::resolve_folder_path(std::string &folder_path) const
     128             : {
     129          13 :     if (!get_folder().empty())
     130           0 :         folder_path = get_folder();
     131          13 :     else if (!get_workspace_path().empty())
     132          13 :         folder_path = get_workspace_path();
     133             :     else
     134           0 :         folder_path = boost::filesystem::current_path().string();
     135             : 
     136          13 :     if (folder_path.empty()) return ESYSREPO_RESULT(ResultCode::INTERNAL_ERROR);
     137          13 :     return ESYSREPO_RESULT(ResultCode::OK);
     138             : }
     139             : 
     140           9 : Result CmdMulti::load_workspace(Workspace &workspace, std::string &git_root_path)
     141             : {
     142           9 :     std::string folder_path;
     143           9 :     Result result = resolve_folder_path(folder_path);
     144           9 :     if (result.error()) return ESYSREPO_RESULT(result);
     145             : 
     146           9 :     result = workspace.load(folder_path);
     147           9 :     if (result.error()) return ESYSREPO_RESULT(result);
     148             : 
     149           9 :     set_workspace_path(workspace.get_path());
     150           9 :     git_root_path = find_git_root_path(folder_path);
     151           9 :     if (git_root_path.empty()) return ESYSREPO_RESULT(ResultCode::INTERNAL_ERROR);
     152           9 :     return ESYSREPO_RESULT(ResultCode::OK);
     153           9 : }
     154             : 
     155           0 : bool CmdMulti::captures_equal(const manifest::Capture &a, const manifest::Capture &b) const
     156             : {
     157           0 :     return a == b;
     158             : }
     159             : 
     160           0 : Result CmdMulti::confirm_capture_mismatch()
     161             : {
     162           0 :     if (get_silent()) return ESYSREPO_RESULT(ResultCode::OK);
     163             : 
     164           0 :     warn("Capture note and live workspace state disagree; capture overrides live dirtiness (ADR-0023).");
     165             : 
     166           0 :     if (!get_interactive() || !tui::is_tty())
     167             :     {
     168           0 :         if (!get_interactive())
     169           0 :             return ESYSREPO_RESULT(ResultCode::GENERIC_ERROR,
     170             :                                    "capture/live mismatch requires --inter to confirm, or --silent");
     171           0 :         return ESYSREPO_RESULT(ResultCode::GENERIC_ERROR,
     172             :                                "capture/live mismatch in non-TTY context; use --silent to accept capture");
     173             :     }
     174             : 
     175           0 :     info("Proceed using the capture note? [y/N]");
     176           0 :     std::string answer;
     177           0 :     if (!std::getline(std::cin, answer))
     178           0 :         return ESYSREPO_RESULT(ResultCode::GENERIC_ERROR, "failed to read confirmation");
     179           0 :     if (answer != "y" && answer != "Y" && answer != "yes" && answer != "YES")
     180           0 :         return ESYSREPO_RESULT(ResultCode::GENERIC_ERROR, "aborted by user");
     181           0 :     return ESYSREPO_RESULT(ResultCode::OK);
     182           0 : }
     183             : 
     184           4 : Result CmdMulti::build_live_capture(Workspace &workspace, std::shared_ptr<manifest::Capture> capture)
     185             : {
     186           4 :     capture->clear();
     187             : 
     188           4 :     auto manifest = workspace.get_manifest();
     189           4 :     Result depends_result = manifest->validate_depends();
     190           4 :     if (depends_result.error())
     191             :     {
     192             :         // No depends graph is fine; DEPENDS scope then falls back to current repo only.
     193           0 :         std::ostringstream dbg;
     194           0 :         dbg << "validate_depends: " << depends_result;
     195           0 :         debug(0, dbg.str());
     196           0 :     }
     197             : 
     198           4 :     std::string folder_path;
     199           4 :     Result result = resolve_folder_path(folder_path);
     200           4 :     if (result.error()) return ESYSREPO_RESULT(result);
     201           4 :     std::string git_root = find_git_root_path(folder_path);
     202           8 :     auto current_rel = boost::filesystem::relative(git_root, workspace.get_path()).generic_string();
     203           8 :     if (current_rel.empty() || current_rel == ".") current_rel = ".";
     204             : 
     205           4 :     std::set<std::string> allowed;
     206           4 :     if (get_capture_scope() == CaptureScope::DEPENDS)
     207             :     {
     208           0 :         allowed.insert(current_rel);
     209           0 :         for (const auto &path : manifest->get_depends_closure(current_rel))
     210           0 :             allowed.insert(path);
     211             :         // Also allow "." alias for the workspace root repo path forms.
     212           0 :         allowed.insert(".");
     213             :     }
     214             : 
     215           4 :     Result rresult;
     216           4 :     git::AheadBehind ahead_behind;
     217           4 :     auto git_helper = new_git_helper();
     218           4 :     std::ostringstream info_oss;
     219             : 
     220           8 :     for (auto location : manifest->get_locations())
     221             :     {
     222          28 :         for (auto repo : location->get_repos())
     223             :         {
     224          24 :             if (get_capture_scope() == CaptureScope::DEPENDS && allowed.find(repo->get_path()) == allowed.end())
     225           0 :                 continue;
     226             : 
     227          48 :             info_oss.str("");
     228          24 :             info_oss << "repo '" << repo->get_path() << "'" << std::endl;
     229             : 
     230          48 :             std::string repo_full_path = workspace.get_full_path(repo);
     231          24 :             result = git_helper->open(repo_full_path, log::Level::DEBUG);
     232          24 :             GitHelper::AutoClose auto_close(git_helper.get());
     233          24 :             if (result.error())
     234             :             {
     235           0 :                 info_oss << "    failed to open the git repository";
     236           0 :                 error(info_oss.str());
     237           0 :                 rresult.add(ESYSREPO_RESULT(ResultCode::GIT_ERROR_OPENING_REPO, repo_full_path));
     238           0 :                 continue;
     239             :             }
     240             : 
     241          24 :             bool detached = false;
     242          24 :             result = git_helper->get_git()->is_detached(detached);
     243          24 :             if (result.error())
     244             :             {
     245           0 :                 info_oss << "    couldn't determine if HEAD is detached";
     246           0 :                 error(info_oss.str());
     247           0 :                 rresult.add(ESYSREPO_RESULT(result, repo->get_path()));
     248           0 :                 continue;
     249             :             }
     250             : 
     251             :             // SHA pins check out detached HEAD. There is no tracking branch, so
     252             :             // get_head_branch_remote cannot succeed. Compare HEAD to the
     253             :             // manifest revision instead.
     254          24 :             if (detached)
     255             :             {
     256           8 :                 git::Commit last_commit;
     257           8 :                 result = git_helper->get_git()->get_last_commit(last_commit, false);
     258           8 :                 if (result.error())
     259             :                 {
     260           0 :                     info_oss << "    couldn't get the information about the HEAD";
     261           0 :                     error(info_oss.str());
     262           0 :                     rresult.add(ESYSREPO_RESULT(result, repo->get_path()));
     263           0 :                     continue;
     264             :                 }
     265             : 
     266           8 :                 const std::string head_hash = last_commit.get_hash().get_hash();
     267          16 :                 auto revision = manifest->get_repo_revision(repo);
     268           8 :                 result = git_helper->get_git()->get_ahead_behind(ahead_behind, head_hash, revision);
     269           8 :                 if (result.error())
     270             :                 {
     271           0 :                     info_oss << "    failed to get ahead/behind vs manifest revision";
     272           0 :                     error(info_oss.str());
     273           0 :                     rresult.add(ESYSREPO_RESULT(result, repo->get_path()));
     274           0 :                     continue;
     275             :                 }
     276             : 
     277           8 :                 if ((ahead_behind.get_ahead() == 0) && (ahead_behind.get_behind() == 0))
     278             :                 {
     279           8 :                     info_oss << "    HEAD matches manifest revision";
     280           8 :                     info(info_oss.str());
     281           8 :                     continue;
     282             :                 }
     283             : 
     284           0 :                 auto item = std::make_shared<manifest::Capture::Item>();
     285           0 :                 item->set_path(repo->get_path());
     286           0 :                 item->set_revision(head_hash);
     287           0 :                 capture->add_item(item);
     288           0 :                 continue;
     289          16 :             }
     290             : 
     291          16 :             git::Branch head_branch;
     292          16 :             git::Remote head_remote;
     293          16 :             result = git_helper->get_git()->get_head_branch_remote(head_branch, head_remote);
     294          16 :             if (result.error())
     295             :             {
     296           0 :                 info_oss << "    couldn't get the information about the HEAD";
     297           0 :                 error(info_oss.str());
     298           0 :                 rresult.add(ESYSREPO_RESULT(result));
     299           0 :                 continue;
     300             :             }
     301             : 
     302          28 :             if ((repo->get_location_str() != head_remote.get_name()) || (repo->get_url() != head_remote.get_url()))
     303             :             {
     304           4 :                 auto item = std::make_shared<manifest::Capture::Item>();
     305           4 :                 item->set_path(repo->get_path());
     306           4 :                 item->set_remote_name(head_remote.get_name());
     307           4 :                 item->set_remote_url(head_remote.get_url());
     308           4 :                 item->set_revision(head_branch.get_name());
     309           8 :                 capture->add_item(item);
     310           4 :                 continue;
     311           4 :             }
     312             : 
     313          24 :             auto revision = manifest->get_repo_revision(repo);
     314          12 :             result = git_helper->get_git()->get_ahead_behind(ahead_behind, head_branch.get_name(), revision);
     315          12 :             if (result.error())
     316             :             {
     317           0 :                 info_oss << "    failed to get ahead/behind vs manifest revision";
     318           0 :                 error(info_oss.str());
     319           0 :                 rresult.add(ESYSREPO_RESULT(result, repo->get_path()));
     320           0 :                 continue;
     321             :             }
     322             : 
     323          12 :             if ((ahead_behind.get_ahead() == 0) && (ahead_behind.get_behind() == 0))
     324             :             {
     325          12 :                 info_oss << "    HEAD matches manifest revision";
     326          12 :                 info(info_oss.str());
     327          12 :                 continue;
     328             :             }
     329             : 
     330           0 :             result = git_helper->get_git()->get_ahead_behind(ahead_behind, head_branch);
     331           0 :             if (result.error())
     332             :             {
     333           0 :                 info_oss << "    failed to get ahead/behind vs upstream";
     334           0 :                 error(info_oss.str());
     335           0 :                 rresult.add(ESYSREPO_RESULT(result, repo_full_path));
     336           0 :                 continue;
     337             :             }
     338             : 
     339           0 :             if (ahead_behind.get_ahead() != 0)
     340             :             {
     341           0 :                 info_oss << "    local branch is " << ahead_behind.get_ahead()
     342           0 :                          << " commits ahead of the remote. Push your changes.";
     343           0 :                 error(info_oss.str());
     344           0 :                 continue;
     345             :             }
     346             : 
     347           0 :             capture->add_item(repo->get_path(), head_branch.get_name());
     348          72 :         }
     349           4 :     }
     350             : 
     351           4 :     if (rresult.error()) return ESYSREPO_RESULT(rresult);
     352          12 :     return apply_pin_shas(workspace, capture);
     353          16 : }
     354             : 
     355           4 : Result CmdMulti::apply_pin_shas(Workspace &workspace, std::shared_ptr<manifest::Capture> capture)
     356             : {
     357           4 :     auto manifest = workspace.get_manifest();
     358           4 :     auto git_helper = new_git_helper();
     359             : 
     360           8 :     for (auto &item : capture->get_items())
     361             :     {
     362           4 :         if (!manifest->has_revision_pin(item->get_path())) continue;
     363             : 
     364           0 :         auto repo = manifest->find_repo_by_path(item->get_path());
     365           0 :         if (repo == nullptr) continue;
     366             : 
     367           0 :         std::string repo_full_path = workspace.get_full_path(repo);
     368           0 :         Result result = git_helper->open(repo_full_path, log::Level::DEBUG);
     369           0 :         GitHelper::AutoClose auto_close(git_helper.get());
     370           0 :         if (result.error()) continue;
     371             : 
     372           0 :         git::Commit last_commit;
     373           0 :         result = git_helper->get_git()->get_last_commit(last_commit, false);
     374           0 :         if (result.error()) continue;
     375             : 
     376           0 :         const std::string &hash = last_commit.get_hash().get_hash();
     377           0 :         if (!hash.empty()) item->set_revision(hash);
     378           4 :     }
     379           4 :     return ESYSREPO_RESULT(ResultCode::OK);
     380           8 : }
     381             : 
     382           1 : Result CmdMulti::load_note_capture(const std::string &git_root_path, std::shared_ptr<manifest::Capture> &capture,
     383             :                                    bool &found)
     384             : {
     385           1 :     found = false;
     386           2 :     capture = std::make_shared<manifest::Capture>();
     387             : 
     388           1 :     auto git_helper = new_git_helper();
     389           1 :     Result result = git_helper->open(git_root_path, log::Level::DEBUG);
     390           1 :     if (result.error()) return ESYSREPO_RESULT(result);
     391           1 :     GitHelper::AutoClose auto_close(git_helper.get());
     392             : 
     393           2 :     git_helper->get_git()->add_notes_ref(k_notes_ref);
     394           1 :     result = git_helper->get_git()->fetch_all_notes();
     395             :     // Fetch may fail offline; still try local notes.
     396           1 :     (void)result;
     397             : 
     398           1 :     git::Commit last_commit;
     399           1 :     result = git_helper->get_git()->get_last_commit(last_commit, true);
     400           1 :     if (result.error()) return ESYSREPO_RESULT(ResultCode::OK);
     401             : 
     402           1 :     std::vector<std::shared_ptr<git::Note>> notes;
     403           1 :     int result_int = last_commit.get_notes(k_notes_ref, notes);
     404           1 :     if (result_int < 0 || notes.empty()) return ESYSREPO_RESULT(ResultCode::OK);
     405             : 
     406           0 :     result = load_capture(capture, notes[0]->get_message());
     407           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     408           0 :     found = !capture->get_items().empty();
     409           0 :     return ESYSREPO_RESULT(ResultCode::OK);
     410           2 : }
     411             : 
     412           1 : Result CmdMulti::resolve_effective_capture(Workspace &workspace, const std::string &git_root_path,
     413             :                                            std::shared_ptr<manifest::Capture> &effective, bool &from_note)
     414             : {
     415           1 :     from_note = false;
     416           1 :     auto live = std::make_shared<manifest::Capture>();
     417           2 :     Result result = build_live_capture(workspace, live);
     418           1 :     if (result.error()) return ESYSREPO_RESULT(result);
     419             : 
     420           1 :     std::shared_ptr<manifest::Capture> note_capture;
     421           1 :     bool note_found = false;
     422           1 :     result = load_note_capture(git_root_path, note_capture, note_found);
     423           1 :     if (result.error()) return ESYSREPO_RESULT(result);
     424             : 
     425           1 :     if (note_found)
     426             :     {
     427           0 :         from_note = true;
     428           0 :         if (!captures_equal(*note_capture, *live))
     429             :         {
     430           0 :             result = confirm_capture_mismatch();
     431           0 :             if (result.error()) return ESYSREPO_RESULT(result);
     432             :         }
     433           0 :         effective = note_capture;
     434           0 :         return ESYSREPO_RESULT(ResultCode::OK);
     435             :     }
     436             : 
     437           1 :     effective = live;
     438           1 :     return ESYSREPO_RESULT(ResultCode::OK);
     439           2 : }
     440             : 
     441           1 : Result CmdMulti::order_capture_for_push(Workspace &workspace, const std::string &current_repo_path,
     442             :                                         std::shared_ptr<manifest::Capture> capture,
     443             :                                         std::vector<std::shared_ptr<manifest::Capture::Item>> &ordered)
     444             : {
     445           1 :     ordered.clear();
     446           1 :     const auto &items = capture->get_items();
     447           1 :     if (items.empty()) return ESYSREPO_RESULT(ResultCode::OK);
     448             : 
     449           1 :     std::map<std::string, std::shared_ptr<manifest::Capture::Item>> by_path;
     450           2 :     for (const auto &item : items)
     451           1 :         by_path[item->get_path()] = item;
     452             : 
     453           1 :     std::map<std::string, int> indegree;
     454           1 :     std::map<std::string, std::vector<std::string>> adj; // dep -> dependents (push dep first)
     455           2 :     for (const auto &kv : by_path)
     456           1 :         indegree[kv.first] = 0;
     457             : 
     458           1 :     auto manifest = workspace.get_manifest();
     459           1 :     (void)manifest->validate_depends();
     460           1 :     for (const auto &edge : manifest->get_depends_edges())
     461             :     {
     462             :         // edge: from depends on to -> push `to` before `from`
     463           0 :         const std::string &from = edge.first;
     464           0 :         const std::string &to = edge.second;
     465           0 :         if (by_path.find(from) == by_path.end() || by_path.find(to) == by_path.end()) continue;
     466           0 :         adj[to].push_back(from);
     467           0 :         indegree[from] += 1;
     468           1 :     }
     469             : 
     470           1 :     std::queue<std::string> ready;
     471           2 :     for (const auto &kv : indegree)
     472             :     {
     473           1 :         if (kv.second == 0 && kv.first != current_repo_path) ready.push(kv.first);
     474             :     }
     475             :     // Prefer non-current first; current last even if indegree 0.
     476           1 :     if (indegree.find(current_repo_path) != indegree.end() && indegree[current_repo_path] == 0)
     477             :     {
     478             :         // delay current
     479             :     }
     480             : 
     481           1 :     std::vector<std::string> sorted;
     482           1 :     while (!ready.empty())
     483             :     {
     484           0 :         std::string cur = ready.front();
     485           0 :         ready.pop();
     486           0 :         sorted.push_back(cur);
     487           0 :         for (const auto &next : adj[cur])
     488             :         {
     489           0 :             indegree[next] -= 1;
     490           0 :             if (indegree[next] == 0 && next != current_repo_path) ready.push(next);
     491           0 :             else if (indegree[next] == 0 && next == current_repo_path)
     492             :             {
     493             :                 // leave for end
     494             :             }
     495             :         }
     496           0 :     }
     497             : 
     498           2 :     for (const auto &kv : indegree)
     499             :     {
     500           1 :         if (kv.first == current_repo_path) continue;
     501           0 :         if (std::find(sorted.begin(), sorted.end(), kv.first) == sorted.end()) sorted.push_back(kv.first);
     502             :     }
     503           1 :     if (by_path.find(current_repo_path) != by_path.end()) sorted.push_back(current_repo_path);
     504             : 
     505             :     // Any leftover paths (e.g. "." vs current_repo_path mismatch)
     506           2 :     for (const auto &kv : by_path)
     507             :     {
     508           1 :         if (std::find(sorted.begin(), sorted.end(), kv.first) == sorted.end()) sorted.push_back(kv.first);
     509             :     }
     510             : 
     511           2 :     for (const auto &path : sorted)
     512           1 :         ordered.push_back(by_path[path]);
     513           1 :     return ESYSREPO_RESULT(ResultCode::OK);
     514           3 : }
     515             : 
     516           5 : void CmdMulti::print_capture_plan(const std::string &title,
     517             :                                   const std::vector<std::shared_ptr<manifest::Capture::Item>> &items)
     518             : {
     519           5 :     std::ostringstream oss;
     520           5 :     oss << title << " (" << items.size() << " repo(s)):";
     521           5 :     info(oss.str());
     522          10 :     for (std::size_t i = 0; i < items.size(); ++i)
     523             :     {
     524           5 :         const auto &item = items[i];
     525          10 :         oss.str("");
     526           5 :         oss << "  [" << i << "] path=" << item->get_path() << " revision=" << item->get_revision();
     527           5 :         if (!item->get_remote_name().empty()) oss << " remote=" << item->get_remote_name();
     528           5 :         if (!item->get_remote_url().empty()) oss << " url=" << item->get_remote_url();
     529          10 :         info(oss.str());
     530             :     }
     531           5 : }
     532             : 
     533           8 : std::string CmdMulti::multi_push_state_path(const Workspace &workspace) const
     534             : {
     535           8 :     boost::filesystem::path path = workspace.get_path();
     536           8 :     path /= ".esysrepo";
     537           8 :     path /= k_push_state_file;
     538           8 :     return path.string();
     539           8 : }
     540             : 
     541           0 : Result CmdMulti::write_push_state(const Workspace &workspace, const std::string &git_root_path,
     542             :                                   const std::vector<std::shared_ptr<manifest::Capture::Item>> &ordered,
     543             :                                   std::size_t index)
     544             : {
     545           0 :     boost::filesystem::path dir = boost::filesystem::path(workspace.get_path()) / ".esysrepo";
     546           0 :     if (!boost::filesystem::exists(dir))
     547             :     {
     548           0 :         if (!boost::filesystem::create_directories(dir))
     549           0 :             return ESYSREPO_RESULT(ResultCode::FOLDER_CREATION_ERROR, dir.string());
     550             :     }
     551             : 
     552           0 :     std::ofstream out(multi_push_state_path(workspace), std::ios::trunc);
     553           0 :     if (!out) return ESYSREPO_RESULT(ResultCode::GENERIC_ERROR, "failed to write multi-push.state");
     554             : 
     555           0 :     out << "version=1\n";
     556           0 :     out << "git_root=" << git_root_path << "\n";
     557           0 :     out << "index=" << index << "\n";
     558           0 :     out << "count=" << ordered.size() << "\n";
     559           0 :     for (std::size_t i = 0; i < ordered.size(); ++i)
     560             :     {
     561           0 :         const auto &item = ordered[i];
     562           0 :         out << "path." << i << "=" << item->get_path() << "\n";
     563           0 :         out << "revision." << i << "=" << item->get_revision() << "\n";
     564           0 :         out << "remote_name." << i << "=" << item->get_remote_name() << "\n";
     565           0 :         out << "remote_url." << i << "=" << item->get_remote_url() << "\n";
     566             :     }
     567           0 :     return ESYSREPO_RESULT(ResultCode::OK);
     568           0 : }
     569             : 
     570           2 : Result CmdMulti::read_push_state(const Workspace &workspace, std::string &git_root_path,
     571             :                                  std::vector<std::shared_ptr<manifest::Capture::Item>> &ordered, std::size_t &index)
     572             : {
     573           2 :     ordered.clear();
     574           2 :     std::ifstream in(multi_push_state_path(workspace));
     575           2 :     if (!in) return ESYSREPO_RESULT(ResultCode::PATH_NOT_EXISTING, multi_push_state_path(workspace));
     576             : 
     577           1 :     std::map<std::string, std::string> kv;
     578           1 :     std::string line;
     579           9 :     while (std::getline(in, line))
     580             :     {
     581           8 :         auto pos = line.find('=');
     582           8 :         if (pos == std::string::npos) continue;
     583           8 :         kv[line.substr(0, pos)] = line.substr(pos + 1);
     584             :     }
     585             : 
     586           1 :     if (kv["version"] != "1") return ESYSREPO_RESULT(ResultCode::GENERIC_ERROR, "unsupported multi-push.state version");
     587           2 :     git_root_path = kv["git_root"];
     588           1 :     index = static_cast<std::size_t>(std::stoul(kv["index"]));
     589           1 :     std::size_t count = static_cast<std::size_t>(std::stoul(kv["count"]));
     590           1 :     ordered.resize(count);
     591           2 :     for (std::size_t i = 0; i < count; ++i)
     592             :     {
     593           1 :         auto item = std::make_shared<manifest::Capture::Item>();
     594           2 :         item->set_path(kv["path." + std::to_string(i)]);
     595           2 :         item->set_revision(kv["revision." + std::to_string(i)]);
     596           2 :         item->set_remote_name(kv["remote_name." + std::to_string(i)]);
     597           2 :         item->set_remote_url(kv["remote_url." + std::to_string(i)]);
     598           1 :         ordered[i] = item;
     599           1 :     }
     600           1 :     return ESYSREPO_RESULT(ResultCode::OK);
     601           3 : }
     602             : 
     603           1 : Result CmdMulti::clear_push_state(const Workspace &workspace)
     604             : {
     605           2 :     boost::filesystem::path path = multi_push_state_path(workspace);
     606           1 :     if (boost::filesystem::exists(path)) boost::filesystem::remove(path);
     607           1 :     return ESYSREPO_RESULT(ResultCode::OK);
     608           1 : }
     609             : 
     610           0 : Result CmdMulti::push_item_branch(Workspace &workspace, const std::shared_ptr<manifest::Capture::Item> &item)
     611             : {
     612           0 :     auto repo = workspace.get_manifest()->find_repo_by_path(item->get_path());
     613           0 :     if (repo == nullptr)
     614           0 :         return ESYSREPO_RESULT(ResultCode::GENERIC_ERROR, "unknown repo path in capture: " + item->get_path());
     615             : 
     616           0 :     std::string remote = item->get_remote_name();
     617           0 :     if (remote.empty()) remote = repo->get_location_str();
     618           0 :     if (remote.empty()) return ESYSREPO_RESULT(ResultCode::MANIFEST_REPOSITORY_WITHOUT_LOCATION);
     619             : 
     620           0 :     std::string repo_full_path = workspace.get_full_path(repo);
     621           0 :     auto git_helper = new_git_helper();
     622           0 :     Result result = git_helper->open(repo_full_path, log::Level::DEBUG);
     623           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     624           0 :     GitHelper::AutoClose auto_close(git_helper.get());
     625             : 
     626           0 :     std::string src_ref = item->get_revision();
     627             :     // SHA revisions: push HEAD to a branch name if revision looks like a hash is awkward;
     628             :     // for SHA pins push HEAD under the current branch name.
     629           0 :     if (src_ref.size() >= 7 && src_ref.find_first_not_of("0123456789abcdefABCDEF") == std::string::npos)
     630             :     {
     631           0 :         git::Branch head_branch;
     632           0 :         git::Remote head_remote;
     633           0 :         result = git_helper->get_git()->get_head_branch_remote(head_branch, head_remote);
     634           0 :         if (result.error()) return ESYSREPO_RESULT(result);
     635           0 :         src_ref = head_branch.get_name();
     636           0 :     }
     637             : 
     638           0 :     std::ostringstream oss;
     639           0 :     oss << "Pushing " << item->get_path() << " ref=" << src_ref << " remote=" << remote;
     640           0 :     info(oss.str());
     641             : 
     642           0 :     result = git_helper->get_git()->push(remote, src_ref, src_ref);
     643           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     644           0 :     return ESYSREPO_RESULT(ResultCode::OK);
     645           0 : }
     646             : 
     647           0 : Result CmdMulti::push_notes_for_current(Workspace &workspace, const std::string &git_root_path)
     648             : {
     649           0 :     auto repo_rel_path = boost::filesystem::relative(git_root_path, workspace.get_path()).generic_string();
     650           0 :     if (repo_rel_path.empty()) repo_rel_path = ".";
     651           0 :     auto repo = workspace.get_manifest()->find_repo_by_path(repo_rel_path);
     652           0 :     if (repo == nullptr) repo = workspace.get_manifest()->find_repo_by_path(".");
     653           0 :     if (repo == nullptr)
     654           0 :         return ESYSREPO_RESULT(ResultCode::GENERIC_ERROR, "current path is not a manifest repository");
     655             : 
     656           0 :     std::string remote_str = repo->get_location_str();
     657           0 :     if (remote_str.empty()) return ESYSREPO_RESULT(ResultCode::MANIFEST_REPOSITORY_WITHOUT_LOCATION);
     658             : 
     659           0 :     auto git_helper = new_git_helper();
     660           0 :     Result result = git_helper->open(git_root_path, log::Level::DEBUG);
     661           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     662           0 :     GitHelper::AutoClose auto_close(git_helper.get());
     663           0 :     set_esysrepo_author(git_helper->get_git().get());
     664             : 
     665           0 :     info("Pushing esysrepo notes to remote '" + remote_str + "'");
     666           0 :     result = git_helper->get_git()->push_notes(remote_str, k_notes_ref);
     667           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     668           0 :     return ESYSREPO_RESULT(ResultCode::OK);
     669           0 : }
     670             : 
     671           0 : Result CmdMulti::run_push_sequence(Workspace &workspace, const std::string &git_root_path,
     672             :                                    std::vector<std::shared_ptr<manifest::Capture::Item>> &ordered,
     673             :                                    std::size_t start_index)
     674             : {
     675           0 :     for (std::size_t i = start_index; i < ordered.size(); ++i)
     676             :     {
     677           0 :         Result result = write_push_state(workspace, git_root_path, ordered, i);
     678           0 :         if (result.error()) return ESYSREPO_RESULT(result);
     679             : 
     680           0 :         result = push_item_branch(workspace, ordered[i]);
     681           0 :         if (result.error())
     682             :         {
     683           0 :             error("Push failed mid-set; fix the repo then run: esysrepo multi --continue");
     684           0 :             error("Or discard state with: esysrepo multi --abort");
     685           0 :             return ESYSREPO_RESULT(result);
     686             :         }
     687           0 :     }
     688             : 
     689           0 :     Result result = push_notes_for_current(workspace, git_root_path);
     690           0 :     if (result.error())
     691             :     {
     692           0 :         error("Branch pushes succeeded but notes push failed; run: esysrepo multi --continue");
     693             :         // Keep state at end so continue retries notes after re-pushing last? Mark index past end.
     694           0 :         (void)write_push_state(workspace, git_root_path, ordered, ordered.size());
     695           0 :         return ESYSREPO_RESULT(result);
     696             :     }
     697             : 
     698           0 :     return clear_push_state(workspace);
     699           0 : }
     700             : 
     701           3 : Result CmdMulti::do_task_capture()
     702             : {
     703           3 :     Workspace workspace;
     704           3 :     std::string git_root_path;
     705           3 :     Result result = load_workspace(workspace, git_root_path);
     706           3 :     if (result.error()) return ESYSREPO_RESULT(result);
     707             : 
     708           3 :     auto capture = std::make_shared<manifest::Capture>();
     709           6 :     result = build_live_capture(workspace, capture);
     710           3 :     if (result.error()) return ESYSREPO_RESULT(result);
     711             : 
     712           3 :     if (capture->get_items().empty())
     713             :     {
     714           0 :         info("Nothing to capture.");
     715           0 :         return ESYSREPO_RESULT(ResultCode::OK);
     716             :     }
     717             : 
     718           5 :     print_capture_plan(get_dry_run() ? "Would capture" : "Capturing", capture->get_items());
     719             : 
     720           3 :     manifest::MultiFileXML file_xml;
     721           3 :     std::ostringstream oss;
     722           6 :     file_xml.set_capture(capture);
     723           3 :     result = file_xml.write(oss);
     724           3 :     if (result.error()) return ESYSREPO_RESULT(result);
     725             : 
     726           3 :     if (get_dry_run())
     727             :     {
     728           1 :         info("Dry-run: note not written.");
     729           1 :         info(oss.str());
     730           1 :         return ESYSREPO_RESULT(ResultCode::OK);
     731             :     }
     732             : 
     733           2 :     auto git_helper = new_git_helper();
     734           2 :     result = git_helper->open(git_root_path, log::Level::DEBUG);
     735           2 :     if (result.error()) return ESYSREPO_RESULT(result);
     736           2 :     GitHelper::AutoClose auto_close(git_helper.get());
     737           2 :     set_esysrepo_author(git_helper->get_git().get());
     738             : 
     739           2 :     git::NoteId note_id;
     740           2 :     result = git_helper->get_git()->add_note(note_id, k_notes_ref, oss.str(), true);
     741           2 :     if (result.error()) return ESYSREPO_RESULT(result);
     742           2 :     return ESYSREPO_RESULT(ResultCode::OK);
     743           8 : }
     744             : 
     745           0 : Result CmdMulti::do_task_delete()
     746             : {
     747           0 :     std::string folder_path;
     748           0 :     Result result = resolve_folder_path(folder_path);
     749           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     750           0 :     folder_path = find_git_root_path(folder_path);
     751             : 
     752           0 :     auto git_helper = new_git_helper();
     753           0 :     result = git_helper->open(folder_path, log::Level::DEBUG);
     754           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     755           0 :     GitHelper::AutoClose auto_close(git_helper.get());
     756             : 
     757           0 :     git::Commit last_commit;
     758           0 :     git_helper->get_git()->add_notes_ref(k_notes_ref);
     759           0 :     result = git_helper->get_git()->get_last_commit(last_commit, true);
     760           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     761             : 
     762           0 :     if (last_commit.get_all_notes().empty())
     763             :     {
     764           0 :         info("No notes found to delete.");
     765           0 :         return ESYSREPO_RESULT(ResultCode::OK);
     766             :     }
     767             : 
     768           0 :     if (get_dry_run())
     769             :     {
     770           0 :         info("Dry-run: would delete esysrepo notes on HEAD.");
     771           0 :         return ESYSREPO_RESULT(ResultCode::OK);
     772             :     }
     773             : 
     774           0 :     set_esysrepo_author(git_helper->get_git().get());
     775           0 :     result = git_helper->get_git()->remove_note_last_commit(k_notes_ref);
     776           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     777           0 :     return ESYSREPO_RESULT(ResultCode::OK);
     778           0 : }
     779             : 
     780           2 : Result CmdMulti::do_task_push()
     781             : {
     782           2 :     Workspace workspace;
     783           2 :     std::string git_root_path;
     784           2 :     Result result = load_workspace(workspace, git_root_path);
     785           2 :     if (result.error()) return ESYSREPO_RESULT(result);
     786             : 
     787           4 :     if (boost::filesystem::exists(multi_push_state_path(workspace)))
     788             :     {
     789           1 :         return ESYSREPO_RESULT(ResultCode::GENERIC_ERROR,
     790             :                                "multi push already in progress; use --continue or --abort");
     791             :     }
     792             : 
     793           1 :     std::shared_ptr<manifest::Capture> effective;
     794           1 :     bool from_note = false;
     795           1 :     result = resolve_effective_capture(workspace, git_root_path, effective, from_note);
     796           1 :     if (result.error()) return ESYSREPO_RESULT(result);
     797             : 
     798           1 :     if (effective->get_items().empty())
     799             :     {
     800           0 :         info(from_note ? "Capture note is empty." : "Nothing to push.");
     801           0 :         return ESYSREPO_RESULT(ResultCode::OK);
     802             :     }
     803             : 
     804           2 :     auto current_rel = boost::filesystem::relative(git_root_path, workspace.get_path()).generic_string();
     805           1 :     if (current_rel.empty()) current_rel = ".";
     806             : 
     807           1 :     std::vector<std::shared_ptr<manifest::Capture::Item>> ordered;
     808           2 :     result = order_capture_for_push(workspace, current_rel, effective, ordered);
     809           1 :     if (result.error()) return ESYSREPO_RESULT(result);
     810             : 
     811           1 :     print_capture_plan(get_dry_run() ? "Would push (dependency order)" : "Push plan (dependency order)", ordered);
     812           3 :     info(std::string("Effective set source: ") + (from_note ? "capture note" : "live workspace"));
     813             : 
     814           1 :     if (get_dry_run())
     815             :     {
     816           1 :         info("Dry-run: branches and notes not pushed.");
     817           1 :         return ESYSREPO_RESULT(ResultCode::OK);
     818             :     }
     819             : 
     820           0 :     return run_push_sequence(workspace, git_root_path, ordered, 0);
     821           4 : }
     822             : 
     823           2 : Result CmdMulti::do_task_abort()
     824             : {
     825           2 :     Workspace workspace;
     826           2 :     std::string git_root_path;
     827           2 :     Result result = load_workspace(workspace, git_root_path);
     828           2 :     if (result.error()) return ESYSREPO_RESULT(result);
     829             : 
     830           4 :     if (!boost::filesystem::exists(multi_push_state_path(workspace)))
     831             :     {
     832           1 :         info("No multi push in progress.");
     833           1 :         return ESYSREPO_RESULT(ResultCode::OK);
     834             :     }
     835             : 
     836           1 :     if (get_dry_run())
     837             :     {
     838           0 :         info("Dry-run: would abort multi push and remove .esysrepo/multi-push.state");
     839           0 :         return ESYSREPO_RESULT(ResultCode::OK);
     840             :     }
     841             : 
     842           1 :     result = clear_push_state(workspace);
     843           1 :     if (result.error()) return ESYSREPO_RESULT(result);
     844           1 :     info("Aborted multi push; state cleared.");
     845           1 :     return ESYSREPO_RESULT(ResultCode::OK);
     846           2 : }
     847             : 
     848           2 : Result CmdMulti::do_task_continue()
     849             : {
     850           2 :     Workspace workspace;
     851           2 :     std::string git_root_path;
     852           2 :     Result result = load_workspace(workspace, git_root_path);
     853           2 :     if (result.error()) return ESYSREPO_RESULT(result);
     854             : 
     855           2 :     std::vector<std::shared_ptr<manifest::Capture::Item>> ordered;
     856           2 :     std::size_t index = 0;
     857           2 :     std::string state_root;
     858           2 :     result = read_push_state(workspace, state_root, ordered, index);
     859           2 :     if (result.error())
     860           1 :         return ESYSREPO_RESULT(ResultCode::GENERIC_ERROR, "no multi push in progress (missing multi-push.state)");
     861             : 
     862           1 :     if (!state_root.empty()) git_root_path = state_root;
     863             : 
     864           1 :     print_capture_plan("Continuing push from saved state", ordered);
     865           1 :     std::ostringstream oss;
     866           1 :     oss << "Resume index=" << index << " / " << ordered.size();
     867           1 :     info(oss.str());
     868             : 
     869           1 :     if (get_dry_run())
     870             :     {
     871           1 :         info("Dry-run: would continue push from saved index.");
     872           1 :         return ESYSREPO_RESULT(ResultCode::OK);
     873             :     }
     874             : 
     875           0 :     if (index >= ordered.size())
     876             :     {
     877             :         // Only notes remain
     878           0 :         result = push_notes_for_current(workspace, git_root_path);
     879           0 :         if (result.error()) return ESYSREPO_RESULT(result);
     880           0 :         return clear_push_state(workspace);
     881             :     }
     882             : 
     883           0 :     return run_push_sequence(workspace, git_root_path, ordered, index);
     884           3 : }
     885             : 
     886           0 : Result CmdMulti::do_task_show()
     887             : {
     888           0 :     std::string folder_path;
     889           0 :     Result result = resolve_folder_path(folder_path);
     890           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     891           0 :     folder_path = find_git_root_path(folder_path);
     892             : 
     893           0 :     auto git_helper = new_git_helper();
     894           0 :     result = git_helper->open(folder_path, log::Level::DEBUG);
     895           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     896           0 :     GitHelper::AutoClose auto_close(git_helper.get());
     897             : 
     898           0 :     int result_int = git_helper->get_git()->add_notes_ref(k_notes_ref);
     899           0 :     if (result_int < 0) return ESYSREPO_RESULT(ResultCode::GIT_RAW_INT_ERROR, result_int);
     900             : 
     901           0 :     result = git_helper->get_git()->fetch_all_notes();
     902           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     903             : 
     904           0 :     git::Commit last_commit;
     905           0 :     result = git_helper->get_git()->get_last_commit(last_commit, true);
     906           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     907             : 
     908           0 :     std::vector<std::shared_ptr<git::Note>> notes;
     909           0 :     result_int = last_commit.get_notes(k_notes_ref, notes);
     910           0 :     if (result_int < 0) return ESYSREPO_RESULT(ResultCode::GIT_RAW_INT_ERROR, result_int);
     911             : 
     912           0 :     std::ostringstream oss;
     913           0 :     oss << "Found " << notes.size() << " esysrepo notes.";
     914           0 :     info(oss.str());
     915           0 :     if (notes.empty()) return ESYSREPO_RESULT(ResultCode::OK);
     916             : 
     917           0 :     for (std::size_t idx = 0; idx < notes.size(); ++idx)
     918             :     {
     919           0 :         oss.str("");
     920           0 :         oss << "[" << idx << "] :";
     921           0 :         result = print_esysrepo_note(oss, notes[idx]->get_message());
     922           0 :         if (result.error()) return ESYSREPO_RESULT(result);
     923           0 :         info(oss.str());
     924             :     }
     925           0 :     return ESYSREPO_RESULT(ResultCode::OK);
     926           0 : }
     927             : 
     928             : } // namespace esys::repo::exe

Generated by: LCOV version 1.14