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

Generated by: LCOV version 1.14