1215 lines · 37.3 KB · Rust
RawDownload
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.