repository.rs

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