@@ -35,8 +35,16 @@ pub struct FakeGitRepository {
pub(crate) is_trusted: Arc<AtomicBool>,
}
+#[derive(Debug, Clone)]
+pub struct FakeCommitSnapshot {
+ pub head_contents: HashMap<RepoPath, String>,
+ pub index_contents: HashMap<RepoPath, String>,
+ pub sha: String,
+}
+
#[derive(Debug, Clone)]
pub struct FakeGitRepositoryState {
+ pub commit_history: Vec<FakeCommitSnapshot>,
pub event_emitter: smol::channel::Sender<PathBuf>,
pub unmerged_paths: HashMap<RepoPath, UnmergedStatus>,
pub head_contents: HashMap<RepoPath, String>,
@@ -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<HashMap<String, String>>,
) -> 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::<usize>()
+ .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<String>,
path: PathBuf,
from_commit: Option<String>,
) -> BoxFuture<'_, Result<()>> {
@@ -508,13 +558,22 @@ impl GitRepository for FakeGitRepository {
fs.create_dir(&path).await?;
// Create .git/worktrees/<name>/ 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<HashMap<String, String>>,
) -> 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<RepoPath> = 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);
@@ -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);