repository.rs

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