Line data Source code
1 : /*!
2 : * \file esys/repo/manifest/syncrepo_manifest.cpp
3 : * \brief
4 : *
5 : * \cond
6 : * __legal_b__
7 : *
8 : * Copyright (c) 2020-2023 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/syncrepo.h"
20 : #include "esys/repo/manifest/location.h"
21 :
22 : #include "esys/repo/filesystem.h"
23 :
24 : #include <esys/trace/call.h>
25 : #include <esys/trace/macros.h>
26 :
27 : #include <boost/filesystem.hpp>
28 :
29 : #include <cctype>
30 : #include <sstream>
31 : #include <string>
32 :
33 : namespace esys::repo::manifest
34 : {
35 :
36 : namespace
37 : {
38 :
39 : //! True for a full 40-char hex commit id (not a branch/tag name).
40 0 : bool looks_like_full_commit_id(const std::string &rev)
41 : {
42 0 : if (rev.size() != 40) return false;
43 0 : for (unsigned char c : rev)
44 : {
45 0 : if (!std::isxdigit(c)) return false;
46 : }
47 : return true;
48 : }
49 :
50 : //! Strip refs/heads/ or refs/tags/ so clone --branch / checkout_branch accept it.
51 0 : std::string rev_name_for_clone(std::string rev)
52 : {
53 0 : const std::string heads = "refs/heads/";
54 0 : const std::string tags = "refs/tags/";
55 0 : if (rev.compare(0, heads.size(), heads) == 0) return rev.substr(heads.size());
56 0 : if (rev.compare(0, tags.size(), tags) == 0) return rev.substr(tags.size());
57 0 : return rev;
58 0 : }
59 :
60 : } // namespace
61 :
62 120 : SyncRepo::SyncRepo()
63 120 : : log::User()
64 : {
65 120 : }
66 :
67 120 : SyncRepo::~SyncRepo() = default;
68 :
69 120 : int SyncRepo::run()
70 : {
71 120 : int result = 0;
72 120 : ETRC_CALL_RET_NP(result);
73 :
74 120 : return ETRC_RET(process_repo());
75 120 : }
76 :
77 120 : int SyncRepo::process_repo()
78 : {
79 120 : int result = 0;
80 120 : ETRC_CALL_RET_NP(result);
81 :
82 120 : boost::filesystem::path path = get_config_folder()->get_workspace_path();
83 345 : if (get_repo()->get_path() != ".") path /= get_repo()->get_path();
84 :
85 120 : if (!GitBase::is_repo(path.string())) return ETRC_RET(clone());
86 7 : return ETRC_RET(fetch_update());
87 120 : }
88 :
89 0 : bool SyncRepo::has_branch(GitHelper &git_helper, const std::string &branch)
90 : {
91 0 : bool result_bool = false;
92 0 : ETRC_CALL_RET(result_bool, branch);
93 :
94 0 : git::Branches branches;
95 0 : Result result = git_helper.get_branches(branches, git::BranchType::REMOTE, log::Level::DEBUG);
96 0 : if (result.error())
97 : {
98 0 : git_helper.error("Couldn't get the list of local branches");
99 0 : return ETRC_RET(false);
100 : }
101 :
102 0 : branches.sort();
103 :
104 0 : auto branch_ptr = branches.find(branch);
105 :
106 0 : if (branch_ptr != nullptr)
107 : {
108 0 : git_helper.debug(0, "Branch found : " + branch);
109 0 : return ETRC_RET(true);
110 : }
111 0 : git_helper.debug(0, "Branch not found : " + branch);
112 0 : return ETRC_RET(false);
113 0 : }
114 :
115 113 : std::string SyncRepo::get_checkout_revision(GitHelper &git_helper)
116 : {
117 113 : std::string result;
118 113 : ETRC_CALL_RET_NP(result);
119 :
120 213 : if (get_branch().empty()) return ETRC_RET(get_repo()->get_revision());
121 :
122 13 : Result_t<bool> branch_exists =
123 13 : git_helper.has_branch(get_branch(), git::BranchType::REMOTE, esys::log::Level::DEBUG);
124 :
125 13 : std::ostringstream oss;
126 13 : oss << "branch_exists = " << branch_exists;
127 13 : debug(0, oss.str());
128 :
129 13 : if (branch_exists) return ETRC_RET(get_branch());
130 12 : return ETRC_RET(get_repo()->get_revision());
131 126 : }
132 :
133 120 : std::shared_ptr<GitHelper> SyncRepo::new_git_helper()
134 : {
135 240 : auto git_helper = std::make_shared<GitHelper>(get_git(), get_logger_if(), static_cast<int>(get_repo_idx()));
136 :
137 360 : get_git()->set_logger_if(git_helper);
138 120 : return git_helper;
139 0 : }
140 :
141 120 : SyncRepo::PhaseCapture::PhaseCapture(SyncRepo &self, GitHelper &helper)
142 120 : : m_self(self)
143 120 : , m_helper(helper)
144 : {
145 120 : }
146 :
147 120 : SyncRepo::PhaseCapture::~PhaseCapture()
148 : {
149 120 : m_self.m_phase_timings = m_helper.get_phase_timings();
150 120 : m_helper.log_phase_summary();
151 120 : }
152 :
153 0 : const GitPhaseTimings &SyncRepo::get_phase_timings() const
154 : {
155 0 : return m_phase_timings;
156 : }
157 :
158 113 : int SyncRepo::clone()
159 : {
160 113 : int result = 0;
161 113 : ETRC_CALL_RET_NP(result);
162 :
163 113 : std::shared_ptr<GitHelper> git_helper = new_git_helper();
164 113 : PhaseCapture phase_capture(*this, *git_helper);
165 113 : std::string url = get_repo_url();
166 113 : std::string branch_to_checkout;
167 :
168 113 : boost::filesystem::path path = get_config_folder()->get_workspace_path();
169 226 : ETRC_MSG("path : " + get_repo()->get_path());
170 :
171 226 : if (get_repo()->get_path() != ".")
172 : {
173 : // A simple clone can be made
174 99 : path /= get_repo()->get_path();
175 :
176 : // Prefer cloning the target branch/tag tip in one step when opted in
177 : // (CloneOptions::checkout_rev). Default options keep the legacy path:
178 : // clone remote default tip, then fetch + checkout.
179 99 : std::string clone_rev;
180 99 : git::CloneOptions clone_opts = get_clone_options();
181 99 : if (clone_opts.checkout_rev)
182 : {
183 0 : if (!get_branch().empty())
184 0 : clone_rev = rev_name_for_clone(get_branch());
185 0 : else if (!get_repo()->get_revision().empty() && !looks_like_full_commit_id(get_repo()->get_revision()))
186 0 : clone_rev = rev_name_for_clone(get_repo()->get_revision());
187 :
188 0 : if (clone_rev.empty()) clone_opts.checkout_rev = false;
189 : }
190 :
191 99 : Result rresult =
192 99 : git_helper->clone_rev(url, clone_rev, path.string(), false, log::Level::INFO, 0, clone_opts);
193 99 : if (rresult.error() && clone_opts.checkout_rev && !clone_rev.empty())
194 : {
195 : // Branch/tag missing on this remote (e.g. sync -b), or odd rev: fall back.
196 0 : git_helper->debug(0, "clone with rev '" + clone_rev + "' failed; cloning default");
197 0 : clone_rev.clear();
198 0 : clone_opts = git::CloneOptions();
199 0 : rresult = git_helper->clone_rev(url, "", path.string(), false, log::Level::INFO, 0, clone_opts);
200 : }
201 99 : if (rresult.error())
202 : {
203 0 : git_helper->close_on_error(log::Level::DEBUG);
204 0 : return ETRC_RET(rresult.get_result_code_int());
205 : }
206 :
207 99 : if (clone_opts.checkout_rev && !clone_rev.empty())
208 : {
209 : // Already on the requested branch/tag tip from clone.
210 0 : rresult = git_helper->close(log::Level::DEBUG);
211 0 : return ETRC_RET(rresult.get_result_code_int());
212 : }
213 :
214 99 : if (!get_branch().empty())
215 : {
216 11 : Result result = git_helper->fetch(get_log_level());
217 11 : if (result.error())
218 : {
219 0 : git_helper->close_on_error(log::Level::DEBUG);
220 0 : return ETRC_RET(result.get_result_code_int());
221 : }
222 11 : }
223 :
224 99 : branch_to_checkout = get_checkout_revision(*git_helper.get());
225 99 : git_helper->debug(0, "branch_to_checkout = " + branch_to_checkout);
226 99 : if (branch_to_checkout.empty()) return ETRC_RET(0);
227 :
228 51 : rresult = git_helper->checkout(branch_to_checkout, false, log::Level::INFO);
229 51 : if (rresult.error())
230 : {
231 0 : git_helper->close_on_error(log::Level::DEBUG);
232 0 : return ETRC_RET(rresult.get_result_code_int());
233 : }
234 51 : rresult = git_helper->close(log::Level::DEBUG);
235 51 : return ETRC_RET(rresult.get_result_code_int());
236 99 : }
237 :
238 14 : if (get_workspace_root_clone_method() == WorkspaceRootCloneMethod::InitFetch)
239 1 : return ETRC_RET(clone_workspace_root_init_fetch(*git_helper.get(), url, path.string()));
240 :
241 13 : return ETRC_RET(clone_workspace_root_temp_move(*git_helper.get(), url, path.string()));
242 226 : }
243 :
244 13 : int SyncRepo::clone_workspace_root_temp_move(GitHelper &git_helper, const std::string &url, const std::string &path)
245 : {
246 13 : int result = 0;
247 13 : ETRC_CALL_RET(result, url, path);
248 :
249 : // Folder is not empty (.esysrepo): clone into temp, then move into the workspace
250 13 : boost::filesystem::path temp_path = get_config_folder()->get_temp_path();
251 13 : std::ostringstream oss;
252 13 : oss << "repo_temp" << get_repo_idx();
253 13 : temp_path /= oss.str();
254 :
255 13 : Result rresult = git_helper.clone(url, temp_path.string(), path, true, get_log_level());
256 13 : if (rresult.error())
257 : {
258 0 : git_helper.close_on_error(log::Level::DEBUG);
259 0 : return ETRC_RET(rresult.get_result_code_int());
260 : }
261 :
262 13 : rresult = git_helper.open(path, esys::log::INFO);
263 13 : if (rresult.error()) return ETRC_RET(rresult.get_result_code_int());
264 :
265 13 : if (!get_branch().empty())
266 : {
267 2 : rresult = git_helper.fetch(get_log_level());
268 2 : if (rresult.error())
269 : {
270 0 : git_helper.close_on_error(log::Level::DEBUG);
271 0 : return ETRC_RET(rresult.get_result_code_int());
272 : }
273 : }
274 :
275 13 : return ETRC_RET(finish_workspace_root_clone(git_helper));
276 13 : }
277 :
278 1 : int SyncRepo::clone_workspace_root_init_fetch(GitHelper &git_helper, const std::string &url, const std::string &path)
279 : {
280 1 : int result = 0;
281 1 : ETRC_CALL_RET(result, url, path);
282 :
283 2 : git_helper.info("Init+fetch workspace root ...\n path : " + path + "\n url : " + url);
284 :
285 1 : Result rresult = git_helper.get_git()->init(path);
286 1 : if (rresult.error())
287 : {
288 0 : git_helper.error("Failed to init workspace root", rresult.get_result_code_int());
289 0 : git_helper.close_on_error(log::Level::DEBUG);
290 0 : return ETRC_RET(rresult.get_result_code_int());
291 : }
292 :
293 1 : rresult = git_helper.get_git()->add_remote("origin", url);
294 1 : if (rresult.error())
295 : {
296 0 : git_helper.error("Failed to add remote origin", rresult.get_result_code_int());
297 0 : git_helper.close_on_error(log::Level::DEBUG);
298 0 : return ETRC_RET(rresult.get_result_code_int());
299 : }
300 :
301 1 : rresult = git_helper.fetch("origin", get_log_level());
302 1 : if (rresult.error())
303 : {
304 0 : git_helper.close_on_error(log::Level::DEBUG);
305 0 : return ETRC_RET(rresult.get_result_code_int());
306 : }
307 :
308 1 : return ETRC_RET(finish_workspace_root_clone(git_helper));
309 1 : }
310 :
311 14 : int SyncRepo::finish_workspace_root_clone(GitHelper &git_helper)
312 : {
313 14 : int result = 0;
314 14 : ETRC_CALL_RET_NP(result);
315 :
316 14 : std::string branch_to_checkout = resolve_checkout_revision(git_helper);
317 14 : git_helper.debug(0, "branch_to_checkout = " + branch_to_checkout);
318 14 : if (branch_to_checkout.empty())
319 : {
320 0 : git_helper.error("No revision to checkout after workspace-root clone");
321 0 : git_helper.close_on_error(log::Level::DEBUG);
322 0 : return ETRC_RET(-1);
323 : }
324 :
325 14 : Result rresult = git_helper.checkout(branch_to_checkout, get_force(), log::Level::INFO);
326 14 : if (rresult.error())
327 : {
328 0 : git_helper.close_on_error(log::Level::DEBUG);
329 0 : return ETRC_RET(rresult.get_result_code_int());
330 : }
331 14 : rresult = git_helper.close(log::Level::DEBUG);
332 14 : return ETRC_RET(rresult.get_result_code_int());
333 28 : }
334 :
335 14 : std::string SyncRepo::resolve_checkout_revision(GitHelper &git_helper)
336 : {
337 14 : std::string rev = get_checkout_revision(git_helper);
338 :
339 14 : const std::string heads = "refs/heads/";
340 14 : if (rev.compare(0, heads.size(), heads) == 0) rev = rev.substr(heads.size());
341 28 : if (!rev.empty()) return rev;
342 :
343 : // Init-fetch leaves no default checkout (unlike clone). Pick a remote branch.
344 13 : git::Branches branches;
345 13 : Result rresult = git_helper.get_branches(branches, git::BranchType::REMOTE, log::Level::DEBUG);
346 13 : if (rresult.error() || branches.size() == 0) return {};
347 :
348 78 : auto short_name = [](const std::string &full) -> std::string {
349 65 : auto pos = full.rfind('/');
350 65 : if (pos == std::string::npos) return full;
351 65 : return full.substr(pos + 1);
352 : };
353 :
354 26 : auto find_named = [&](const std::string &want) -> std::string {
355 65 : for (const auto &branch : branches.get())
356 : {
357 65 : if (short_name(branch->get_name()) == want) return want;
358 : }
359 0 : return {};
360 13 : };
361 :
362 13 : rev = find_named("master");
363 13 : if (rev.empty()) rev = find_named("main");
364 13 : if (rev.empty()) rev = short_name(branches.get()[0]->get_name());
365 13 : return rev;
366 27 : }
367 :
368 7 : int SyncRepo::fetch_update()
369 : {
370 7 : int result_int = 0;
371 7 : ETRC_CALL_RET_NP(result_int);
372 :
373 7 : std::shared_ptr<GitHelper> git_helper = new_git_helper();
374 7 : PhaseCapture phase_capture(*this, *git_helper);
375 7 : git_helper->set_display_repo_idx(get_display_repo_idx());
376 :
377 7 : boost::filesystem::path path = get_config_folder()->get_workspace_path();
378 20 : if (get_repo()->get_path() != ".") path /= get_repo()->get_path();
379 :
380 7 : Result rresult = git_helper->open(path.string(), log::Level::INFO);
381 7 : if (rresult.error()) return ETRC_RET(rresult.get_result_code_int());
382 :
383 7 : bool dirty = false;
384 7 : rresult = git_helper->is_dirty(dirty, log::Level::DEBUG);
385 7 : if (rresult.error()) return ETRC_RET(rresult.get_result_code_int());
386 :
387 7 : if (dirty)
388 : {
389 0 : git_helper->warn("Changes detected in repo, no sync.");
390 0 : return ETRC_RET(0);
391 : }
392 :
393 7 : if (get_branch().empty())
394 7 : return ETRC_RET(normal_sync(*git_helper.get()));
395 : else
396 0 : return ETRC_RET(branch_sync(*git_helper.get()));
397 14 : }
398 :
399 7 : int SyncRepo::normal_sync(GitHelper &git_helper)
400 : {
401 7 : int result_int = 0;
402 7 : ETRC_CALL_RET_NP(result_int);
403 :
404 7 : bool detached = false;
405 7 : Result result = git_helper.is_detached(detached, log::Level::DEBUG);
406 7 : if (result.error())
407 : {
408 0 : git_helper.error("Couldn't detect if the git repo is detached or not.");
409 0 : return ETRC_RET(result.get_result_code_int());
410 : }
411 :
412 7 : if (detached)
413 : {
414 : // Soft skip (same as dirty): detached HEADs are left alone on a normal sync
415 2 : git_helper.warn("Sync repo aborted: the git repo is detached.");
416 2 : return ETRC_RET(0);
417 : }
418 :
419 5 : result = git_helper.fetch(log::Level::DEBUG);
420 5 : if (result.error())
421 : {
422 0 : git_helper.error("Fetch failed on the git repo");
423 0 : return ETRC_RET(result.get_result_code_int());
424 : }
425 :
426 5 : git::Branches branches;
427 5 : result = git_helper.get_branches(branches, git::BranchType::LOCAL, log::Level::DEBUG);
428 5 : if (result.error())
429 : {
430 0 : git_helper.error("Couldn't get the list of local branches");
431 0 : return ETRC_RET(result.get_result_code_int());
432 : }
433 :
434 5 : branches.sort();
435 :
436 10 : if (branches.get_head() == nullptr)
437 : {
438 0 : git_helper.error("Couldn't get the HEAD.");
439 0 : return ETRC_RET(-4);
440 : }
441 :
442 5 : std::vector<std::string> refs;
443 5 : git::MergeAnalysisResult merge_analysis_result;
444 5 : std::vector<git::CommitHash> commits;
445 :
446 5 : refs.push_back(branches.get_head()->get_remote_branch());
447 :
448 5 : result = git_helper.merge_analysis(refs, merge_analysis_result, commits, log::Level::DEBUG);
449 5 : if (result.error())
450 : {
451 0 : git_helper.error("Merge analysis failed.");
452 0 : return ETRC_RET(-5);
453 : }
454 :
455 5 : if (merge_analysis_result == git::MergeAnalysisResult::UP_TO_DATE)
456 : {
457 4 : git_helper.info("Git repo up to date.");
458 4 : return ETRC_RET(0);
459 : }
460 1 : if (merge_analysis_result != git::MergeAnalysisResult::FASTFORWARD)
461 : {
462 0 : git_helper.warn("Sync manifest aborted: can't be fastforwarded.");
463 0 : return ETRC_RET(-6);
464 : }
465 :
466 : // For a fastforward, there should be only one commit
467 1 : if (commits.size() != 1)
468 : {
469 0 : git_helper.error("Fast forward failed.");
470 0 : return ETRC_RET(-7);
471 : }
472 :
473 1 : git_helper.info("Fast forwarding git repo ...");
474 :
475 1 : result = git_helper.fastforward(commits[0], log::Level::DEBUG);
476 1 : if (result.error())
477 : {
478 0 : git_helper.error("Fast forward failed.");
479 0 : return ETRC_RET(-8);
480 : }
481 1 : git_helper.info("Fast forward completed.");
482 1 : return ETRC_RET(0);
483 7 : }
484 :
485 0 : int SyncRepo::branch_sync(GitHelper &git_helper)
486 : {
487 0 : int result_int = false;
488 0 : ETRC_CALL_RET_NP(result_int);
489 :
490 0 : if (!has_branch(git_helper, get_branch())) return ETRC_RET(normal_sync(git_helper));
491 :
492 0 : Result result = git_helper.checkout(get_branch(), get_force(), log::Level::DEBUG);
493 0 : if (result.ok())
494 0 : info("Checkout branch " + get_branch() + ".");
495 : else
496 0 : error("Failed to checkout branch " + get_branch() + ".");
497 0 : return ETRC_RET(result.get_result_code_int());
498 0 : }
499 :
500 0 : void SyncRepo::set_log_level(log::Level log_level)
501 : {
502 0 : m_log_level = log_level;
503 0 : }
504 :
505 27 : log::Level SyncRepo::get_log_level()
506 : {
507 27 : return m_log_level;
508 : }
509 :
510 120 : void SyncRepo::set_repo_idx(std::size_t repo_idx)
511 : {
512 120 : m_repo_idx = repo_idx;
513 120 : }
514 :
515 0 : std::size_t SyncRepo::get_repo_idx() const
516 : {
517 0 : return m_repo_idx;
518 : }
519 :
520 253 : std::size_t &SyncRepo::get_repo_idx()
521 : {
522 253 : return m_repo_idx;
523 : }
524 :
525 120 : void SyncRepo::set_display_repo_idx(bool display_repo_idx)
526 : {
527 120 : m_display_repo_idx = display_repo_idx;
528 120 : }
529 :
530 7 : bool SyncRepo::get_display_repo_idx() const
531 : {
532 7 : return m_display_repo_idx;
533 : }
534 :
535 113 : std::string SyncRepo::get_repo_url()
536 : {
537 113 : assert(get_repo() != nullptr);
538 113 : assert(get_repo()->get_location() != nullptr);
539 :
540 113 : std::string url;
541 113 : if (!get_alt_address())
542 339 : url = get_repo()->get_location()->get_address();
543 : else
544 0 : url = get_repo()->get_location()->get_alt_address();
545 :
546 226 : url += "/" + get_repo()->get_name();
547 :
548 113 : return url;
549 0 : }
550 :
551 13 : void SyncRepo::set_branch(const std::string &branch)
552 : {
553 13 : m_branch = branch;
554 13 : }
555 :
556 252 : const std::string &SyncRepo::get_branch() const
557 : {
558 252 : return m_branch;
559 : }
560 :
561 120 : void SyncRepo::set_alt_address(bool alt_address)
562 : {
563 120 : m_alt_address = alt_address;
564 120 : }
565 :
566 113 : bool SyncRepo::get_alt_address() const
567 : {
568 113 : return m_alt_address;
569 : }
570 :
571 0 : void SyncRepo::set_force(bool force)
572 : {
573 0 : m_force = force;
574 0 : }
575 :
576 14 : bool SyncRepo::get_force() const
577 : {
578 14 : return m_force;
579 : }
580 :
581 120 : void SyncRepo::set_workspace_root_clone_method(WorkspaceRootCloneMethod method)
582 : {
583 120 : m_workspace_root_clone_method = method;
584 120 : }
585 :
586 14 : WorkspaceRootCloneMethod SyncRepo::get_workspace_root_clone_method() const
587 : {
588 14 : return m_workspace_root_clone_method;
589 : }
590 :
591 120 : void SyncRepo::set_clone_options(const git::CloneOptions &options)
592 : {
593 120 : m_clone_options = options;
594 120 : }
595 :
596 99 : const git::CloneOptions &SyncRepo::get_clone_options() const
597 : {
598 99 : return m_clone_options;
599 : }
600 :
601 : } // namespace esys::repo::manifest
|