1964 lines · 64.8 KB · Rust
RawDownload
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, Default)]pub struct CommitOptions {    pub amend: bool,} #[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;     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)>,        options: CommitOptions,        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>>;     /// 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 {    pub 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(|| {                            format!("invalid object size output from cat-file {}", info_line)                        })?;                        let mut parent_text = vec![0; len];                        stdout.read_exact(&mut parent_text)?;

Lines 1–500 of 1964.