LCOV - code coverage report
Current view: top level - src/esys/repo/gitcmdline - git_cmdline.cpp (source / functions) Hit Total Coverage
Test: esysrepo_coverage.info Lines: 464 625 74.2 %
Date: 2026-08-12 14:26:50 Functions: 38 66 57.6 %

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

Generated by: LCOV version 1.14