Line data Source code
1 : /*!
2 : * \file esys/repo/gitcmdline/git_cmdline.cpp
3 : * \brief System ``git`` CLI backend (Phase-1 esysrepo surface)
4 : *
5 : * \cond
6 : * __legal_b__
7 : *
8 : * Copyright (c) 2020-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/gitcmdline/git.h"
20 :
21 : #include "esys/repo/git/aheadbehind.h"
22 : #include "esys/repo/git/branch.h"
23 : #include "esys/repo/git/commit.h"
24 : #include "esys/repo/git/commithash.h"
25 : #include "esys/repo/git/remote.h"
26 : #include "esys/repo/git/repostatus.h"
27 : #include "esys/repo/git/resettype.h"
28 : #include "esys/repo/git/status.h"
29 : #include "esys/repo/git/statussubtype.h"
30 : #include "esys/repo/git/statustype.h"
31 : #include "esys/repo/git/worktree.h"
32 :
33 : #include <boost/filesystem.hpp>
34 : #include <boost/process/search_path.hpp>
35 :
36 : #include <chrono>
37 : #include <cstdio>
38 : #include <cstdlib>
39 : #include <ctime>
40 : #include <map>
41 : #include <memory>
42 : #include <sstream>
43 : #include <string>
44 : #include <vector>
45 :
46 : #ifdef _WIN32
47 : #define ESYSREPO_POPEN _popen
48 : #define ESYSREPO_PCLOSE _pclose
49 : #else
50 : #define ESYSREPO_POPEN popen
51 : #define ESYSREPO_PCLOSE pclose
52 : #endif
53 :
54 : namespace esys::repo::gitcmdline
55 : {
56 :
57 : namespace
58 : {
59 :
60 1840 : std::string shell_quote(const std::string &arg)
61 : {
62 : #ifdef _WIN32
63 : // cmd.exe: double % (env expansion) and quote when spaces or shell metacharacters
64 : // appear (notably '|' in git for-each-ref --format).
65 : std::string escaped;
66 : escaped.reserve(arg.size() * 2);
67 : for (char c : arg)
68 : {
69 : if (c == '%') escaped += '%';
70 : escaped += c;
71 : }
72 : const bool need_quote = escaped.find_first_of(" \t\"|&<>^!") != std::string::npos;
73 : if (!need_quote) return escaped;
74 : std::string out = "\"";
75 : for (char c : escaped)
76 : {
77 : // cmd embedded quote is ""
78 : if (c == '"') out += '"';
79 : out += c;
80 : }
81 : out += '"';
82 : return out;
83 : #else
84 1840 : if (arg.find_first_of(" \t\"'\\") == std::string::npos) return arg;
85 0 : std::string out = "\"";
86 0 : for (char c : arg)
87 : {
88 0 : if (c == '"') out += '\\';
89 0 : out += c;
90 : }
91 0 : out += '"';
92 0 : return out;
93 : #endif
94 1840 : }
95 :
96 48 : std::string trim(std::string s)
97 : {
98 85 : while (!s.empty() && (s.back() == '\n' || s.back() == '\r' || s.back() == ' ' || s.back() == '\t')) s.pop_back();
99 : std::size_t i = 0;
100 50 : while (i < s.size() && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r')) ++i;
101 48 : return s.substr(i);
102 : }
103 :
104 2 : std::string strip_rev_prefix(std::string rev)
105 : {
106 2 : const std::string heads = "refs/heads/";
107 2 : const std::string tags = "refs/tags/";
108 2 : if (rev.compare(0, heads.size(), heads) == 0) return rev.substr(heads.size());
109 2 : if (rev.compare(0, tags.size(), tags) == 0) return rev.substr(tags.size());
110 4 : return rev;
111 2 : }
112 :
113 7 : std::vector<std::string> split_lines(const std::string &text)
114 : {
115 7 : std::vector<std::string> lines;
116 7 : std::string cur;
117 745 : for (char c : text)
118 : {
119 738 : if (c == '\n')
120 : {
121 14 : if (!cur.empty() && cur.back() == '\r') cur.pop_back();
122 14 : lines.push_back(cur);
123 14 : cur.clear();
124 : }
125 : else
126 1462 : cur += c;
127 : }
128 7 : if (!cur.empty())
129 : {
130 0 : if (cur.back() == '\r') cur.pop_back();
131 0 : lines.push_back(cur);
132 : }
133 7 : return lines;
134 7 : }
135 :
136 1 : git::StatusSubType porcelain_to_subtype(char code)
137 : {
138 1 : switch (code)
139 : {
140 : case 'A': return git::StatusSubType::NEW;
141 : case 'M': return git::StatusSubType::MODIFIED;
142 : case 'D': return git::StatusSubType::DELETED;
143 : case 'R': return git::StatusSubType::RENAMED;
144 : case 'C': return git::StatusSubType::NEW;
145 : case 'T': return git::StatusSubType::TYPECHANGE;
146 : case '?': return git::StatusSubType::NEW;
147 : default: return git::StatusSubType::NOT_SET;
148 : }
149 : }
150 :
151 1 : git::DiffDeltaType porcelain_to_delta_type(char code, bool worktree)
152 : {
153 1 : switch (code)
154 : {
155 : case 'A': return git::DiffDeltaType::ADDED;
156 1 : case 'M': return git::DiffDeltaType::MODIFIED;
157 0 : case 'D': return git::DiffDeltaType::DELETED;
158 0 : case 'R': return git::DiffDeltaType::RENAMED;
159 0 : case 'C': return git::DiffDeltaType::COPIED;
160 0 : case 'T': return git::DiffDeltaType::TYPECHANGE;
161 0 : case '?': return worktree ? git::DiffDeltaType::UNTRACKED : git::DiffDeltaType::ADDED;
162 0 : case '!': return git::DiffDeltaType::IGNORED;
163 0 : case 'U': return git::DiffDeltaType::CONFLICTED;
164 0 : default: return git::DiffDeltaType::NOT_SET;
165 : }
166 : }
167 :
168 2 : void fill_status_paths(git::Status &status, const std::string &old_path, const std::string &new_path,
169 : git::DiffDeltaType delta_type)
170 : {
171 2 : auto &delta = status.get_diff_delta();
172 2 : delta.set_type(delta_type);
173 2 : delta.get_old_file().set_path(old_path);
174 4 : delta.get_new_file().set_path(new_path.empty() ? old_path : new_path);
175 2 : delta.set_file_count(old_path == new_path || new_path.empty() ? 1 : 2);
176 2 : }
177 :
178 2 : bool parse_porcelain_paths(const std::string &rest, std::string &old_path, std::string &new_path)
179 : {
180 : // XY<path> or XY <ORIG> -> <PATH> (rename/copy). Optional leading space after XY.
181 2 : std::string path_part = rest;
182 2 : if (!path_part.empty() && path_part[0] == ' ') path_part.erase(0, 1);
183 :
184 2 : const std::string arrow = " -> ";
185 2 : const auto arrow_pos = path_part.find(arrow);
186 2 : if (arrow_pos != std::string::npos)
187 : {
188 0 : old_path = path_part.substr(0, arrow_pos);
189 0 : new_path = path_part.substr(arrow_pos + arrow.size());
190 0 : return !old_path.empty() && !new_path.empty();
191 : }
192 :
193 2 : old_path = path_part;
194 2 : new_path = path_part;
195 2 : return !old_path.empty();
196 2 : }
197 :
198 2 : void add_porcelain_status(git::RepoStatus &repo_status, git::StatusType type, git::StatusSubType sub_type,
199 : git::DiffDeltaType delta_type, const std::string &old_path, const std::string &new_path)
200 : {
201 2 : auto status = std::make_shared<git::Status>();
202 2 : status->set_type(type);
203 2 : status->set_sub_type(sub_type);
204 : // RepoStatus::add keys the file map by old_file path — always set it.
205 2 : fill_status_paths(*status, old_path, new_path, delta_type);
206 6 : repo_status.add(status);
207 2 : }
208 :
209 : } // namespace
210 :
211 : bool Git::s_detect_ssh_agent_done = false;
212 : bool Git::s_ssh_agent_running = false;
213 :
214 0 : std::shared_ptr<GitBase> Git::new_ptr()
215 : {
216 0 : return std::make_shared<Git>();
217 : }
218 :
219 196 : Git::Git()
220 196 : : GitBase()
221 : {
222 196 : }
223 :
224 196 : Git::~Git()
225 : {
226 196 : if (m_open) close();
227 196 : }
228 :
229 622 : std::string Git::find_git_executable()
230 : {
231 622 : if (const char *env = std::getenv("ESYSREPO_GIT_EXE"))
232 : {
233 0 : if (env[0] != '\0' && boost::filesystem::exists(env)) return std::string(env);
234 : }
235 622 : if (const char *env = std::getenv("GIT_EXECUTABLE"))
236 : {
237 0 : if (env[0] != '\0' && boost::filesystem::exists(env)) return std::string(env);
238 : }
239 1244 : boost::filesystem::path found = boost::process::search_path("git");
240 : // Prefer the bare name so std::system/cmd.exe does not break on spaces in
241 : // "C:\Program Files\Git\..." (PATH already contains the directory).
242 622 : if (!found.empty()) return "git";
243 622 : return {};
244 622 : }
245 :
246 0 : Result Git::not_implemented() const
247 : {
248 0 : return ESYSREPO_RESULT(ResultCode::NOT_IMPLEMENTED);
249 : }
250 :
251 305 : std::string Git::build_git_command(const std::vector<std::string> &args, const std::string &cwd) const
252 : {
253 305 : const std::string git = find_git_executable();
254 305 : std::ostringstream cmd;
255 610 : cmd << shell_quote(git);
256 426 : if (!cwd.empty()) cmd << " -C " << shell_quote(cwd);
257 1719 : for (const auto &arg : args) cmd << ' ' << shell_quote(arg);
258 305 : return cmd.str();
259 305 : }
260 :
261 18 : Result Git::run_git(const std::vector<std::string> &args, const std::string &cwd)
262 : {
263 18 : const std::string git = find_git_executable();
264 18 : if (git.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "git executable not found");
265 :
266 18 : const std::string cmd = build_git_command(args, cwd);
267 36 : debug(1, std::string("[gitcmdline] ") + cmd);
268 18 : cmd_start();
269 18 : const int rc = std::system(cmd.c_str());
270 18 : cmd_end();
271 18 : if (rc != 0) return ESYSREPO_RESULT(ResultCode::RAW_INT_ERROR, rc, cmd);
272 16 : return ESYSREPO_RESULT(ResultCode::OK);
273 18 : }
274 :
275 836 : void Git::feed_progress_stderr(std::string &pending, const char *data, std::size_t len)
276 : {
277 836 : pending.append(data, len);
278 836 : std::size_t start = 0;
279 752089 : for (std::size_t i = 0; i < pending.size(); ++i)
280 : {
281 751253 : const char c = pending[i];
282 751253 : if (c != '\r' && c != '\n') continue;
283 34750 : if (i > start) handle_sideband_progress(pending.substr(start, i - start));
284 17375 : start = i + 1;
285 : }
286 836 : if (start > 0) pending.erase(0, start);
287 836 : }
288 :
289 239 : Result Git::run_git_progress(const std::vector<std::string> &args, const std::string &cwd)
290 : {
291 239 : const std::string git = find_git_executable();
292 239 : if (git.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "git executable not found");
293 :
294 : // Merge stderr into the pipe so ``--progress`` lines (CR-updated) are readable.
295 239 : std::string cmd = build_git_command(args, cwd);
296 239 : cmd += " 2>&1";
297 :
298 478 : debug(1, std::string("[gitcmdline] ") + cmd);
299 239 : cmd_start();
300 239 : FILE *pipe = ESYSREPO_POPEN(cmd.c_str(), "r");
301 239 : if (pipe == nullptr)
302 : {
303 0 : cmd_end();
304 0 : return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "failed to start git");
305 : }
306 :
307 239 : std::string pending;
308 884 : char buf[1024];
309 884 : while (true)
310 : {
311 884 : const std::size_t n = std::fread(buf, 1, sizeof(buf), pipe);
312 884 : if (n > 0) feed_progress_stderr(pending, buf, n);
313 884 : if (n < sizeof(buf))
314 : {
315 239 : if (std::feof(pipe)) break;
316 0 : if (std::ferror(pipe)) break;
317 : }
318 : }
319 239 : if (!pending.empty()) handle_sideband_progress(pending);
320 :
321 239 : const int rc = ESYSREPO_PCLOSE(pipe);
322 239 : cmd_end();
323 :
324 239 : git::Progress done;
325 239 : done.set_done(true);
326 239 : done.set_percentage(git::Progress::MAX_PERCENTAGE);
327 239 : handle_transfer_progress(done);
328 :
329 239 : if (rc != 0) return ESYSREPO_RESULT(ResultCode::RAW_INT_ERROR, rc, cmd);
330 209 : return ESYSREPO_RESULT(ResultCode::OK);
331 478 : }
332 :
333 48 : Result Git::run_git_out(const std::vector<std::string> &args, const std::string &cwd, std::string &output)
334 : {
335 48 : output.clear();
336 48 : const std::string git = find_git_executable();
337 48 : if (git.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "git executable not found");
338 :
339 : // Capture stdout; discard stderr so porcelain/status noise does not pollute parsing.
340 48 : std::string cmd = build_git_command(args, cwd);
341 : #ifdef _WIN32
342 : cmd += " 2>NUL";
343 : #else
344 48 : cmd += " 2>/dev/null";
345 : #endif
346 :
347 96 : debug(1, std::string("[gitcmdline] ") + cmd);
348 48 : cmd_start();
349 48 : FILE *pipe = ESYSREPO_POPEN(cmd.c_str(), "r");
350 48 : if (pipe == nullptr)
351 : {
352 0 : cmd_end();
353 0 : return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "failed to start git");
354 : }
355 : char buf[4096];
356 125 : while (std::fgets(buf, sizeof(buf), pipe) != nullptr) output += buf;
357 48 : const int rc = ESYSREPO_PCLOSE(pipe);
358 48 : cmd_end();
359 48 : if (rc != 0) return ESYSREPO_RESULT(ResultCode::RAW_INT_ERROR, rc, cmd);
360 48 : return ESYSREPO_RESULT(ResultCode::OK);
361 48 : }
362 :
363 282 : Result Git::open(const std::string &folder)
364 : {
365 282 : if (!is_repo(folder)) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "not a git repository");
366 282 : m_folder = folder;
367 282 : m_open = true;
368 282 : open_time();
369 282 : return ESYSREPO_RESULT(ResultCode::OK);
370 : }
371 :
372 769 : bool Git::is_open()
373 : {
374 769 : return m_open;
375 : }
376 :
377 282 : Result Git::close()
378 : {
379 282 : m_open = false;
380 282 : m_folder.clear();
381 282 : close_time();
382 282 : return ESYSREPO_RESULT(ResultCode::OK);
383 : }
384 :
385 0 : void Git::close_on_error()
386 : {
387 0 : if (m_open) close();
388 0 : }
389 :
390 184 : Result Git::clone(const std::string &url, const std::string &path, const std::string &rev,
391 : const git::CloneOptions &options)
392 : {
393 184 : if (url.empty() || path.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty url or path");
394 :
395 552 : std::vector<std::string> args = {"clone", "--progress"};
396 : // Default CloneOptions: ignore rev (legacy remote-default tip only).
397 184 : if (options.checkout_rev && !rev.empty())
398 : {
399 125 : args.emplace_back("--branch");
400 125 : args.push_back(rev);
401 : }
402 184 : if (options.checkout_rev && options.single_branch) args.emplace_back("--single-branch");
403 184 : args.push_back(url);
404 184 : args.push_back(path);
405 :
406 184 : auto result = run_git_progress(args);
407 214 : if (result.error()) return ESYSREPO_RESULT(result);
408 :
409 154 : return open(path);
410 184 : }
411 :
412 55 : Result Git::fetch(const std::string &remote)
413 : {
414 55 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
415 :
416 159 : std::vector<std::string> args = {"fetch", "--progress"};
417 53 : if (!remote.empty()) args.push_back(remote);
418 53 : return run_git_progress(args, m_folder);
419 53 : }
420 :
421 2 : Result Git::fetch_refspec(const std::string &refspec, const std::string &remote)
422 : {
423 2 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
424 2 : if (refspec.empty()) return fetch(remote);
425 :
426 6 : std::vector<std::string> args = {"fetch", "--progress"};
427 2 : args.push_back(remote.empty() ? "origin" : remote);
428 2 : args.push_back(refspec);
429 2 : return run_git_progress(args, m_folder);
430 2 : }
431 :
432 0 : Result Git::update_submodules(const std::string &path, bool recursive)
433 : {
434 0 : if (path.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty path");
435 0 : std::vector<std::string> args = {"submodule", "update", "--init"};
436 0 : if (recursive) args.push_back("--recursive");
437 0 : return run_git(args, path);
438 0 : }
439 :
440 4 : Result Git::worktree_add_detach(const std::string &path, const std::string &commit)
441 : {
442 4 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_REPO_NOT_OPEN);
443 4 : if (path.empty() || commit.empty())
444 0 : return ESYSREPO_RESULT(ResultCode::CMD_MISSING_PARAMETERS, "worktree_add_detach needs path and commit");
445 24 : return run_git({"worktree", "add", "--detach", path, commit}, m_folder);
446 : }
447 :
448 3 : Result Git::worktree_list(std::vector<git::Worktree> &worktrees)
449 : {
450 3 : worktrees.clear();
451 3 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_REPO_NOT_OPEN);
452 :
453 3 : std::string out;
454 15 : Result result = run_git_out({"worktree", "list", "--porcelain"}, m_folder, out);
455 3 : if (result.error()) return result;
456 :
457 3 : git::Worktree current;
458 3 : bool have = false;
459 3 : std::istringstream iss(out);
460 3 : std::string line;
461 18 : auto flush = [&]() {
462 15 : if (!have) return;
463 6 : worktrees.push_back(current);
464 6 : current = git::Worktree{};
465 6 : have = false;
466 3 : };
467 :
468 27 : while (std::getline(iss, line))
469 : {
470 24 : if (!line.empty() && line.back() == '\r') line.pop_back();
471 24 : if (line.empty())
472 : {
473 6 : flush();
474 6 : continue;
475 : }
476 18 : if (line.rfind("worktree ", 0) == 0)
477 : {
478 6 : flush();
479 6 : current.set_path(line.substr(9));
480 6 : have = true;
481 : }
482 12 : else if (line.rfind("HEAD ", 0) == 0)
483 : {
484 6 : current.set_head(line.substr(5));
485 6 : have = true;
486 : }
487 6 : else if (line == "bare")
488 : {
489 0 : current.set_bare(true);
490 0 : have = true;
491 : }
492 6 : else if (line == "detached")
493 : {
494 3 : current.set_detached(true);
495 3 : have = true;
496 : }
497 : }
498 3 : flush();
499 3 : return ESYSREPO_RESULT(ResultCode::OK);
500 9 : }
501 :
502 2 : Result Git::worktree_remove(const std::string &path, bool force)
503 : {
504 2 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_REPO_NOT_OPEN);
505 2 : if (path.empty()) return ESYSREPO_RESULT(ResultCode::CMD_MISSING_PARAMETERS, "worktree_remove needs path");
506 6 : std::vector<std::string> args = {"worktree", "remove"};
507 4 : if (force) args.push_back("--force");
508 2 : args.push_back(path);
509 2 : return run_git(args, m_folder);
510 2 : }
511 :
512 8 : Result_t<bool> Git::has_object(const std::string &commit)
513 : {
514 8 : if (!m_open) return Result_t<bool>(ESYSREPO_RESULT(ResultCode::GIT_REPO_NOT_OPEN));
515 16 : if (commit.empty()) return Result_t<bool>(false);
516 40 : Result result = run_git({"cat-file", "-e", commit + "^{commit}"}, m_folder);
517 8 : if (result.ok()) return Result_t<bool>(true);
518 2 : return Result_t<bool>(false);
519 8 : }
520 :
521 1 : Result Git::list_gitlinks(std::map<std::string, std::string> &out, const std::string &rev)
522 : {
523 1 : out.clear();
524 1 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_REPO_NOT_OPEN);
525 1 : std::string treeish = rev.empty() ? "HEAD" : rev;
526 1 : std::string text;
527 5 : Result result = run_git_out({"ls-tree", "-r", treeish}, m_folder, text);
528 1 : if (result.error()) return result;
529 :
530 1 : std::istringstream iss(text);
531 1 : std::string line;
532 3 : while (std::getline(iss, line))
533 : {
534 2 : if (!line.empty() && line.back() == '\r') line.pop_back();
535 : // <mode> <type> <sha>\t<path>
536 2 : if (line.size() < 6 || line.compare(0, 6, "160000") != 0) continue;
537 1 : auto tab = line.find('\t');
538 1 : if (tab == std::string::npos) continue;
539 1 : std::istringstream left(line.substr(0, tab));
540 1 : std::string mode;
541 1 : std::string type;
542 1 : std::string sha;
543 1 : left >> mode >> type >> sha;
544 1 : if (sha.empty()) continue;
545 2 : out[line.substr(tab + 1)] = sha;
546 1 : }
547 1 : return ESYSREPO_RESULT(ResultCode::OK);
548 2 : }
549 :
550 1 : Result Git::list_index_gitlinks(std::map<std::string, std::string> &out)
551 : {
552 1 : out.clear();
553 1 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_REPO_NOT_OPEN);
554 1 : std::string text;
555 4 : Result result = run_git_out({"ls-files", "-s"}, m_folder, text);
556 1 : if (result.error()) return result;
557 :
558 1 : std::istringstream iss(text);
559 1 : std::string line;
560 3 : while (std::getline(iss, line))
561 : {
562 2 : if (!line.empty() && line.back() == '\r') line.pop_back();
563 : // <mode> <sha> <stage>\t<path>
564 2 : if (line.size() < 6 || line.compare(0, 6, "160000") != 0) continue;
565 1 : auto tab = line.find('\t');
566 1 : if (tab == std::string::npos) continue;
567 1 : std::istringstream fields(line.substr(0, tab));
568 1 : std::string mode;
569 1 : std::string sha;
570 1 : std::string stage;
571 1 : fields >> mode >> sha >> stage;
572 2 : out[line.substr(tab + 1)] = sha;
573 1 : }
574 1 : return ESYSREPO_RESULT(ResultCode::OK);
575 2 : }
576 :
577 0 : Result Git::init_bare(const std::string &folder_path)
578 : {
579 0 : if (folder_path.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty path");
580 0 : auto result = run_git({"init", "--bare", folder_path});
581 0 : if (result.error()) return ESYSREPO_RESULT(result);
582 0 : return open(folder_path);
583 0 : }
584 :
585 0 : Result Git::init(const std::string &folder_path)
586 : {
587 0 : if (folder_path.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty path");
588 0 : auto result = run_git({"init", folder_path});
589 0 : if (result.error()) return ESYSREPO_RESULT(result);
590 0 : return open(folder_path);
591 0 : }
592 :
593 1 : Result Git::get_remotes(std::vector<git::Remote> &remotes)
594 : {
595 1 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
596 1 : remotes.clear();
597 :
598 1 : std::string out;
599 4 : auto result = run_git_out({"remote", "-v"}, m_folder, out);
600 1 : if (result.error()) return ESYSREPO_RESULT(result);
601 :
602 1 : std::map<std::string, std::string> name_to_url;
603 3 : for (const auto &line : split_lines(out))
604 : {
605 : // name <tab/sp> url <sp>(fetch|push)
606 2 : const auto first_sp = line.find_first_of(" \t");
607 2 : if (first_sp == std::string::npos) continue;
608 2 : const std::string name = line.substr(0, first_sp);
609 2 : std::string rest = trim(line.substr(first_sp + 1));
610 2 : const bool is_fetch = rest.size() >= 8 && rest.compare(rest.size() - 8, 8, " (fetch)") == 0;
611 2 : const bool is_push = rest.size() >= 7 && rest.compare(rest.size() - 7, 7, " (push)") == 0;
612 2 : if (is_fetch)
613 1 : rest = trim(rest.substr(0, rest.size() - 8));
614 1 : else if (is_push)
615 1 : rest = trim(rest.substr(0, rest.size() - 7));
616 : else
617 0 : continue;
618 :
619 3 : if (is_fetch || name_to_url.find(name) == name_to_url.end()) name_to_url[name] = rest;
620 3 : }
621 :
622 2 : for (const auto &kv : name_to_url)
623 : {
624 1 : git::Remote remote;
625 1 : remote.set_name(kv.first);
626 1 : remote.set_url(kv.second);
627 1 : remotes.push_back(remote);
628 1 : }
629 1 : return ESYSREPO_RESULT(ResultCode::OK);
630 2 : }
631 :
632 1 : Result Git::get_head_branch_remote(git::Branch &branch, git::Remote &remote)
633 : {
634 1 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
635 :
636 1 : std::string ref_name;
637 5 : auto result = run_git_out({"symbolic-ref", "-q", "HEAD"}, m_folder, ref_name);
638 1 : const bool detached = result.error();
639 1 : ref_name = trim(ref_name);
640 :
641 1 : std::string short_name;
642 4 : result = run_git_out({"rev-parse", "--abbrev-ref", "HEAD"}, m_folder, short_name);
643 1 : if (result.error()) return ESYSREPO_RESULT(result);
644 1 : short_name = trim(short_name);
645 :
646 1 : branch.set_is_head(true);
647 1 : branch.set_type(git::BranchType::LOCAL);
648 1 : branch.set_detached(detached);
649 1 : branch.set_name(short_name == "HEAD" ? std::string() : short_name);
650 1 : if (!detached && !ref_name.empty())
651 1 : branch.set_ref_name(ref_name);
652 0 : else if (!branch.get_name().empty())
653 0 : branch.set_ref_name("refs/heads/" + branch.get_name());
654 :
655 1 : if (detached || branch.get_name().empty())
656 0 : return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "HEAD is detached or has no branch");
657 :
658 1 : std::string remote_name;
659 1 : std::string merge_ref;
660 1 : std::string cfg_out;
661 4 : if (run_git_out({"config", "--get", "branch." + branch.get_name() + ".remote"}, m_folder, cfg_out).ok())
662 1 : remote_name = trim(cfg_out);
663 4 : if (run_git_out({"config", "--get", "branch." + branch.get_name() + ".merge"}, m_folder, cfg_out).ok())
664 1 : merge_ref = trim(cfg_out);
665 :
666 1 : if (remote_name.empty() || merge_ref.empty())
667 0 : return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "branch has no upstream remote");
668 :
669 1 : branch.set_remote_name(remote_name);
670 1 : remote.set_name(remote_name);
671 :
672 1 : std::string remote_url;
673 4 : result = run_git_out({"remote", "get-url", remote_name}, m_folder, remote_url);
674 1 : if (result.error()) return ESYSREPO_RESULT(result);
675 2 : remote.set_url(trim(remote_url));
676 :
677 1 : const std::string merge_heads = "refs/heads/";
678 1 : std::string short_merge = merge_ref;
679 1 : if (merge_ref.compare(0, merge_heads.size(), merge_heads) == 0)
680 1 : short_merge = merge_ref.substr(merge_heads.size());
681 :
682 2 : branch.set_remote_branch("refs/remotes/" + remote_name + "/" + short_merge);
683 2 : branch.set_remote_branch_name(remote_name + "/" + short_merge);
684 1 : return ESYSREPO_RESULT(ResultCode::OK);
685 5 : }
686 :
687 0 : Result Git::add_remote(const std::string &name, const std::string &url)
688 : {
689 0 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
690 0 : if (name.empty() || url.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty name or url");
691 0 : return run_git({"remote", "add", name, url}, m_folder);
692 : }
693 :
694 4 : Result Git::get_branches(git::Branches &branches, git::BranchType branch_type)
695 : {
696 4 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
697 4 : branches.clear();
698 :
699 4 : if (branch_type == git::BranchType::LOCAL || branch_type == git::BranchType::ALL)
700 : {
701 4 : std::string head_name;
702 20 : auto result = run_git_out({"rev-parse", "--abbrev-ref", "HEAD"}, m_folder, head_name);
703 4 : if (result.error()) return ESYSREPO_RESULT(result);
704 4 : head_name = trim(head_name);
705 4 : if (head_name == "HEAD") head_name.clear(); // detached
706 :
707 4 : std::string out;
708 12 : result = run_git_out({"show-ref", "--heads"}, m_folder, out);
709 : // Empty repo / no heads: not an error for listing.
710 4 : if (result.ok())
711 : {
712 8 : for (const auto &line : split_lines(out))
713 : {
714 : // <hash> <sp> refs/heads/<name>
715 4 : const auto sp = line.find(' ');
716 4 : if (sp == std::string::npos) continue;
717 4 : const std::string ref = trim(line.substr(sp + 1));
718 4 : const std::string heads = "refs/heads/";
719 4 : if (ref.compare(0, heads.size(), heads) != 0) continue;
720 4 : const std::string name = ref.substr(heads.size());
721 :
722 4 : auto branch = std::make_shared<git::Branch>();
723 4 : branch->set_ref_name(ref);
724 4 : branch->set_name(name);
725 4 : branch->set_type(git::BranchType::LOCAL);
726 4 : branch->set_is_head(!head_name.empty() && name == head_name);
727 :
728 4 : std::string remote;
729 4 : std::string merge;
730 4 : std::string cfg_out;
731 16 : if (run_git_out({"config", "--get", "branch." + name + ".remote"}, m_folder, cfg_out).ok())
732 4 : remote = trim(cfg_out);
733 16 : if (run_git_out({"config", "--get", "branch." + name + ".merge"}, m_folder, cfg_out).ok())
734 4 : merge = trim(cfg_out);
735 4 : if (!remote.empty() && !merge.empty())
736 : {
737 4 : branch->set_remote_name(remote);
738 : // Prefer refs/remotes/<remote>/<branch> for merge_analysis tips.
739 4 : const std::string merge_heads = "refs/heads/";
740 4 : std::string short_merge = merge;
741 4 : if (merge.compare(0, merge_heads.size(), merge_heads) == 0)
742 4 : short_merge = merge.substr(merge_heads.size());
743 8 : branch->set_remote_branch("refs/remotes/" + remote + "/" + short_merge);
744 4 : }
745 12 : branches.add(branch);
746 12 : }
747 : }
748 4 : }
749 :
750 4 : if (branch_type == git::BranchType::REMOTE || branch_type == git::BranchType::ALL)
751 : {
752 0 : std::string out;
753 0 : auto result = run_git_out({"show-ref"}, m_folder, out);
754 0 : if (result.ok())
755 : {
756 0 : for (const auto &line : split_lines(out))
757 : {
758 0 : const auto sp = line.find(' ');
759 0 : if (sp == std::string::npos) continue;
760 0 : const std::string ref = trim(line.substr(sp + 1));
761 0 : const std::string remotes = "refs/remotes/";
762 0 : if (ref.compare(0, remotes.size(), remotes) != 0) continue;
763 0 : if (ref.size() >= 5 && ref.compare(ref.size() - 5, 5, "/HEAD") == 0) continue;
764 :
765 0 : auto branch = std::make_shared<git::Branch>();
766 0 : branch->set_ref_name(ref);
767 0 : branch->set_name(ref.substr(remotes.size()));
768 0 : branch->set_type(git::BranchType::REMOTE);
769 0 : branches.add(branch);
770 0 : }
771 : }
772 0 : }
773 :
774 4 : return ESYSREPO_RESULT(ResultCode::OK);
775 : }
776 :
777 1 : Result Git::checkout(const std::string &branch, bool force)
778 : {
779 1 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
780 1 : if (branch.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty branch");
781 :
782 2 : std::vector<std::string> args = {"checkout"};
783 1 : if (force) args.emplace_back("-f");
784 2 : args.push_back(strip_rev_prefix(branch));
785 1 : return run_git(args, m_folder);
786 1 : }
787 :
788 1 : Result Git::reset(const git::CommitHash &commit, git::ResetType type)
789 : {
790 1 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
791 1 : if (commit.get_hash().empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty commit");
792 :
793 1 : const char *mode = nullptr;
794 1 : switch (type)
795 : {
796 0 : case git::ResetType::NOT_SET: return ESYSREPO_RESULT(ResultCode::GIT_RESET_TYPE_NOT_SET);
797 : case git::ResetType::SOFT: mode = "--soft"; break;
798 0 : case git::ResetType::MIXED: mode = "--mixed"; break;
799 0 : case git::ResetType::HARD: mode = "--hard"; break;
800 0 : default: return ESYSREPO_RESULT(ResultCode::GIT_RESET_TYPE_UNKNOWN);
801 : }
802 4 : return run_git({"reset", mode, commit.get_hash()}, m_folder);
803 : }
804 :
805 1 : Result Git::fastforward(const git::CommitHash &commit)
806 : {
807 1 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
808 1 : if (commit.get_hash().empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty commit");
809 : // Matches libgit2 path: move HEAD + worktree to the tip (FF only).
810 4 : return run_git({"merge", "--ff-only", commit.get_hash()}, m_folder);
811 : }
812 :
813 9 : Result Git::get_last_commit(git::CommitHash &commit)
814 : {
815 9 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
816 9 : std::string out;
817 36 : auto result = run_git_out({"rev-parse", "HEAD"}, m_folder, out);
818 9 : if (result.error()) return ESYSREPO_RESULT(result);
819 18 : commit.set_hash(trim(out));
820 9 : return ESYSREPO_RESULT(ResultCode::OK);
821 18 : }
822 :
823 1 : Result Git::get_last_commit(git::Commit &commit, bool /*get_all_notes*/)
824 : {
825 1 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
826 :
827 1 : commit.clear();
828 : // Notes stay empty on gitcmdline (use hybrid/libgit2 for note-heavy cmds).
829 : // Parse cat-file (not --format=%…): shell_quote doubles '%' for cmd.exe, which
830 : // breaks git pretty placeholders (%% becomes a literal '%' in the format).
831 1 : git::CommitHash hash;
832 1 : auto result = get_last_commit(hash);
833 1 : if (result.error()) return ESYSREPO_RESULT(result);
834 1 : commit.set_hash(hash.get_hash());
835 :
836 1 : std::string out;
837 4 : result = run_git_out({"cat-file", "-p", "HEAD"}, m_folder, out);
838 1 : if (result.error()) return ESYSREPO_RESULT(result);
839 :
840 3 : auto parse_ident = [](const std::string &line, git::Signature &sig) {
841 : // author Name <email> unix_ts tz
842 2 : const auto lt = line.find('<');
843 2 : const auto gt = line.find('>');
844 2 : if (lt == std::string::npos || gt == std::string::npos || gt <= lt) return;
845 4 : sig.set_name(trim(line.substr(0, lt)));
846 2 : sig.set_email(line.substr(lt + 1, gt - lt - 1));
847 2 : const std::string rest = trim(line.substr(gt + 1));
848 2 : const auto sp = rest.find(' ');
849 2 : try
850 : {
851 4 : const auto epoch = static_cast<std::time_t>(std::stoll(sp == std::string::npos ? rest : rest.substr(0, sp)));
852 2 : sig.set_date_time(std::chrono::system_clock::from_time_t(epoch));
853 : }
854 0 : catch (...)
855 : {
856 0 : }
857 2 : };
858 :
859 1 : std::string message;
860 1 : bool in_message = false;
861 7 : for (const auto &line : split_lines(out))
862 : {
863 6 : if (in_message)
864 : {
865 1 : if (!message.empty()) message.push_back('\n');
866 1 : message += line;
867 1 : continue;
868 : }
869 5 : if (line.empty())
870 : {
871 1 : in_message = true;
872 1 : continue;
873 : }
874 4 : if (line.rfind("author ", 0) == 0)
875 2 : parse_ident(line.substr(7), commit.get_author_sign());
876 3 : else if (line.rfind("committer ", 0) == 0)
877 2 : parse_ident(line.substr(10), commit.get_committer_sign());
878 1 : }
879 :
880 1 : while (!message.empty() && (message.back() == '\n' || message.back() == '\r')) message.pop_back();
881 1 : commit.set_message(message);
882 1 : const auto nl = message.find('\n');
883 1 : if (nl == std::string::npos)
884 : {
885 1 : commit.set_summary(message);
886 2 : commit.set_body({});
887 : }
888 : else
889 : {
890 0 : commit.set_summary(message.substr(0, nl));
891 0 : std::string body = message.substr(nl + 1);
892 0 : while (!body.empty() && (body.front() == '\n' || body.front() == '\r')) body.erase(body.begin());
893 0 : commit.set_body(body);
894 0 : }
895 :
896 1 : return ESYSREPO_RESULT(ResultCode::OK);
897 2 : }
898 :
899 2 : Result Git::get_parent_commit(const git::CommitHash &commit, git::CommitHash &parent, int nth_parent)
900 : {
901 2 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
902 2 : if (commit.get_hash().empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty commit");
903 :
904 2 : if (nth_parent == 0)
905 : {
906 0 : parent = commit;
907 0 : return ESYSREPO_RESULT(ResultCode::OK);
908 : }
909 2 : if (nth_parent < 0) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "invalid nth_parent");
910 :
911 : // First-parent walk (matches libgit2 get_parent_commit).
912 2 : std::string out;
913 2 : auto result =
914 8 : run_git_out({"rev-parse", commit.get_hash() + "~" + std::to_string(nth_parent)}, m_folder, out);
915 2 : if (result.error()) return ESYSREPO_RESULT(result);
916 4 : parent.set_hash(trim(out));
917 2 : return ESYSREPO_RESULT(ResultCode::OK);
918 4 : }
919 :
920 1 : Result Git::is_dirty(bool &dirty)
921 : {
922 1 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
923 1 : dirty = false;
924 1 : std::string out;
925 4 : auto result = run_git_out({"status", "--porcelain"}, m_folder, out);
926 1 : if (result.error()) return ESYSREPO_RESULT(result);
927 1 : dirty = !trim(out).empty();
928 1 : return ESYSREPO_RESULT(ResultCode::OK);
929 2 : }
930 :
931 1 : Result Git::is_detached(bool &detached)
932 : {
933 1 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
934 1 : detached = false;
935 : // symbolic-ref fails (nonzero) when HEAD is detached.
936 5 : auto result = run_git({"symbolic-ref", "-q", "HEAD"}, m_folder);
937 1 : detached = result.error();
938 1 : return ESYSREPO_RESULT(ResultCode::OK);
939 1 : }
940 :
941 1 : Result Git::get_status(git::RepoStatus &repo_status)
942 : {
943 1 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
944 :
945 1 : std::string out;
946 5 : auto result = run_git_out({"status", "--porcelain=v1", "-uall"}, m_folder, out);
947 1 : if (result.error()) return ESYSREPO_RESULT(result);
948 :
949 3 : for (const auto &line : split_lines(out))
950 : {
951 2 : if (line.size() < 2) continue;
952 2 : const char x = line[0];
953 2 : const char y = line[1];
954 2 : std::string old_path;
955 2 : std::string new_path;
956 2 : if (!parse_porcelain_paths(line.substr(2), old_path, new_path)) continue;
957 :
958 2 : if (x == '?' && y == '?')
959 : {
960 1 : add_porcelain_status(repo_status, git::StatusType::WORKING_DIR, git::StatusSubType::NEW,
961 : git::DiffDeltaType::UNTRACKED, old_path, new_path);
962 1 : continue;
963 : }
964 1 : if (x == '!' && y == '!')
965 : {
966 0 : add_porcelain_status(repo_status, git::StatusType::IGNORED, git::StatusSubType::NOT_SET,
967 : git::DiffDeltaType::IGNORED, old_path, new_path);
968 0 : continue;
969 : }
970 1 : if (x == 'U' || y == 'U' || (x == 'A' && y == 'A') || (x == 'D' && y == 'D'))
971 : {
972 0 : add_porcelain_status(repo_status, git::StatusType::CONFLICTED, git::StatusSubType::NOT_SET,
973 : git::DiffDeltaType::CONFLICTED, old_path, new_path);
974 0 : continue;
975 : }
976 :
977 1 : if (x != ' ' && x != '?')
978 : {
979 0 : add_porcelain_status(repo_status, git::StatusType::INDEX, porcelain_to_subtype(x),
980 : porcelain_to_delta_type(x, false), old_path, new_path);
981 : }
982 1 : if (y != ' ' && y != '?')
983 : {
984 2 : add_porcelain_status(repo_status, git::StatusType::WORKING_DIR, porcelain_to_subtype(y),
985 : porcelain_to_delta_type(y, true), old_path, new_path);
986 : }
987 4 : }
988 :
989 1 : return ESYSREPO_RESULT(ResultCode::OK);
990 2 : }
991 :
992 0 : Result_t<bool> Git::is_ssh_agent_running(bool log_once)
993 : {
994 0 : detect_ssh_agent(log_once);
995 0 : return ESYSREPO_RESULT_T(ResultCode::OK, s_ssh_agent_running);
996 : }
997 :
998 0 : void Git::detect_ssh_agent(bool /*log_once*/)
999 : {
1000 0 : if (s_detect_ssh_agent_done) return;
1001 0 : s_detect_ssh_agent_done = true;
1002 : #ifdef _WIN32
1003 : // Pageant / Windows OpenSSH agent: presence of SSH_AUTH_SOCK or ssh-agent service is enough for CLI git.
1004 : const char *sock = std::getenv("SSH_AUTH_SOCK");
1005 : s_ssh_agent_running = (sock != nullptr && sock[0] != '\0') || (std::getenv("SSH_AGENT_PID") != nullptr);
1006 : #else
1007 0 : const char *sock = std::getenv("SSH_AUTH_SOCK");
1008 0 : s_ssh_agent_running = (sock != nullptr && sock[0] != '\0');
1009 : #endif
1010 : }
1011 :
1012 2 : Result Git::merge_analysis(const std::vector<std::string> &refs, git::MergeAnalysisResult &merge_analysis_result,
1013 : std::vector<git::CommitHash> &commits)
1014 : {
1015 2 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
1016 2 : merge_analysis_result = git::MergeAnalysisResult::NOT_SET;
1017 2 : commits.clear();
1018 2 : if (refs.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty refs");
1019 :
1020 2 : std::string head;
1021 8 : auto result = run_git_out({"rev-parse", "HEAD"}, m_folder, head);
1022 2 : if (result.error()) return ESYSREPO_RESULT(result);
1023 2 : head = trim(head);
1024 :
1025 : // Sync only uses the first upstream ref today; analyze that tip.
1026 2 : const std::string &ref = refs.front();
1027 2 : std::string target;
1028 6 : result = run_git_out({"rev-parse", ref}, m_folder, target);
1029 2 : if (result.error()) return ESYSREPO_RESULT(result);
1030 2 : target = trim(target);
1031 :
1032 2 : git::CommitHash target_commit;
1033 2 : target_commit.set_hash(target);
1034 2 : commits.push_back(target_commit);
1035 :
1036 2 : if (head == target)
1037 : {
1038 1 : merge_analysis_result = git::MergeAnalysisResult::UP_TO_DATE;
1039 1 : return ESYSREPO_RESULT(ResultCode::OK);
1040 : }
1041 :
1042 1 : std::string base;
1043 4 : result = run_git_out({"merge-base", "HEAD", ref}, m_folder, base);
1044 1 : if (result.error()) return ESYSREPO_RESULT(result);
1045 1 : base = trim(base);
1046 :
1047 1 : if (base == head)
1048 1 : merge_analysis_result = git::MergeAnalysisResult::FASTFORWARD;
1049 0 : else if (base == target)
1050 0 : merge_analysis_result = git::MergeAnalysisResult::UP_TO_DATE;
1051 : else
1052 0 : merge_analysis_result = git::MergeAnalysisResult::NORMAL;
1053 :
1054 1 : return ESYSREPO_RESULT(ResultCode::OK);
1055 6 : }
1056 :
1057 0 : Result Git::fetch_all_notes(const std::string &)
1058 : {
1059 0 : return not_implemented();
1060 : }
1061 :
1062 1 : Result_t<bool> Git::has_branch(const std::string &name, git::BranchType branch_type)
1063 : {
1064 1 : if (!m_open) return ESYSREPO_RESULT_T(ResultCode::GIT_GENERIC_ERROR, false);
1065 1 : git::Branches branches;
1066 1 : auto result = get_branches(branches, branch_type);
1067 1 : if (result.error()) return ESYSREPO_RESULT_T(result, false);
1068 1 : const std::string tip = strip_rev_prefix(name);
1069 1 : for (const auto &b : branches.get())
1070 : {
1071 1 : if (b->get_name() == tip || b->get_name() == name || b->get_ref_name() == name)
1072 1 : return ESYSREPO_RESULT_T(ResultCode::OK, true);
1073 : }
1074 0 : return ESYSREPO_RESULT_T(ResultCode::OK, false);
1075 1 : }
1076 :
1077 0 : Result Git::get_hash(const std::string &revision, std::string &hash, git::BranchType /*branch_type*/)
1078 : {
1079 0 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
1080 0 : hash.clear();
1081 0 : std::string out;
1082 0 : auto result = run_git_out({"rev-parse", revision}, m_folder, out);
1083 0 : if (result.error()) return ESYSREPO_RESULT(result);
1084 0 : hash = trim(out);
1085 0 : return ESYSREPO_RESULT(ResultCode::OK);
1086 0 : }
1087 :
1088 0 : Result Git::walk_commits(std::shared_ptr<git::WalkCommit>)
1089 : {
1090 0 : return not_implemented();
1091 : }
1092 :
1093 0 : Result Git::diff(const git::CommitHash &, std::shared_ptr<git::Diff>)
1094 : {
1095 0 : return not_implemented();
1096 : }
1097 :
1098 1 : Result Git::get_ahead_behind(git::AheadBehind &ahead_behind, const git::Branch &local_branch)
1099 : {
1100 1 : ahead_behind.set_ahead(0);
1101 1 : ahead_behind.set_behind(0);
1102 1 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
1103 1 : if (local_branch.get_name().empty() || local_branch.get_remote_branch().empty())
1104 0 : return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "branch missing name or upstream");
1105 :
1106 1 : return get_ahead_behind(ahead_behind, local_branch.get_name(), local_branch.get_remote_branch());
1107 : }
1108 :
1109 2 : Result Git::get_ahead_behind(git::AheadBehind &ahead_behind, const std::string &first_ref,
1110 : const std::string &second_ref)
1111 : {
1112 2 : ahead_behind.set_ahead(0);
1113 2 : ahead_behind.set_behind(0);
1114 2 : if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
1115 2 : if (first_ref.empty() || second_ref.empty())
1116 0 : return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty ref");
1117 :
1118 2 : std::string out;
1119 12 : auto result = run_git_out({"rev-list", "--left-right", "--count", first_ref + "..." + second_ref}, m_folder, out);
1120 2 : if (result.error()) return ESYSREPO_RESULT(result);
1121 :
1122 2 : out = trim(out);
1123 2 : const auto tab = out.find('\t');
1124 2 : if (tab == std::string::npos) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "unexpected rev-list output");
1125 :
1126 2 : try
1127 : {
1128 4 : ahead_behind.set_ahead(static_cast<std::size_t>(std::stoull(out.substr(0, tab))));
1129 4 : ahead_behind.set_behind(static_cast<std::size_t>(std::stoull(out.substr(tab + 1))));
1130 : }
1131 0 : catch (...)
1132 : {
1133 0 : return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "failed to parse ahead/behind counts");
1134 0 : }
1135 2 : return ESYSREPO_RESULT(ResultCode::OK);
1136 4 : }
1137 :
1138 0 : Result Git::add_note(git::NoteId &, const std::string &, const std::string &, bool)
1139 : {
1140 0 : return not_implemented();
1141 : }
1142 :
1143 0 : Result Git::remove_note(const git::NoteId &)
1144 : {
1145 0 : return not_implemented();
1146 : }
1147 :
1148 0 : Result Git::remove_note(const git::CommitHash &, const std::string &)
1149 : {
1150 0 : return not_implemented();
1151 : }
1152 :
1153 0 : Result Git::remove_note_last_commit(const std::string &)
1154 : {
1155 0 : return not_implemented();
1156 : }
1157 :
1158 0 : Result Git::push(const std::string &, const std::string &, const std::string &)
1159 : {
1160 0 : return not_implemented();
1161 : }
1162 :
1163 0 : Result Git::push_notes(const std::string &, const std::string &)
1164 : {
1165 0 : return not_implemented();
1166 : }
1167 :
1168 0 : const std::string &Git::get_version()
1169 : {
1170 0 : return s_get_version();
1171 : }
1172 :
1173 0 : const std::string &Git::get_lib_name()
1174 : {
1175 0 : return s_get_lib_name();
1176 : }
1177 :
1178 0 : const std::string &Git::s_get_version()
1179 : {
1180 0 : static const std::string version = "git-cmdline";
1181 0 : return version;
1182 : }
1183 :
1184 0 : const std::string &Git::s_get_lib_name()
1185 : {
1186 0 : static const std::string name = "gitcmdline";
1187 0 : return name;
1188 : }
1189 :
1190 0 : bool Git::do_is_ssh_backend_supported(git::SshBackend backend) const
1191 : {
1192 0 : return backend == git::SshBackend::Exec;
1193 : }
1194 :
1195 0 : bool Git::do_is_ssh_backend_available(git::SshBackend backend, bool /*force*/)
1196 : {
1197 0 : if (backend != git::SshBackend::Exec) return false;
1198 0 : return !find_git_executable().empty();
1199 : }
1200 :
1201 0 : Result Git::do_set_ssh_backend(git::SshBackend backend)
1202 : {
1203 0 : if (backend != git::SshBackend::Exec)
1204 0 : return ESYSREPO_RESULT(ResultCode::GIT_SSH_BACKEND_NOT_SUPPORTED);
1205 0 : m_ssh_backend = backend;
1206 0 : return ESYSREPO_RESULT(ResultCode::OK);
1207 : }
1208 :
1209 0 : Result_t<git::SshBackend> Git::do_get_ssh_backend() const
1210 : {
1211 0 : return ESYSREPO_RESULT_T(ResultCode::OK, m_ssh_backend);
1212 : }
1213 :
1214 : } // namespace esys::repo::gitcmdline
|