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: 472 633 74.6 %
Date: 2026-09-26 15:38:18 Functions: 39 67 58.2 %

          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        1708 : 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        1708 :     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        1708 : }
      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         191 : Git::Git()
     219         191 :     : GitBase()
     220             : {
     221         191 : }
     222             : 
     223         191 : Git::~Git()
     224             : {
     225         191 :     if (m_open) close();
     226         191 : }
     227             : 
     228         567 : std::string Git::find_git_executable()
     229             : {
     230         567 :     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         567 :     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        1134 :     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         567 :     if (!found.empty()) return "git";
     242         567 :     return {};
     243         567 : }
     244             : 
     245           0 : Result Git::not_implemented() const
     246             : {
     247           0 :     return ESYSREPO_RESULT(ResultCode::NOT_IMPLEMENTED);
     248             : }
     249             : 
     250         279 : std::string Git::build_git_command(const std::vector<std::string> &args, const std::string &cwd) const
     251             : {
     252         279 :     const std::string git = find_git_executable();
     253         279 :     std::ostringstream cmd;
     254         558 :     cmd << shell_quote(git);
     255         374 :     if (!cwd.empty()) cmd << " -C " << shell_quote(cwd);
     256        1613 :     for (const auto &arg : args) cmd << ' ' << shell_quote(arg);
     257         279 :     return cmd.str();
     258         279 : }
     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         877 : void Git::feed_progress_stderr(std::string &pending, const char *data, std::size_t len)
     275             : {
     276         877 :     pending.append(data, len);
     277         877 :     std::size_t start = 0;
     278      793519 :     for (std::size_t i = 0; i < pending.size(); ++i)
     279             :     {
     280      792642 :         const char c = pending[i];
     281      792642 :         if (c != '\r' && c != '\n') continue;
     282       36518 :         if (i > start) handle_sideband_progress(pending.substr(start, i - start));
     283       18259 :         start = i + 1;
     284             :     }
     285         877 :     if (start > 0) pending.erase(0, start);
     286         877 : }
     287             : 
     288         239 : Result Git::run_git_progress(const std::vector<std::string> &args, const std::string &cwd)
     289             : {
     290         239 :     const std::string git = find_git_executable();
     291         239 :     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         239 :     std::string cmd = build_git_command(args, cwd);
     295         239 :     cmd += " 2>&1";
     296             : 
     297         478 :     debug(1, std::string("[gitcmdline] ") + cmd);
     298         239 :     cmd_start();
     299         239 :     FILE *pipe = ESYSREPO_POPEN(cmd.c_str(), "r");
     300         239 :     if (pipe == nullptr)
     301             :     {
     302           0 :         cmd_end();
     303           0 :         return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "failed to start git");
     304             :     }
     305             : 
     306         239 :     std::string pending;
     307         925 :     char buf[1024];
     308         925 :     while (true)
     309             :     {
     310         925 :         const std::size_t n = std::fread(buf, 1, sizeof(buf), pipe);
     311         925 :         if (n > 0) feed_progress_stderr(pending, buf, n);
     312         925 :         if (n < sizeof(buf))
     313             :         {
     314         239 :             if (std::feof(pipe)) break;
     315           0 :             if (std::ferror(pipe)) break;
     316             :         }
     317             :     }
     318         239 :     if (!pending.empty()) handle_sideband_progress(pending);
     319             : 
     320         239 :     const int rc = ESYSREPO_PCLOSE(pipe);
     321         239 :     cmd_end();
     322             : 
     323         239 :     git::Progress done;
     324         239 :     done.set_done(true);
     325         239 :     done.set_percentage(git::Progress::MAX_PERCENTAGE);
     326         239 :     handle_transfer_progress(done);
     327             : 
     328         239 :     if (rc != 0) return ESYSREPO_RESULT(ResultCode::RAW_INT_ERROR, rc, cmd);
     329         209 :     return ESYSREPO_RESULT(ResultCode::OK);
     330         478 : }
     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         277 : Result Git::open(const std::string &folder)
     363             : {
     364         277 :     if (!is_repo(folder)) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "not a git repository");
     365         277 :     m_folder = folder;
     366         277 :     m_open = true;
     367         277 :     open_time();
     368         277 :     return ESYSREPO_RESULT(ResultCode::OK);
     369             : }
     370             : 
     371         757 : bool Git::is_open()
     372             : {
     373         757 :     return m_open;
     374             : }
     375             : 
     376         277 : Result Git::close()
     377             : {
     378         277 :     m_open = false;
     379         277 :     m_folder.clear();
     380         277 :     close_time();
     381         277 :     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         184 : Result Git::clone(const std::string &url, const std::string &path, const std::string &rev,
     390             :                   const git::CloneOptions &options)
     391             : {
     392         184 :     if (url.empty() || path.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty url or path");
     393             : 
     394         552 :     std::vector<std::string> args = {"clone", "--progress"};
     395             :     // Default CloneOptions: ignore rev (legacy remote-default tip only).
     396         184 :     if (options.checkout_rev && !rev.empty())
     397             :     {
     398         125 :         args.emplace_back("--branch");
     399         125 :         args.push_back(rev);
     400             :     }
     401         184 :     if (options.checkout_rev && options.single_branch) args.emplace_back("--single-branch");
     402         184 :     args.push_back(url);
     403         184 :     args.push_back(path);
     404             : 
     405         184 :     auto result = run_git_progress(args);
     406         214 :     if (result.error()) return ESYSREPO_RESULT(result);
     407             : 
     408         154 :     return open(path);
     409         184 : }
     410             : 
     411          55 : Result Git::fetch(const std::string &remote)
     412             : {
     413          55 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     414             : 
     415         159 :     std::vector<std::string> args = {"fetch", "--progress"};
     416          53 :     if (!remote.empty()) args.push_back(remote);
     417          53 :     return run_git_progress(args, m_folder);
     418          53 : }
     419             : 
     420           2 : Result Git::fetch_refspec(const std::string &refspec, const std::string &remote)
     421             : {
     422           2 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     423           2 :     if (refspec.empty()) return fetch(remote);
     424             : 
     425           6 :     std::vector<std::string> args = {"fetch", "--progress"};
     426           2 :     args.push_back(remote.empty() ? "origin" : remote);
     427           2 :     args.push_back(refspec);
     428           2 :     return run_git_progress(args, m_folder);
     429           2 : }
     430             : 
     431           0 : Result Git::update_submodules(const std::string &path, bool recursive)
     432             : {
     433           0 :     if (path.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty path");
     434           0 :     std::vector<std::string> args = {"submodule", "update", "--init"};
     435           0 :     if (recursive) args.push_back("--recursive");
     436           0 :     return run_git(args, path);
     437           0 : }
     438             : 
     439           0 : Result Git::init_bare(const std::string &folder_path)
     440             : {
     441           0 :     if (folder_path.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty path");
     442           0 :     auto result = run_git({"init", "--bare", folder_path});
     443           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     444           0 :     return open(folder_path);
     445           0 : }
     446             : 
     447           0 : Result Git::init(const std::string &folder_path)
     448             : {
     449           0 :     if (folder_path.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty path");
     450           0 :     auto result = run_git({"init", folder_path});
     451           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     452           0 :     return open(folder_path);
     453           0 : }
     454             : 
     455           1 : Result Git::get_remotes(std::vector<git::Remote> &remotes)
     456             : {
     457           1 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     458           1 :     remotes.clear();
     459             : 
     460           1 :     std::string out;
     461           4 :     auto result = run_git_out({"remote", "-v"}, m_folder, out);
     462           1 :     if (result.error()) return ESYSREPO_RESULT(result);
     463             : 
     464           1 :     std::map<std::string, std::string> name_to_url;
     465           3 :     for (const auto &line : split_lines(out))
     466             :     {
     467             :         // name <tab/sp> url <sp>(fetch|push)
     468           2 :         const auto first_sp = line.find_first_of(" \t");
     469           2 :         if (first_sp == std::string::npos) continue;
     470           2 :         const std::string name = line.substr(0, first_sp);
     471           2 :         std::string rest = trim(line.substr(first_sp + 1));
     472           2 :         const bool is_fetch = rest.size() >= 8 && rest.compare(rest.size() - 8, 8, " (fetch)") == 0;
     473           2 :         const bool is_push = rest.size() >= 7 && rest.compare(rest.size() - 7, 7, " (push)") == 0;
     474           2 :         if (is_fetch)
     475           1 :             rest = trim(rest.substr(0, rest.size() - 8));
     476           1 :         else if (is_push)
     477           1 :             rest = trim(rest.substr(0, rest.size() - 7));
     478             :         else
     479           0 :             continue;
     480             : 
     481           3 :         if (is_fetch || name_to_url.find(name) == name_to_url.end()) name_to_url[name] = rest;
     482           3 :     }
     483             : 
     484           2 :     for (const auto &kv : name_to_url)
     485             :     {
     486           1 :         git::Remote remote;
     487           1 :         remote.set_name(kv.first);
     488           1 :         remote.set_url(kv.second);
     489           1 :         remotes.push_back(remote);
     490           1 :     }
     491           1 :     return ESYSREPO_RESULT(ResultCode::OK);
     492           2 : }
     493             : 
     494           1 : Result Git::get_head_branch_remote(git::Branch &branch, git::Remote &remote)
     495             : {
     496           1 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     497             : 
     498           1 :     std::string ref_name;
     499           5 :     auto result = run_git_out({"symbolic-ref", "-q", "HEAD"}, m_folder, ref_name);
     500           1 :     const bool detached = result.error();
     501           1 :     ref_name = trim(ref_name);
     502             : 
     503           1 :     std::string short_name;
     504           4 :     result = run_git_out({"rev-parse", "--abbrev-ref", "HEAD"}, m_folder, short_name);
     505           1 :     if (result.error()) return ESYSREPO_RESULT(result);
     506           1 :     short_name = trim(short_name);
     507             : 
     508           1 :     branch.set_is_head(true);
     509           1 :     branch.set_type(git::BranchType::LOCAL);
     510           1 :     branch.set_detached(detached);
     511           1 :     branch.set_name(short_name == "HEAD" ? std::string() : short_name);
     512           1 :     if (!detached && !ref_name.empty())
     513           1 :         branch.set_ref_name(ref_name);
     514           0 :     else if (!branch.get_name().empty())
     515           0 :         branch.set_ref_name("refs/heads/" + branch.get_name());
     516             : 
     517           1 :     if (detached || branch.get_name().empty())
     518           0 :         return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "HEAD is detached or has no branch");
     519             : 
     520           1 :     std::string remote_name;
     521           1 :     std::string merge_ref;
     522           1 :     std::string cfg_out;
     523           4 :     if (run_git_out({"config", "--get", "branch." + branch.get_name() + ".remote"}, m_folder, cfg_out).ok())
     524           1 :         remote_name = trim(cfg_out);
     525           4 :     if (run_git_out({"config", "--get", "branch." + branch.get_name() + ".merge"}, m_folder, cfg_out).ok())
     526           1 :         merge_ref = trim(cfg_out);
     527             : 
     528           1 :     if (remote_name.empty() || merge_ref.empty())
     529           0 :         return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "branch has no upstream remote");
     530             : 
     531           1 :     branch.set_remote_name(remote_name);
     532           1 :     remote.set_name(remote_name);
     533             : 
     534           1 :     std::string remote_url;
     535           4 :     result = run_git_out({"remote", "get-url", remote_name}, m_folder, remote_url);
     536           1 :     if (result.error()) return ESYSREPO_RESULT(result);
     537           2 :     remote.set_url(trim(remote_url));
     538             : 
     539           1 :     const std::string merge_heads = "refs/heads/";
     540           1 :     std::string short_merge = merge_ref;
     541           1 :     if (merge_ref.compare(0, merge_heads.size(), merge_heads) == 0)
     542           1 :         short_merge = merge_ref.substr(merge_heads.size());
     543             : 
     544           2 :     branch.set_remote_branch("refs/remotes/" + remote_name + "/" + short_merge);
     545           2 :     branch.set_remote_branch_name(remote_name + "/" + short_merge);
     546           1 :     return ESYSREPO_RESULT(ResultCode::OK);
     547           5 : }
     548             : 
     549           0 : Result Git::add_remote(const std::string &name, const std::string &url)
     550             : {
     551           0 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     552           0 :     if (name.empty() || url.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty name or url");
     553           0 :     return run_git({"remote", "add", name, url}, m_folder);
     554             : }
     555             : 
     556           4 : Result Git::get_branches(git::Branches &branches, git::BranchType branch_type)
     557             : {
     558           4 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     559           4 :     branches.clear();
     560             : 
     561           4 :     if (branch_type == git::BranchType::LOCAL || branch_type == git::BranchType::ALL)
     562             :     {
     563           4 :         std::string head_name;
     564          20 :         auto result = run_git_out({"rev-parse", "--abbrev-ref", "HEAD"}, m_folder, head_name);
     565           4 :         if (result.error()) return ESYSREPO_RESULT(result);
     566           4 :         head_name = trim(head_name);
     567           4 :         if (head_name == "HEAD") head_name.clear(); // detached
     568             : 
     569           4 :         std::string out;
     570          12 :         result = run_git_out({"show-ref", "--heads"}, m_folder, out);
     571             :         // Empty repo / no heads: not an error for listing.
     572           4 :         if (result.ok())
     573             :         {
     574           8 :             for (const auto &line : split_lines(out))
     575             :             {
     576             :                 // <hash> <sp> refs/heads/<name>
     577           4 :                 const auto sp = line.find(' ');
     578           4 :                 if (sp == std::string::npos) continue;
     579           4 :                 const std::string ref = trim(line.substr(sp + 1));
     580           4 :                 const std::string heads = "refs/heads/";
     581           4 :                 if (ref.compare(0, heads.size(), heads) != 0) continue;
     582           4 :                 const std::string name = ref.substr(heads.size());
     583             : 
     584           4 :                 auto branch = std::make_shared<git::Branch>();
     585           4 :                 branch->set_ref_name(ref);
     586           4 :                 branch->set_name(name);
     587           4 :                 branch->set_type(git::BranchType::LOCAL);
     588           4 :                 branch->set_is_head(!head_name.empty() && name == head_name);
     589             : 
     590           4 :                 std::string remote;
     591           4 :                 std::string merge;
     592           4 :                 std::string cfg_out;
     593          16 :                 if (run_git_out({"config", "--get", "branch." + name + ".remote"}, m_folder, cfg_out).ok())
     594           4 :                     remote = trim(cfg_out);
     595          16 :                 if (run_git_out({"config", "--get", "branch." + name + ".merge"}, m_folder, cfg_out).ok())
     596           4 :                     merge = trim(cfg_out);
     597           4 :                 if (!remote.empty() && !merge.empty())
     598             :                 {
     599           4 :                     branch->set_remote_name(remote);
     600             :                     // Prefer refs/remotes/<remote>/<branch> for merge_analysis tips.
     601           4 :                     const std::string merge_heads = "refs/heads/";
     602           4 :                     std::string short_merge = merge;
     603           4 :                     if (merge.compare(0, merge_heads.size(), merge_heads) == 0)
     604           4 :                         short_merge = merge.substr(merge_heads.size());
     605           8 :                     branch->set_remote_branch("refs/remotes/" + remote + "/" + short_merge);
     606           4 :                 }
     607          12 :                 branches.add(branch);
     608          12 :             }
     609             :         }
     610           4 :     }
     611             : 
     612           4 :     if (branch_type == git::BranchType::REMOTE || branch_type == git::BranchType::ALL)
     613             :     {
     614           0 :         std::string out;
     615           0 :         auto result = run_git_out({"show-ref"}, m_folder, out);
     616           0 :         if (result.ok())
     617             :         {
     618           0 :             for (const auto &line : split_lines(out))
     619             :             {
     620           0 :                 const auto sp = line.find(' ');
     621           0 :                 if (sp == std::string::npos) continue;
     622           0 :                 const std::string ref = trim(line.substr(sp + 1));
     623           0 :                 const std::string remotes = "refs/remotes/";
     624           0 :                 if (ref.compare(0, remotes.size(), remotes) != 0) continue;
     625           0 :                 if (ref.size() >= 5 && ref.compare(ref.size() - 5, 5, "/HEAD") == 0) continue;
     626             : 
     627           0 :                 auto branch = std::make_shared<git::Branch>();
     628           0 :                 branch->set_ref_name(ref);
     629           0 :                 branch->set_name(ref.substr(remotes.size()));
     630           0 :                 branch->set_type(git::BranchType::REMOTE);
     631           0 :                 branches.add(branch);
     632           0 :             }
     633             :         }
     634           0 :     }
     635             : 
     636           4 :     return ESYSREPO_RESULT(ResultCode::OK);
     637             : }
     638             : 
     639           1 : Result Git::checkout(const std::string &branch, bool force)
     640             : {
     641           1 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     642           1 :     if (branch.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty branch");
     643             : 
     644           2 :     std::vector<std::string> args = {"checkout"};
     645           1 :     if (force) args.emplace_back("-f");
     646           2 :     args.push_back(strip_rev_prefix(branch));
     647           1 :     return run_git(args, m_folder);
     648           1 : }
     649             : 
     650           1 : Result Git::reset(const git::CommitHash &commit, git::ResetType type)
     651             : {
     652           1 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     653           1 :     if (commit.get_hash().empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty commit");
     654             : 
     655           1 :     const char *mode = nullptr;
     656           1 :     switch (type)
     657             :     {
     658           0 :         case git::ResetType::NOT_SET: return ESYSREPO_RESULT(ResultCode::GIT_RESET_TYPE_NOT_SET);
     659             :         case git::ResetType::SOFT: mode = "--soft"; break;
     660           0 :         case git::ResetType::MIXED: mode = "--mixed"; break;
     661           0 :         case git::ResetType::HARD: mode = "--hard"; break;
     662           0 :         default: return ESYSREPO_RESULT(ResultCode::GIT_RESET_TYPE_UNKNOWN);
     663             :     }
     664           4 :     return run_git({"reset", mode, commit.get_hash()}, m_folder);
     665             : }
     666             : 
     667           1 : Result Git::fastforward(const git::CommitHash &commit)
     668             : {
     669           1 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     670           1 :     if (commit.get_hash().empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty commit");
     671             :     // Matches libgit2 path: move HEAD + worktree to the tip (FF only).
     672           4 :     return run_git({"merge", "--ff-only", commit.get_hash()}, m_folder);
     673             : }
     674             : 
     675           3 : Result Git::get_last_commit(git::CommitHash &commit)
     676             : {
     677           3 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     678           3 :     std::string out;
     679          12 :     auto result = run_git_out({"rev-parse", "HEAD"}, m_folder, out);
     680           3 :     if (result.error()) return ESYSREPO_RESULT(result);
     681           6 :     commit.set_hash(trim(out));
     682           3 :     return ESYSREPO_RESULT(ResultCode::OK);
     683           6 : }
     684             : 
     685           1 : Result Git::get_last_commit(git::Commit &commit, bool /*get_all_notes*/)
     686             : {
     687           1 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     688             : 
     689           1 :     commit.clear();
     690             :     // Notes stay empty on gitcmdline (use hybrid/libgit2 for note-heavy cmds).
     691             :     // Parse cat-file (not --format=%…): shell_quote doubles '%' for cmd.exe, which
     692             :     // breaks git pretty placeholders (%% becomes a literal '%' in the format).
     693           1 :     git::CommitHash hash;
     694           1 :     auto result = get_last_commit(hash);
     695           1 :     if (result.error()) return ESYSREPO_RESULT(result);
     696           1 :     commit.set_hash(hash.get_hash());
     697             : 
     698           1 :     std::string out;
     699           4 :     result = run_git_out({"cat-file", "-p", "HEAD"}, m_folder, out);
     700           1 :     if (result.error()) return ESYSREPO_RESULT(result);
     701             : 
     702           3 :     auto parse_ident = [](const std::string &line, git::Signature &sig) {
     703             :         // author Name <email> unix_ts tz
     704           2 :         const auto lt = line.find('<');
     705           2 :         const auto gt = line.find('>');
     706           2 :         if (lt == std::string::npos || gt == std::string::npos || gt <= lt) return;
     707           4 :         sig.set_name(trim(line.substr(0, lt)));
     708           2 :         sig.set_email(line.substr(lt + 1, gt - lt - 1));
     709           2 :         const std::string rest = trim(line.substr(gt + 1));
     710           2 :         const auto sp = rest.find(' ');
     711           2 :         try
     712             :         {
     713           4 :             const auto epoch = static_cast<std::time_t>(std::stoll(sp == std::string::npos ? rest : rest.substr(0, sp)));
     714           2 :             sig.set_date_time(std::chrono::system_clock::from_time_t(epoch));
     715             :         }
     716           0 :         catch (...)
     717             :         {
     718           0 :         }
     719           2 :     };
     720             : 
     721           1 :     std::string message;
     722           1 :     bool in_message = false;
     723           7 :     for (const auto &line : split_lines(out))
     724             :     {
     725           6 :         if (in_message)
     726             :         {
     727           1 :             if (!message.empty()) message.push_back('\n');
     728           1 :             message += line;
     729           1 :             continue;
     730             :         }
     731           5 :         if (line.empty())
     732             :         {
     733           1 :             in_message = true;
     734           1 :             continue;
     735             :         }
     736           4 :         if (line.rfind("author ", 0) == 0)
     737           2 :             parse_ident(line.substr(7), commit.get_author_sign());
     738           3 :         else if (line.rfind("committer ", 0) == 0)
     739           2 :             parse_ident(line.substr(10), commit.get_committer_sign());
     740           1 :     }
     741             : 
     742           1 :     while (!message.empty() && (message.back() == '\n' || message.back() == '\r')) message.pop_back();
     743           1 :     commit.set_message(message);
     744           1 :     const auto nl = message.find('\n');
     745           1 :     if (nl == std::string::npos)
     746             :     {
     747           1 :         commit.set_summary(message);
     748           2 :         commit.set_body({});
     749             :     }
     750             :     else
     751             :     {
     752           0 :         commit.set_summary(message.substr(0, nl));
     753           0 :         std::string body = message.substr(nl + 1);
     754           0 :         while (!body.empty() && (body.front() == '\n' || body.front() == '\r')) body.erase(body.begin());
     755           0 :         commit.set_body(body);
     756           0 :     }
     757             : 
     758           1 :     return ESYSREPO_RESULT(ResultCode::OK);
     759           2 : }
     760             : 
     761           1 : Result Git::get_parent_commit(const git::CommitHash &commit, git::CommitHash &parent, int nth_parent)
     762             : {
     763           1 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     764           1 :     if (commit.get_hash().empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty commit");
     765             : 
     766           1 :     if (nth_parent == 0)
     767             :     {
     768           0 :         parent = commit;
     769           0 :         return ESYSREPO_RESULT(ResultCode::OK);
     770             :     }
     771           1 :     if (nth_parent < 0) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "invalid nth_parent");
     772             : 
     773             :     // First-parent walk (matches libgit2 get_parent_commit).
     774           1 :     std::string out;
     775           1 :     auto result =
     776           4 :         run_git_out({"rev-parse", commit.get_hash() + "~" + std::to_string(nth_parent)}, m_folder, out);
     777           1 :     if (result.error()) return ESYSREPO_RESULT(result);
     778           2 :     parent.set_hash(trim(out));
     779           1 :     return ESYSREPO_RESULT(ResultCode::OK);
     780           2 : }
     781             : 
     782           1 : Result Git::is_dirty(bool &dirty)
     783             : {
     784           1 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     785           1 :     dirty = false;
     786           1 :     std::string out;
     787           4 :     auto result = run_git_out({"status", "--porcelain"}, m_folder, out);
     788           1 :     if (result.error()) return ESYSREPO_RESULT(result);
     789           1 :     dirty = !trim(out).empty();
     790           1 :     return ESYSREPO_RESULT(ResultCode::OK);
     791           2 : }
     792             : 
     793           1 : Result Git::is_detached(bool &detached)
     794             : {
     795           1 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     796           1 :     detached = false;
     797             :     // symbolic-ref fails (nonzero) when HEAD is detached.
     798           5 :     auto result = run_git({"symbolic-ref", "-q", "HEAD"}, m_folder);
     799           1 :     detached = result.error();
     800           1 :     return ESYSREPO_RESULT(ResultCode::OK);
     801           1 : }
     802             : 
     803           1 : Result Git::get_status(git::RepoStatus &repo_status)
     804             : {
     805           1 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     806             : 
     807           1 :     std::string out;
     808           5 :     auto result = run_git_out({"status", "--porcelain=v1", "-uall"}, m_folder, out);
     809           1 :     if (result.error()) return ESYSREPO_RESULT(result);
     810             : 
     811           3 :     for (const auto &line : split_lines(out))
     812             :     {
     813           2 :         if (line.size() < 2) continue;
     814           2 :         const char x = line[0];
     815           2 :         const char y = line[1];
     816           2 :         std::string old_path;
     817           2 :         std::string new_path;
     818           2 :         if (!parse_porcelain_paths(line.substr(2), old_path, new_path)) continue;
     819             : 
     820           2 :         if (x == '?' && y == '?')
     821             :         {
     822           1 :             add_porcelain_status(repo_status, git::StatusType::WORKING_DIR, git::StatusSubType::NEW,
     823             :                                  git::DiffDeltaType::UNTRACKED, old_path, new_path);
     824           1 :             continue;
     825             :         }
     826           1 :         if (x == '!' && y == '!')
     827             :         {
     828           0 :             add_porcelain_status(repo_status, git::StatusType::IGNORED, git::StatusSubType::NOT_SET,
     829             :                                  git::DiffDeltaType::IGNORED, old_path, new_path);
     830           0 :             continue;
     831             :         }
     832           1 :         if (x == 'U' || y == 'U' || (x == 'A' && y == 'A') || (x == 'D' && y == 'D'))
     833             :         {
     834           0 :             add_porcelain_status(repo_status, git::StatusType::CONFLICTED, git::StatusSubType::NOT_SET,
     835             :                                  git::DiffDeltaType::CONFLICTED, old_path, new_path);
     836           0 :             continue;
     837             :         }
     838             : 
     839           1 :         if (x != ' ' && x != '?')
     840             :         {
     841           0 :             add_porcelain_status(repo_status, git::StatusType::INDEX, porcelain_to_subtype(x),
     842             :                                  porcelain_to_delta_type(x, false), old_path, new_path);
     843             :         }
     844           1 :         if (y != ' ' && y != '?')
     845             :         {
     846           2 :             add_porcelain_status(repo_status, git::StatusType::WORKING_DIR, porcelain_to_subtype(y),
     847             :                                  porcelain_to_delta_type(y, true), old_path, new_path);
     848             :         }
     849           4 :     }
     850             : 
     851           1 :     return ESYSREPO_RESULT(ResultCode::OK);
     852           2 : }
     853             : 
     854           0 : Result_t<bool> Git::is_ssh_agent_running(bool log_once)
     855             : {
     856           0 :     detect_ssh_agent(log_once);
     857           0 :     return ESYSREPO_RESULT_T(ResultCode::OK, s_ssh_agent_running);
     858             : }
     859             : 
     860           0 : void Git::detect_ssh_agent(bool /*log_once*/)
     861             : {
     862           0 :     if (s_detect_ssh_agent_done) return;
     863           0 :     s_detect_ssh_agent_done = true;
     864             : #ifdef _WIN32
     865             :     // Pageant / Windows OpenSSH agent: presence of SSH_AUTH_SOCK or ssh-agent service is enough for CLI git.
     866             :     const char *sock = std::getenv("SSH_AUTH_SOCK");
     867             :     s_ssh_agent_running = (sock != nullptr && sock[0] != '\0') || (std::getenv("SSH_AGENT_PID") != nullptr);
     868             : #else
     869           0 :     const char *sock = std::getenv("SSH_AUTH_SOCK");
     870           0 :     s_ssh_agent_running = (sock != nullptr && sock[0] != '\0');
     871             : #endif
     872             : }
     873             : 
     874           2 : Result Git::merge_analysis(const std::vector<std::string> &refs, git::MergeAnalysisResult &merge_analysis_result,
     875             :                            std::vector<git::CommitHash> &commits)
     876             : {
     877           2 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     878           2 :     merge_analysis_result = git::MergeAnalysisResult::NOT_SET;
     879           2 :     commits.clear();
     880           2 :     if (refs.empty()) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty refs");
     881             : 
     882           2 :     std::string head;
     883           8 :     auto result = run_git_out({"rev-parse", "HEAD"}, m_folder, head);
     884           2 :     if (result.error()) return ESYSREPO_RESULT(result);
     885           2 :     head = trim(head);
     886             : 
     887             :     // Sync only uses the first upstream ref today; analyze that tip.
     888           2 :     const std::string &ref = refs.front();
     889           2 :     std::string target;
     890           6 :     result = run_git_out({"rev-parse", ref}, m_folder, target);
     891           2 :     if (result.error()) return ESYSREPO_RESULT(result);
     892           2 :     target = trim(target);
     893             : 
     894           2 :     git::CommitHash target_commit;
     895           2 :     target_commit.set_hash(target);
     896           2 :     commits.push_back(target_commit);
     897             : 
     898           2 :     if (head == target)
     899             :     {
     900           1 :         merge_analysis_result = git::MergeAnalysisResult::UP_TO_DATE;
     901           1 :         return ESYSREPO_RESULT(ResultCode::OK);
     902             :     }
     903             : 
     904           1 :     std::string base;
     905           4 :     result = run_git_out({"merge-base", "HEAD", ref}, m_folder, base);
     906           1 :     if (result.error()) return ESYSREPO_RESULT(result);
     907           1 :     base = trim(base);
     908             : 
     909           1 :     if (base == head)
     910           1 :         merge_analysis_result = git::MergeAnalysisResult::FASTFORWARD;
     911           0 :     else if (base == target)
     912           0 :         merge_analysis_result = git::MergeAnalysisResult::UP_TO_DATE;
     913             :     else
     914           0 :         merge_analysis_result = git::MergeAnalysisResult::NORMAL;
     915             : 
     916           1 :     return ESYSREPO_RESULT(ResultCode::OK);
     917           6 : }
     918             : 
     919           0 : Result Git::fetch_all_notes(const std::string &)
     920             : {
     921           0 :     return not_implemented();
     922             : }
     923             : 
     924           1 : Result_t<bool> Git::has_branch(const std::string &name, git::BranchType branch_type)
     925             : {
     926           1 :     if (!m_open) return ESYSREPO_RESULT_T(ResultCode::GIT_GENERIC_ERROR, false);
     927           1 :     git::Branches branches;
     928           1 :     auto result = get_branches(branches, branch_type);
     929           1 :     if (result.error()) return ESYSREPO_RESULT_T(result, false);
     930           1 :     const std::string tip = strip_rev_prefix(name);
     931           1 :     for (const auto &b : branches.get())
     932             :     {
     933           1 :         if (b->get_name() == tip || b->get_name() == name || b->get_ref_name() == name)
     934           1 :             return ESYSREPO_RESULT_T(ResultCode::OK, true);
     935             :     }
     936           0 :     return ESYSREPO_RESULT_T(ResultCode::OK, false);
     937           1 : }
     938             : 
     939           0 : Result Git::get_hash(const std::string &revision, std::string &hash, git::BranchType /*branch_type*/)
     940             : {
     941           0 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     942           0 :     hash.clear();
     943           0 :     std::string out;
     944           0 :     auto result = run_git_out({"rev-parse", revision}, m_folder, out);
     945           0 :     if (result.error()) return ESYSREPO_RESULT(result);
     946           0 :     hash = trim(out);
     947           0 :     return ESYSREPO_RESULT(ResultCode::OK);
     948           0 : }
     949             : 
     950           0 : Result Git::walk_commits(std::shared_ptr<git::WalkCommit>)
     951             : {
     952           0 :     return not_implemented();
     953             : }
     954             : 
     955           0 : Result Git::diff(const git::CommitHash &, std::shared_ptr<git::Diff>)
     956             : {
     957           0 :     return not_implemented();
     958             : }
     959             : 
     960           1 : Result Git::get_ahead_behind(git::AheadBehind &ahead_behind, const git::Branch &local_branch)
     961             : {
     962           1 :     ahead_behind.set_ahead(0);
     963           1 :     ahead_behind.set_behind(0);
     964           1 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     965           1 :     if (local_branch.get_name().empty() || local_branch.get_remote_branch().empty())
     966           0 :         return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "branch missing name or upstream");
     967             : 
     968           1 :     return get_ahead_behind(ahead_behind, local_branch.get_name(), local_branch.get_remote_branch());
     969             : }
     970             : 
     971           2 : Result Git::get_ahead_behind(git::AheadBehind &ahead_behind, const std::string &first_ref,
     972             :                              const std::string &second_ref)
     973             : {
     974           2 :     ahead_behind.set_ahead(0);
     975           2 :     ahead_behind.set_behind(0);
     976           2 :     if (!m_open) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "repository not open");
     977           2 :     if (first_ref.empty() || second_ref.empty())
     978           0 :         return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "empty ref");
     979             : 
     980           2 :     std::string out;
     981          12 :     auto result = run_git_out({"rev-list", "--left-right", "--count", first_ref + "..." + second_ref}, m_folder, out);
     982           2 :     if (result.error()) return ESYSREPO_RESULT(result);
     983             : 
     984           2 :     out = trim(out);
     985           2 :     const auto tab = out.find('\t');
     986           2 :     if (tab == std::string::npos) return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "unexpected rev-list output");
     987             : 
     988           2 :     try
     989             :     {
     990           4 :         ahead_behind.set_ahead(static_cast<std::size_t>(std::stoull(out.substr(0, tab))));
     991           4 :         ahead_behind.set_behind(static_cast<std::size_t>(std::stoull(out.substr(tab + 1))));
     992             :     }
     993           0 :     catch (...)
     994             :     {
     995           0 :         return ESYSREPO_RESULT(ResultCode::GIT_GENERIC_ERROR, "failed to parse ahead/behind counts");
     996           0 :     }
     997           2 :     return ESYSREPO_RESULT(ResultCode::OK);
     998           4 : }
     999             : 
    1000           0 : Result Git::add_note(git::NoteId &, const std::string &, const std::string &, bool)
    1001             : {
    1002           0 :     return not_implemented();
    1003             : }
    1004             : 
    1005           0 : Result Git::remove_note(const git::NoteId &)
    1006             : {
    1007           0 :     return not_implemented();
    1008             : }
    1009             : 
    1010           0 : Result Git::remove_note(const git::CommitHash &, const std::string &)
    1011             : {
    1012           0 :     return not_implemented();
    1013             : }
    1014             : 
    1015           0 : Result Git::remove_note_last_commit(const std::string &)
    1016             : {
    1017           0 :     return not_implemented();
    1018             : }
    1019             : 
    1020           0 : Result Git::push(const std::string &, const std::string &, const std::string &)
    1021             : {
    1022           0 :     return not_implemented();
    1023             : }
    1024             : 
    1025           0 : Result Git::push_notes(const std::string &, const std::string &)
    1026             : {
    1027           0 :     return not_implemented();
    1028             : }
    1029             : 
    1030           0 : const std::string &Git::get_version()
    1031             : {
    1032           0 :     return s_get_version();
    1033             : }
    1034             : 
    1035           0 : const std::string &Git::get_lib_name()
    1036             : {
    1037           0 :     return s_get_lib_name();
    1038             : }
    1039             : 
    1040           0 : const std::string &Git::s_get_version()
    1041             : {
    1042           0 :     static const std::string version = "git-cmdline";
    1043           0 :     return version;
    1044             : }
    1045             : 
    1046           0 : const std::string &Git::s_get_lib_name()
    1047             : {
    1048           0 :     static const std::string name = "gitcmdline";
    1049           0 :     return name;
    1050             : }
    1051             : 
    1052           0 : bool Git::do_is_ssh_backend_supported(git::SshBackend backend) const
    1053             : {
    1054           0 :     return backend == git::SshBackend::Exec;
    1055             : }
    1056             : 
    1057           0 : bool Git::do_is_ssh_backend_available(git::SshBackend backend, bool /*force*/)
    1058             : {
    1059           0 :     if (backend != git::SshBackend::Exec) return false;
    1060           0 :     return !find_git_executable().empty();
    1061             : }
    1062             : 
    1063           0 : Result Git::do_set_ssh_backend(git::SshBackend backend)
    1064             : {
    1065           0 :     if (backend != git::SshBackend::Exec)
    1066           0 :         return ESYSREPO_RESULT(ResultCode::GIT_SSH_BACKEND_NOT_SUPPORTED);
    1067           0 :     m_ssh_backend = backend;
    1068           0 :     return ESYSREPO_RESULT(ResultCode::OK);
    1069             : }
    1070             : 
    1071           0 : Result_t<git::SshBackend> Git::do_get_ssh_backend() const
    1072             : {
    1073           0 :     return ESYSREPO_RESULT_T(ResultCode::OK, m_ssh_backend);
    1074             : }
    1075             : 
    1076             : } // namespace esys::repo::gitcmdline

Generated by: LCOV version 1.14