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