use crate::commit::parse_git_diff_name_status;use crate::status::{GitStatus, StatusCode};use crate::{Oid, SHORT_SHA_LENGTH};use anyhow::{Context as _, Result, anyhow};use collections::HashMap;use futures::future::BoxFuture;use futures::{AsyncWriteExt, FutureExt as _, select_biased};use git2::BranchType;use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, SharedString};use parking_lot::Mutex;use rope::Rope;use schemars::JsonSchema;use serde::Deserialize;use std::borrow::{Borrow, Cow};use std::ffi::{OsStr, OsString};use std::path::Component;use std::process::{ExitStatus, Stdio};use std::sync::LazyLock;use std::{ cmp::Ordering, path::{Path, PathBuf}, sync::Arc,};use std::{ future, io::{BufRead, BufReader, BufWriter, Read},};use sum_tree::MapSeekTarget;use thiserror::Error;use util::ResultExt;use util::command::{new_smol_command, new_std_command};use uuid::Uuid; pub use askpass::{AskPassDelegate, AskPassResult, AskPassSession}; pub const REMOTE_CANCELLED_BY_USER: &str = "Operation cancelled by user"; #[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,} impl Upstream { pub fn remote_name(&self) -> Option<&str> { self.ref_name .strip_prefix("refs/remotes/") .and_then(|stripped| stripped.split("/").next()) }} #[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, Clone)]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, pub has_parent: bool,} #[derive(Clone, Debug, Hash, PartialEq, Eq)]pub struct CommitDetails { pub sha: SharedString, pub message: SharedString, pub commit_timestamp: i64, pub author_email: SharedString, pub author_name: SharedString,} #[derive(Debug)]pub struct CommitDiff { pub files: Vec<CommitFile>,} #[derive(Debug)]pub struct CommitFile { pub path: RepoPath, pub old_text: Option<String>, pub new_text: Option<String>,} impl CommitDetails { pub fn short_sha(&self) -> SharedString { self.sha[..SHORT_SHA_LENGTH].to_string().into() }} #[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) -> BoxFuture<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) -> BoxFuture<Option<String>>; fn set_index_text( &self, path: RepoPath, content: Option<String>, env: Arc<HashMap<String, String>>, ) -> BoxFuture<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>; fn merge_message(&self) -> BoxFuture<Option<String>>; fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>>; fn branches(&self) -> BoxFuture<Result<Vec<Branch>>>; fn change_branch(&self, name: String) -> BoxFuture<Result<()>>; fn create_branch(&self, name: String) -> BoxFuture<Result<()>>; fn reset( &self, commit: String, mode: ResetMode, env: Arc<HashMap<String, String>>, ) -> BoxFuture<Result<()>>; fn checkout_files( &self, commit: String, paths: Vec<RepoPath>, env: Arc<HashMap<String, String>>, ) -> BoxFuture<Result<()>>; fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>>; fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>>; fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<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: Vec<RepoPath>, env: Arc<HashMap<String, String>>, ) -> BoxFuture<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: Vec<RepoPath>, env: Arc<HashMap<String, String>>, ) -> BoxFuture<Result<()>>; fn commit( &self, message: SharedString, name_and_email: Option<(SharedString, SharedString)>, env: Arc<HashMap<String, String>>, ) -> BoxFuture<Result<()>>; fn push( &self, branch_name: String, upstream_name: String, options: Option<PushOptions>, askpass: AskPassDelegate, env: Arc<HashMap<String, String>>, // This method takes an AsyncApp to ensure it's invoked on the main thread, // otherwise git-credentials-manager won't work. cx: AsyncApp, ) -> BoxFuture<Result<RemoteCommandOutput>>; fn pull( &self, branch_name: String, upstream_name: String, askpass: AskPassDelegate, env: Arc<HashMap<String, String>>, // This method takes an AsyncApp to ensure it's invoked on the main thread, // otherwise git-credentials-manager won't work. cx: AsyncApp, ) -> BoxFuture<Result<RemoteCommandOutput>>; fn fetch( &self, askpass: AskPassDelegate, env: Arc<HashMap<String, String>>, // This method takes an AsyncApp to ensure it's invoked on the main thread, // otherwise git-credentials-manager won't work. cx: AsyncApp, ) -> BoxFuture<Result<RemoteCommandOutput>>; fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<Result<Vec<Remote>>>; /// returns a list of remote branches that contain HEAD fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<SharedString>>>; /// Run git diff fn diff(&self, diff: DiffType) -> BoxFuture<Result<String>>; /// Creates a checkpoint for the repository. fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>>; /// Resets to a previously-created checkpoint. fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>>; /// Compares two checkpoints, returning true if they are equal fn compare_checkpoints( &self, left: GitRepositoryCheckpoint, right: GitRepositoryCheckpoint, ) -> BoxFuture<Result<bool>>; /// Deletes a previously-created checkpoint. fn delete_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>>; /// Computes a diff between two checkpoints. fn diff_checkpoints( &self, base_checkpoint: GitRepositoryCheckpoint, target_checkpoint: GitRepositoryCheckpoint, ) -> BoxFuture<Result<String>>;} pub enum DiffType { HeadToIndex, HeadToWorktree,} #[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: Arc<Mutex<git2::Repository>>, pub git_binary_path: PathBuf, executor: BackgroundExecutor,} impl RealGitRepository { pub fn new( dotgit_path: &Path, git_binary_path: Option<PathBuf>, executor: BackgroundExecutor, ) -> Option<Self> { let workdir_root = dotgit_path.parent()?; let repository = git2::Repository::open(workdir_root).log_err()?; Some(Self { repository: Arc::new(Mutex::new(repository)), git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")), executor, }) } fn working_directory(&self) -> Result<PathBuf> { self.repository .lock() .workdir() .context("failed to read git work directory") .map(Path::to_path_buf) }} #[derive(Clone, Debug)]pub struct GitRepositoryCheckpoint { ref_name: String, commit_sha: Oid,} 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: String) -> BoxFuture<Result<CommitDetails>> { let repo = self.repository.clone(); self.executor .spawn(async move { let repo = repo.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(), author_email: String::from_utf8_lossy(commit.author().email_bytes()) .to_string() .into(), author_name: String::from_utf8_lossy(commit.author().name_bytes()) .to_string() .into(), }; Ok(details) }) .boxed() } fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>> { let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned) else { return future::ready(Err(anyhow!("no working directory"))).boxed(); }; cx.background_spawn(async move { let show_output = util::command::new_std_command("git") .current_dir(&working_directory) .args([ "--no-optional-locks", "show", "--format=%P", "-z", "--no-renames", "--name-status", ]) .arg(&commit) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .map_err(|e| anyhow!("Failed to start git show process: {e}"))?; let show_stdout = String::from_utf8_lossy(&show_output.stdout); let mut lines = show_stdout.split('\n'); let parent_sha = lines.next().unwrap().trim().trim_end_matches('\0'); let changes = parse_git_diff_name_status(lines.next().unwrap_or("")); let mut cat_file_process = util::command::new_std_command("git") .current_dir(&working_directory) .args(["--no-optional-locks", "cat-file", "--batch=%(objectsize)"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .map_err(|e| anyhow!("Failed to start git cat-file process: {e}"))?; use std::io::Write as _; let mut files = Vec::<CommitFile>::new(); let mut stdin = BufWriter::with_capacity(512, cat_file_process.stdin.take().unwrap()); let mut stdout = BufReader::new(cat_file_process.stdout.take().unwrap()); let mut info_line = String::new(); let mut newline = [b'\0']; for (path, status_code) in changes { match status_code { StatusCode::Modified => { writeln!(&mut stdin, "{commit}:{}", path.display())?; writeln!(&mut stdin, "{parent_sha}:{}", path.display())?; } StatusCode::Added => { writeln!(&mut stdin, "{commit}:{}", path.display())?; } StatusCode::Deleted => { writeln!(&mut stdin, "{parent_sha}:{}", path.display())?; } _ => continue, } stdin.flush()?; info_line.clear(); stdout.read_line(&mut info_line)?; let len = info_line.trim_end().parse().with_context(|| { format!("invalid object size output from cat-file {info_line}") })?; let mut text = vec![0; len]; stdout.read_exact(&mut text)?; stdout.read_exact(&mut newline)?; let text = String::from_utf8_lossy(&text).to_string(); let mut old_text = None; let mut new_text = None; match status_code { StatusCode::Modified => { info_line.clear(); stdout.read_line(&mut info_line)?; let len = info_line.trim_end().parse().with_context(|| {Lines 1–500 of 1959. Load more
use crate::commit::parse_git_diff_name_status; use crate::status::{GitStatus, StatusCode}; use crate::{Oid, SHORT_SHA_LENGTH}; use anyhow::{Context as _, Result, anyhow}; use collections::HashMap; use futures::future::BoxFuture; use futures::{AsyncWriteExt, FutureExt as _, select_biased}; use git2::BranchType; use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, SharedString}; use parking_lot::Mutex; use rope::Rope; use schemars::JsonSchema; use serde::Deserialize; use std::borrow::{Borrow, Cow}; use std::ffi::{OsStr, OsString}; use std::path::Component; use std::process::{ExitStatus, Stdio}; use std::sync::LazyLock; use std::{ cmp::Ordering, path::{Path, PathBuf}, sync::Arc, }; use std::{ future, io::{BufRead, BufReader, BufWriter, Read}, }; use sum_tree::MapSeekTarget; use thiserror::Error; use util::ResultExt; use util::command::{new_smol_command, new_std_command}; use uuid::Uuid; pub use askpass::{AskPassDelegate, AskPassResult, AskPassSession}; pub const REMOTE_CANCELLED_BY_USER: &str = "Operation cancelled by user"; #[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, } impl Upstream { pub fn remote_name(&self) -> Option<&str> { self.ref_name .strip_prefix("refs/remotes/") .and_then(|stripped| stripped.split("/").next()) } } #[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, Clone)] 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, pub has_parent: bool, } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct CommitDetails { pub sha: SharedString, pub message: SharedString, pub commit_timestamp: i64, pub author_email: SharedString, pub author_name: SharedString, } #[derive(Debug)] pub struct CommitDiff { pub files: Vec<CommitFile>, } #[derive(Debug)] pub struct CommitFile { pub path: RepoPath, pub old_text: Option<String>, pub new_text: Option<String>, } impl CommitDetails { pub fn short_sha(&self) -> SharedString { self.sha[..SHORT_SHA_LENGTH].to_string().into() } } #[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) -> BoxFuture<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) -> BoxFuture<Option<String>>; fn set_index_text( &self, path: RepoPath, content: Option<String>, env: Arc<HashMap<String, String>>, ) -> BoxFuture<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>; fn merge_message(&self) -> BoxFuture<Option<String>>; fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>>; fn branches(&self) -> BoxFuture<Result<Vec<Branch>>>; fn change_branch(&self, name: String) -> BoxFuture<Result<()>>; fn create_branch(&self, name: String) -> BoxFuture<Result<()>>; fn reset( &self, commit: String, mode: ResetMode, env: Arc<HashMap<String, String>>, ) -> BoxFuture<Result<()>>; fn checkout_files( &self, commit: String, paths: Vec<RepoPath>, env: Arc<HashMap<String, String>>, ) -> BoxFuture<Result<()>>; fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>>; fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>>; fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<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: Vec<RepoPath>, env: Arc<HashMap<String, String>>, ) -> BoxFuture<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: Vec<RepoPath>, env: Arc<HashMap<String, String>>, ) -> BoxFuture<Result<()>>; fn commit( &self, message: SharedString, name_and_email: Option<(SharedString, SharedString)>, env: Arc<HashMap<String, String>>, ) -> BoxFuture<Result<()>>; fn push( &self, branch_name: String, upstream_name: String, options: Option<PushOptions>, askpass: AskPassDelegate, env: Arc<HashMap<String, String>>, // This method takes an AsyncApp to ensure it's invoked on the main thread, // otherwise git-credentials-manager won't work. cx: AsyncApp, ) -> BoxFuture<Result<RemoteCommandOutput>>; fn pull( &self, branch_name: String, upstream_name: String, askpass: AskPassDelegate, env: Arc<HashMap<String, String>>, // This method takes an AsyncApp to ensure it's invoked on the main thread, // otherwise git-credentials-manager won't work. cx: AsyncApp, ) -> BoxFuture<Result<RemoteCommandOutput>>; fn fetch( &self, askpass: AskPassDelegate, env: Arc<HashMap<String, String>>, // This method takes an AsyncApp to ensure it's invoked on the main thread, // otherwise git-credentials-manager won't work. cx: AsyncApp, ) -> BoxFuture<Result<RemoteCommandOutput>>; fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<Result<Vec<Remote>>>; /// returns a list of remote branches that contain HEAD fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<SharedString>>>; /// Run git diff fn diff(&self, diff: DiffType) -> BoxFuture<Result<String>>; /// Creates a checkpoint for the repository. fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>>; /// Resets to a previously-created checkpoint. fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>>; /// Compares two checkpoints, returning true if they are equal fn compare_checkpoints( &self, left: GitRepositoryCheckpoint, right: GitRepositoryCheckpoint, ) -> BoxFuture<Result<bool>>; /// Deletes a previously-created checkpoint. fn delete_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>>; /// Computes a diff between two checkpoints. fn diff_checkpoints( &self, base_checkpoint: GitRepositoryCheckpoint, target_checkpoint: GitRepositoryCheckpoint, ) -> BoxFuture<Result<String>>; } pub enum DiffType { HeadToIndex, HeadToWorktree, } #[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: Arc<Mutex<git2::Repository>>, pub git_binary_path: PathBuf, executor: BackgroundExecutor, } impl RealGitRepository { pub fn new( dotgit_path: &Path, git_binary_path: Option<PathBuf>, executor: BackgroundExecutor, ) -> Option<Self> { let workdir_root = dotgit_path.parent()?; let repository = git2::Repository::open(workdir_root).log_err()?; Some(Self { repository: Arc::new(Mutex::new(repository)), git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")), executor, }) } fn working_directory(&self) -> Result<PathBuf> { self.repository .lock() .workdir() .context("failed to read git work directory") .map(Path::to_path_buf) } } #[derive(Clone, Debug)] pub struct GitRepositoryCheckpoint { ref_name: String, commit_sha: Oid, } 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: String) -> BoxFuture<Result<CommitDetails>> { let repo = self.repository.clone(); self.executor .spawn(async move { let repo = repo.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(), author_email: String::from_utf8_lossy(commit.author().email_bytes()) .to_string() .into(), author_name: String::from_utf8_lossy(commit.author().name_bytes()) .to_string() .into(), }; Ok(details) }) .boxed() } fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>> { let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned) else { return future::ready(Err(anyhow!("no working directory"))).boxed(); }; cx.background_spawn(async move { let show_output = util::command::new_std_command("git") .current_dir(&working_directory) .args([ "--no-optional-locks", "show", "--format=%P", "-z", "--no-renames", "--name-status", ]) .arg(&commit) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .map_err(|e| anyhow!("Failed to start git show process: {e}"))?; let show_stdout = String::from_utf8_lossy(&show_output.stdout); let mut lines = show_stdout.split('\n'); let parent_sha = lines.next().unwrap().trim().trim_end_matches('\0'); let changes = parse_git_diff_name_status(lines.next().unwrap_or("")); let mut cat_file_process = util::command::new_std_command("git") .current_dir(&working_directory) .args(["--no-optional-locks", "cat-file", "--batch=%(objectsize)"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .map_err(|e| anyhow!("Failed to start git cat-file process: {e}"))?; use std::io::Write as _; let mut files = Vec::<CommitFile>::new(); let mut stdin = BufWriter::with_capacity(512, cat_file_process.stdin.take().unwrap()); let mut stdout = BufReader::new(cat_file_process.stdout.take().unwrap()); let mut info_line = String::new(); let mut newline = [b'\0']; for (path, status_code) in changes { match status_code { StatusCode::Modified => { writeln!(&mut stdin, "{commit}:{}", path.display())?; writeln!(&mut stdin, "{parent_sha}:{}", path.display())?; } StatusCode::Added => { writeln!(&mut stdin, "{commit}:{}", path.display())?; } StatusCode::Deleted => { writeln!(&mut stdin, "{parent_sha}:{}", path.display())?; } _ => continue, } stdin.flush()?; info_line.clear(); stdout.read_line(&mut info_line)?; let len = info_line.trim_end().parse().with_context(|| { format!("invalid object size output from cat-file {info_line}") })?; let mut text = vec![0; len]; stdout.read_exact(&mut text)?; stdout.read_exact(&mut newline)?; let text = String::from_utf8_lossy(&text).to_string(); let mut old_text = None; let mut new_text = None; match status_code { StatusCode::Modified => { info_line.clear(); stdout.read_line(&mut info_line)?; let len = info_line.trim_end().parse().with_context(|| {