use crate::status::FileStatus;use crate::GitHostingProviderRegistry;use crate::{blame::Blame, status::GitStatus};use anyhow::{anyhow, Context, Result};use collections::{HashMap, HashSet};use git2::BranchType;use gpui::SharedString;use parking_lot::Mutex;use rope::Rope;use schemars::JsonSchema;use serde::Deserialize;use std::borrow::Borrow;use std::io::Write as _;#[cfg(not(windows))]use std::os::unix::fs::PermissionsExt;use std::process::Stdio;use std::sync::LazyLock;use std::{ cmp::Ordering, path::{Component, Path, PathBuf}, sync::Arc,};use sum_tree::MapSeekTarget;use util::command::new_std_command;use util::ResultExt; #[derive(Clone, Debug, Hash, PartialEq, Eq)]pub struct Branch { pub is_head: bool, pub name: SharedString, pub upstream: Option<Upstream>, pub most_recent_commit: Option<CommitSummary>,} impl Branch { pub fn tracking_status(&self) -> Option<UpstreamTrackingStatus> { self.upstream .as_ref() .and_then(|upstream| upstream.tracking.status()) } pub fn priority_key(&self) -> (bool, Option<i64>) { ( self.is_head, self.most_recent_commit .as_ref() .map(|commit| commit.commit_timestamp), ) }} #[derive(Clone, Debug, Hash, PartialEq, Eq)]pub struct Upstream { pub ref_name: SharedString, pub tracking: UpstreamTracking,} #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]pub enum UpstreamTracking { /// Remote ref not present in local repository. Gone, /// Remote ref present in local repository (fetched from remote). Tracked(UpstreamTrackingStatus),} impl From<UpstreamTrackingStatus> for UpstreamTracking { fn from(status: UpstreamTrackingStatus) -> Self { UpstreamTracking::Tracked(status) }} impl UpstreamTracking { pub fn is_gone(&self) -> bool { matches!(self, UpstreamTracking::Gone) } pub fn status(&self) -> Option<UpstreamTrackingStatus> { match self { UpstreamTracking::Gone => None, UpstreamTracking::Tracked(status) => Some(*status), } }} #[derive(Debug)]pub struct RemoteCommandOutput { pub stdout: String, pub stderr: String,} impl RemoteCommandOutput { pub fn is_empty(&self) -> bool { self.stdout.is_empty() && self.stderr.is_empty() }} #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]pub struct UpstreamTrackingStatus { pub ahead: u32, pub behind: u32,} #[derive(Clone, Debug, Hash, PartialEq, Eq)]pub struct CommitSummary { pub sha: SharedString, pub subject: SharedString, /// This is a unix timestamp pub commit_timestamp: i64,} #[derive(Clone, Debug, Hash, PartialEq, Eq)]pub struct CommitDetails { pub sha: SharedString, pub message: SharedString, pub commit_timestamp: i64, pub committer_email: SharedString, pub committer_name: SharedString,} #[derive(Debug, Clone, Hash, PartialEq, Eq)]pub struct Remote { pub name: SharedString,} pub enum ResetMode { // reset the branch pointer, leave index and worktree unchanged // (this will make it look like things that were committed are now // staged) Soft, // reset the branch pointer and index, leave worktree unchanged // (this makes it look as though things that were committed are now // unstaged) Mixed,} pub trait GitRepository: Send + Sync { fn reload_index(&self); /// Returns the contents of an entry in the repository's index, or None if there is no entry for the given path. /// /// Also returns `None` for symlinks. fn load_index_text(&self, path: &RepoPath) -> Option<String>; /// Returns the contents of an entry in the repository's HEAD, or None if HEAD does not exist or has no entry for the given path. /// /// Also returns `None` for symlinks. fn load_committed_text(&self, path: &RepoPath) -> Option<String>; fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()>; /// Returns the URL of the remote with the given name. fn remote_url(&self, name: &str) -> Option<String>; /// Returns the SHA of the current HEAD. fn head_sha(&self) -> Option<String>; fn merge_head_shas(&self) -> Vec<String>; /// Returns the list of git statuses, sorted by path fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>; fn branches(&self) -> Result<Vec<Branch>>; fn change_branch(&self, _: &str) -> Result<()>; fn create_branch(&self, _: &str) -> Result<()>; fn branch_exits(&self, _: &str) -> Result<bool>; fn reset(&self, commit: &str, mode: ResetMode) -> Result<()>; fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()>; fn show(&self, commit: &str) -> Result<CommitDetails>; fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>; /// Returns the absolute path to the repository. For worktrees, this will be the path to the /// worktree's gitdir within the main repository (typically `.git/worktrees/<name>`). fn path(&self) -> PathBuf; /// Returns the absolute path to the ".git" dir for the main repository, typically a `.git` /// folder. For worktrees, this will be the path to the repository the worktree was created /// from. Otherwise, this is the same value as `path()`. /// /// Git documentation calls this the "commondir", and for git CLI is overridden by /// `GIT_COMMON_DIR`. fn main_repository_path(&self) -> PathBuf; /// Updates the index to match the worktree at the given paths. /// /// If any of the paths have been deleted from the worktree, they will be removed from the index if found there. fn stage_paths(&self, paths: &[RepoPath]) -> Result<()>; /// Updates the index to match HEAD at the given paths. /// /// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index. fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>; fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>; fn push( &self, branch_name: &str, upstream_name: &str, options: Option<PushOptions>, ) -> Result<RemoteCommandOutput>; fn pull(&self, branch_name: &str, upstream_name: &str) -> Result<RemoteCommandOutput>; fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>; fn fetch(&self) -> Result<RemoteCommandOutput>;} #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]pub enum PushOptions { SetUpstream, Force,} impl std::fmt::Debug for dyn GitRepository { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("dyn GitRepository<...>").finish() }} pub struct RealGitRepository { pub repository: Mutex<git2::Repository>, pub git_binary_path: PathBuf, hosting_provider_registry: Arc<GitHostingProviderRegistry>,} impl RealGitRepository { pub fn new( repository: git2::Repository, git_binary_path: Option<PathBuf>, hosting_provider_registry: Arc<GitHostingProviderRegistry>, ) -> Self { Self { repository: Mutex::new(repository), git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")), hosting_provider_registry, } } fn working_directory(&self) -> Result<PathBuf> { self.repository .lock() .workdir() .context("failed to read git work directory") .map(Path::to_path_buf) }} // https://git-scm.com/book/en/v2/Git-Internals-Git-Objectsconst GIT_MODE_SYMLINK: u32 = 0o120000; impl GitRepository for RealGitRepository { fn reload_index(&self) { if let Ok(mut index) = self.repository.lock().index() { _ = index.read(false); } } fn path(&self) -> PathBuf { let repo = self.repository.lock(); repo.path().into() } fn main_repository_path(&self) -> PathBuf { let repo = self.repository.lock(); repo.commondir().into() } fn show(&self, commit: &str) -> Result<CommitDetails> { let repo = self.repository.lock(); let Ok(commit) = repo.revparse_single(commit)?.into_commit() else { anyhow::bail!("{} is not a commit", commit); }; let details = CommitDetails { sha: commit.id().to_string().into(), message: String::from_utf8_lossy(commit.message_raw_bytes()) .to_string() .into(), commit_timestamp: commit.time().seconds(), committer_email: String::from_utf8_lossy(commit.committer().email_bytes()) .to_string() .into(), committer_name: String::from_utf8_lossy(commit.committer().name_bytes()) .to_string() .into(), }; Ok(details) } fn reset(&self, commit: &str, mode: ResetMode) -> Result<()> { let working_directory = self.working_directory()?; let mode_flag = match mode { ResetMode::Mixed => "--mixed", ResetMode::Soft => "--soft", }; let output = new_std_command(&self.git_binary_path) .current_dir(&working_directory) .args(["reset", mode_flag, commit]) .output()?; if !output.status.success() { return Err(anyhow!( "Failed to reset:\n{}", String::from_utf8_lossy(&output.stderr) )); } Ok(()) } fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()> { if paths.is_empty() { return Ok(()); } let working_directory = self.working_directory()?; let output = new_std_command(&self.git_binary_path) .current_dir(&working_directory) .args(["checkout", commit, "--"]) .args(paths.iter().map(|path| path.as_ref())) .output()?; if !output.status.success() { return Err(anyhow!( "Failed to checkout files:\n{}", String::from_utf8_lossy(&output.stderr) )); } Ok(()) } fn load_index_text(&self, path: &RepoPath) -> Option<String> { fn logic(repo: &git2::Repository, path: &RepoPath) -> Result<Option<String>> { const STAGE_NORMAL: i32 = 0; let index = repo.index()?; // This check is required because index.get_path() unwraps internally :( check_path_to_repo_path_errors(path)?; let oid = match index.get_path(path, STAGE_NORMAL) { Some(entry) if entry.mode != GIT_MODE_SYMLINK => entry.id, _ => return Ok(None), }; let content = repo.find_blob(oid)?.content().to_owned(); Ok(Some(String::from_utf8(content)?)) } match logic(&self.repository.lock(), path) { Ok(value) => return value, Err(err) => log::error!("Error loading index text: {:?}", err), } None } fn load_committed_text(&self, path: &RepoPath) -> Option<String> { let repo = self.repository.lock(); let head = repo.head().ok()?.peel_to_tree().log_err()?; let entry = head.get_path(path).ok()?; if entry.filemode() == i32::from(git2::FileMode::Link) { return None; } let content = repo.find_blob(entry.id()).log_err()?.content().to_owned(); let content = String::from_utf8(content).log_err()?; Some(content) } fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> { let working_directory = self.working_directory()?; if let Some(content) = content { let mut child = new_std_command(&self.git_binary_path) .current_dir(&working_directory) .args(["hash-object", "-w", "--stdin"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?; child.stdin.take().unwrap().write_all(content.as_bytes())?; let output = child.wait_with_output()?.stdout; let sha = String::from_utf8(output)?; log::debug!("indexing SHA: {sha}, path {path:?}"); let output = new_std_command(&self.git_binary_path) .current_dir(&working_directory) .args(["update-index", "--add", "--cacheinfo", "100644", &sha]) .arg(path.as_ref()) .output()?; if !output.status.success() { return Err(anyhow!( "Failed to stage:\n{}", String::from_utf8_lossy(&output.stderr) )); } } else { let output = new_std_command(&self.git_binary_path) .current_dir(&working_directory) .args(["update-index", "--force-remove"]) .arg(path.as_ref()) .output()?; if !output.status.success() { return Err(anyhow!( "Failed to unstage:\n{}", String::from_utf8_lossy(&output.stderr) )); } } Ok(()) } fn remote_url(&self, name: &str) -> Option<String> { let repo = self.repository.lock(); let remote = repo.find_remote(name).ok()?; remote.url().map(|url| url.to_string()) } fn head_sha(&self) -> Option<String> { Some(self.repository.lock().head().ok()?.target()?.to_string()) } fn merge_head_shas(&self) -> Vec<String> { let mut shas = Vec::default(); self.repository .lock() .mergehead_foreach(|oid| { shas.push(oid.to_string()); true }) .ok(); shas } fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> { let working_directory = self .repository .lock() .workdir() .context("failed to read git work directory")? .to_path_buf(); GitStatus::new(&self.git_binary_path, &working_directory, path_prefixes) } fn branch_exits(&self, name: &str) -> Result<bool> { let repo = self.repository.lock(); let branch = repo.find_branch(name, BranchType::Local); match branch { Ok(_) => Ok(true), Err(e) => match e.code() { git2::ErrorCode::NotFound => Ok(false), _ => Err(anyhow!(e)), }, } } fn branches(&self) -> Result<Vec<Branch>> { let working_directory = self .repository .lock() .workdir() .context("failed to read git work directory")? .to_path_buf(); let fields = [ "%(HEAD)", "%(objectname)", "%(refname)", "%(upstream)", "%(upstream:track)", "%(committerdate:unix)", "%(contents:subject)", ] .join("%00"); let args = vec!["for-each-ref", "refs/heads/**/*", "--format", &fields]; let output = new_std_command(&self.git_binary_path) .current_dir(&working_directory) .args(args) .output()?; if !output.status.success() { return Err(anyhow!( "Failed to git git branches:\n{}", String::from_utf8_lossy(&output.stderr) )); } let input = String::from_utf8_lossy(&output.stdout); let mut branches = parse_branch_input(&input)?; if branches.is_empty() { let args = vec!["symbolic-ref", "--quiet", "--short", "HEAD"]; let output = new_std_command(&self.git_binary_path) .current_dir(&working_directory) .args(args) .output()?; // git symbolic-ref returns a non-0 exit code if HEAD points // to something other than a branch if output.status.success() { let name = String::from_utf8_lossy(&output.stdout).trim().to_string();Lines 1–500 of 1215. Load more
use crate::status::FileStatus; use crate::GitHostingProviderRegistry; use crate::{blame::Blame, status::GitStatus}; use anyhow::{anyhow, Context, Result}; use collections::{HashMap, HashSet}; use git2::BranchType; use gpui::SharedString; use parking_lot::Mutex; use rope::Rope; use schemars::JsonSchema; use serde::Deserialize; use std::borrow::Borrow; use std::io::Write as _; #[cfg(not(windows))] use std::os::unix::fs::PermissionsExt; use std::process::Stdio; use std::sync::LazyLock; use std::{ cmp::Ordering, path::{Component, Path, PathBuf}, sync::Arc, }; use sum_tree::MapSeekTarget; use util::command::new_std_command; use util::ResultExt; #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct Branch { pub is_head: bool, pub name: SharedString, pub upstream: Option<Upstream>, pub most_recent_commit: Option<CommitSummary>, } impl Branch { pub fn tracking_status(&self) -> Option<UpstreamTrackingStatus> { self.upstream .as_ref() .and_then(|upstream| upstream.tracking.status()) } pub fn priority_key(&self) -> (bool, Option<i64>) { ( self.is_head, self.most_recent_commit .as_ref() .map(|commit| commit.commit_timestamp), ) } } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct Upstream { pub ref_name: SharedString, pub tracking: UpstreamTracking, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum UpstreamTracking { /// Remote ref not present in local repository. Gone, /// Remote ref present in local repository (fetched from remote). Tracked(UpstreamTrackingStatus), } impl From<UpstreamTrackingStatus> for UpstreamTracking { fn from(status: UpstreamTrackingStatus) -> Self { UpstreamTracking::Tracked(status) } } impl UpstreamTracking { pub fn is_gone(&self) -> bool { matches!(self, UpstreamTracking::Gone) } pub fn status(&self) -> Option<UpstreamTrackingStatus> { match self { UpstreamTracking::Gone => None, UpstreamTracking::Tracked(status) => Some(*status), } } } #[derive(Debug)] pub struct RemoteCommandOutput { pub stdout: String, pub stderr: String, } impl RemoteCommandOutput { pub fn is_empty(&self) -> bool { self.stdout.is_empty() && self.stderr.is_empty() } } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub struct UpstreamTrackingStatus { pub ahead: u32, pub behind: u32, } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct CommitSummary { pub sha: SharedString, pub subject: SharedString, /// This is a unix timestamp pub commit_timestamp: i64, } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct CommitDetails { pub sha: SharedString, pub message: SharedString, pub commit_timestamp: i64, pub committer_email: SharedString, pub committer_name: SharedString, } #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Remote { pub name: SharedString, } pub enum ResetMode { // reset the branch pointer, leave index and worktree unchanged // (this will make it look like things that were committed are now // staged) Soft, // reset the branch pointer and index, leave worktree unchanged // (this makes it look as though things that were committed are now // unstaged) Mixed, } pub trait GitRepository: Send + Sync { fn reload_index(&self); /// Returns the contents of an entry in the repository's index, or None if there is no entry for the given path. /// /// Also returns `None` for symlinks. fn load_index_text(&self, path: &RepoPath) -> Option<String>; /// Returns the contents of an entry in the repository's HEAD, or None if HEAD does not exist or has no entry for the given path. /// /// Also returns `None` for symlinks. fn load_committed_text(&self, path: &RepoPath) -> Option<String>; fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()>; /// Returns the URL of the remote with the given name. fn remote_url(&self, name: &str) -> Option<String>; /// Returns the SHA of the current HEAD. fn head_sha(&self) -> Option<String>; fn merge_head_shas(&self) -> Vec<String>; /// Returns the list of git statuses, sorted by path fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>; fn branches(&self) -> Result<Vec<Branch>>; fn change_branch(&self, _: &str) -> Result<()>; fn create_branch(&self, _: &str) -> Result<()>; fn branch_exits(&self, _: &str) -> Result<bool>; fn reset(&self, commit: &str, mode: ResetMode) -> Result<()>; fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()>; fn show(&self, commit: &str) -> Result<CommitDetails>; fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>; /// Returns the absolute path to the repository. For worktrees, this will be the path to the /// worktree's gitdir within the main repository (typically `.git/worktrees/<name>`). fn path(&self) -> PathBuf; /// Returns the absolute path to the ".git" dir for the main repository, typically a `.git` /// folder. For worktrees, this will be the path to the repository the worktree was created /// from. Otherwise, this is the same value as `path()`. /// /// Git documentation calls this the "commondir", and for git CLI is overridden by /// `GIT_COMMON_DIR`. fn main_repository_path(&self) -> PathBuf; /// Updates the index to match the worktree at the given paths. /// /// If any of the paths have been deleted from the worktree, they will be removed from the index if found there. fn stage_paths(&self, paths: &[RepoPath]) -> Result<()>; /// Updates the index to match HEAD at the given paths. /// /// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index. fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>; fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>; fn push( &self, branch_name: &str, upstream_name: &str, options: Option<PushOptions>, ) -> Result<RemoteCommandOutput>; fn pull(&self, branch_name: &str, upstream_name: &str) -> Result<RemoteCommandOutput>; fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>; fn fetch(&self) -> Result<RemoteCommandOutput>; } #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)] pub enum PushOptions { SetUpstream, Force, } impl std::fmt::Debug for dyn GitRepository { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("dyn GitRepository<...>").finish() } } pub struct RealGitRepository { pub repository: Mutex<git2::Repository>, pub git_binary_path: PathBuf, hosting_provider_registry: Arc<GitHostingProviderRegistry>, } impl RealGitRepository { pub fn new( repository: git2::Repository, git_binary_path: Option<PathBuf>, hosting_provider_registry: Arc<GitHostingProviderRegistry>, ) -> Self { Self { repository: Mutex::new(repository), git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")), hosting_provider_registry, } } fn working_directory(&self) -> Result<PathBuf> { self.repository .lock() .workdir() .context("failed to read git work directory") .map(Path::to_path_buf) } } // https://git-scm.com/book/en/v2/Git-Internals-Git-Objects const GIT_MODE_SYMLINK: u32 = 0o120000; impl GitRepository for RealGitRepository { fn reload_index(&self) { if let Ok(mut index) = self.repository.lock().index() { _ = index.read(false); } } fn path(&self) -> PathBuf { let repo = self.repository.lock(); repo.path().into() } fn main_repository_path(&self) -> PathBuf { let repo = self.repository.lock(); repo.commondir().into() } fn show(&self, commit: &str) -> Result<CommitDetails> { let repo = self.repository.lock(); let Ok(commit) = repo.revparse_single(commit)?.into_commit() else { anyhow::bail!("{} is not a commit", commit); }; let details = CommitDetails { sha: commit.id().to_string().into(), message: String::from_utf8_lossy(commit.message_raw_bytes()) .to_string() .into(), commit_timestamp: commit.time().seconds(), committer_email: String::from_utf8_lossy(commit.committer().email_bytes()) .to_string() .into(), committer_name: String::from_utf8_lossy(commit.committer().name_bytes()) .to_string() .into(), }; Ok(details) } fn reset(&self, commit: &str, mode: ResetMode) -> Result<()> { let working_directory = self.working_directory()?; let mode_flag = match mode { ResetMode::Mixed => "--mixed", ResetMode::Soft => "--soft", }; let output = new_std_command(&self.git_binary_path) .current_dir(&working_directory) .args(["reset", mode_flag, commit]) .output()?; if !output.status.success() { return Err(anyhow!( "Failed to reset:\n{}", String::from_utf8_lossy(&output.stderr) )); } Ok(()) } fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()> { if paths.is_empty() { return Ok(()); } let working_directory = self.working_directory()?; let output = new_std_command(&self.git_binary_path) .current_dir(&working_directory) .args(["checkout", commit, "--"]) .args(paths.iter().map(|path| path.as_ref())) .output()?; if !output.status.success() { return Err(anyhow!( "Failed to checkout files:\n{}", String::from_utf8_lossy(&output.stderr) )); } Ok(()) } fn load_index_text(&self, path: &RepoPath) -> Option<String> { fn logic(repo: &git2::Repository, path: &RepoPath) -> Result<Option<String>> { const STAGE_NORMAL: i32 = 0; let index = repo.index()?; // This check is required because index.get_path() unwraps internally :( check_path_to_repo_path_errors(path)?; let oid = match index.get_path(path, STAGE_NORMAL) { Some(entry) if entry.mode != GIT_MODE_SYMLINK => entry.id, _ => return Ok(None), }; let content = repo.find_blob(oid)?.content().to_owned(); Ok(Some(String::from_utf8(content)?)) } match logic(&self.repository.lock(), path) { Ok(value) => return value, Err(err) => log::error!("Error loading index text: {:?}", err), } None } fn load_committed_text(&self, path: &RepoPath) -> Option<String> { let repo = self.repository.lock(); let head = repo.head().ok()?.peel_to_tree().log_err()?; let entry = head.get_path(path).ok()?; if entry.filemode() == i32::from(git2::FileMode::Link) { return None; } let content = repo.find_blob(entry.id()).log_err()?.content().to_owned(); let content = String::from_utf8(content).log_err()?; Some(content) } fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> { let working_directory = self.working_directory()?; if let Some(content) = content { let mut child = new_std_command(&self.git_binary_path) .current_dir(&working_directory) .args(["hash-object", "-w", "--stdin"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?; child.stdin.take().unwrap().write_all(content.as_bytes())?; let output = child.wait_with_output()?.stdout; let sha = String::from_utf8(output)?; log::debug!("indexing SHA: {sha}, path {path:?}"); let output = new_std_command(&self.git_binary_path) .current_dir(&working_directory) .args(["update-index", "--add", "--cacheinfo", "100644", &sha]) .arg(path.as_ref()) .output()?; if !output.status.success() { return Err(anyhow!( "Failed to stage:\n{}", String::from_utf8_lossy(&output.stderr) )); } } else { let output = new_std_command(&self.git_binary_path) .current_dir(&working_directory) .args(["update-index", "--force-remove"]) .arg(path.as_ref()) .output()?; if !output.status.success() { return Err(anyhow!( "Failed to unstage:\n{}", String::from_utf8_lossy(&output.stderr) )); } } Ok(()) } fn remote_url(&self, name: &str) -> Option<String> { let repo = self.repository.lock(); let remote = repo.find_remote(name).ok()?; remote.url().map(|url| url.to_string()) } fn head_sha(&self) -> Option<String> { Some(self.repository.lock().head().ok()?.target()?.to_string()) } fn merge_head_shas(&self) -> Vec<String> { let mut shas = Vec::default(); self.repository .lock() .mergehead_foreach(|oid| { shas.push(oid.to_string()); true }) .ok(); shas } fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> { let working_directory = self .repository .lock() .workdir() .context("failed to read git work directory")? .to_path_buf(); GitStatus::new(&self.git_binary_path, &working_directory, path_prefixes) } fn branch_exits(&self, name: &str) -> Result<bool> { let repo = self.repository.lock(); let branch = repo.find_branch(name, BranchType::Local); match branch { Ok(_) => Ok(true), Err(e) => match e.code() { git2::ErrorCode::NotFound => Ok(false), _ => Err(anyhow!(e)), }, } } fn branches(&self) -> Result<Vec<Branch>> { let working_directory = self .repository .lock() .workdir() .context("failed to read git work directory")? .to_path_buf(); let fields = [ "%(HEAD)", "%(objectname)", "%(refname)", "%(upstream)", "%(upstream:track)", "%(committerdate:unix)", "%(contents:subject)", ] .join("%00"); let args = vec!["for-each-ref", "refs/heads/**/*", "--format", &fields]; let output = new_std_command(&self.git_binary_path) .current_dir(&working_directory) .args(args) .output()?; if !output.status.success() { return Err(anyhow!( "Failed to git git branches:\n{}", String::from_utf8_lossy(&output.stderr) )); } let input = String::from_utf8_lossy(&output.stdout); let mut branches = parse_branch_input(&input)?; if branches.is_empty() { let args = vec!["symbolic-ref", "--quiet", "--short", "HEAD"]; let output = new_std_command(&self.git_binary_path) .current_dir(&working_directory) .args(args) .output()?; // git symbolic-ref returns a non-0 exit code if HEAD points // to something other than a branch if output.status.success() { let name = String::from_utf8_lossy(&output.stdout).trim().to_string();