2731 lines · 91.9 KB · Rust
RawDownload
use crate::commit::parse_git_diff_name_status;use crate::stash::GitStash;use crate::status::{DiffTreeType, GitStatus, StatusCode, TreeDiff};use crate::{Oid, SHORT_SHA_LENGTH};use anyhow::{Context as _, Result, anyhow, bail};use collections::HashMap;use futures::future::BoxFuture;use futures::io::BufWriter;use futures::{AsyncWriteExt, FutureExt as _, select_biased};use git2::BranchType;use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, SharedString, Task};use parking_lot::Mutex;use rope::Rope;use schemars::JsonSchema;use serde::Deserialize;use smol::io::{AsyncBufReadExt, AsyncReadExt, BufReader};use std::borrow::Cow;use std::ffi::{OsStr, OsString};use std::process::{ExitStatus, Stdio};use std::{    cmp::Ordering,    future,    path::{Path, PathBuf},    sync::Arc,};use sum_tree::MapSeekTarget;use thiserror::Error;use util::command::new_smol_command;use util::paths::PathStyle;use util::rel_path::RelPath;use util::{ResultExt, paths};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 ref_name: SharedString,    pub upstream: Option<Upstream>,    pub most_recent_commit: Option<CommitSummary>,} impl Branch {    pub fn name(&self) -> &str {        self.ref_name            .as_ref()            .strip_prefix("refs/heads/")            .or_else(|| self.ref_name.as_ref().strip_prefix("refs/remotes/"))            .unwrap_or(self.ref_name.as_ref())    }     pub fn is_remote(&self) -> bool {        self.ref_name.starts_with("refs/remotes/")    }     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 Worktree {    pub path: PathBuf,    pub ref_name: SharedString,    pub sha: SharedString,} impl Worktree {    pub fn branch(&self) -> &str {        self.ref_name            .as_ref()            .strip_prefix("refs/heads/")            .or_else(|| self.ref_name.as_ref().strip_prefix("refs/remotes/"))            .unwrap_or(self.ref_name.as_ref())    }} pub fn parse_worktrees_from_str<T: AsRef<str>>(raw_worktrees: T) -> Vec<Worktree> {    let mut worktrees = Vec::new();    let entries = raw_worktrees.as_ref().split("\n\n");    for entry in entries {        let mut parts = entry.splitn(3, '\n');        let path = parts            .next()            .and_then(|p| p.split_once(' ').map(|(_, path)| path.to_string()));        let sha = parts            .next()            .and_then(|p| p.split_once(' ').map(|(_, sha)| sha.to_string()));        let ref_name = parts            .next()            .and_then(|p| p.split_once(' ').map(|(_, ref_name)| ref_name.to_string()));         if let (Some(path), Some(sha), Some(ref_name)) = (path, sha, ref_name) {            worktrees.push(Worktree {                path: PathBuf::from(path),                ref_name: ref_name.into(),                sha: sha.into(),            })        }    }     worktrees} #[derive(Clone, Debug, Hash, PartialEq, Eq)]pub struct Upstream {    pub ref_name: SharedString,    pub tracking: UpstreamTracking,} impl Upstream {    pub fn is_remote(&self) -> bool {        self.remote_name().is_some()    }     pub fn remote_name(&self) -> Option<&str> {        self.ref_name            .strip_prefix("refs/remotes/")            .and_then(|stripped| stripped.split("/").next())    }     pub fn stripped_ref_name(&self) -> Option<&str> {        self.ref_name.strip_prefix("refs/remotes/")    }} #[derive(Clone, Copy, Default)]pub struct CommitOptions {    pub amend: bool,    pub signoff: 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 author_name: SharedString,    pub has_parent: bool,} #[derive(Clone, Debug, Default, 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,} #[derive(Debug, Clone, Hash, PartialEq, Eq)]pub enum FetchOptions {    All,    Remote(Remote),} impl FetchOptions {    pub fn to_proto(&self) -> Option<String> {        match self {            FetchOptions::All => None,            FetchOptions::Remote(remote) => Some(remote.clone().name.into()),        }    }     pub fn from_proto(remote_name: Option<String>) -> Self {        match remote_name {            Some(name) => FetchOptions::Remote(Remote { name: name.into() }),            None => FetchOptions::All,        }    }     pub fn name(&self) -> SharedString {        match self {            Self::All => "Fetch all remotes".into(),            Self::Remote(remote) => remote.name.clone(),        }    }} impl std::fmt::Display for FetchOptions {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        match self {            FetchOptions::All => write!(f, "--all"),            FetchOptions::Remote(remote) => write!(f, "{}", remote.name),        }    }} /// Modifies .git/info/exclude temporarilypub struct GitExcludeOverride {    git_exclude_path: PathBuf,    original_excludes: Option<String>,    added_excludes: Option<String>,} impl GitExcludeOverride {    const START_BLOCK_MARKER: &str = "\n\n#  ====== Auto-added by Zed: =======\n";    const END_BLOCK_MARKER: &str = "\n#  ====== End of auto-added by Zed =======\n";     pub async fn new(git_exclude_path: PathBuf) -> Result<Self> {        let original_excludes =            smol::fs::read_to_string(&git_exclude_path)                .await                .ok()                .map(|content| {                    // Auto-generated lines are normally cleaned up in                    // `restore_original()` or `drop()`, but may stuck in rare cases.                    // Make sure to remove them.                    Self::remove_auto_generated_block(&content)                });         Ok(GitExcludeOverride {            git_exclude_path,            original_excludes,            added_excludes: None,        })    }     pub async fn add_excludes(&mut self, excludes: &str) -> Result<()> {        self.added_excludes = Some(if let Some(ref already_added) = self.added_excludes {            format!("{already_added}\n{excludes}")        } else {            excludes.to_string()        });         let mut content = self.original_excludes.clone().unwrap_or_default();         content.push_str(Self::START_BLOCK_MARKER);        content.push_str(self.added_excludes.as_ref().unwrap());        content.push_str(Self::END_BLOCK_MARKER);         smol::fs::write(&self.git_exclude_path, content).await?;        Ok(())    }     pub async fn restore_original(&mut self) -> Result<()> {        if let Some(ref original) = self.original_excludes {            smol::fs::write(&self.git_exclude_path, original).await?;        } else if self.git_exclude_path.exists() {            smol::fs::remove_file(&self.git_exclude_path).await?;        }         self.added_excludes = None;         Ok(())    }     fn remove_auto_generated_block(content: &str) -> String {        let start_marker = Self::START_BLOCK_MARKER;        let end_marker = Self::END_BLOCK_MARKER;        let mut content = content.to_string();         let start_index = content.find(start_marker);        let end_index = content.rfind(end_marker);         if let (Some(start), Some(end)) = (start_index, end_index) {            if end > start {                content.replace_range(start..end + end_marker.len(), "");            }        }         // Older versions of Zed didn't have end-of-block markers,        // so it's impossible to determine auto-generated lines.        // Conservatively remove the standard list of excludes        let standard_excludes = format!(            "{}{}",            Self::START_BLOCK_MARKER,            include_str!("./checkpoint.gitignore")        );        content = content.replace(&standard_excludes, "");         content    }} impl Drop for GitExcludeOverride {    fn drop(&mut self) {        if self.added_excludes.is_some() {            let git_exclude_path = self.git_exclude_path.clone();            let original_excludes = self.original_excludes.clone();            smol::spawn(async move {                if let Some(original) = original_excludes {                    smol::fs::write(&git_exclude_path, original).await                } else {                    smol::fs::remove_file(&git_exclude_path).await                }            })            .detach();        }    }} 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 load_blob_content(&self, oid: Oid) -> BoxFuture<'_, Result<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>;     /// Resolve a list of refs to SHAs.    fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>>;     fn head_sha(&self) -> BoxFuture<'_, Option<String>> {        async move {            self.revparse_batch(vec!["HEAD".into()])                .await                .unwrap_or_default()                .into_iter()                .next()                .flatten()        }        .boxed()    }     fn merge_message(&self) -> BoxFuture<'_, Option<String>>;     fn status(&self, path_prefixes: &[RepoPath]) -> Task<Result<GitStatus>>;    fn diff_tree(&self, request: DiffTreeType) -> BoxFuture<'_, Result<TreeDiff>>;     fn stash_entries(&self) -> BoxFuture<'_, Result<GitStash>>;     fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>>;     fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>>;    fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>>;    fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>>;     fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>>;     fn create_worktree(        &self,        name: String,        directory: PathBuf,        from_commit: Option<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 stash_paths(        &self,        paths: Vec<RepoPath>,        env: Arc<HashMap<String, String>>,    ) -> BoxFuture<'_, Result<()>>;

Lines 1–500 of 2731.