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