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 ¤t_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
|