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