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