From ef6ba9d89bf33b0502fb9cd66bec1aea83758014 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Wed, 1 Apr 2026 15:31:47 -0400 Subject: [PATCH] Implement new git operations in FakeGitRepository - Implement reset() with commit_history tracking (was unimplemented!) - Implement commit() to track history and validate allow_empty - Add FakeCommitSnapshot for commit history management - Implement update_ref, delete_ref, stage_all_including_untracked - Support Option branch name in create_worktree (detached mode) --- crates/fs/src/fake_git_repo.rs | 154 +++++++++++++++++-- crates/fs/tests/integration/fake_git_repo.rs | 12 +- 2 files changed, 150 insertions(+), 16 deletions(-) diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index fc66e27fc9a32c2a8897eb5c9faee917c21177c5..477d83907f26e2694297f9b6d4dafb1aedbf7eb6 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -35,8 +35,16 @@ pub struct FakeGitRepository { pub(crate) is_trusted: Arc, } +#[derive(Debug, Clone)] +pub struct FakeCommitSnapshot { + pub head_contents: HashMap, + pub index_contents: HashMap, + pub sha: String, +} + #[derive(Debug, Clone)] pub struct FakeGitRepositoryState { + pub commit_history: Vec, pub event_emitter: smol::channel::Sender, pub unmerged_paths: HashMap, pub head_contents: HashMap, @@ -72,6 +80,7 @@ impl FakeGitRepositoryState { oids: Default::default(), remotes: HashMap::default(), graph_commits: Vec::new(), + commit_history: Vec::new(), } } } @@ -214,11 +223,52 @@ impl GitRepository for FakeGitRepository { fn reset( &self, - _commit: String, - _mode: ResetMode, + commit: String, + mode: ResetMode, _env: Arc>, ) -> BoxFuture<'_, Result<()>> { - unimplemented!() + self.with_state_async(true, move |state| { + let pop_count = if commit == "HEAD~" { + 1 + } else if let Some(suffix) = commit.strip_prefix("HEAD~") { + suffix + .parse::() + .with_context(|| format!("Invalid HEAD~ offset: {commit}"))? + } else { + match state + .commit_history + .iter() + .rposition(|entry| entry.sha == commit) + { + Some(index) => state.commit_history.len() - index, + None => anyhow::bail!("Unknown commit ref: {commit}"), + } + }; + + if pop_count == 0 || pop_count > state.commit_history.len() { + anyhow::bail!( + "Cannot reset {pop_count} commit(s): only {} in history", + state.commit_history.len() + ); + } + + let target_index = state.commit_history.len() - pop_count; + let snapshot = state.commit_history[target_index].clone(); + state.commit_history.truncate(target_index); + + match mode { + ResetMode::Soft => { + state.head_contents = snapshot.head_contents; + } + ResetMode::Mixed => { + state.head_contents = snapshot.head_contents; + state.index_contents = state.head_contents.clone(); + } + } + + state.refs.insert("HEAD".into(), snapshot.sha); + Ok(()) + }) } fn checkout_files( @@ -483,7 +533,7 @@ impl GitRepository for FakeGitRepository { fn create_worktree( &self, - branch_name: String, + branch_name: Option, path: PathBuf, from_commit: Option, ) -> BoxFuture<'_, Result<()>> { @@ -508,13 +558,22 @@ impl GitRepository for FakeGitRepository { fs.create_dir(&path).await?; // Create .git/worktrees// directory with HEAD, commondir, gitdir. - let ref_name = format!("refs/heads/{branch_name}"); - let worktrees_entry_dir = common_dir_path.join("worktrees").join(&branch_name); + let worktree_entry_name = branch_name + .as_deref() + .unwrap_or_else(|| path.file_name().unwrap().to_str().unwrap()); + let worktrees_entry_dir = common_dir_path.join("worktrees").join(worktree_entry_name); fs.create_dir(&worktrees_entry_dir).await?; + let sha = from_commit.unwrap_or_else(|| "fake-sha".to_string()); + let head_content = if let Some(ref branch_name) = branch_name { + let ref_name = format!("refs/heads/{branch_name}"); + format!("ref: {ref_name}") + } else { + sha.clone() + }; fs.write_file_internal( worktrees_entry_dir.join("HEAD"), - format!("ref: {ref_name}").into_bytes(), + head_content.into_bytes(), false, )?; fs.write_file_internal( @@ -537,10 +596,14 @@ impl GitRepository for FakeGitRepository { )?; // Update git state: add ref and branch. - let sha = from_commit.unwrap_or_else(|| "fake-sha".to_string()); fs.with_git_state(&dot_git_path, true, move |state| { - state.refs.insert(ref_name, sha); - state.branches.insert(branch_name); + if let Some(branch_name) = branch_name { + let ref_name = format!("refs/heads/{branch_name}"); + state.refs.insert(ref_name, sha); + state.branches.insert(branch_name); + } else { + state.refs.insert("HEAD".into(), sha); + } Ok::<(), anyhow::Error>(()) })??; Ok(()) @@ -815,11 +878,29 @@ impl GitRepository for FakeGitRepository { &self, _message: gpui::SharedString, _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>, - _options: CommitOptions, + options: CommitOptions, _askpass: AskPassDelegate, _env: Arc>, ) -> BoxFuture<'_, Result<()>> { - async { Ok(()) }.boxed() + self.with_state_async(true, move |state| { + if !options.allow_empty && state.index_contents == state.head_contents { + anyhow::bail!("nothing to commit (use allow_empty to create an empty commit)"); + } + + let old_sha = state.refs.get("HEAD").cloned().unwrap_or_default(); + state.commit_history.push(FakeCommitSnapshot { + head_contents: state.head_contents.clone(), + index_contents: state.index_contents.clone(), + sha: old_sha, + }); + + state.head_contents = state.index_contents.clone(); + + let new_sha = format!("fake-commit-{}", state.commit_history.len()); + state.refs.insert("HEAD".into(), new_sha); + + Ok(()) + }) } fn run_hook( @@ -1125,6 +1206,55 @@ impl GitRepository for FakeGitRepository { anyhow::bail!("commit_data_reader not supported for FakeGitRepository") } + fn update_ref(&self, ref_name: String, commit: String) -> BoxFuture<'_, Result<()>> { + self.with_state_async(true, move |state| { + state.refs.insert(ref_name, commit); + Ok(()) + }) + } + + fn delete_ref(&self, ref_name: String) -> BoxFuture<'_, Result<()>> { + self.with_state_async(true, move |state| { + state.refs.remove(&ref_name); + Ok(()) + }) + } + + fn stage_all_including_untracked(&self) -> BoxFuture<'_, Result<()>> { + let workdir_path = self.dot_git_path.parent().unwrap(); + let git_files: Vec<(RepoPath, String)> = self + .fs + .files() + .iter() + .filter_map(|path| { + let repo_path = path.strip_prefix(workdir_path).ok()?; + if repo_path.starts_with(".git") { + return None; + } + let content = self + .fs + .read_file_sync(path) + .ok() + .and_then(|bytes| String::from_utf8(bytes).ok())?; + let rel_path = RelPath::new(repo_path, PathStyle::local()).ok()?; + Some((RepoPath::from_rel_path(&rel_path), content)) + }) + .collect(); + + self.with_state_async(true, move |state| { + // Stage all filesystem contents, mirroring `git add -A`. + let fs_paths: HashSet = git_files.iter().map(|(p, _)| p.clone()).collect(); + for (path, content) in git_files { + state.index_contents.insert(path, content); + } + // Remove index entries for files that no longer exist on disk. + state + .index_contents + .retain(|path, _| fs_paths.contains(path)); + Ok(()) + }) + } + fn set_trusted(&self, trusted: bool) { self.is_trusted .store(trusted, std::sync::atomic::Ordering::Release); diff --git a/crates/fs/tests/integration/fake_git_repo.rs b/crates/fs/tests/integration/fake_git_repo.rs index e327f92e996bfa0e89cc60a0a9c0d919bec8bc47..1d7c64a4d07e05bbb16a2864cea1a37248ed5f51 100644 --- a/crates/fs/tests/integration/fake_git_repo.rs +++ b/crates/fs/tests/integration/fake_git_repo.rs @@ -24,7 +24,7 @@ async fn test_fake_worktree_lifecycle(cx: &mut TestAppContext) { // Create a worktree let worktree_1_dir = worktrees_dir.join("feature-branch"); repo.create_worktree( - "feature-branch".to_string(), + Some("feature-branch".to_string()), worktree_1_dir.clone(), Some("abc123".to_string()), ) @@ -47,9 +47,13 @@ async fn test_fake_worktree_lifecycle(cx: &mut TestAppContext) { // Create a second worktree (without explicit commit) let worktree_2_dir = worktrees_dir.join("bugfix-branch"); - repo.create_worktree("bugfix-branch".to_string(), worktree_2_dir.clone(), None) - .await - .unwrap(); + repo.create_worktree( + Some("bugfix-branch".to_string()), + worktree_2_dir.clone(), + None, + ) + .await + .unwrap(); let worktrees = repo.worktrees().await.unwrap(); assert_eq!(worktrees.len(), 3);