repository.rs

   1use crate::commit::parse_git_diff_name_status;
   2use crate::status::{GitStatus, StatusCode};
   3use crate::{Oid, SHORT_SHA_LENGTH};
   4use anyhow::{Context as _, Result, anyhow};
   5use collections::HashMap;
   6use futures::future::BoxFuture;
   7use futures::{AsyncWriteExt, FutureExt as _, select_biased};
   8use git2::BranchType;
   9use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, SharedString};
  10use parking_lot::Mutex;
  11use rope::Rope;
  12use schemars::JsonSchema;
  13use serde::Deserialize;
  14use std::borrow::{Borrow, Cow};
  15use std::ffi::{OsStr, OsString};
  16use std::path::Component;
  17use std::process::{ExitStatus, Stdio};
  18use std::sync::LazyLock;
  19use std::{
  20    cmp::Ordering,
  21    path::{Path, PathBuf},
  22    sync::Arc,
  23};
  24use std::{
  25    future,
  26    io::{BufRead, BufReader, BufWriter, Read},
  27};
  28use sum_tree::MapSeekTarget;
  29use thiserror::Error;
  30use util::ResultExt;
  31use util::command::{new_smol_command, new_std_command};
  32use uuid::Uuid;
  33
  34pub use askpass::{AskPassDelegate, AskPassResult, AskPassSession};
  35
  36pub const REMOTE_CANCELLED_BY_USER: &str = "Operation cancelled by user";
  37
  38#[derive(Clone, Debug, Hash, PartialEq, Eq)]
  39pub struct Branch {
  40    pub is_head: bool,
  41    pub name: SharedString,
  42    pub upstream: Option<Upstream>,
  43    pub most_recent_commit: Option<CommitSummary>,
  44}
  45
  46impl Branch {
  47    pub fn tracking_status(&self) -> Option<UpstreamTrackingStatus> {
  48        self.upstream
  49            .as_ref()
  50            .and_then(|upstream| upstream.tracking.status())
  51    }
  52
  53    pub fn priority_key(&self) -> (bool, Option<i64>) {
  54        (
  55            self.is_head,
  56            self.most_recent_commit
  57                .as_ref()
  58                .map(|commit| commit.commit_timestamp),
  59        )
  60    }
  61}
  62
  63#[derive(Clone, Debug, Hash, PartialEq, Eq)]
  64pub struct Upstream {
  65    pub ref_name: SharedString,
  66    pub tracking: UpstreamTracking,
  67}
  68
  69impl Upstream {
  70    pub fn remote_name(&self) -> Option<&str> {
  71        self.ref_name
  72            .strip_prefix("refs/remotes/")
  73            .and_then(|stripped| stripped.split("/").next())
  74    }
  75}
  76
  77#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
  78pub enum UpstreamTracking {
  79    /// Remote ref not present in local repository.
  80    Gone,
  81    /// Remote ref present in local repository (fetched from remote).
  82    Tracked(UpstreamTrackingStatus),
  83}
  84
  85impl From<UpstreamTrackingStatus> for UpstreamTracking {
  86    fn from(status: UpstreamTrackingStatus) -> Self {
  87        UpstreamTracking::Tracked(status)
  88    }
  89}
  90
  91impl UpstreamTracking {
  92    pub fn is_gone(&self) -> bool {
  93        matches!(self, UpstreamTracking::Gone)
  94    }
  95
  96    pub fn status(&self) -> Option<UpstreamTrackingStatus> {
  97        match self {
  98            UpstreamTracking::Gone => None,
  99            UpstreamTracking::Tracked(status) => Some(*status),
 100        }
 101    }
 102}
 103
 104#[derive(Debug, Clone)]
 105pub struct RemoteCommandOutput {
 106    pub stdout: String,
 107    pub stderr: String,
 108}
 109
 110impl RemoteCommandOutput {
 111    pub fn is_empty(&self) -> bool {
 112        self.stdout.is_empty() && self.stderr.is_empty()
 113    }
 114}
 115
 116#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
 117pub struct UpstreamTrackingStatus {
 118    pub ahead: u32,
 119    pub behind: u32,
 120}
 121
 122#[derive(Clone, Debug, Hash, PartialEq, Eq)]
 123pub struct CommitSummary {
 124    pub sha: SharedString,
 125    pub subject: SharedString,
 126    /// This is a unix timestamp
 127    pub commit_timestamp: i64,
 128    pub has_parent: bool,
 129}
 130
 131#[derive(Clone, Debug, Hash, PartialEq, Eq)]
 132pub struct CommitDetails {
 133    pub sha: SharedString,
 134    pub message: SharedString,
 135    pub commit_timestamp: i64,
 136    pub author_email: SharedString,
 137    pub author_name: SharedString,
 138}
 139
 140#[derive(Debug)]
 141pub struct CommitDiff {
 142    pub files: Vec<CommitFile>,
 143}
 144
 145#[derive(Debug)]
 146pub struct CommitFile {
 147    pub path: RepoPath,
 148    pub old_text: Option<String>,
 149    pub new_text: Option<String>,
 150}
 151
 152impl CommitDetails {
 153    pub fn short_sha(&self) -> SharedString {
 154        self.sha[..SHORT_SHA_LENGTH].to_string().into()
 155    }
 156}
 157
 158#[derive(Debug, Clone, Hash, PartialEq, Eq)]
 159pub struct Remote {
 160    pub name: SharedString,
 161}
 162
 163pub enum ResetMode {
 164    /// Reset the branch pointer, leave index and worktree unchanged (this will make it look like things that were
 165    /// committed are now staged).
 166    Soft,
 167    /// Reset the branch pointer and index, leave worktree unchanged (this makes it look as though things that were
 168    /// committed are now unstaged).
 169    Mixed,
 170}
 171
 172pub trait GitRepository: Send + Sync {
 173    fn reload_index(&self);
 174
 175    /// Returns the contents of an entry in the repository's index, or None if there is no entry for the given path.
 176    ///
 177    /// Also returns `None` for symlinks.
 178    fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>>;
 179
 180    /// 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.
 181    ///
 182    /// Also returns `None` for symlinks.
 183    fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>>;
 184
 185    fn set_index_text(
 186        &self,
 187        path: RepoPath,
 188        content: Option<String>,
 189        env: Arc<HashMap<String, String>>,
 190    ) -> BoxFuture<anyhow::Result<()>>;
 191
 192    /// Returns the URL of the remote with the given name.
 193    fn remote_url(&self, name: &str) -> Option<String>;
 194
 195    /// Returns the SHA of the current HEAD.
 196    fn head_sha(&self) -> Option<String>;
 197
 198    fn merge_head_shas(&self) -> Vec<String>;
 199
 200    fn merge_message(&self) -> BoxFuture<Option<String>>;
 201
 202    fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>>;
 203
 204    fn branches(&self) -> BoxFuture<Result<Vec<Branch>>>;
 205
 206    fn change_branch(&self, name: String) -> BoxFuture<Result<()>>;
 207    fn create_branch(&self, name: String) -> BoxFuture<Result<()>>;
 208
 209    fn reset(
 210        &self,
 211        commit: String,
 212        mode: ResetMode,
 213        env: Arc<HashMap<String, String>>,
 214    ) -> BoxFuture<Result<()>>;
 215
 216    fn checkout_files(
 217        &self,
 218        commit: String,
 219        paths: Vec<RepoPath>,
 220        env: Arc<HashMap<String, String>>,
 221    ) -> BoxFuture<Result<()>>;
 222
 223    fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>>;
 224
 225    fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>>;
 226    fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<Result<crate::blame::Blame>>;
 227
 228    /// Returns the absolute path to the repository. For worktrees, this will be the path to the
 229    /// worktree's gitdir within the main repository (typically `.git/worktrees/<name>`).
 230    fn path(&self) -> PathBuf;
 231
 232    fn main_repository_path(&self) -> PathBuf;
 233
 234    /// Updates the index to match the worktree at the given paths.
 235    ///
 236    /// If any of the paths have been deleted from the worktree, they will be removed from the index if found there.
 237    fn stage_paths(
 238        &self,
 239        paths: Vec<RepoPath>,
 240        env: Arc<HashMap<String, String>>,
 241    ) -> BoxFuture<Result<()>>;
 242    /// Updates the index to match HEAD at the given paths.
 243    ///
 244    /// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index.
 245    fn unstage_paths(
 246        &self,
 247        paths: Vec<RepoPath>,
 248        env: Arc<HashMap<String, String>>,
 249    ) -> BoxFuture<Result<()>>;
 250
 251    fn commit(
 252        &self,
 253        message: SharedString,
 254        name_and_email: Option<(SharedString, SharedString)>,
 255        env: Arc<HashMap<String, String>>,
 256    ) -> BoxFuture<Result<()>>;
 257
 258    fn push(
 259        &self,
 260        branch_name: String,
 261        upstream_name: String,
 262        options: Option<PushOptions>,
 263        askpass: AskPassDelegate,
 264        env: Arc<HashMap<String, String>>,
 265        // This method takes an AsyncApp to ensure it's invoked on the main thread,
 266        // otherwise git-credentials-manager won't work.
 267        cx: AsyncApp,
 268    ) -> BoxFuture<Result<RemoteCommandOutput>>;
 269
 270    fn pull(
 271        &self,
 272        branch_name: String,
 273        upstream_name: String,
 274        askpass: AskPassDelegate,
 275        env: Arc<HashMap<String, String>>,
 276        // This method takes an AsyncApp to ensure it's invoked on the main thread,
 277        // otherwise git-credentials-manager won't work.
 278        cx: AsyncApp,
 279    ) -> BoxFuture<Result<RemoteCommandOutput>>;
 280
 281    fn fetch(
 282        &self,
 283        askpass: AskPassDelegate,
 284        env: Arc<HashMap<String, String>>,
 285        // This method takes an AsyncApp to ensure it's invoked on the main thread,
 286        // otherwise git-credentials-manager won't work.
 287        cx: AsyncApp,
 288    ) -> BoxFuture<Result<RemoteCommandOutput>>;
 289
 290    fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<Result<Vec<Remote>>>;
 291
 292    /// returns a list of remote branches that contain HEAD
 293    fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<SharedString>>>;
 294
 295    /// Run git diff
 296    fn diff(&self, diff: DiffType) -> BoxFuture<Result<String>>;
 297
 298    /// Creates a checkpoint for the repository.
 299    fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>>;
 300
 301    /// Resets to a previously-created checkpoint.
 302    fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>>;
 303
 304    /// Compares two checkpoints, returning true if they are equal
 305    fn compare_checkpoints(
 306        &self,
 307        left: GitRepositoryCheckpoint,
 308        right: GitRepositoryCheckpoint,
 309    ) -> BoxFuture<Result<bool>>;
 310
 311    /// Deletes a previously-created checkpoint.
 312    fn delete_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>>;
 313
 314    /// Computes a diff between two checkpoints.
 315    fn diff_checkpoints(
 316        &self,
 317        base_checkpoint: GitRepositoryCheckpoint,
 318        target_checkpoint: GitRepositoryCheckpoint,
 319    ) -> BoxFuture<Result<String>>;
 320}
 321
 322pub enum DiffType {
 323    HeadToIndex,
 324    HeadToWorktree,
 325}
 326
 327#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
 328pub enum PushOptions {
 329    SetUpstream,
 330    Force,
 331}
 332
 333impl std::fmt::Debug for dyn GitRepository {
 334    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 335        f.debug_struct("dyn GitRepository<...>").finish()
 336    }
 337}
 338
 339pub struct RealGitRepository {
 340    pub repository: Arc<Mutex<git2::Repository>>,
 341    pub git_binary_path: PathBuf,
 342    executor: BackgroundExecutor,
 343}
 344
 345impl RealGitRepository {
 346    pub fn new(
 347        dotgit_path: &Path,
 348        git_binary_path: Option<PathBuf>,
 349        executor: BackgroundExecutor,
 350    ) -> Option<Self> {
 351        let workdir_root = dotgit_path.parent()?;
 352        let repository = git2::Repository::open(workdir_root).log_err()?;
 353        Some(Self {
 354            repository: Arc::new(Mutex::new(repository)),
 355            git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
 356            executor,
 357        })
 358    }
 359
 360    fn working_directory(&self) -> Result<PathBuf> {
 361        self.repository
 362            .lock()
 363            .workdir()
 364            .context("failed to read git work directory")
 365            .map(Path::to_path_buf)
 366    }
 367}
 368
 369#[derive(Clone, Debug)]
 370pub struct GitRepositoryCheckpoint {
 371    ref_name: String,
 372    commit_sha: Oid,
 373}
 374
 375impl GitRepository for RealGitRepository {
 376    fn reload_index(&self) {
 377        if let Ok(mut index) = self.repository.lock().index() {
 378            _ = index.read(false);
 379        }
 380    }
 381
 382    fn path(&self) -> PathBuf {
 383        let repo = self.repository.lock();
 384        repo.path().into()
 385    }
 386
 387    fn main_repository_path(&self) -> PathBuf {
 388        let repo = self.repository.lock();
 389        repo.commondir().into()
 390    }
 391
 392    fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>> {
 393        let repo = self.repository.clone();
 394        self.executor
 395            .spawn(async move {
 396                let repo = repo.lock();
 397                let Ok(commit) = repo.revparse_single(&commit)?.into_commit() else {
 398                    anyhow::bail!("{} is not a commit", commit);
 399                };
 400                let details = CommitDetails {
 401                    sha: commit.id().to_string().into(),
 402                    message: String::from_utf8_lossy(commit.message_raw_bytes())
 403                        .to_string()
 404                        .into(),
 405                    commit_timestamp: commit.time().seconds(),
 406                    author_email: String::from_utf8_lossy(commit.author().email_bytes())
 407                        .to_string()
 408                        .into(),
 409                    author_name: String::from_utf8_lossy(commit.author().name_bytes())
 410                        .to_string()
 411                        .into(),
 412                };
 413                Ok(details)
 414            })
 415            .boxed()
 416    }
 417
 418    fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>> {
 419        let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned)
 420        else {
 421            return future::ready(Err(anyhow!("no working directory"))).boxed();
 422        };
 423        cx.background_spawn(async move {
 424            let show_output = util::command::new_std_command("git")
 425                .current_dir(&working_directory)
 426                .args([
 427                    "--no-optional-locks",
 428                    "show",
 429                    "--format=%P",
 430                    "-z",
 431                    "--no-renames",
 432                    "--name-status",
 433                ])
 434                .arg(&commit)
 435                .stdin(Stdio::null())
 436                .stdout(Stdio::piped())
 437                .stderr(Stdio::piped())
 438                .output()
 439                .map_err(|e| anyhow!("Failed to start git show process: {e}"))?;
 440
 441            let show_stdout = String::from_utf8_lossy(&show_output.stdout);
 442            let mut lines = show_stdout.split('\n');
 443            let parent_sha = lines.next().unwrap().trim().trim_end_matches('\0');
 444            let changes = parse_git_diff_name_status(lines.next().unwrap_or(""));
 445
 446            let mut cat_file_process = util::command::new_std_command("git")
 447                .current_dir(&working_directory)
 448                .args(["--no-optional-locks", "cat-file", "--batch=%(objectsize)"])
 449                .stdin(Stdio::piped())
 450                .stdout(Stdio::piped())
 451                .stderr(Stdio::piped())
 452                .spawn()
 453                .map_err(|e| anyhow!("Failed to start git cat-file process: {e}"))?;
 454
 455            use std::io::Write as _;
 456            let mut files = Vec::<CommitFile>::new();
 457            let mut stdin = BufWriter::with_capacity(512, cat_file_process.stdin.take().unwrap());
 458            let mut stdout = BufReader::new(cat_file_process.stdout.take().unwrap());
 459            let mut info_line = String::new();
 460            let mut newline = [b'\0'];
 461            for (path, status_code) in changes {
 462                match status_code {
 463                    StatusCode::Modified => {
 464                        writeln!(&mut stdin, "{commit}:{}", path.display())?;
 465                        writeln!(&mut stdin, "{parent_sha}:{}", path.display())?;
 466                    }
 467                    StatusCode::Added => {
 468                        writeln!(&mut stdin, "{commit}:{}", path.display())?;
 469                    }
 470                    StatusCode::Deleted => {
 471                        writeln!(&mut stdin, "{parent_sha}:{}", path.display())?;
 472                    }
 473                    _ => continue,
 474                }
 475                stdin.flush()?;
 476
 477                info_line.clear();
 478                stdout.read_line(&mut info_line)?;
 479
 480                let len = info_line.trim_end().parse().with_context(|| {
 481                    format!("invalid object size output from cat-file {info_line}")
 482                })?;
 483                let mut text = vec![0; len];
 484                stdout.read_exact(&mut text)?;
 485                stdout.read_exact(&mut newline)?;
 486                let text = String::from_utf8_lossy(&text).to_string();
 487
 488                let mut old_text = None;
 489                let mut new_text = None;
 490                match status_code {
 491                    StatusCode::Modified => {
 492                        info_line.clear();
 493                        stdout.read_line(&mut info_line)?;
 494                        let len = info_line.trim_end().parse().with_context(|| {
 495                            format!("invalid object size output from cat-file {}", info_line)
 496                        })?;
 497                        let mut parent_text = vec![0; len];
 498                        stdout.read_exact(&mut parent_text)?;
 499                        stdout.read_exact(&mut newline)?;
 500                        old_text = Some(String::from_utf8_lossy(&parent_text).to_string());
 501                        new_text = Some(text);
 502                    }
 503                    StatusCode::Added => new_text = Some(text),
 504                    StatusCode::Deleted => old_text = Some(text),
 505                    _ => continue,
 506                }
 507
 508                files.push(CommitFile {
 509                    path: path.into(),
 510                    old_text,
 511                    new_text,
 512                })
 513            }
 514
 515            Ok(CommitDiff { files })
 516        })
 517        .boxed()
 518    }
 519
 520    fn reset(
 521        &self,
 522        commit: String,
 523        mode: ResetMode,
 524        env: Arc<HashMap<String, String>>,
 525    ) -> BoxFuture<Result<()>> {
 526        async move {
 527            let working_directory = self.working_directory();
 528
 529            let mode_flag = match mode {
 530                ResetMode::Mixed => "--mixed",
 531                ResetMode::Soft => "--soft",
 532            };
 533
 534            let output = new_smol_command(&self.git_binary_path)
 535                .envs(env.iter())
 536                .current_dir(&working_directory?)
 537                .args(["reset", mode_flag, &commit])
 538                .output()
 539                .await?;
 540            if !output.status.success() {
 541                return Err(anyhow!(
 542                    "Failed to reset:\n{}",
 543                    String::from_utf8_lossy(&output.stderr)
 544                ));
 545            }
 546            Ok(())
 547        }
 548        .boxed()
 549    }
 550
 551    fn checkout_files(
 552        &self,
 553        commit: String,
 554        paths: Vec<RepoPath>,
 555        env: Arc<HashMap<String, String>>,
 556    ) -> BoxFuture<Result<()>> {
 557        let working_directory = self.working_directory();
 558        let git_binary_path = self.git_binary_path.clone();
 559        async move {
 560            if paths.is_empty() {
 561                return Ok(());
 562            }
 563
 564            let output = new_smol_command(&git_binary_path)
 565                .current_dir(&working_directory?)
 566                .envs(env.iter())
 567                .args(["checkout", &commit, "--"])
 568                .args(paths.iter().map(|path| path.as_ref()))
 569                .output()
 570                .await?;
 571            if !output.status.success() {
 572                return Err(anyhow!(
 573                    "Failed to checkout files:\n{}",
 574                    String::from_utf8_lossy(&output.stderr)
 575                ));
 576            }
 577            Ok(())
 578        }
 579        .boxed()
 580    }
 581
 582    fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
 583        // https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
 584        const GIT_MODE_SYMLINK: u32 = 0o120000;
 585
 586        let repo = self.repository.clone();
 587        self.executor
 588            .spawn(async move {
 589                fn logic(repo: &git2::Repository, path: &RepoPath) -> Result<Option<String>> {
 590                    // This check is required because index.get_path() unwraps internally :(
 591                    check_path_to_repo_path_errors(path)?;
 592
 593                    let mut index = repo.index()?;
 594                    index.read(false)?;
 595
 596                    const STAGE_NORMAL: i32 = 0;
 597                    let oid = match index.get_path(path, STAGE_NORMAL) {
 598                        Some(entry) if entry.mode != GIT_MODE_SYMLINK => entry.id,
 599                        _ => return Ok(None),
 600                    };
 601
 602                    let content = repo.find_blob(oid)?.content().to_owned();
 603                    Ok(String::from_utf8(content).ok())
 604                }
 605
 606                match logic(&repo.lock(), &path) {
 607                    Ok(value) => return value,
 608                    Err(err) => log::error!("Error loading index text: {:?}", err),
 609                }
 610                None
 611            })
 612            .boxed()
 613    }
 614
 615    fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
 616        let repo = self.repository.clone();
 617        self.executor
 618            .spawn(async move {
 619                let repo = repo.lock();
 620                let head = repo.head().ok()?.peel_to_tree().log_err()?;
 621                let entry = head.get_path(&path).ok()?;
 622                if entry.filemode() == i32::from(git2::FileMode::Link) {
 623                    return None;
 624                }
 625                let content = repo.find_blob(entry.id()).log_err()?.content().to_owned();
 626                String::from_utf8(content).ok()
 627            })
 628            .boxed()
 629    }
 630
 631    fn set_index_text(
 632        &self,
 633        path: RepoPath,
 634        content: Option<String>,
 635        env: Arc<HashMap<String, String>>,
 636    ) -> BoxFuture<anyhow::Result<()>> {
 637        let working_directory = self.working_directory();
 638        let git_binary_path = self.git_binary_path.clone();
 639        self.executor
 640            .spawn(async move {
 641                let working_directory = working_directory?;
 642                if let Some(content) = content {
 643                    let mut child = new_smol_command(&git_binary_path)
 644                        .current_dir(&working_directory)
 645                        .envs(env.iter())
 646                        .args(["hash-object", "-w", "--stdin"])
 647                        .stdin(Stdio::piped())
 648                        .stdout(Stdio::piped())
 649                        .spawn()?;
 650                    child
 651                        .stdin
 652                        .take()
 653                        .unwrap()
 654                        .write_all(content.as_bytes())
 655                        .await?;
 656                    let output = child.output().await?.stdout;
 657                    let sha = String::from_utf8(output)?;
 658
 659                    log::debug!("indexing SHA: {sha}, path {path:?}");
 660
 661                    let output = new_smol_command(&git_binary_path)
 662                        .current_dir(&working_directory)
 663                        .envs(env.iter())
 664                        .args(["update-index", "--add", "--cacheinfo", "100644", &sha])
 665                        .arg(path.to_unix_style())
 666                        .output()
 667                        .await?;
 668
 669                    if !output.status.success() {
 670                        return Err(anyhow!(
 671                            "Failed to stage:\n{}",
 672                            String::from_utf8_lossy(&output.stderr)
 673                        ));
 674                    }
 675                } else {
 676                    let output = new_smol_command(&git_binary_path)
 677                        .current_dir(&working_directory)
 678                        .envs(env.iter())
 679                        .args(["update-index", "--force-remove"])
 680                        .arg(path.to_unix_style())
 681                        .output()
 682                        .await?;
 683
 684                    if !output.status.success() {
 685                        return Err(anyhow!(
 686                            "Failed to unstage:\n{}",
 687                            String::from_utf8_lossy(&output.stderr)
 688                        ));
 689                    }
 690                }
 691
 692                Ok(())
 693            })
 694            .boxed()
 695    }
 696
 697    fn remote_url(&self, name: &str) -> Option<String> {
 698        let repo = self.repository.lock();
 699        let remote = repo.find_remote(name).ok()?;
 700        remote.url().map(|url| url.to_string())
 701    }
 702
 703    fn head_sha(&self) -> Option<String> {
 704        Some(self.repository.lock().head().ok()?.target()?.to_string())
 705    }
 706
 707    fn merge_head_shas(&self) -> Vec<String> {
 708        let mut shas = Vec::default();
 709        self.repository
 710            .lock()
 711            .mergehead_foreach(|oid| {
 712                shas.push(oid.to_string());
 713                true
 714            })
 715            .ok();
 716        if let Some(oid) = self
 717            .repository
 718            .lock()
 719            .find_reference("CHERRY_PICK_HEAD")
 720            .ok()
 721            .and_then(|reference| reference.target())
 722        {
 723            shas.push(oid.to_string())
 724        }
 725        shas
 726    }
 727
 728    fn merge_message(&self) -> BoxFuture<Option<String>> {
 729        let path = self.path().join("MERGE_MSG");
 730        async move { std::fs::read_to_string(&path).ok() }.boxed()
 731    }
 732
 733    fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>> {
 734        let git_binary_path = self.git_binary_path.clone();
 735        let working_directory = self.working_directory();
 736        let path_prefixes = path_prefixes.to_owned();
 737        self.executor
 738            .spawn(async move {
 739                let output = new_std_command(&git_binary_path)
 740                    .current_dir(working_directory?)
 741                    .args(git_status_args(&path_prefixes))
 742                    .output()?;
 743                if output.status.success() {
 744                    let stdout = String::from_utf8_lossy(&output.stdout);
 745                    stdout.parse()
 746                } else {
 747                    let stderr = String::from_utf8_lossy(&output.stderr);
 748                    Err(anyhow!("git status failed: {}", stderr))
 749                }
 750            })
 751            .boxed()
 752    }
 753
 754    fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
 755        let working_directory = self.working_directory();
 756        let git_binary_path = self.git_binary_path.clone();
 757        async move {
 758            let fields = [
 759                "%(HEAD)",
 760                "%(objectname)",
 761                "%(parent)",
 762                "%(refname)",
 763                "%(upstream)",
 764                "%(upstream:track)",
 765                "%(committerdate:unix)",
 766                "%(contents:subject)",
 767            ]
 768            .join("%00");
 769            let args = vec!["for-each-ref", "refs/heads/**/*", "--format", &fields];
 770            let working_directory = working_directory?;
 771            let output = new_smol_command(&git_binary_path)
 772                .current_dir(&working_directory)
 773                .args(args)
 774                .output()
 775                .await?;
 776
 777            if !output.status.success() {
 778                return Err(anyhow!(
 779                    "Failed to git git branches:\n{}",
 780                    String::from_utf8_lossy(&output.stderr)
 781                ));
 782            }
 783
 784            let input = String::from_utf8_lossy(&output.stdout);
 785
 786            let mut branches = parse_branch_input(&input)?;
 787            if branches.is_empty() {
 788                let args = vec!["symbolic-ref", "--quiet", "--short", "HEAD"];
 789
 790                let output = new_smol_command(&git_binary_path)
 791                    .current_dir(&working_directory)
 792                    .args(args)
 793                    .output()
 794                    .await?;
 795
 796                // git symbolic-ref returns a non-0 exit code if HEAD points
 797                // to something other than a branch
 798                if output.status.success() {
 799                    let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
 800
 801                    branches.push(Branch {
 802                        name: name.into(),
 803                        is_head: true,
 804                        upstream: None,
 805                        most_recent_commit: None,
 806                    });
 807                }
 808            }
 809
 810            Ok(branches)
 811        }
 812        .boxed()
 813    }
 814
 815    fn change_branch(&self, name: String) -> BoxFuture<Result<()>> {
 816        let repo = self.repository.clone();
 817        self.executor
 818            .spawn(async move {
 819                let repo = repo.lock();
 820                let revision = repo.find_branch(&name, BranchType::Local)?;
 821                let revision = revision.get();
 822                let as_tree = revision.peel_to_tree()?;
 823                repo.checkout_tree(as_tree.as_object(), None)?;
 824                repo.set_head(
 825                    revision
 826                        .name()
 827                        .ok_or_else(|| anyhow!("Branch name could not be retrieved"))?,
 828                )?;
 829                Ok(())
 830            })
 831            .boxed()
 832    }
 833
 834    fn create_branch(&self, name: String) -> BoxFuture<Result<()>> {
 835        let repo = self.repository.clone();
 836        self.executor
 837            .spawn(async move {
 838                let repo = repo.lock();
 839                let current_commit = repo.head()?.peel_to_commit()?;
 840                repo.branch(&name, &current_commit, false)?;
 841                Ok(())
 842            })
 843            .boxed()
 844    }
 845
 846    fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<Result<crate::blame::Blame>> {
 847        let working_directory = self.working_directory();
 848        let git_binary_path = self.git_binary_path.clone();
 849
 850        const REMOTE_NAME: &str = "origin";
 851        let remote_url = self.remote_url(REMOTE_NAME);
 852
 853        self.executor
 854            .spawn(async move {
 855                crate::blame::Blame::for_path(
 856                    &git_binary_path,
 857                    &working_directory?,
 858                    &path,
 859                    &content,
 860                    remote_url,
 861                )
 862                .await
 863            })
 864            .boxed()
 865    }
 866
 867    fn diff(&self, diff: DiffType) -> BoxFuture<Result<String>> {
 868        let working_directory = self.working_directory();
 869        let git_binary_path = self.git_binary_path.clone();
 870        self.executor
 871            .spawn(async move {
 872                let args = match diff {
 873                    DiffType::HeadToIndex => Some("--staged"),
 874                    DiffType::HeadToWorktree => None,
 875                };
 876
 877                let output = new_smol_command(&git_binary_path)
 878                    .current_dir(&working_directory?)
 879                    .args(["diff"])
 880                    .args(args)
 881                    .output()
 882                    .await?;
 883
 884                if !output.status.success() {
 885                    return Err(anyhow!(
 886                        "Failed to run git diff:\n{}",
 887                        String::from_utf8_lossy(&output.stderr)
 888                    ));
 889                }
 890                Ok(String::from_utf8_lossy(&output.stdout).to_string())
 891            })
 892            .boxed()
 893    }
 894
 895    fn stage_paths(
 896        &self,
 897        paths: Vec<RepoPath>,
 898        env: Arc<HashMap<String, String>>,
 899    ) -> BoxFuture<Result<()>> {
 900        let working_directory = self.working_directory();
 901        let git_binary_path = self.git_binary_path.clone();
 902        self.executor
 903            .spawn(async move {
 904                if !paths.is_empty() {
 905                    let output = new_smol_command(&git_binary_path)
 906                        .current_dir(&working_directory?)
 907                        .envs(env.iter())
 908                        .args(["update-index", "--add", "--remove", "--"])
 909                        .args(paths.iter().map(|p| p.to_unix_style()))
 910                        .output()
 911                        .await?;
 912
 913                    if !output.status.success() {
 914                        return Err(anyhow!(
 915                            "Failed to stage paths:\n{}",
 916                            String::from_utf8_lossy(&output.stderr)
 917                        ));
 918                    }
 919                }
 920                Ok(())
 921            })
 922            .boxed()
 923    }
 924
 925    fn unstage_paths(
 926        &self,
 927        paths: Vec<RepoPath>,
 928        env: Arc<HashMap<String, String>>,
 929    ) -> BoxFuture<Result<()>> {
 930        let working_directory = self.working_directory();
 931        let git_binary_path = self.git_binary_path.clone();
 932
 933        self.executor
 934            .spawn(async move {
 935                if !paths.is_empty() {
 936                    let output = new_smol_command(&git_binary_path)
 937                        .current_dir(&working_directory?)
 938                        .envs(env.iter())
 939                        .args(["reset", "--quiet", "--"])
 940                        .args(paths.iter().map(|p| p.as_ref()))
 941                        .output()
 942                        .await?;
 943
 944                    if !output.status.success() {
 945                        return Err(anyhow!(
 946                            "Failed to unstage:\n{}",
 947                            String::from_utf8_lossy(&output.stderr)
 948                        ));
 949                    }
 950                }
 951                Ok(())
 952            })
 953            .boxed()
 954    }
 955
 956    fn commit(
 957        &self,
 958        message: SharedString,
 959        name_and_email: Option<(SharedString, SharedString)>,
 960        env: Arc<HashMap<String, String>>,
 961    ) -> BoxFuture<Result<()>> {
 962        let working_directory = self.working_directory();
 963        self.executor
 964            .spawn(async move {
 965                let mut cmd = new_smol_command("git");
 966                cmd.current_dir(&working_directory?)
 967                    .envs(env.iter())
 968                    .args(["commit", "--quiet", "-m"])
 969                    .arg(&message.to_string())
 970                    .arg("--cleanup=strip");
 971
 972                if let Some((name, email)) = name_and_email {
 973                    cmd.arg("--author").arg(&format!("{name} <{email}>"));
 974                }
 975
 976                let output = cmd.output().await?;
 977
 978                if !output.status.success() {
 979                    return Err(anyhow!(
 980                        "Failed to commit:\n{}",
 981                        String::from_utf8_lossy(&output.stderr)
 982                    ));
 983                }
 984                Ok(())
 985            })
 986            .boxed()
 987    }
 988
 989    fn push(
 990        &self,
 991        branch_name: String,
 992        remote_name: String,
 993        options: Option<PushOptions>,
 994        ask_pass: AskPassDelegate,
 995        env: Arc<HashMap<String, String>>,
 996        cx: AsyncApp,
 997    ) -> BoxFuture<Result<RemoteCommandOutput>> {
 998        let working_directory = self.working_directory();
 999        let executor = cx.background_executor().clone();
1000        async move {
1001            let working_directory = working_directory?;
1002            let mut command = new_smol_command("git");
1003            command
1004                .envs(env.iter())
1005                .env("GIT_HTTP_USER_AGENT", "Zed")
1006                .current_dir(&working_directory)
1007                .args(["push"])
1008                .args(options.map(|option| match option {
1009                    PushOptions::SetUpstream => "--set-upstream",
1010                    PushOptions::Force => "--force-with-lease",
1011                }))
1012                .arg(remote_name)
1013                .arg(format!("{}:{}", branch_name, branch_name))
1014                .stdin(smol::process::Stdio::null())
1015                .stdout(smol::process::Stdio::piped())
1016                .stderr(smol::process::Stdio::piped());
1017
1018            run_git_command(env, ask_pass, command, &executor).await
1019        }
1020        .boxed()
1021    }
1022
1023    fn pull(
1024        &self,
1025        branch_name: String,
1026        remote_name: String,
1027        ask_pass: AskPassDelegate,
1028        env: Arc<HashMap<String, String>>,
1029        cx: AsyncApp,
1030    ) -> BoxFuture<Result<RemoteCommandOutput>> {
1031        let working_directory = self.working_directory();
1032        let executor = cx.background_executor().clone();
1033        async move {
1034            let mut command = new_smol_command("git");
1035            command
1036                .envs(env.iter())
1037                .env("GIT_HTTP_USER_AGENT", "Zed")
1038                .current_dir(&working_directory?)
1039                .args(["pull"])
1040                .arg(remote_name)
1041                .arg(branch_name)
1042                .stdout(smol::process::Stdio::piped())
1043                .stderr(smol::process::Stdio::piped());
1044
1045            run_git_command(env, ask_pass, command, &executor).await
1046        }
1047        .boxed()
1048    }
1049
1050    fn fetch(
1051        &self,
1052        ask_pass: AskPassDelegate,
1053        env: Arc<HashMap<String, String>>,
1054        cx: AsyncApp,
1055    ) -> BoxFuture<Result<RemoteCommandOutput>> {
1056        let working_directory = self.working_directory();
1057        let executor = cx.background_executor().clone();
1058        async move {
1059            let mut command = new_smol_command("git");
1060            command
1061                .envs(env.iter())
1062                .env("GIT_HTTP_USER_AGENT", "Zed")
1063                .current_dir(&working_directory?)
1064                .args(["fetch", "--all"])
1065                .stdout(smol::process::Stdio::piped())
1066                .stderr(smol::process::Stdio::piped());
1067
1068            run_git_command(env, ask_pass, command, &executor).await
1069        }
1070        .boxed()
1071    }
1072
1073    fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<Result<Vec<Remote>>> {
1074        let working_directory = self.working_directory();
1075        let git_binary_path = self.git_binary_path.clone();
1076        self.executor
1077            .spawn(async move {
1078                let working_directory = working_directory?;
1079                if let Some(branch_name) = branch_name {
1080                    let output = new_smol_command(&git_binary_path)
1081                        .current_dir(&working_directory)
1082                        .args(["config", "--get"])
1083                        .arg(format!("branch.{}.remote", branch_name))
1084                        .output()
1085                        .await?;
1086
1087                    if output.status.success() {
1088                        let remote_name = String::from_utf8_lossy(&output.stdout);
1089
1090                        return Ok(vec![Remote {
1091                            name: remote_name.trim().to_string().into(),
1092                        }]);
1093                    }
1094                }
1095
1096                let output = new_smol_command(&git_binary_path)
1097                    .current_dir(&working_directory)
1098                    .args(["remote"])
1099                    .output()
1100                    .await?;
1101
1102                if output.status.success() {
1103                    let remote_names = String::from_utf8_lossy(&output.stdout)
1104                        .split('\n')
1105                        .filter(|name| !name.is_empty())
1106                        .map(|name| Remote {
1107                            name: name.trim().to_string().into(),
1108                        })
1109                        .collect();
1110
1111                    return Ok(remote_names);
1112                } else {
1113                    return Err(anyhow!(
1114                        "Failed to get remotes:\n{}",
1115                        String::from_utf8_lossy(&output.stderr)
1116                    ));
1117                }
1118            })
1119            .boxed()
1120    }
1121
1122    fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<SharedString>>> {
1123        let working_directory = self.working_directory();
1124        let git_binary_path = self.git_binary_path.clone();
1125        self.executor
1126            .spawn(async move {
1127                let working_directory = working_directory?;
1128                let git_cmd = async |args: &[&str]| -> Result<String> {
1129                    let output = new_smol_command(&git_binary_path)
1130                        .current_dir(&working_directory)
1131                        .args(args)
1132                        .output()
1133                        .await?;
1134                    if output.status.success() {
1135                        Ok(String::from_utf8(output.stdout)?)
1136                    } else {
1137                        Err(anyhow!(String::from_utf8_lossy(&output.stderr).to_string()))
1138                    }
1139                };
1140
1141                let head = git_cmd(&["rev-parse", "HEAD"])
1142                    .await
1143                    .context("Failed to get HEAD")?
1144                    .trim()
1145                    .to_owned();
1146
1147                let mut remote_branches = vec![];
1148                let mut add_if_matching = async |remote_head: &str| {
1149                    if let Ok(merge_base) = git_cmd(&["merge-base", &head, remote_head]).await {
1150                        if merge_base.trim() == head {
1151                            if let Some(s) = remote_head.strip_prefix("refs/remotes/") {
1152                                remote_branches.push(s.to_owned().into());
1153                            }
1154                        }
1155                    }
1156                };
1157
1158                // check the main branch of each remote
1159                let remotes = git_cmd(&["remote"])
1160                    .await
1161                    .context("Failed to get remotes")?;
1162                for remote in remotes.lines() {
1163                    if let Ok(remote_head) =
1164                        git_cmd(&["symbolic-ref", &format!("refs/remotes/{remote}/HEAD")]).await
1165                    {
1166                        add_if_matching(remote_head.trim()).await;
1167                    }
1168                }
1169
1170                // ... and the remote branch that the checked-out one is tracking
1171                if let Ok(remote_head) =
1172                    git_cmd(&["rev-parse", "--symbolic-full-name", "@{u}"]).await
1173                {
1174                    add_if_matching(remote_head.trim()).await;
1175                }
1176
1177                Ok(remote_branches)
1178            })
1179            .boxed()
1180    }
1181
1182    fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
1183        let working_directory = self.working_directory();
1184        let git_binary_path = self.git_binary_path.clone();
1185        let executor = self.executor.clone();
1186        self.executor
1187            .spawn(async move {
1188                let working_directory = working_directory?;
1189                let mut git = GitBinary::new(git_binary_path, working_directory, executor)
1190                    .envs(checkpoint_author_envs());
1191                git.with_temp_index(async |git| {
1192                    let head_sha = git.run(&["rev-parse", "HEAD"]).await.ok();
1193                    git.run(&["add", "--all"]).await?;
1194                    let tree = git.run(&["write-tree"]).await?;
1195                    let checkpoint_sha = if let Some(head_sha) = head_sha.as_deref() {
1196                        git.run(&["commit-tree", &tree, "-p", head_sha, "-m", "Checkpoint"])
1197                            .await?
1198                    } else {
1199                        git.run(&["commit-tree", &tree, "-m", "Checkpoint"]).await?
1200                    };
1201                    let ref_name = format!("refs/zed/{}", Uuid::new_v4());
1202                    git.run(&["update-ref", &ref_name, &checkpoint_sha]).await?;
1203
1204                    Ok(GitRepositoryCheckpoint {
1205                        ref_name,
1206                        commit_sha: checkpoint_sha.parse()?,
1207                    })
1208                })
1209                .await
1210            })
1211            .boxed()
1212    }
1213
1214    fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
1215        let working_directory = self.working_directory();
1216        let git_binary_path = self.git_binary_path.clone();
1217
1218        let executor = self.executor.clone();
1219        self.executor
1220            .spawn(async move {
1221                let working_directory = working_directory?;
1222
1223                let mut git = GitBinary::new(git_binary_path, working_directory, executor);
1224                git.run(&[
1225                    "restore",
1226                    "--source",
1227                    &checkpoint.commit_sha.to_string(),
1228                    "--worktree",
1229                    ".",
1230                ])
1231                .await?;
1232
1233                git.with_temp_index(async move |git| {
1234                    git.run(&["read-tree", &checkpoint.commit_sha.to_string()])
1235                        .await?;
1236                    git.run(&["clean", "-d", "--force"]).await
1237                })
1238                .await?;
1239
1240                Ok(())
1241            })
1242            .boxed()
1243    }
1244
1245    fn compare_checkpoints(
1246        &self,
1247        left: GitRepositoryCheckpoint,
1248        right: GitRepositoryCheckpoint,
1249    ) -> BoxFuture<Result<bool>> {
1250        let working_directory = self.working_directory();
1251        let git_binary_path = self.git_binary_path.clone();
1252
1253        let executor = self.executor.clone();
1254        self.executor
1255            .spawn(async move {
1256                let working_directory = working_directory?;
1257                let git = GitBinary::new(git_binary_path, working_directory, executor);
1258                let result = git
1259                    .run(&[
1260                        "diff-tree",
1261                        "--quiet",
1262                        &left.commit_sha.to_string(),
1263                        &right.commit_sha.to_string(),
1264                    ])
1265                    .await;
1266                match result {
1267                    Ok(_) => Ok(true),
1268                    Err(error) => {
1269                        if let Some(GitBinaryCommandError { status, .. }) =
1270                            error.downcast_ref::<GitBinaryCommandError>()
1271                        {
1272                            if status.code() == Some(1) {
1273                                return Ok(false);
1274                            }
1275                        }
1276
1277                        Err(error)
1278                    }
1279                }
1280            })
1281            .boxed()
1282    }
1283
1284    fn delete_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
1285        let working_directory = self.working_directory();
1286        let git_binary_path = self.git_binary_path.clone();
1287
1288        let executor = self.executor.clone();
1289        self.executor
1290            .spawn(async move {
1291                let working_directory = working_directory?;
1292                let git = GitBinary::new(git_binary_path, working_directory, executor);
1293                git.run(&["update-ref", "-d", &checkpoint.ref_name]).await?;
1294                Ok(())
1295            })
1296            .boxed()
1297    }
1298
1299    fn diff_checkpoints(
1300        &self,
1301        base_checkpoint: GitRepositoryCheckpoint,
1302        target_checkpoint: GitRepositoryCheckpoint,
1303    ) -> BoxFuture<Result<String>> {
1304        let working_directory = self.working_directory();
1305        let git_binary_path = self.git_binary_path.clone();
1306
1307        let executor = self.executor.clone();
1308        self.executor
1309            .spawn(async move {
1310                let working_directory = working_directory?;
1311                let git = GitBinary::new(git_binary_path, working_directory, executor);
1312                git.run(&[
1313                    "diff",
1314                    "--find-renames",
1315                    "--patch",
1316                    &base_checkpoint.ref_name,
1317                    &target_checkpoint.ref_name,
1318                ])
1319                .await
1320            })
1321            .boxed()
1322    }
1323}
1324
1325fn git_status_args(path_prefixes: &[RepoPath]) -> Vec<OsString> {
1326    let mut args = vec![
1327        OsString::from("--no-optional-locks"),
1328        OsString::from("status"),
1329        OsString::from("--porcelain=v1"),
1330        OsString::from("--untracked-files=all"),
1331        OsString::from("--no-renames"),
1332        OsString::from("-z"),
1333    ];
1334    args.extend(path_prefixes.iter().map(|path_prefix| {
1335        if path_prefix.0.as_ref() == Path::new("") {
1336            Path::new(".").into()
1337        } else {
1338            path_prefix.as_os_str().into()
1339        }
1340    }));
1341    args
1342}
1343
1344struct GitBinary {
1345    git_binary_path: PathBuf,
1346    working_directory: PathBuf,
1347    executor: BackgroundExecutor,
1348    index_file_path: Option<PathBuf>,
1349    envs: HashMap<String, String>,
1350}
1351
1352impl GitBinary {
1353    fn new(
1354        git_binary_path: PathBuf,
1355        working_directory: PathBuf,
1356        executor: BackgroundExecutor,
1357    ) -> Self {
1358        Self {
1359            git_binary_path,
1360            working_directory,
1361            executor,
1362            index_file_path: None,
1363            envs: HashMap::default(),
1364        }
1365    }
1366
1367    fn envs(mut self, envs: HashMap<String, String>) -> Self {
1368        self.envs = envs;
1369        self
1370    }
1371
1372    pub async fn with_temp_index<R>(
1373        &mut self,
1374        f: impl AsyncFnOnce(&Self) -> Result<R>,
1375    ) -> Result<R> {
1376        let index_file_path = self.path_for_index_id(Uuid::new_v4());
1377
1378        let delete_temp_index = util::defer({
1379            let index_file_path = index_file_path.clone();
1380            let executor = self.executor.clone();
1381            move || {
1382                executor
1383                    .spawn(async move {
1384                        smol::fs::remove_file(index_file_path).await.log_err();
1385                    })
1386                    .detach();
1387            }
1388        });
1389
1390        self.index_file_path = Some(index_file_path.clone());
1391        let result = f(self).await;
1392        self.index_file_path = None;
1393        let result = result?;
1394
1395        smol::fs::remove_file(index_file_path).await.ok();
1396        delete_temp_index.abort();
1397
1398        Ok(result)
1399    }
1400
1401    fn path_for_index_id(&self, id: Uuid) -> PathBuf {
1402        self.working_directory
1403            .join(".git")
1404            .join(format!("index-{}.tmp", id))
1405    }
1406
1407    pub async fn run<S>(&self, args: impl IntoIterator<Item = S>) -> Result<String>
1408    where
1409        S: AsRef<OsStr>,
1410    {
1411        let mut stdout = self.run_raw(args).await?;
1412        if stdout.chars().last() == Some('\n') {
1413            stdout.pop();
1414        }
1415        Ok(stdout)
1416    }
1417
1418    /// Returns the result of the command without trimming the trailing newline.
1419    pub async fn run_raw<S>(&self, args: impl IntoIterator<Item = S>) -> Result<String>
1420    where
1421        S: AsRef<OsStr>,
1422    {
1423        let mut command = self.build_command(args);
1424        let output = command.output().await?;
1425        if output.status.success() {
1426            Ok(String::from_utf8(output.stdout)?)
1427        } else {
1428            Err(anyhow!(GitBinaryCommandError {
1429                stdout: String::from_utf8_lossy(&output.stdout).to_string(),
1430                status: output.status,
1431            }))
1432        }
1433    }
1434
1435    fn build_command<S>(&self, args: impl IntoIterator<Item = S>) -> smol::process::Command
1436    where
1437        S: AsRef<OsStr>,
1438    {
1439        let mut command = new_smol_command(&self.git_binary_path);
1440        command.current_dir(&self.working_directory);
1441        command.args(args);
1442        if let Some(index_file_path) = self.index_file_path.as_ref() {
1443            command.env("GIT_INDEX_FILE", index_file_path);
1444        }
1445        command.envs(&self.envs);
1446        command
1447    }
1448}
1449
1450#[derive(Error, Debug)]
1451#[error("Git command failed: {stdout}")]
1452struct GitBinaryCommandError {
1453    stdout: String,
1454    status: ExitStatus,
1455}
1456
1457async fn run_git_command(
1458    env: Arc<HashMap<String, String>>,
1459    ask_pass: AskPassDelegate,
1460    mut command: smol::process::Command,
1461    executor: &BackgroundExecutor,
1462) -> Result<RemoteCommandOutput> {
1463    if env.contains_key("GIT_ASKPASS") {
1464        let git_process = command.spawn()?;
1465        let output = git_process.output().await?;
1466        if !output.status.success() {
1467            Err(anyhow!("{}", String::from_utf8_lossy(&output.stderr)))
1468        } else {
1469            Ok(RemoteCommandOutput {
1470                stdout: String::from_utf8_lossy(&output.stdout).to_string(),
1471                stderr: String::from_utf8_lossy(&output.stderr).to_string(),
1472            })
1473        }
1474    } else {
1475        let ask_pass = AskPassSession::new(executor, ask_pass).await?;
1476        command
1477            .env("GIT_ASKPASS", ask_pass.script_path())
1478            .env("SSH_ASKPASS", ask_pass.script_path())
1479            .env("SSH_ASKPASS_REQUIRE", "force");
1480        let git_process = command.spawn()?;
1481
1482        run_askpass_command(ask_pass, git_process).await
1483    }
1484}
1485
1486async fn run_askpass_command(
1487    mut ask_pass: AskPassSession,
1488    git_process: smol::process::Child,
1489) -> std::result::Result<RemoteCommandOutput, anyhow::Error> {
1490    select_biased! {
1491        result = ask_pass.run().fuse() => {
1492            match result {
1493                AskPassResult::CancelledByUser => {
1494                    Err(anyhow!(REMOTE_CANCELLED_BY_USER))?
1495                }
1496                AskPassResult::Timedout => {
1497                    Err(anyhow!("Connecting to host timed out"))?
1498                }
1499            }
1500        }
1501        output = git_process.output().fuse() => {
1502            let output = output?;
1503            if !output.status.success() {
1504                Err(anyhow!(
1505                    "{}",
1506                    String::from_utf8_lossy(&output.stderr)
1507                ))
1508            } else {
1509                Ok(RemoteCommandOutput {
1510                    stdout: String::from_utf8_lossy(&output.stdout).to_string(),
1511                    stderr: String::from_utf8_lossy(&output.stderr).to_string(),
1512                })
1513            }
1514        }
1515    }
1516}
1517
1518pub static WORK_DIRECTORY_REPO_PATH: LazyLock<RepoPath> =
1519    LazyLock::new(|| RepoPath(Path::new("").into()));
1520
1521#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
1522pub struct RepoPath(pub Arc<Path>);
1523
1524impl RepoPath {
1525    pub fn new(path: PathBuf) -> Self {
1526        debug_assert!(path.is_relative(), "Repo paths must be relative");
1527
1528        RepoPath(path.into())
1529    }
1530
1531    pub fn from_str(path: &str) -> Self {
1532        let path = Path::new(path);
1533        debug_assert!(path.is_relative(), "Repo paths must be relative");
1534
1535        RepoPath(path.into())
1536    }
1537
1538    pub fn to_unix_style(&self) -> Cow<'_, OsStr> {
1539        #[cfg(target_os = "windows")]
1540        {
1541            use std::ffi::OsString;
1542
1543            let path = self.0.as_os_str().to_string_lossy().replace("\\", "/");
1544            Cow::Owned(OsString::from(path))
1545        }
1546        #[cfg(not(target_os = "windows"))]
1547        {
1548            Cow::Borrowed(self.0.as_os_str())
1549        }
1550    }
1551}
1552
1553impl std::fmt::Display for RepoPath {
1554    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1555        self.0.to_string_lossy().fmt(f)
1556    }
1557}
1558
1559impl From<&Path> for RepoPath {
1560    fn from(value: &Path) -> Self {
1561        RepoPath::new(value.into())
1562    }
1563}
1564
1565impl From<Arc<Path>> for RepoPath {
1566    fn from(value: Arc<Path>) -> Self {
1567        RepoPath(value)
1568    }
1569}
1570
1571impl From<PathBuf> for RepoPath {
1572    fn from(value: PathBuf) -> Self {
1573        RepoPath::new(value)
1574    }
1575}
1576
1577impl From<&str> for RepoPath {
1578    fn from(value: &str) -> Self {
1579        Self::from_str(value)
1580    }
1581}
1582
1583impl Default for RepoPath {
1584    fn default() -> Self {
1585        RepoPath(Path::new("").into())
1586    }
1587}
1588
1589impl AsRef<Path> for RepoPath {
1590    fn as_ref(&self) -> &Path {
1591        self.0.as_ref()
1592    }
1593}
1594
1595impl std::ops::Deref for RepoPath {
1596    type Target = Path;
1597
1598    fn deref(&self) -> &Self::Target {
1599        &self.0
1600    }
1601}
1602
1603impl Borrow<Path> for RepoPath {
1604    fn borrow(&self) -> &Path {
1605        self.0.as_ref()
1606    }
1607}
1608
1609#[derive(Debug)]
1610pub struct RepoPathDescendants<'a>(pub &'a Path);
1611
1612impl MapSeekTarget<RepoPath> for RepoPathDescendants<'_> {
1613    fn cmp_cursor(&self, key: &RepoPath) -> Ordering {
1614        if key.starts_with(self.0) {
1615            Ordering::Greater
1616        } else {
1617            self.0.cmp(key)
1618        }
1619    }
1620}
1621
1622fn parse_branch_input(input: &str) -> Result<Vec<Branch>> {
1623    let mut branches = Vec::new();
1624    for line in input.split('\n') {
1625        if line.is_empty() {
1626            continue;
1627        }
1628        let mut fields = line.split('\x00');
1629        let is_current_branch = fields.next().context("no HEAD")? == "*";
1630        let head_sha: SharedString = fields.next().context("no objectname")?.to_string().into();
1631        let parent_sha: SharedString = fields.next().context("no parent")?.to_string().into();
1632        let ref_name: SharedString = fields
1633            .next()
1634            .context("no refname")?
1635            .strip_prefix("refs/heads/")
1636            .context("unexpected format for refname")?
1637            .to_string()
1638            .into();
1639        let upstream_name = fields.next().context("no upstream")?.to_string();
1640        let upstream_tracking = parse_upstream_track(fields.next().context("no upstream:track")?)?;
1641        let commiterdate = fields.next().context("no committerdate")?.parse::<i64>()?;
1642        let subject: SharedString = fields
1643            .next()
1644            .context("no contents:subject")?
1645            .to_string()
1646            .into();
1647
1648        branches.push(Branch {
1649            is_head: is_current_branch,
1650            name: ref_name,
1651            most_recent_commit: Some(CommitSummary {
1652                sha: head_sha,
1653                subject,
1654                commit_timestamp: commiterdate,
1655                has_parent: !parent_sha.is_empty(),
1656            }),
1657            upstream: if upstream_name.is_empty() {
1658                None
1659            } else {
1660                Some(Upstream {
1661                    ref_name: upstream_name.into(),
1662                    tracking: upstream_tracking,
1663                })
1664            },
1665        })
1666    }
1667
1668    Ok(branches)
1669}
1670
1671fn parse_upstream_track(upstream_track: &str) -> Result<UpstreamTracking> {
1672    if upstream_track == "" {
1673        return Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
1674            ahead: 0,
1675            behind: 0,
1676        }));
1677    }
1678
1679    let upstream_track = upstream_track
1680        .strip_prefix("[")
1681        .ok_or_else(|| anyhow!("missing ["))?;
1682    let upstream_track = upstream_track
1683        .strip_suffix("]")
1684        .ok_or_else(|| anyhow!("missing ["))?;
1685    let mut ahead: u32 = 0;
1686    let mut behind: u32 = 0;
1687    for component in upstream_track.split(", ") {
1688        if component == "gone" {
1689            return Ok(UpstreamTracking::Gone);
1690        }
1691        if let Some(ahead_num) = component.strip_prefix("ahead ") {
1692            ahead = ahead_num.parse::<u32>()?;
1693        }
1694        if let Some(behind_num) = component.strip_prefix("behind ") {
1695            behind = behind_num.parse::<u32>()?;
1696        }
1697    }
1698    Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
1699        ahead,
1700        behind,
1701    }))
1702}
1703
1704fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
1705    match relative_file_path.components().next() {
1706        None => anyhow::bail!("repo path should not be empty"),
1707        Some(Component::Prefix(_)) => anyhow::bail!(
1708            "repo path `{}` should be relative, not a windows prefix",
1709            relative_file_path.to_string_lossy()
1710        ),
1711        Some(Component::RootDir) => {
1712            anyhow::bail!(
1713                "repo path `{}` should be relative",
1714                relative_file_path.to_string_lossy()
1715            )
1716        }
1717        Some(Component::CurDir) => {
1718            anyhow::bail!(
1719                "repo path `{}` should not start with `.`",
1720                relative_file_path.to_string_lossy()
1721            )
1722        }
1723        Some(Component::ParentDir) => {
1724            anyhow::bail!(
1725                "repo path `{}` should not start with `..`",
1726                relative_file_path.to_string_lossy()
1727            )
1728        }
1729        _ => Ok(()),
1730    }
1731}
1732
1733fn checkpoint_author_envs() -> HashMap<String, String> {
1734    HashMap::from_iter([
1735        ("GIT_AUTHOR_NAME".to_string(), "Zed".to_string()),
1736        ("GIT_AUTHOR_EMAIL".to_string(), "hi@zed.dev".to_string()),
1737        ("GIT_COMMITTER_NAME".to_string(), "Zed".to_string()),
1738        ("GIT_COMMITTER_EMAIL".to_string(), "hi@zed.dev".to_string()),
1739    ])
1740}
1741
1742#[cfg(test)]
1743mod tests {
1744    use super::*;
1745    use gpui::TestAppContext;
1746
1747    #[gpui::test]
1748    async fn test_checkpoint_basic(cx: &mut TestAppContext) {
1749        cx.executor().allow_parking();
1750
1751        let repo_dir = tempfile::tempdir().unwrap();
1752
1753        git2::Repository::init(repo_dir.path()).unwrap();
1754        let file_path = repo_dir.path().join("file");
1755        smol::fs::write(&file_path, "initial").await.unwrap();
1756
1757        let repo =
1758            RealGitRepository::new(&repo_dir.path().join(".git"), None, cx.executor()).unwrap();
1759        repo.stage_paths(
1760            vec![RepoPath::from_str("file")],
1761            Arc::new(HashMap::default()),
1762        )
1763        .await
1764        .unwrap();
1765        repo.commit(
1766            "Initial commit".into(),
1767            None,
1768            Arc::new(checkpoint_author_envs()),
1769        )
1770        .await
1771        .unwrap();
1772
1773        smol::fs::write(&file_path, "modified before checkpoint")
1774            .await
1775            .unwrap();
1776        smol::fs::write(repo_dir.path().join("new_file_before_checkpoint"), "1")
1777            .await
1778            .unwrap();
1779        let checkpoint = repo.checkpoint().await.unwrap();
1780
1781        // Ensure the user can't see any branches after creating a checkpoint.
1782        assert_eq!(repo.branches().await.unwrap().len(), 1);
1783
1784        smol::fs::write(&file_path, "modified after checkpoint")
1785            .await
1786            .unwrap();
1787        repo.stage_paths(
1788            vec![RepoPath::from_str("file")],
1789            Arc::new(HashMap::default()),
1790        )
1791        .await
1792        .unwrap();
1793        repo.commit(
1794            "Commit after checkpoint".into(),
1795            None,
1796            Arc::new(checkpoint_author_envs()),
1797        )
1798        .await
1799        .unwrap();
1800
1801        smol::fs::remove_file(repo_dir.path().join("new_file_before_checkpoint"))
1802            .await
1803            .unwrap();
1804        smol::fs::write(repo_dir.path().join("new_file_after_checkpoint"), "2")
1805            .await
1806            .unwrap();
1807
1808        // Ensure checkpoint stays alive even after a Git GC.
1809        repo.gc().await.unwrap();
1810        repo.restore_checkpoint(checkpoint.clone()).await.unwrap();
1811
1812        assert_eq!(
1813            smol::fs::read_to_string(&file_path).await.unwrap(),
1814            "modified before checkpoint"
1815        );
1816        assert_eq!(
1817            smol::fs::read_to_string(repo_dir.path().join("new_file_before_checkpoint"))
1818                .await
1819                .unwrap(),
1820            "1"
1821        );
1822        assert_eq!(
1823            smol::fs::read_to_string(repo_dir.path().join("new_file_after_checkpoint"))
1824                .await
1825                .ok(),
1826            None
1827        );
1828
1829        // Garbage collecting after deleting a checkpoint makes it unreachable.
1830        repo.delete_checkpoint(checkpoint.clone()).await.unwrap();
1831        repo.gc().await.unwrap();
1832        repo.restore_checkpoint(checkpoint.clone())
1833            .await
1834            .unwrap_err();
1835    }
1836
1837    #[gpui::test]
1838    async fn test_checkpoint_empty_repo(cx: &mut TestAppContext) {
1839        cx.executor().allow_parking();
1840
1841        let repo_dir = tempfile::tempdir().unwrap();
1842        git2::Repository::init(repo_dir.path()).unwrap();
1843        let repo =
1844            RealGitRepository::new(&repo_dir.path().join(".git"), None, cx.executor()).unwrap();
1845
1846        smol::fs::write(repo_dir.path().join("foo"), "foo")
1847            .await
1848            .unwrap();
1849        let checkpoint_sha = repo.checkpoint().await.unwrap();
1850
1851        // Ensure the user can't see any branches after creating a checkpoint.
1852        assert_eq!(repo.branches().await.unwrap().len(), 1);
1853
1854        smol::fs::write(repo_dir.path().join("foo"), "bar")
1855            .await
1856            .unwrap();
1857        smol::fs::write(repo_dir.path().join("baz"), "qux")
1858            .await
1859            .unwrap();
1860        repo.restore_checkpoint(checkpoint_sha).await.unwrap();
1861        assert_eq!(
1862            smol::fs::read_to_string(repo_dir.path().join("foo"))
1863                .await
1864                .unwrap(),
1865            "foo"
1866        );
1867        assert_eq!(
1868            smol::fs::read_to_string(repo_dir.path().join("baz"))
1869                .await
1870                .ok(),
1871            None
1872        );
1873    }
1874
1875    #[gpui::test]
1876    async fn test_compare_checkpoints(cx: &mut TestAppContext) {
1877        cx.executor().allow_parking();
1878
1879        let repo_dir = tempfile::tempdir().unwrap();
1880        git2::Repository::init(repo_dir.path()).unwrap();
1881        let repo =
1882            RealGitRepository::new(&repo_dir.path().join(".git"), None, cx.executor()).unwrap();
1883
1884        smol::fs::write(repo_dir.path().join("file1"), "content1")
1885            .await
1886            .unwrap();
1887        let checkpoint1 = repo.checkpoint().await.unwrap();
1888
1889        smol::fs::write(repo_dir.path().join("file2"), "content2")
1890            .await
1891            .unwrap();
1892        let checkpoint2 = repo.checkpoint().await.unwrap();
1893
1894        assert!(
1895            !repo
1896                .compare_checkpoints(checkpoint1, checkpoint2.clone())
1897                .await
1898                .unwrap()
1899        );
1900
1901        let checkpoint3 = repo.checkpoint().await.unwrap();
1902        assert!(
1903            repo.compare_checkpoints(checkpoint2, checkpoint3)
1904                .await
1905                .unwrap()
1906        );
1907    }
1908
1909    #[test]
1910    fn test_branches_parsing() {
1911        // suppress "help: octal escapes are not supported, `\0` is always null"
1912        #[allow(clippy::octal_escapes)]
1913        let input = "*\0060964da10574cd9bf06463a53bf6e0769c5c45e\0\0refs/heads/zed-patches\0refs/remotes/origin/zed-patches\0\01733187470\0generated protobuf\n";
1914        assert_eq!(
1915            parse_branch_input(&input).unwrap(),
1916            vec![Branch {
1917                is_head: true,
1918                name: "zed-patches".into(),
1919                upstream: Some(Upstream {
1920                    ref_name: "refs/remotes/origin/zed-patches".into(),
1921                    tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus {
1922                        ahead: 0,
1923                        behind: 0
1924                    })
1925                }),
1926                most_recent_commit: Some(CommitSummary {
1927                    sha: "060964da10574cd9bf06463a53bf6e0769c5c45e".into(),
1928                    subject: "generated protobuf".into(),
1929                    commit_timestamp: 1733187470,
1930                    has_parent: false,
1931                })
1932            }]
1933        )
1934    }
1935
1936    impl RealGitRepository {
1937        /// Force a Git garbage collection on the repository.
1938        fn gc(&self) -> BoxFuture<Result<()>> {
1939            let working_directory = self.working_directory();
1940            let git_binary_path = self.git_binary_path.clone();
1941            let executor = self.executor.clone();
1942            self.executor
1943                .spawn(async move {
1944                    let git_binary_path = git_binary_path.clone();
1945                    let working_directory = working_directory?;
1946                    let git = GitBinary::new(git_binary_path, working_directory, executor);
1947                    git.run(&["gc", "--prune=now"]).await?;
1948                    Ok(())
1949                })
1950                .boxed()
1951        }
1952    }
1953}