repository.rs

   1use crate::status::FileStatus;
   2use crate::GitHostingProviderRegistry;
   3use crate::{blame::Blame, status::GitStatus};
   4use anyhow::{anyhow, Context, Result};
   5use askpass::{AskPassResult, AskPassSession};
   6use collections::{HashMap, HashSet};
   7use futures::{select_biased, FutureExt as _};
   8use git2::BranchType;
   9use gpui::SharedString;
  10use parking_lot::Mutex;
  11use rope::Rope;
  12use schemars::JsonSchema;
  13use serde::Deserialize;
  14use std::borrow::Borrow;
  15use std::io::Write as _;
  16use std::process::Stdio;
  17use std::sync::LazyLock;
  18use std::{
  19    cmp::Ordering,
  20    path::{Component, Path, PathBuf},
  21    sync::Arc,
  22};
  23use sum_tree::MapSeekTarget;
  24use util::command::{new_smol_command, new_std_command};
  25use util::ResultExt;
  26
  27pub const REMOTE_CANCELLED_BY_USER: &str = "Operation cancelled by user";
  28
  29#[derive(Clone, Debug, Hash, PartialEq, Eq)]
  30pub struct Branch {
  31    pub is_head: bool,
  32    pub name: SharedString,
  33    pub upstream: Option<Upstream>,
  34    pub most_recent_commit: Option<CommitSummary>,
  35}
  36
  37impl Branch {
  38    pub fn tracking_status(&self) -> Option<UpstreamTrackingStatus> {
  39        self.upstream
  40            .as_ref()
  41            .and_then(|upstream| upstream.tracking.status())
  42    }
  43
  44    pub fn priority_key(&self) -> (bool, Option<i64>) {
  45        (
  46            self.is_head,
  47            self.most_recent_commit
  48                .as_ref()
  49                .map(|commit| commit.commit_timestamp),
  50        )
  51    }
  52}
  53
  54#[derive(Clone, Debug, Hash, PartialEq, Eq)]
  55pub struct Upstream {
  56    pub ref_name: SharedString,
  57    pub tracking: UpstreamTracking,
  58}
  59
  60#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
  61pub enum UpstreamTracking {
  62    /// Remote ref not present in local repository.
  63    Gone,
  64    /// Remote ref present in local repository (fetched from remote).
  65    Tracked(UpstreamTrackingStatus),
  66}
  67
  68impl From<UpstreamTrackingStatus> for UpstreamTracking {
  69    fn from(status: UpstreamTrackingStatus) -> Self {
  70        UpstreamTracking::Tracked(status)
  71    }
  72}
  73
  74impl UpstreamTracking {
  75    pub fn is_gone(&self) -> bool {
  76        matches!(self, UpstreamTracking::Gone)
  77    }
  78
  79    pub fn status(&self) -> Option<UpstreamTrackingStatus> {
  80        match self {
  81            UpstreamTracking::Gone => None,
  82            UpstreamTracking::Tracked(status) => Some(*status),
  83        }
  84    }
  85}
  86
  87#[derive(Debug)]
  88pub struct RemoteCommandOutput {
  89    pub stdout: String,
  90    pub stderr: String,
  91}
  92
  93impl RemoteCommandOutput {
  94    pub fn is_empty(&self) -> bool {
  95        self.stdout.is_empty() && self.stderr.is_empty()
  96    }
  97}
  98
  99#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
 100pub struct UpstreamTrackingStatus {
 101    pub ahead: u32,
 102    pub behind: u32,
 103}
 104
 105#[derive(Clone, Debug, Hash, PartialEq, Eq)]
 106pub struct CommitSummary {
 107    pub sha: SharedString,
 108    pub subject: SharedString,
 109    /// This is a unix timestamp
 110    pub commit_timestamp: i64,
 111}
 112
 113#[derive(Clone, Debug, Hash, PartialEq, Eq)]
 114pub struct CommitDetails {
 115    pub sha: SharedString,
 116    pub message: SharedString,
 117    pub commit_timestamp: i64,
 118    pub committer_email: SharedString,
 119    pub committer_name: SharedString,
 120}
 121
 122#[derive(Debug, Clone, Hash, PartialEq, Eq)]
 123pub struct Remote {
 124    pub name: SharedString,
 125}
 126
 127pub enum ResetMode {
 128    // reset the branch pointer, leave index and worktree unchanged
 129    // (this will make it look like things that were committed are now
 130    // staged)
 131    Soft,
 132    // reset the branch pointer and index, leave worktree unchanged
 133    // (this makes it look as though things that were committed are now
 134    // unstaged)
 135    Mixed,
 136}
 137
 138pub trait GitRepository: Send + Sync {
 139    fn reload_index(&self);
 140
 141    /// Returns the contents of an entry in the repository's index, or None if there is no entry for the given path.
 142    ///
 143    /// Also returns `None` for symlinks.
 144    fn load_index_text(&self, path: &RepoPath) -> Option<String>;
 145
 146    /// 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.
 147    ///
 148    /// Also returns `None` for symlinks.
 149    fn load_committed_text(&self, path: &RepoPath) -> Option<String>;
 150
 151    fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()>;
 152
 153    /// Returns the URL of the remote with the given name.
 154    fn remote_url(&self, name: &str) -> Option<String>;
 155
 156    /// Returns the SHA of the current HEAD.
 157    fn head_sha(&self) -> Option<String>;
 158
 159    fn merge_head_shas(&self) -> Vec<String>;
 160
 161    /// Returns the list of git statuses, sorted by path
 162    fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>;
 163
 164    fn branches(&self) -> Result<Vec<Branch>>;
 165    fn change_branch(&self, _: &str) -> Result<()>;
 166    fn create_branch(&self, _: &str) -> Result<()>;
 167    fn branch_exits(&self, _: &str) -> Result<bool>;
 168
 169    fn reset(&self, commit: &str, mode: ResetMode) -> Result<()>;
 170    fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()>;
 171
 172    fn show(&self, commit: &str) -> Result<CommitDetails>;
 173
 174    fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>;
 175
 176    /// Returns the absolute path to the repository. For worktrees, this will be the path to the
 177    /// worktree's gitdir within the main repository (typically `.git/worktrees/<name>`).
 178    fn path(&self) -> PathBuf;
 179
 180    /// Returns the absolute path to the ".git" dir for the main repository, typically a `.git`
 181    /// folder. For worktrees, this will be the path to the repository the worktree was created
 182    /// from. Otherwise, this is the same value as `path()`.
 183    ///
 184    /// Git documentation calls this the "commondir", and for git CLI is overridden by
 185    /// `GIT_COMMON_DIR`.
 186    fn main_repository_path(&self) -> PathBuf;
 187
 188    /// Updates the index to match the worktree at the given paths.
 189    ///
 190    /// If any of the paths have been deleted from the worktree, they will be removed from the index if found there.
 191    fn stage_paths(&self, paths: &[RepoPath]) -> Result<()>;
 192    /// Updates the index to match HEAD at the given paths.
 193    ///
 194    /// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index.
 195    fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
 196
 197    fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>;
 198
 199    fn push(
 200        &self,
 201        branch_name: &str,
 202        upstream_name: &str,
 203        options: Option<PushOptions>,
 204        askpass: AskPassSession,
 205    ) -> Result<RemoteCommandOutput>;
 206
 207    fn pull(
 208        &self,
 209        branch_name: &str,
 210        upstream_name: &str,
 211        askpass: AskPassSession,
 212    ) -> Result<RemoteCommandOutput>;
 213    fn fetch(&self, askpass: AskPassSession) -> Result<RemoteCommandOutput>;
 214
 215    fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
 216
 217    /// returns a list of remote branches that contain HEAD
 218    fn check_for_pushed_commit(&self) -> Result<Vec<SharedString>>;
 219
 220    /// Run git diff
 221    fn diff(&self, diff: DiffType) -> Result<String>;
 222}
 223
 224pub enum DiffType {
 225    HeadToIndex,
 226    HeadToWorktree,
 227}
 228
 229#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
 230pub enum PushOptions {
 231    SetUpstream,
 232    Force,
 233}
 234
 235impl std::fmt::Debug for dyn GitRepository {
 236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 237        f.debug_struct("dyn GitRepository<...>").finish()
 238    }
 239}
 240
 241pub struct RealGitRepository {
 242    pub repository: Mutex<git2::Repository>,
 243    pub git_binary_path: PathBuf,
 244    hosting_provider_registry: Arc<GitHostingProviderRegistry>,
 245}
 246
 247impl RealGitRepository {
 248    pub fn new(
 249        repository: git2::Repository,
 250        git_binary_path: Option<PathBuf>,
 251        hosting_provider_registry: Arc<GitHostingProviderRegistry>,
 252    ) -> Self {
 253        Self {
 254            repository: Mutex::new(repository),
 255            git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
 256            hosting_provider_registry,
 257        }
 258    }
 259
 260    fn working_directory(&self) -> Result<PathBuf> {
 261        self.repository
 262            .lock()
 263            .workdir()
 264            .context("failed to read git work directory")
 265            .map(Path::to_path_buf)
 266    }
 267}
 268
 269// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
 270const GIT_MODE_SYMLINK: u32 = 0o120000;
 271
 272impl GitRepository for RealGitRepository {
 273    fn reload_index(&self) {
 274        if let Ok(mut index) = self.repository.lock().index() {
 275            _ = index.read(false);
 276        }
 277    }
 278
 279    fn path(&self) -> PathBuf {
 280        let repo = self.repository.lock();
 281        repo.path().into()
 282    }
 283
 284    fn main_repository_path(&self) -> PathBuf {
 285        let repo = self.repository.lock();
 286        repo.commondir().into()
 287    }
 288
 289    fn show(&self, commit: &str) -> Result<CommitDetails> {
 290        let repo = self.repository.lock();
 291        let Ok(commit) = repo.revparse_single(commit)?.into_commit() else {
 292            anyhow::bail!("{} is not a commit", commit);
 293        };
 294        let details = CommitDetails {
 295            sha: commit.id().to_string().into(),
 296            message: String::from_utf8_lossy(commit.message_raw_bytes())
 297                .to_string()
 298                .into(),
 299            commit_timestamp: commit.time().seconds(),
 300            committer_email: String::from_utf8_lossy(commit.committer().email_bytes())
 301                .to_string()
 302                .into(),
 303            committer_name: String::from_utf8_lossy(commit.committer().name_bytes())
 304                .to_string()
 305                .into(),
 306        };
 307        Ok(details)
 308    }
 309
 310    fn reset(&self, commit: &str, mode: ResetMode) -> Result<()> {
 311        let working_directory = self.working_directory()?;
 312
 313        let mode_flag = match mode {
 314            ResetMode::Mixed => "--mixed",
 315            ResetMode::Soft => "--soft",
 316        };
 317
 318        let output = new_std_command(&self.git_binary_path)
 319            .current_dir(&working_directory)
 320            .args(["reset", mode_flag, commit])
 321            .output()?;
 322        if !output.status.success() {
 323            return Err(anyhow!(
 324                "Failed to reset:\n{}",
 325                String::from_utf8_lossy(&output.stderr)
 326            ));
 327        }
 328        Ok(())
 329    }
 330
 331    fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()> {
 332        if paths.is_empty() {
 333            return Ok(());
 334        }
 335        let working_directory = self.working_directory()?;
 336
 337        let output = new_std_command(&self.git_binary_path)
 338            .current_dir(&working_directory)
 339            .args(["checkout", commit, "--"])
 340            .args(paths.iter().map(|path| path.as_ref()))
 341            .output()?;
 342        if !output.status.success() {
 343            return Err(anyhow!(
 344                "Failed to checkout files:\n{}",
 345                String::from_utf8_lossy(&output.stderr)
 346            ));
 347        }
 348        Ok(())
 349    }
 350
 351    fn load_index_text(&self, path: &RepoPath) -> Option<String> {
 352        fn logic(repo: &git2::Repository, path: &RepoPath) -> Result<Option<String>> {
 353            const STAGE_NORMAL: i32 = 0;
 354            let index = repo.index()?;
 355
 356            // This check is required because index.get_path() unwraps internally :(
 357            check_path_to_repo_path_errors(path)?;
 358
 359            let oid = match index.get_path(path, STAGE_NORMAL) {
 360                Some(entry) if entry.mode != GIT_MODE_SYMLINK => entry.id,
 361                _ => return Ok(None),
 362            };
 363
 364            let content = repo.find_blob(oid)?.content().to_owned();
 365            Ok(Some(String::from_utf8(content)?))
 366        }
 367
 368        match logic(&self.repository.lock(), path) {
 369            Ok(value) => return value,
 370            Err(err) => log::error!("Error loading index text: {:?}", err),
 371        }
 372        None
 373    }
 374
 375    fn load_committed_text(&self, path: &RepoPath) -> Option<String> {
 376        let repo = self.repository.lock();
 377        let head = repo.head().ok()?.peel_to_tree().log_err()?;
 378        let entry = head.get_path(path).ok()?;
 379        if entry.filemode() == i32::from(git2::FileMode::Link) {
 380            return None;
 381        }
 382        let content = repo.find_blob(entry.id()).log_err()?.content().to_owned();
 383        let content = String::from_utf8(content).log_err()?;
 384        Some(content)
 385    }
 386
 387    fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
 388        let working_directory = self.working_directory()?;
 389        if let Some(content) = content {
 390            let mut child = new_std_command(&self.git_binary_path)
 391                .current_dir(&working_directory)
 392                .args(["hash-object", "-w", "--stdin"])
 393                .stdin(Stdio::piped())
 394                .stdout(Stdio::piped())
 395                .spawn()?;
 396            child.stdin.take().unwrap().write_all(content.as_bytes())?;
 397            let output = child.wait_with_output()?.stdout;
 398            let sha = String::from_utf8(output)?;
 399
 400            log::debug!("indexing SHA: {sha}, path {path:?}");
 401
 402            let output = new_std_command(&self.git_binary_path)
 403                .current_dir(&working_directory)
 404                .args(["update-index", "--add", "--cacheinfo", "100644", &sha])
 405                .arg(path.as_ref())
 406                .output()?;
 407
 408            if !output.status.success() {
 409                return Err(anyhow!(
 410                    "Failed to stage:\n{}",
 411                    String::from_utf8_lossy(&output.stderr)
 412                ));
 413            }
 414        } else {
 415            let output = new_std_command(&self.git_binary_path)
 416                .current_dir(&working_directory)
 417                .args(["update-index", "--force-remove"])
 418                .arg(path.as_ref())
 419                .output()?;
 420
 421            if !output.status.success() {
 422                return Err(anyhow!(
 423                    "Failed to unstage:\n{}",
 424                    String::from_utf8_lossy(&output.stderr)
 425                ));
 426            }
 427        }
 428
 429        Ok(())
 430    }
 431
 432    fn remote_url(&self, name: &str) -> Option<String> {
 433        let repo = self.repository.lock();
 434        let remote = repo.find_remote(name).ok()?;
 435        remote.url().map(|url| url.to_string())
 436    }
 437
 438    fn head_sha(&self) -> Option<String> {
 439        Some(self.repository.lock().head().ok()?.target()?.to_string())
 440    }
 441
 442    fn merge_head_shas(&self) -> Vec<String> {
 443        let mut shas = Vec::default();
 444        self.repository
 445            .lock()
 446            .mergehead_foreach(|oid| {
 447                shas.push(oid.to_string());
 448                true
 449            })
 450            .ok();
 451        if let Some(oid) = self
 452            .repository
 453            .lock()
 454            .find_reference("CHERRY_PICK_HEAD")
 455            .ok()
 456            .and_then(|reference| reference.target())
 457        {
 458            shas.push(oid.to_string())
 459        }
 460        shas
 461    }
 462
 463    fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
 464        let working_directory = self
 465            .repository
 466            .lock()
 467            .workdir()
 468            .context("failed to read git work directory")?
 469            .to_path_buf();
 470        GitStatus::new(&self.git_binary_path, &working_directory, path_prefixes)
 471    }
 472
 473    fn branch_exits(&self, name: &str) -> Result<bool> {
 474        let repo = self.repository.lock();
 475        let branch = repo.find_branch(name, BranchType::Local);
 476        match branch {
 477            Ok(_) => Ok(true),
 478            Err(e) => match e.code() {
 479                git2::ErrorCode::NotFound => Ok(false),
 480                _ => Err(anyhow!(e)),
 481            },
 482        }
 483    }
 484
 485    fn branches(&self) -> Result<Vec<Branch>> {
 486        let working_directory = self
 487            .repository
 488            .lock()
 489            .workdir()
 490            .context("failed to read git work directory")?
 491            .to_path_buf();
 492        let fields = [
 493            "%(HEAD)",
 494            "%(objectname)",
 495            "%(refname)",
 496            "%(upstream)",
 497            "%(upstream:track)",
 498            "%(committerdate:unix)",
 499            "%(contents:subject)",
 500        ]
 501        .join("%00");
 502        let args = vec!["for-each-ref", "refs/heads/**/*", "--format", &fields];
 503
 504        let output = new_std_command(&self.git_binary_path)
 505            .current_dir(&working_directory)
 506            .args(args)
 507            .output()?;
 508
 509        if !output.status.success() {
 510            return Err(anyhow!(
 511                "Failed to git git branches:\n{}",
 512                String::from_utf8_lossy(&output.stderr)
 513            ));
 514        }
 515
 516        let input = String::from_utf8_lossy(&output.stdout);
 517
 518        let mut branches = parse_branch_input(&input)?;
 519        if branches.is_empty() {
 520            let args = vec!["symbolic-ref", "--quiet", "--short", "HEAD"];
 521
 522            let output = new_std_command(&self.git_binary_path)
 523                .current_dir(&working_directory)
 524                .args(args)
 525                .output()?;
 526
 527            // git symbolic-ref returns a non-0 exit code if HEAD points
 528            // to something other than a branch
 529            if output.status.success() {
 530                let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
 531
 532                branches.push(Branch {
 533                    name: name.into(),
 534                    is_head: true,
 535                    upstream: None,
 536                    most_recent_commit: None,
 537                });
 538            }
 539        }
 540
 541        Ok(branches)
 542    }
 543
 544    fn change_branch(&self, name: &str) -> Result<()> {
 545        let repo = self.repository.lock();
 546        let revision = repo.find_branch(name, BranchType::Local)?;
 547        let revision = revision.get();
 548        let as_tree = revision.peel_to_tree()?;
 549        repo.checkout_tree(as_tree.as_object(), None)?;
 550        repo.set_head(
 551            revision
 552                .name()
 553                .ok_or_else(|| anyhow!("Branch name could not be retrieved"))?,
 554        )?;
 555        Ok(())
 556    }
 557
 558    fn create_branch(&self, name: &str) -> Result<()> {
 559        let repo = self.repository.lock();
 560        let current_commit = repo.head()?.peel_to_commit()?;
 561        repo.branch(name, &current_commit, false)?;
 562        Ok(())
 563    }
 564
 565    fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame> {
 566        let working_directory = self
 567            .repository
 568            .lock()
 569            .workdir()
 570            .with_context(|| format!("failed to get git working directory for file {:?}", path))?
 571            .to_path_buf();
 572
 573        const REMOTE_NAME: &str = "origin";
 574        let remote_url = self.remote_url(REMOTE_NAME);
 575
 576        crate::blame::Blame::for_path(
 577            &self.git_binary_path,
 578            &working_directory,
 579            path,
 580            &content,
 581            remote_url,
 582            self.hosting_provider_registry.clone(),
 583        )
 584    }
 585
 586    fn diff(&self, diff: DiffType) -> Result<String> {
 587        let working_directory = self.working_directory()?;
 588        let args = match diff {
 589            DiffType::HeadToIndex => Some("--staged"),
 590            DiffType::HeadToWorktree => None,
 591        };
 592
 593        let output = new_std_command(&self.git_binary_path)
 594            .current_dir(&working_directory)
 595            .args(["diff"])
 596            .args(args)
 597            .output()?;
 598
 599        if !output.status.success() {
 600            return Err(anyhow!(
 601                "Failed to run git diff:\n{}",
 602                String::from_utf8_lossy(&output.stderr)
 603            ));
 604        }
 605        Ok(String::from_utf8_lossy(&output.stdout).to_string())
 606    }
 607
 608    fn stage_paths(&self, paths: &[RepoPath]) -> Result<()> {
 609        let working_directory = self.working_directory()?;
 610
 611        if !paths.is_empty() {
 612            let output = new_std_command(&self.git_binary_path)
 613                .current_dir(&working_directory)
 614                .args(["update-index", "--add", "--remove", "--"])
 615                .args(paths.iter().map(|p| p.as_ref()))
 616                .output()?;
 617
 618            if !output.status.success() {
 619                return Err(anyhow!(
 620                    "Failed to stage paths:\n{}",
 621                    String::from_utf8_lossy(&output.stderr)
 622                ));
 623            }
 624        }
 625        Ok(())
 626    }
 627
 628    fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()> {
 629        let working_directory = self.working_directory()?;
 630
 631        if !paths.is_empty() {
 632            let output = new_std_command(&self.git_binary_path)
 633                .current_dir(&working_directory)
 634                .args(["reset", "--quiet", "--"])
 635                .args(paths.iter().map(|p| p.as_ref()))
 636                .output()?;
 637
 638            if !output.status.success() {
 639                return Err(anyhow!(
 640                    "Failed to unstage:\n{}",
 641                    String::from_utf8_lossy(&output.stderr)
 642                ));
 643            }
 644        }
 645        Ok(())
 646    }
 647
 648    fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> {
 649        let working_directory = self.working_directory()?;
 650
 651        let mut cmd = new_std_command(&self.git_binary_path);
 652        cmd.current_dir(&working_directory)
 653            .args(["commit", "--quiet", "-m"])
 654            .arg(message)
 655            .arg("--cleanup=strip");
 656
 657        if let Some((name, email)) = name_and_email {
 658            cmd.arg("--author").arg(&format!("{name} <{email}>"));
 659        }
 660
 661        let output = cmd.output()?;
 662
 663        if !output.status.success() {
 664            return Err(anyhow!(
 665                "Failed to commit:\n{}",
 666                String::from_utf8_lossy(&output.stderr)
 667            ));
 668        }
 669        Ok(())
 670    }
 671
 672    fn push(
 673        &self,
 674        branch_name: &str,
 675        remote_name: &str,
 676        options: Option<PushOptions>,
 677        ask_pass: AskPassSession,
 678    ) -> Result<RemoteCommandOutput> {
 679        let working_directory = self.working_directory()?;
 680
 681        let mut command = new_smol_command("git");
 682        command
 683            .env("GIT_ASKPASS", ask_pass.script_path())
 684            .env("SSH_ASKPASS", ask_pass.script_path())
 685            .env("SSH_ASKPASS_REQUIRE", "force")
 686            .current_dir(&working_directory)
 687            .args(["push"])
 688            .args(options.map(|option| match option {
 689                PushOptions::SetUpstream => "--set-upstream",
 690                PushOptions::Force => "--force-with-lease",
 691            }))
 692            .arg(remote_name)
 693            .arg(format!("{}:{}", branch_name, branch_name));
 694        let git_process = command.spawn()?;
 695
 696        run_remote_command(ask_pass, git_process)
 697    }
 698
 699    fn pull(
 700        &self,
 701        branch_name: &str,
 702        remote_name: &str,
 703        ask_pass: AskPassSession,
 704    ) -> Result<RemoteCommandOutput> {
 705        let working_directory = self.working_directory()?;
 706
 707        let mut command = new_smol_command("git");
 708        command
 709            .env("GIT_ASKPASS", ask_pass.script_path())
 710            .env("SSH_ASKPASS", ask_pass.script_path())
 711            .env("SSH_ASKPASS_REQUIRE", "force")
 712            .current_dir(&working_directory)
 713            .args(["pull"])
 714            .arg(remote_name)
 715            .arg(branch_name);
 716        let git_process = command.spawn()?;
 717
 718        run_remote_command(ask_pass, git_process)
 719    }
 720
 721    fn fetch(&self, ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
 722        let working_directory = self.working_directory()?;
 723
 724        let mut command = new_smol_command("git");
 725        command
 726            .env("GIT_ASKPASS", ask_pass.script_path())
 727            .env("SSH_ASKPASS", ask_pass.script_path())
 728            .env("SSH_ASKPASS_REQUIRE", "force")
 729            .current_dir(&working_directory)
 730            .args(["fetch", "--all"]);
 731        let git_process = command.spawn()?;
 732
 733        run_remote_command(ask_pass, git_process)
 734    }
 735
 736    fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>> {
 737        let working_directory = self.working_directory()?;
 738
 739        if let Some(branch_name) = branch_name {
 740            let output = new_std_command(&self.git_binary_path)
 741                .current_dir(&working_directory)
 742                .args(["config", "--get"])
 743                .arg(format!("branch.{}.remote", branch_name))
 744                .output()?;
 745
 746            if output.status.success() {
 747                let remote_name = String::from_utf8_lossy(&output.stdout);
 748
 749                return Ok(vec![Remote {
 750                    name: remote_name.trim().to_string().into(),
 751                }]);
 752            }
 753        }
 754
 755        let output = new_std_command(&self.git_binary_path)
 756            .current_dir(&working_directory)
 757            .args(["remote"])
 758            .output()?;
 759
 760        if output.status.success() {
 761            let remote_names = String::from_utf8_lossy(&output.stdout)
 762                .split('\n')
 763                .filter(|name| !name.is_empty())
 764                .map(|name| Remote {
 765                    name: name.trim().to_string().into(),
 766                })
 767                .collect();
 768
 769            return Ok(remote_names);
 770        } else {
 771            return Err(anyhow!(
 772                "Failed to get remotes:\n{}",
 773                String::from_utf8_lossy(&output.stderr)
 774            ));
 775        }
 776    }
 777
 778    fn check_for_pushed_commit(&self) -> Result<Vec<SharedString>> {
 779        let working_directory = self.working_directory()?;
 780        let git_cmd = |args: &[&str]| -> Result<String> {
 781            let output = new_std_command(&self.git_binary_path)
 782                .current_dir(&working_directory)
 783                .args(args)
 784                .output()?;
 785            if output.status.success() {
 786                Ok(String::from_utf8(output.stdout)?)
 787            } else {
 788                Err(anyhow!(String::from_utf8_lossy(&output.stderr).to_string()))
 789            }
 790        };
 791
 792        let head = git_cmd(&["rev-parse", "HEAD"])
 793            .context("Failed to get HEAD")?
 794            .trim()
 795            .to_owned();
 796
 797        let mut remote_branches = vec![];
 798        let mut add_if_matching = |remote_head: &str| {
 799            if let Ok(merge_base) = git_cmd(&["merge-base", &head, remote_head]) {
 800                if merge_base.trim() == head {
 801                    if let Some(s) = remote_head.strip_prefix("refs/remotes/") {
 802                        remote_branches.push(s.to_owned().into());
 803                    }
 804                }
 805            }
 806        };
 807
 808        // check the main branch of each remote
 809        let remotes = git_cmd(&["remote"]).context("Failed to get remotes")?;
 810        for remote in remotes.lines() {
 811            if let Ok(remote_head) =
 812                git_cmd(&["symbolic-ref", &format!("refs/remotes/{remote}/HEAD")])
 813            {
 814                add_if_matching(remote_head.trim());
 815            }
 816        }
 817
 818        // ... and the remote branch that the checked-out one is tracking
 819        if let Ok(remote_head) = git_cmd(&["rev-parse", "--symbolic-full-name", "@{u}"]) {
 820            add_if_matching(remote_head.trim());
 821        }
 822
 823        Ok(remote_branches)
 824    }
 825}
 826
 827fn run_remote_command(
 828    mut ask_pass: AskPassSession,
 829    git_process: smol::process::Child,
 830) -> std::result::Result<RemoteCommandOutput, anyhow::Error> {
 831    smol::block_on(async {
 832        select_biased! {
 833            result = ask_pass.run().fuse() => {
 834                match result {
 835                    AskPassResult::CancelledByUser => {
 836                        Err(anyhow!(REMOTE_CANCELLED_BY_USER))?
 837                    }
 838                    AskPassResult::Timedout => {
 839                        Err(anyhow!("Connecting to host timed out"))?
 840                    }
 841                }
 842            }
 843            output = git_process.output().fuse() => {
 844                let output = output?;
 845                if !output.status.success() {
 846                    Err(anyhow!(
 847                        "Operation failed:\n{}",
 848                        String::from_utf8_lossy(&output.stderr)
 849                    ))
 850                } else {
 851                    Ok(RemoteCommandOutput {
 852                        stdout: String::from_utf8_lossy(&output.stdout).to_string(),
 853                        stderr: String::from_utf8_lossy(&output.stderr).to_string(),
 854                    })
 855                }
 856            }
 857        }
 858    })
 859}
 860
 861#[derive(Debug, Clone)]
 862pub struct FakeGitRepository {
 863    state: Arc<Mutex<FakeGitRepositoryState>>,
 864}
 865
 866#[derive(Debug, Clone)]
 867pub struct FakeGitRepositoryState {
 868    pub path: PathBuf,
 869    pub event_emitter: smol::channel::Sender<PathBuf>,
 870    pub head_contents: HashMap<RepoPath, String>,
 871    pub index_contents: HashMap<RepoPath, String>,
 872    pub blames: HashMap<RepoPath, Blame>,
 873    pub statuses: HashMap<RepoPath, FileStatus>,
 874    pub current_branch_name: Option<String>,
 875    pub branches: HashSet<String>,
 876    pub simulated_index_write_error_message: Option<String>,
 877}
 878
 879impl FakeGitRepository {
 880    pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<dyn GitRepository> {
 881        Arc::new(FakeGitRepository { state })
 882    }
 883}
 884
 885impl FakeGitRepositoryState {
 886    pub fn new(path: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self {
 887        FakeGitRepositoryState {
 888            path,
 889            event_emitter,
 890            head_contents: Default::default(),
 891            index_contents: Default::default(),
 892            blames: Default::default(),
 893            statuses: Default::default(),
 894            current_branch_name: Default::default(),
 895            branches: Default::default(),
 896            simulated_index_write_error_message: None,
 897        }
 898    }
 899}
 900
 901impl GitRepository for FakeGitRepository {
 902    fn reload_index(&self) {}
 903
 904    fn load_index_text(&self, path: &RepoPath) -> Option<String> {
 905        let state = self.state.lock();
 906        state.index_contents.get(path.as_ref()).cloned()
 907    }
 908
 909    fn load_committed_text(&self, path: &RepoPath) -> Option<String> {
 910        let state = self.state.lock();
 911        state.head_contents.get(path.as_ref()).cloned()
 912    }
 913
 914    fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
 915        let mut state = self.state.lock();
 916        if let Some(message) = state.simulated_index_write_error_message.clone() {
 917            return Err(anyhow::anyhow!(message));
 918        }
 919        if let Some(content) = content {
 920            state.index_contents.insert(path.clone(), content);
 921        } else {
 922            state.index_contents.remove(path);
 923        }
 924        state
 925            .event_emitter
 926            .try_send(state.path.clone())
 927            .expect("Dropped repo change event");
 928        Ok(())
 929    }
 930
 931    fn remote_url(&self, _name: &str) -> Option<String> {
 932        None
 933    }
 934
 935    fn head_sha(&self) -> Option<String> {
 936        None
 937    }
 938
 939    fn merge_head_shas(&self) -> Vec<String> {
 940        vec![]
 941    }
 942
 943    fn show(&self, _: &str) -> Result<CommitDetails> {
 944        unimplemented!()
 945    }
 946
 947    fn reset(&self, _: &str, _: ResetMode) -> Result<()> {
 948        unimplemented!()
 949    }
 950
 951    fn checkout_files(&self, _: &str, _: &[RepoPath]) -> Result<()> {
 952        unimplemented!()
 953    }
 954
 955    fn path(&self) -> PathBuf {
 956        let state = self.state.lock();
 957        state.path.clone()
 958    }
 959
 960    fn main_repository_path(&self) -> PathBuf {
 961        self.path()
 962    }
 963
 964    fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
 965        let state = self.state.lock();
 966
 967        let mut entries = state
 968            .statuses
 969            .iter()
 970            .filter_map(|(repo_path, status)| {
 971                if path_prefixes
 972                    .iter()
 973                    .any(|path_prefix| repo_path.0.starts_with(path_prefix))
 974                {
 975                    Some((repo_path.to_owned(), *status))
 976                } else {
 977                    None
 978                }
 979            })
 980            .collect::<Vec<_>>();
 981        entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
 982
 983        Ok(GitStatus {
 984            entries: entries.into(),
 985        })
 986    }
 987
 988    fn branches(&self) -> Result<Vec<Branch>> {
 989        let state = self.state.lock();
 990        let current_branch = &state.current_branch_name;
 991        Ok(state
 992            .branches
 993            .iter()
 994            .map(|branch_name| Branch {
 995                is_head: Some(branch_name) == current_branch.as_ref(),
 996                name: branch_name.into(),
 997                most_recent_commit: None,
 998                upstream: None,
 999            })
1000            .collect())
1001    }
1002
1003    fn branch_exits(&self, name: &str) -> Result<bool> {
1004        let state = self.state.lock();
1005        Ok(state.branches.contains(name))
1006    }
1007
1008    fn change_branch(&self, name: &str) -> Result<()> {
1009        let mut state = self.state.lock();
1010        state.current_branch_name = Some(name.to_owned());
1011        state
1012            .event_emitter
1013            .try_send(state.path.clone())
1014            .expect("Dropped repo change event");
1015        Ok(())
1016    }
1017
1018    fn create_branch(&self, name: &str) -> Result<()> {
1019        let mut state = self.state.lock();
1020        state.branches.insert(name.to_owned());
1021        state
1022            .event_emitter
1023            .try_send(state.path.clone())
1024            .expect("Dropped repo change event");
1025        Ok(())
1026    }
1027
1028    fn blame(&self, path: &Path, _content: Rope) -> Result<crate::blame::Blame> {
1029        let state = self.state.lock();
1030        state
1031            .blames
1032            .get(path)
1033            .with_context(|| format!("failed to get blame for {:?}", path))
1034            .cloned()
1035    }
1036
1037    fn stage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
1038        unimplemented!()
1039    }
1040
1041    fn unstage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
1042        unimplemented!()
1043    }
1044
1045    fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
1046        unimplemented!()
1047    }
1048
1049    fn push(
1050        &self,
1051        _branch: &str,
1052        _remote: &str,
1053        _options: Option<PushOptions>,
1054        _ask_pass: AskPassSession,
1055    ) -> Result<RemoteCommandOutput> {
1056        unimplemented!()
1057    }
1058
1059    fn pull(
1060        &self,
1061        _branch: &str,
1062        _remote: &str,
1063        _ask_pass: AskPassSession,
1064    ) -> Result<RemoteCommandOutput> {
1065        unimplemented!()
1066    }
1067
1068    fn fetch(&self, _ask_pass: AskPassSession) -> Result<RemoteCommandOutput> {
1069        unimplemented!()
1070    }
1071
1072    fn get_remotes(&self, _branch: Option<&str>) -> Result<Vec<Remote>> {
1073        unimplemented!()
1074    }
1075
1076    fn check_for_pushed_commit(&self) -> Result<Vec<SharedString>> {
1077        unimplemented!()
1078    }
1079
1080    fn diff(&self, _diff: DiffType) -> Result<String> {
1081        unimplemented!()
1082    }
1083}
1084
1085fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
1086    match relative_file_path.components().next() {
1087        None => anyhow::bail!("repo path should not be empty"),
1088        Some(Component::Prefix(_)) => anyhow::bail!(
1089            "repo path `{}` should be relative, not a windows prefix",
1090            relative_file_path.to_string_lossy()
1091        ),
1092        Some(Component::RootDir) => {
1093            anyhow::bail!(
1094                "repo path `{}` should be relative",
1095                relative_file_path.to_string_lossy()
1096            )
1097        }
1098        Some(Component::CurDir) => {
1099            anyhow::bail!(
1100                "repo path `{}` should not start with `.`",
1101                relative_file_path.to_string_lossy()
1102            )
1103        }
1104        Some(Component::ParentDir) => {
1105            anyhow::bail!(
1106                "repo path `{}` should not start with `..`",
1107                relative_file_path.to_string_lossy()
1108            )
1109        }
1110        _ => Ok(()),
1111    }
1112}
1113
1114pub static WORK_DIRECTORY_REPO_PATH: LazyLock<RepoPath> =
1115    LazyLock::new(|| RepoPath(Path::new("").into()));
1116
1117#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
1118pub struct RepoPath(pub Arc<Path>);
1119
1120impl RepoPath {
1121    pub fn new(path: PathBuf) -> Self {
1122        debug_assert!(path.is_relative(), "Repo paths must be relative");
1123
1124        RepoPath(path.into())
1125    }
1126
1127    pub fn from_str(path: &str) -> Self {
1128        let path = Path::new(path);
1129        debug_assert!(path.is_relative(), "Repo paths must be relative");
1130
1131        RepoPath(path.into())
1132    }
1133}
1134
1135impl std::fmt::Display for RepoPath {
1136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1137        self.0.to_string_lossy().fmt(f)
1138    }
1139}
1140
1141impl From<&Path> for RepoPath {
1142    fn from(value: &Path) -> Self {
1143        RepoPath::new(value.into())
1144    }
1145}
1146
1147impl From<Arc<Path>> for RepoPath {
1148    fn from(value: Arc<Path>) -> Self {
1149        RepoPath(value)
1150    }
1151}
1152
1153impl From<PathBuf> for RepoPath {
1154    fn from(value: PathBuf) -> Self {
1155        RepoPath::new(value)
1156    }
1157}
1158
1159impl From<&str> for RepoPath {
1160    fn from(value: &str) -> Self {
1161        Self::from_str(value)
1162    }
1163}
1164
1165impl Default for RepoPath {
1166    fn default() -> Self {
1167        RepoPath(Path::new("").into())
1168    }
1169}
1170
1171impl AsRef<Path> for RepoPath {
1172    fn as_ref(&self) -> &Path {
1173        self.0.as_ref()
1174    }
1175}
1176
1177impl std::ops::Deref for RepoPath {
1178    type Target = Path;
1179
1180    fn deref(&self) -> &Self::Target {
1181        &self.0
1182    }
1183}
1184
1185impl Borrow<Path> for RepoPath {
1186    fn borrow(&self) -> &Path {
1187        self.0.as_ref()
1188    }
1189}
1190
1191#[derive(Debug)]
1192pub struct RepoPathDescendants<'a>(pub &'a Path);
1193
1194impl MapSeekTarget<RepoPath> for RepoPathDescendants<'_> {
1195    fn cmp_cursor(&self, key: &RepoPath) -> Ordering {
1196        if key.starts_with(self.0) {
1197            Ordering::Greater
1198        } else {
1199            self.0.cmp(key)
1200        }
1201    }
1202}
1203
1204fn parse_branch_input(input: &str) -> Result<Vec<Branch>> {
1205    let mut branches = Vec::new();
1206    for line in input.split('\n') {
1207        if line.is_empty() {
1208            continue;
1209        }
1210        let mut fields = line.split('\x00');
1211        let is_current_branch = fields.next().context("no HEAD")? == "*";
1212        let head_sha: SharedString = fields.next().context("no objectname")?.to_string().into();
1213        let ref_name: SharedString = fields
1214            .next()
1215            .context("no refname")?
1216            .strip_prefix("refs/heads/")
1217            .context("unexpected format for refname")?
1218            .to_string()
1219            .into();
1220        let upstream_name = fields.next().context("no upstream")?.to_string();
1221        let upstream_tracking = parse_upstream_track(fields.next().context("no upstream:track")?)?;
1222        let commiterdate = fields.next().context("no committerdate")?.parse::<i64>()?;
1223        let subject: SharedString = fields
1224            .next()
1225            .context("no contents:subject")?
1226            .to_string()
1227            .into();
1228
1229        branches.push(Branch {
1230            is_head: is_current_branch,
1231            name: ref_name,
1232            most_recent_commit: Some(CommitSummary {
1233                sha: head_sha,
1234                subject,
1235                commit_timestamp: commiterdate,
1236            }),
1237            upstream: if upstream_name.is_empty() {
1238                None
1239            } else {
1240                Some(Upstream {
1241                    ref_name: upstream_name.into(),
1242                    tracking: upstream_tracking,
1243                })
1244            },
1245        })
1246    }
1247
1248    Ok(branches)
1249}
1250
1251fn parse_upstream_track(upstream_track: &str) -> Result<UpstreamTracking> {
1252    if upstream_track == "" {
1253        return Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
1254            ahead: 0,
1255            behind: 0,
1256        }));
1257    }
1258
1259    let upstream_track = upstream_track
1260        .strip_prefix("[")
1261        .ok_or_else(|| anyhow!("missing ["))?;
1262    let upstream_track = upstream_track
1263        .strip_suffix("]")
1264        .ok_or_else(|| anyhow!("missing ["))?;
1265    let mut ahead: u32 = 0;
1266    let mut behind: u32 = 0;
1267    for component in upstream_track.split(", ") {
1268        if component == "gone" {
1269            return Ok(UpstreamTracking::Gone);
1270        }
1271        if let Some(ahead_num) = component.strip_prefix("ahead ") {
1272            ahead = ahead_num.parse::<u32>()?;
1273        }
1274        if let Some(behind_num) = component.strip_prefix("behind ") {
1275            behind = behind_num.parse::<u32>()?;
1276        }
1277    }
1278    Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
1279        ahead,
1280        behind,
1281    }))
1282}
1283
1284#[test]
1285fn test_branches_parsing() {
1286    // suppress "help: octal escapes are not supported, `\0` is always null"
1287    #[allow(clippy::octal_escapes)]
1288    let input = "*\0060964da10574cd9bf06463a53bf6e0769c5c45e\0refs/heads/zed-patches\0refs/remotes/origin/zed-patches\0\01733187470\0generated protobuf\n";
1289    assert_eq!(
1290        parse_branch_input(&input).unwrap(),
1291        vec![Branch {
1292            is_head: true,
1293            name: "zed-patches".into(),
1294            upstream: Some(Upstream {
1295                ref_name: "refs/remotes/origin/zed-patches".into(),
1296                tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus {
1297                    ahead: 0,
1298                    behind: 0
1299                })
1300            }),
1301            most_recent_commit: Some(CommitSummary {
1302                sha: "060964da10574cd9bf06463a53bf6e0769c5c45e".into(),
1303                subject: "generated protobuf".into(),
1304                commit_timestamp: 1733187470,
1305            })
1306        }]
1307    )
1308}