Detailed changes
@@ -435,6 +435,7 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::GitCreateRemote>)
.add_request_handler(forward_mutating_project_request::<proto::GitRemoveRemote>)
.add_request_handler(forward_read_only_project_request::<proto::GitGetWorktrees>)
+ .add_request_handler(forward_read_only_project_request::<proto::GitGetHeadSha>)
.add_request_handler(forward_mutating_project_request::<proto::GitCreateWorktree>)
.add_request_handler(disallow_guest_request::<proto::GitRemoveWorktree>)
.add_request_handler(disallow_guest_request::<proto::GitRenameWorktree>)
@@ -424,6 +424,58 @@ async fn test_remote_git_worktrees(
);
}
+#[gpui::test]
+async fn test_remote_git_head_sha(
+ executor: BackgroundExecutor,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+) {
+ let mut server = TestServer::start(executor.clone()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+
+ client_a
+ .fs()
+ .insert_tree(
+ path!("/project"),
+ json!({ ".git": {}, "file.txt": "content" }),
+ )
+ .await;
+
+ let (project_a, _) = client_a.build_local_project(path!("/project"), cx_a).await;
+ let local_head_sha = cx_a.update(|cx| {
+ project_a
+ .read(cx)
+ .active_repository(cx)
+ .unwrap()
+ .update(cx, |repository, _| repository.head_sha())
+ });
+ let local_head_sha = local_head_sha.await.unwrap().unwrap();
+
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ let project_b = client_b.join_remote_project(project_id, cx_b).await;
+
+ executor.run_until_parked();
+
+ let remote_head_sha = cx_b.update(|cx| {
+ project_b
+ .read(cx)
+ .active_repository(cx)
+ .unwrap()
+ .update(cx, |repository, _| repository.head_sha())
+ });
+ let remote_head_sha = remote_head_sha.await.unwrap();
+
+ assert_eq!(remote_head_sha.unwrap(), local_head_sha);
+}
+
#[gpui::test]
async fn test_linked_worktrees_sync(
executor: BackgroundExecutor,
@@ -36,8 +36,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>,
@@ -74,6 +82,7 @@ impl FakeGitRepositoryState {
oids: Default::default(),
remotes: HashMap::default(),
graph_commits: Vec::new(),
+ commit_history: Vec::new(),
stash_entries: Default::default(),
}
}
@@ -217,11 +226,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~" || 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(
@@ -490,7 +540,7 @@ impl GitRepository for FakeGitRepository {
fn create_worktree(
&self,
- branch_name: String,
+ branch_name: Option<String>,
path: PathBuf,
from_commit: Option<String>,
) -> BoxFuture<'_, Result<()>> {
@@ -505,8 +555,10 @@ impl GitRepository for FakeGitRepository {
if let Some(message) = &state.simulated_create_worktree_error {
anyhow::bail!("{message}");
}
- if state.branches.contains(&branch_name) {
- bail!("a branch named '{}' already exists", branch_name);
+ if let Some(ref name) = branch_name {
+ if state.branches.contains(name) {
+ bail!("a branch named '{}' already exists", name);
+ }
}
Ok(())
})??;
@@ -515,13 +567,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(
@@ -544,10 +605,12 @@ 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);
+ }
Ok::<(), anyhow::Error>(())
})??;
Ok(())
@@ -822,11 +885,30 @@ 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 && !options.amend && 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(
@@ -1210,6 +1292,24 @@ 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 repair_worktrees(&self) -> BoxFuture<'_, Result<()>> {
+ async { Ok(()) }.boxed()
+ }
+
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);
@@ -329,6 +329,7 @@ impl Upstream {
pub struct CommitOptions {
pub amend: bool,
pub signoff: bool,
+ pub allow_empty: bool,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
@@ -715,7 +716,7 @@ pub trait GitRepository: Send + Sync {
fn create_worktree(
&self,
- branch_name: String,
+ branch_name: Option<String>,
path: PathBuf,
from_commit: Option<String>,
) -> BoxFuture<'_, Result<()>>;
@@ -916,6 +917,12 @@ pub trait GitRepository: Send + Sync {
fn commit_data_reader(&self) -> Result<CommitDataReader>;
+ fn update_ref(&self, ref_name: String, commit: String) -> BoxFuture<'_, Result<()>>;
+
+ fn delete_ref(&self, ref_name: String) -> BoxFuture<'_, Result<()>>;
+
+ fn repair_worktrees(&self) -> BoxFuture<'_, Result<()>>;
+
fn set_trusted(&self, trusted: bool);
fn is_trusted(&self) -> bool;
}
@@ -1660,19 +1667,20 @@ impl GitRepository for RealGitRepository {
fn create_worktree(
&self,
- branch_name: String,
+ branch_name: Option<String>,
path: PathBuf,
from_commit: Option<String>,
) -> BoxFuture<'_, Result<()>> {
let git_binary = self.git_binary();
- let mut args = vec![
- OsString::from("worktree"),
- OsString::from("add"),
- OsString::from("-b"),
- OsString::from(branch_name.as_str()),
- OsString::from("--"),
- OsString::from(path.as_os_str()),
- ];
+ let mut args = vec![OsString::from("worktree"), OsString::from("add")];
+ if let Some(branch_name) = &branch_name {
+ args.push(OsString::from("-b"));
+ args.push(OsString::from(branch_name.as_str()));
+ } else {
+ args.push(OsString::from("--detach"));
+ }
+ args.push(OsString::from("--"));
+ args.push(OsString::from(path.as_os_str()));
if let Some(from_commit) = from_commit {
args.push(OsString::from(from_commit));
} else {
@@ -2165,6 +2173,10 @@ impl GitRepository for RealGitRepository {
cmd.arg("--signoff");
}
+ if options.allow_empty {
+ cmd.arg("--allow-empty");
+ }
+
if let Some((name, email)) = name_and_email {
cmd.arg("--author").arg(&format!("{name} <{email}>"));
}
@@ -2176,6 +2188,39 @@ impl GitRepository for RealGitRepository {
.boxed()
}
+ fn update_ref(&self, ref_name: String, commit: String) -> BoxFuture<'_, Result<()>> {
+ let git_binary = self.git_binary();
+ self.executor
+ .spawn(async move {
+ let args: Vec<OsString> = vec!["update-ref".into(), ref_name.into(), commit.into()];
+ git_binary?.run(&args).await?;
+ Ok(())
+ })
+ .boxed()
+ }
+
+ fn delete_ref(&self, ref_name: String) -> BoxFuture<'_, Result<()>> {
+ let git_binary = self.git_binary();
+ self.executor
+ .spawn(async move {
+ let args: Vec<OsString> = vec!["update-ref".into(), "-d".into(), ref_name.into()];
+ git_binary?.run(&args).await?;
+ Ok(())
+ })
+ .boxed()
+ }
+
+ fn repair_worktrees(&self) -> BoxFuture<'_, Result<()>> {
+ let git_binary = self.git_binary();
+ self.executor
+ .spawn(async move {
+ let args: Vec<OsString> = vec!["worktree".into(), "repair".into()];
+ git_binary?.run(&args).await?;
+ Ok(())
+ })
+ .boxed()
+ }
+
fn push(
&self,
branch_name: String,
@@ -4009,7 +4054,7 @@ mod tests {
// Create a new worktree
repo.create_worktree(
- "test-branch".to_string(),
+ Some("test-branch".to_string()),
worktree_path.clone(),
Some("HEAD".to_string()),
)
@@ -4068,7 +4113,7 @@ mod tests {
// Create a worktree
let worktree_path = worktrees_dir.join("worktree-to-remove");
repo.create_worktree(
- "to-remove".to_string(),
+ Some("to-remove".to_string()),
worktree_path.clone(),
Some("HEAD".to_string()),
)
@@ -4092,7 +4137,7 @@ mod tests {
// Create a worktree
let worktree_path = worktrees_dir.join("dirty-wt");
repo.create_worktree(
- "dirty-wt".to_string(),
+ Some("dirty-wt".to_string()),
worktree_path.clone(),
Some("HEAD".to_string()),
)
@@ -4162,7 +4207,7 @@ mod tests {
// Create a worktree
let old_path = worktrees_dir.join("old-worktree-name");
repo.create_worktree(
- "old-name".to_string(),
+ Some("old-name".to_string()),
old_path.clone(),
Some("HEAD".to_string()),
)
@@ -453,6 +453,7 @@ impl CommitModal {
CommitOptions {
amend: is_amend_pending,
signoff: is_signoff_enabled,
+ allow_empty: false,
},
window,
cx,
@@ -2155,6 +2155,7 @@ impl GitPanel {
CommitOptions {
amend: false,
signoff: self.signoff_enabled,
+ allow_empty: false,
},
window,
cx,
@@ -2195,6 +2196,7 @@ impl GitPanel {
CommitOptions {
amend: true,
signoff: self.signoff_enabled,
+ allow_empty: false,
},
window,
cx,
@@ -4454,7 +4456,11 @@ impl GitPanel {
git_panel
.update(cx, |git_panel, cx| {
git_panel.commit_changes(
- CommitOptions { amend, signoff },
+ CommitOptions {
+ amend,
+ signoff,
+ allow_empty: false,
+ },
window,
cx,
);
@@ -329,6 +329,12 @@ pub struct GraphDataResponse<'a> {
pub error: Option<SharedString>,
}
+#[derive(Clone, Debug)]
+enum CreateWorktreeStartPoint {
+ Detached,
+ Branched { name: String },
+}
+
pub struct Repository {
this: WeakEntity<Self>,
snapshot: RepositorySnapshot,
@@ -588,6 +594,7 @@ impl GitStore {
client.add_entity_request_handler(Self::handle_create_worktree);
client.add_entity_request_handler(Self::handle_remove_worktree);
client.add_entity_request_handler(Self::handle_rename_worktree);
+ client.add_entity_request_handler(Self::handle_get_head_sha);
}
pub fn is_local(&self) -> bool {
@@ -2340,6 +2347,7 @@ impl GitStore {
CommitOptions {
amend: options.amend,
signoff: options.signoff,
+ allow_empty: options.allow_empty,
},
askpass,
cx,
@@ -2406,12 +2414,18 @@ impl GitStore {
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
let directory = PathBuf::from(envelope.payload.directory);
- let name = envelope.payload.name;
+ let start_point = if envelope.payload.name.is_empty() {
+ CreateWorktreeStartPoint::Detached
+ } else {
+ CreateWorktreeStartPoint::Branched {
+ name: envelope.payload.name,
+ }
+ };
let commit = envelope.payload.commit;
repository_handle
.update(&mut cx, |repository_handle, _| {
- repository_handle.create_worktree(name, directory, commit)
+ repository_handle.create_worktree_with_start_point(start_point, directory, commit)
})
.await??;
@@ -2456,6 +2470,21 @@ impl GitStore {
Ok(proto::Ack {})
}
+ async fn handle_get_head_sha(
+ this: Entity<Self>,
+ envelope: TypedEnvelope<proto::GitGetHeadSha>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::GitGetHeadShaResponse> {
+ let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
+ let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
+
+ let head_sha = repository_handle
+ .update(&mut cx, |repository_handle, _| repository_handle.head_sha())
+ .await??;
+
+ Ok(proto::GitGetHeadShaResponse { sha: head_sha })
+ }
+
async fn handle_get_branches(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GitGetBranches>,
@@ -5493,6 +5522,7 @@ impl Repository {
options: Some(proto::commit::CommitOptions {
amend: options.amend,
signoff: options.signoff,
+ allow_empty: options.allow_empty,
}),
askpass_id,
})
@@ -5974,36 +6004,174 @@ impl Repository {
})
}
+ fn create_worktree_with_start_point(
+ &mut self,
+ start_point: CreateWorktreeStartPoint,
+ path: PathBuf,
+ commit: Option<String>,
+ ) -> oneshot::Receiver<Result<()>> {
+ if matches!(
+ &start_point,
+ CreateWorktreeStartPoint::Branched { name } if name.is_empty()
+ ) {
+ let (sender, receiver) = oneshot::channel();
+ sender
+ .send(Err(anyhow!("branch name cannot be empty")))
+ .ok();
+ return receiver;
+ }
+
+ let id = self.id;
+ let message = match &start_point {
+ CreateWorktreeStartPoint::Detached => "git worktree add (detached)".into(),
+ CreateWorktreeStartPoint::Branched { name } => {
+ format!("git worktree add: {name}").into()
+ }
+ };
+
+ self.send_job(Some(message), move |repo, _cx| async move {
+ let branch_name = match start_point {
+ CreateWorktreeStartPoint::Detached => None,
+ CreateWorktreeStartPoint::Branched { name } => Some(name),
+ };
+ let remote_name = branch_name.clone().unwrap_or_default();
+
+ match repo {
+ RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
+ backend.create_worktree(branch_name, path, commit).await
+ }
+ RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
+ client
+ .request(proto::GitCreateWorktree {
+ project_id: project_id.0,
+ repository_id: id.to_proto(),
+ name: remote_name,
+ directory: path.to_string_lossy().to_string(),
+ commit,
+ })
+ .await?;
+
+ Ok(())
+ }
+ }
+ })
+ }
+
pub fn create_worktree(
&mut self,
branch_name: String,
path: PathBuf,
commit: Option<String>,
) -> oneshot::Receiver<Result<()>> {
+ self.create_worktree_with_start_point(
+ CreateWorktreeStartPoint::Branched { name: branch_name },
+ path,
+ commit,
+ )
+ }
+
+ pub fn create_worktree_detached(
+ &mut self,
+ path: PathBuf,
+ commit: String,
+ ) -> oneshot::Receiver<Result<()>> {
+ self.create_worktree_with_start_point(
+ CreateWorktreeStartPoint::Detached,
+ path,
+ Some(commit),
+ )
+ }
+
+ pub fn head_sha(&mut self) -> oneshot::Receiver<Result<Option<String>>> {
let id = self.id;
- self.send_job(
- Some(format!("git worktree add: {}", branch_name).into()),
- move |repo, _cx| async move {
- match repo {
- RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
- backend.create_worktree(branch_name, path, commit).await
- }
- RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
- client
- .request(proto::GitCreateWorktree {
- project_id: project_id.0,
- repository_id: id.to_proto(),
- name: branch_name,
- directory: path.to_string_lossy().to_string(),
- commit,
- })
- .await?;
+ self.send_job(None, move |repo, _cx| async move {
+ match repo {
+ RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
+ Ok(backend.head_sha().await)
+ }
+ RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
+ let response = client
+ .request(proto::GitGetHeadSha {
+ project_id: project_id.0,
+ repository_id: id.to_proto(),
+ })
+ .await?;
- Ok(())
- }
+ Ok(response.sha)
}
- },
- )
+ }
+ })
+ }
+
+ pub fn update_ref(
+ &mut self,
+ ref_name: String,
+ commit: String,
+ ) -> oneshot::Receiver<Result<()>> {
+ self.send_job(None, move |repo, _cx| async move {
+ match repo {
+ RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
+ backend.update_ref(ref_name, commit).await
+ }
+ RepositoryState::Remote(_) => {
+ anyhow::bail!("update_ref is not supported for remote repositories")
+ }
+ }
+ })
+ }
+
+ pub fn delete_ref(&mut self, ref_name: String) -> oneshot::Receiver<Result<()>> {
+ self.send_job(None, move |repo, _cx| async move {
+ match repo {
+ RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
+ backend.delete_ref(ref_name).await
+ }
+ RepositoryState::Remote(_) => {
+ anyhow::bail!("delete_ref is not supported for remote repositories")
+ }
+ }
+ })
+ }
+
+ pub fn resolve_commit(&mut self, sha: String) -> oneshot::Receiver<Result<bool>> {
+ self.send_job(None, move |repo, _cx| async move {
+ match repo {
+ RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
+ let results = backend.revparse_batch(vec![sha]).await?;
+ Ok(results.into_iter().next().flatten().is_some())
+ }
+ RepositoryState::Remote(_) => {
+ anyhow::bail!("resolve_commit is not supported for remote repositories")
+ }
+ }
+ })
+ }
+
+ pub fn repair_worktrees(&mut self) -> oneshot::Receiver<Result<()>> {
+ self.send_job(None, move |repo, _cx| async move {
+ match repo {
+ RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
+ backend.repair_worktrees().await
+ }
+ RepositoryState::Remote(_) => {
+ anyhow::bail!("repair_worktrees is not supported for remote repositories")
+ }
+ }
+ })
+ }
+
+ pub fn commit_exists(&mut self, sha: String) -> oneshot::Receiver<Result<bool>> {
+ self.send_job(None, move |repo, _cx| async move {
+ match repo {
+ RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
+ let results = backend.revparse_batch(vec![sha]).await?;
+ Ok(results.into_iter().next().flatten().is_some())
+ }
+ RepositoryState::Remote(_) => {
+ anyhow::bail!("commit_exists is not supported for remote repositories")
+ }
+ }
+ })
}
pub fn remove_worktree(&mut self, path: PathBuf, force: bool) -> oneshot::Receiver<Result<()>> {
@@ -403,6 +403,7 @@ message Commit {
message CommitOptions {
bool amend = 1;
bool signoff = 2;
+ bool allow_empty = 3;
}
}
@@ -567,6 +568,15 @@ message GitGetWorktrees {
uint64 repository_id = 2;
}
+message GitGetHeadSha {
+ uint64 project_id = 1;
+ uint64 repository_id = 2;
+}
+
+message GitGetHeadShaResponse {
+ optional string sha = 1;
+}
+
message GitWorktreesResponse {
repeated Worktree worktrees = 1;
}
@@ -474,7 +474,9 @@ message Envelope {
GitCompareCheckpoints git_compare_checkpoints = 436;
GitCompareCheckpointsResponse git_compare_checkpoints_response = 437;
GitDiffCheckpoints git_diff_checkpoints = 438;
- GitDiffCheckpointsResponse git_diff_checkpoints_response = 439; // current max
+ GitDiffCheckpointsResponse git_diff_checkpoints_response = 439;
+ GitGetHeadSha git_get_head_sha = 440;
+ GitGetHeadShaResponse git_get_head_sha_response = 441; // current max
}
reserved 87 to 88;
@@ -351,6 +351,8 @@ messages!(
(NewExternalAgentVersionAvailable, Background),
(RemoteStarted, Background),
(GitGetWorktrees, Background),
+ (GitGetHeadSha, Background),
+ (GitGetHeadShaResponse, Background),
(GitWorktreesResponse, Background),
(GitCreateWorktree, Background),
(GitRemoveWorktree, Background),
@@ -558,6 +560,7 @@ request_messages!(
(GetContextServerCommand, ContextServerCommand),
(RemoteStarted, Ack),
(GitGetWorktrees, GitWorktreesResponse),
+ (GitGetHeadSha, GitGetHeadShaResponse),
(GitCreateWorktree, Ack),
(GitRemoveWorktree, Ack),
(GitRenameWorktree, Ack),
@@ -749,6 +752,7 @@ entity_messages!(
ExternalAgentLoadingStatusUpdated,
NewExternalAgentVersionAvailable,
GitGetWorktrees,
+ GitGetHeadSha,
GitCreateWorktree,
GitRemoveWorktree,
GitRenameWorktree,