Add support for git branches on remote projects (#19755)
Release Notes: - Fixed a bug where the branch switcher could not be used remotely.
This commit is contained in:
parent
5506669b06
commit
c69da2df70
25 changed files with 993 additions and 127 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -12843,6 +12843,7 @@ dependencies = [
|
||||||
"git",
|
"git",
|
||||||
"gpui",
|
"gpui",
|
||||||
"picker",
|
"picker",
|
||||||
|
"project",
|
||||||
"ui",
|
"ui",
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
|
|
|
@ -308,6 +308,8 @@ impl Server {
|
||||||
.add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
|
.add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
|
||||||
.add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
|
.add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
|
||||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
|
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
|
||||||
|
.add_request_handler(forward_read_only_project_request::<proto::GitBranches>)
|
||||||
|
.add_request_handler(forward_mutating_project_request::<proto::UpdateGitBranch>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::GetCompletions>)
|
.add_request_handler(forward_mutating_project_request::<proto::GetCompletions>)
|
||||||
.add_request_handler(
|
.add_request_handler(
|
||||||
forward_mutating_project_request::<proto::ApplyCompletionAdditionalEdits>,
|
forward_mutating_project_request::<proto::ApplyCompletionAdditionalEdits>,
|
||||||
|
|
|
@ -6575,3 +6575,95 @@ async fn test_context_collaboration_with_reconnect(
|
||||||
assert!(context.buffer().read(cx).read_only());
|
assert!(context.buffer().read(cx).read_only());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_remote_git_branches(
|
||||||
|
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("/project", serde_json::json!({ ".git":{} }))
|
||||||
|
.await;
|
||||||
|
let branches = ["main", "dev", "feature-1"];
|
||||||
|
client_a
|
||||||
|
.fs()
|
||||||
|
.insert_branches(Path::new("/project/.git"), &branches);
|
||||||
|
|
||||||
|
let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await;
|
||||||
|
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;
|
||||||
|
|
||||||
|
let root_path = ProjectPath::root_path(worktree_id);
|
||||||
|
// Client A sees that a guest has joined.
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
let branches_b = cx_b
|
||||||
|
.update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let new_branch = branches[2];
|
||||||
|
|
||||||
|
let branches_b = branches_b
|
||||||
|
.into_iter()
|
||||||
|
.map(|branch| branch.name)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(&branches_b, &branches);
|
||||||
|
|
||||||
|
cx_b.update(|cx| {
|
||||||
|
project_b.update(cx, |project, cx| {
|
||||||
|
project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
let host_branch = cx_a.update(|cx| {
|
||||||
|
project_a.update(cx, |project, cx| {
|
||||||
|
project.worktree_store().update(cx, |worktree_store, cx| {
|
||||||
|
worktree_store
|
||||||
|
.current_branch(root_path.clone(), cx)
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(host_branch.as_ref(), branches[2]);
|
||||||
|
|
||||||
|
// Also try creating a new branch
|
||||||
|
cx_b.update(|cx| {
|
||||||
|
project_b.update(cx, |project, cx| {
|
||||||
|
project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
let host_branch = cx_a.update(|cx| {
|
||||||
|
project_a.update(cx, |project, cx| {
|
||||||
|
project.worktree_store().update(cx, |worktree_store, cx| {
|
||||||
|
worktree_store.current_branch(root_path, cx).unwrap()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(host_branch.as_ref(), "totally-new-branch");
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::tests::TestServer;
|
use crate::tests::TestServer;
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use fs::{FakeFs, Fs as _};
|
use fs::{FakeFs, Fs as _};
|
||||||
use gpui::{Context as _, TestAppContext};
|
use gpui::{BackgroundExecutor, Context as _, TestAppContext};
|
||||||
use http_client::BlockedHttpClient;
|
use http_client::BlockedHttpClient;
|
||||||
use language::{language_settings::language_settings, LanguageRegistry};
|
use language::{language_settings::language_settings, LanguageRegistry};
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
|
@ -174,3 +174,133 @@ async fn test_sharing_an_ssh_remote_project(
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_ssh_collaboration_git_branches(
|
||||||
|
executor: BackgroundExecutor,
|
||||||
|
cx_a: &mut TestAppContext,
|
||||||
|
cx_b: &mut TestAppContext,
|
||||||
|
server_cx: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
cx_a.set_name("a");
|
||||||
|
cx_b.set_name("b");
|
||||||
|
server_cx.set_name("server");
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Set up project on remote FS
|
||||||
|
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
|
||||||
|
let remote_fs = FakeFs::new(server_cx.executor());
|
||||||
|
remote_fs
|
||||||
|
.insert_tree("/project", serde_json::json!({ ".git":{} }))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let branches = ["main", "dev", "feature-1"];
|
||||||
|
remote_fs.insert_branches(Path::new("/project/.git"), &branches);
|
||||||
|
|
||||||
|
// User A connects to the remote project via SSH.
|
||||||
|
server_cx.update(HeadlessProject::init);
|
||||||
|
let remote_http_client = Arc::new(BlockedHttpClient);
|
||||||
|
let node = NodeRuntime::unavailable();
|
||||||
|
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
|
||||||
|
let headless_project = server_cx.new_model(|cx| {
|
||||||
|
client::init_settings(cx);
|
||||||
|
HeadlessProject::new(
|
||||||
|
HeadlessAppState {
|
||||||
|
session: server_ssh,
|
||||||
|
fs: remote_fs.clone(),
|
||||||
|
http_client: remote_http_client,
|
||||||
|
node_runtime: node,
|
||||||
|
languages,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
|
||||||
|
let (project_a, worktree_id) = client_a
|
||||||
|
.build_ssh_project("/project", client_ssh, cx_a)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// While the SSH worktree is being scanned, user A shares the remote project.
|
||||||
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
let project_id = active_call_a
|
||||||
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// User B joins the project.
|
||||||
|
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||||
|
|
||||||
|
// Give client A sometime to see that B has joined, and that the headless server
|
||||||
|
// has some git repositories
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
let root_path = ProjectPath::root_path(worktree_id);
|
||||||
|
|
||||||
|
let branches_b = cx_b
|
||||||
|
.update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let new_branch = branches[2];
|
||||||
|
|
||||||
|
let branches_b = branches_b
|
||||||
|
.into_iter()
|
||||||
|
.map(|branch| branch.name)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(&branches_b, &branches);
|
||||||
|
|
||||||
|
cx_b.update(|cx| {
|
||||||
|
project_b.update(cx, |project, cx| {
|
||||||
|
project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
let server_branch = server_cx.update(|cx| {
|
||||||
|
headless_project.update(cx, |headless_project, cx| {
|
||||||
|
headless_project
|
||||||
|
.worktree_store
|
||||||
|
.update(cx, |worktree_store, cx| {
|
||||||
|
worktree_store
|
||||||
|
.current_branch(root_path.clone(), cx)
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(server_branch.as_ref(), branches[2]);
|
||||||
|
|
||||||
|
// Also try creating a new branch
|
||||||
|
cx_b.update(|cx| {
|
||||||
|
project_b.update(cx, |project, cx| {
|
||||||
|
project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
let server_branch = server_cx.update(|cx| {
|
||||||
|
headless_project.update(cx, |headless_project, cx| {
|
||||||
|
headless_project
|
||||||
|
.worktree_store
|
||||||
|
.update(cx, |worktree_store, cx| {
|
||||||
|
worktree_store.current_branch(root_path, cx).unwrap()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(server_branch.as_ref(), "totally-new-branch");
|
||||||
|
}
|
||||||
|
|
|
@ -813,6 +813,7 @@ struct FakeFsState {
|
||||||
root: Arc<Mutex<FakeFsEntry>>,
|
root: Arc<Mutex<FakeFsEntry>>,
|
||||||
next_inode: u64,
|
next_inode: u64,
|
||||||
next_mtime: SystemTime,
|
next_mtime: SystemTime,
|
||||||
|
git_event_tx: smol::channel::Sender<PathBuf>,
|
||||||
event_txs: Vec<smol::channel::Sender<Vec<PathEvent>>>,
|
event_txs: Vec<smol::channel::Sender<Vec<PathEvent>>>,
|
||||||
events_paused: bool,
|
events_paused: bool,
|
||||||
buffered_events: Vec<PathEvent>,
|
buffered_events: Vec<PathEvent>,
|
||||||
|
@ -969,8 +970,10 @@ impl FakeFs {
|
||||||
const SYSTEMTIME_INTERVAL: u64 = 100;
|
const SYSTEMTIME_INTERVAL: u64 = 100;
|
||||||
|
|
||||||
pub fn new(executor: gpui::BackgroundExecutor) -> Arc<Self> {
|
pub fn new(executor: gpui::BackgroundExecutor) -> Arc<Self> {
|
||||||
Arc::new(Self {
|
let (tx, mut rx) = smol::channel::bounded::<PathBuf>(10);
|
||||||
executor,
|
|
||||||
|
let this = Arc::new(Self {
|
||||||
|
executor: executor.clone(),
|
||||||
state: Mutex::new(FakeFsState {
|
state: Mutex::new(FakeFsState {
|
||||||
root: Arc::new(Mutex::new(FakeFsEntry::Dir {
|
root: Arc::new(Mutex::new(FakeFsEntry::Dir {
|
||||||
inode: 0,
|
inode: 0,
|
||||||
|
@ -979,6 +982,7 @@ impl FakeFs {
|
||||||
entries: Default::default(),
|
entries: Default::default(),
|
||||||
git_repo_state: None,
|
git_repo_state: None,
|
||||||
})),
|
})),
|
||||||
|
git_event_tx: tx,
|
||||||
next_mtime: SystemTime::UNIX_EPOCH,
|
next_mtime: SystemTime::UNIX_EPOCH,
|
||||||
next_inode: 1,
|
next_inode: 1,
|
||||||
event_txs: Default::default(),
|
event_txs: Default::default(),
|
||||||
|
@ -987,7 +991,22 @@ impl FakeFs {
|
||||||
read_dir_call_count: 0,
|
read_dir_call_count: 0,
|
||||||
metadata_call_count: 0,
|
metadata_call_count: 0,
|
||||||
}),
|
}),
|
||||||
})
|
});
|
||||||
|
|
||||||
|
executor.spawn({
|
||||||
|
let this = this.clone();
|
||||||
|
async move {
|
||||||
|
while let Some(git_event) = rx.next().await {
|
||||||
|
if let Some(mut state) = this.state.try_lock() {
|
||||||
|
state.emit_event([(git_event, None)]);
|
||||||
|
} else {
|
||||||
|
panic!("Failed to lock file system state, this execution would have caused a test hang");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).detach();
|
||||||
|
|
||||||
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_next_mtime(&self, next_mtime: SystemTime) {
|
pub fn set_next_mtime(&self, next_mtime: SystemTime) {
|
||||||
|
@ -1181,7 +1200,12 @@ impl FakeFs {
|
||||||
let mut entry = entry.lock();
|
let mut entry = entry.lock();
|
||||||
|
|
||||||
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
|
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
|
||||||
let repo_state = git_repo_state.get_or_insert_with(Default::default);
|
let repo_state = git_repo_state.get_or_insert_with(|| {
|
||||||
|
Arc::new(Mutex::new(FakeGitRepositoryState::new(
|
||||||
|
dot_git.to_path_buf(),
|
||||||
|
state.git_event_tx.clone(),
|
||||||
|
)))
|
||||||
|
});
|
||||||
let mut repo_state = repo_state.lock();
|
let mut repo_state = repo_state.lock();
|
||||||
|
|
||||||
f(&mut repo_state);
|
f(&mut repo_state);
|
||||||
|
@ -1196,7 +1220,22 @@ impl FakeFs {
|
||||||
|
|
||||||
pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
|
pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
|
||||||
self.with_git_state(dot_git, true, |state| {
|
self.with_git_state(dot_git, true, |state| {
|
||||||
state.branch_name = branch.map(Into::into)
|
let branch = branch.map(Into::into);
|
||||||
|
state.branches.extend(branch.clone());
|
||||||
|
state.current_branch_name = branch.map(Into::into)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_branches(&self, dot_git: &Path, branches: &[&str]) {
|
||||||
|
self.with_git_state(dot_git, true, |state| {
|
||||||
|
if let Some(first) = branches.first() {
|
||||||
|
if state.current_branch_name.is_none() {
|
||||||
|
state.current_branch_name = Some(first.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state
|
||||||
|
.branches
|
||||||
|
.extend(branches.iter().map(ToString::to_string));
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1836,7 +1875,12 @@ impl Fs for FakeFs {
|
||||||
let mut entry = entry.lock();
|
let mut entry = entry.lock();
|
||||||
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
|
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
|
||||||
let state = git_repo_state
|
let state = git_repo_state
|
||||||
.get_or_insert_with(|| Arc::new(Mutex::new(FakeGitRepositoryState::default())))
|
.get_or_insert_with(|| {
|
||||||
|
Arc::new(Mutex::new(FakeGitRepositoryState::new(
|
||||||
|
abs_dot_git.to_path_buf(),
|
||||||
|
state.git_event_tx.clone(),
|
||||||
|
)))
|
||||||
|
})
|
||||||
.clone();
|
.clone();
|
||||||
Some(git::repository::FakeGitRepository::open(state))
|
Some(git::repository::FakeGitRepository::open(state))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use crate::GitHostingProviderRegistry;
|
use crate::GitHostingProviderRegistry;
|
||||||
use crate::{blame::Blame, status::GitStatus};
|
use crate::{blame::Blame, status::GitStatus};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use collections::HashMap;
|
use collections::{HashMap, HashSet};
|
||||||
use git2::BranchType;
|
use git2::BranchType;
|
||||||
|
use gpui::SharedString;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -17,7 +18,7 @@ use util::ResultExt;
|
||||||
#[derive(Clone, Debug, Hash, PartialEq)]
|
#[derive(Clone, Debug, Hash, PartialEq)]
|
||||||
pub struct Branch {
|
pub struct Branch {
|
||||||
pub is_head: bool,
|
pub is_head: bool,
|
||||||
pub name: Box<str>,
|
pub name: SharedString,
|
||||||
/// Timestamp of most recent commit, normalized to Unix Epoch format.
|
/// Timestamp of most recent commit, normalized to Unix Epoch format.
|
||||||
pub unix_timestamp: Option<i64>,
|
pub unix_timestamp: Option<i64>,
|
||||||
}
|
}
|
||||||
|
@ -41,6 +42,7 @@ pub trait GitRepository: Send + Sync {
|
||||||
fn branches(&self) -> Result<Vec<Branch>>;
|
fn branches(&self) -> Result<Vec<Branch>>;
|
||||||
fn change_branch(&self, _: &str) -> Result<()>;
|
fn change_branch(&self, _: &str) -> Result<()>;
|
||||||
fn create_branch(&self, _: &str) -> Result<()>;
|
fn create_branch(&self, _: &str) -> Result<()>;
|
||||||
|
fn branch_exits(&self, _: &str) -> Result<bool>;
|
||||||
|
|
||||||
fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>;
|
fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>;
|
||||||
}
|
}
|
||||||
|
@ -132,6 +134,18 @@ impl GitRepository for RealGitRepository {
|
||||||
GitStatus::new(&self.git_binary_path, &working_directory, path_prefixes)
|
GitStatus::new(&self.git_binary_path, &working_directory, path_prefixes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn branch_exits(&self, name: &str) -> Result<bool> {
|
||||||
|
let repo = self.repository.lock();
|
||||||
|
let branch = repo.find_branch(name, BranchType::Local);
|
||||||
|
match branch {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(e) => match e.code() {
|
||||||
|
git2::ErrorCode::NotFound => Ok(false),
|
||||||
|
_ => Err(anyhow::anyhow!(e)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn branches(&self) -> Result<Vec<Branch>> {
|
fn branches(&self) -> Result<Vec<Branch>> {
|
||||||
let repo = self.repository.lock();
|
let repo = self.repository.lock();
|
||||||
let local_branches = repo.branches(Some(BranchType::Local))?;
|
let local_branches = repo.branches(Some(BranchType::Local))?;
|
||||||
|
@ -139,7 +153,11 @@ impl GitRepository for RealGitRepository {
|
||||||
.filter_map(|branch| {
|
.filter_map(|branch| {
|
||||||
branch.ok().and_then(|(branch, _)| {
|
branch.ok().and_then(|(branch, _)| {
|
||||||
let is_head = branch.is_head();
|
let is_head = branch.is_head();
|
||||||
let name = branch.name().ok().flatten().map(Box::from)?;
|
let name = branch
|
||||||
|
.name()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.map(|name| name.to_string().into())?;
|
||||||
let timestamp = branch.get().peel_to_commit().ok()?.time();
|
let timestamp = branch.get().peel_to_commit().ok()?.time();
|
||||||
let unix_timestamp = timestamp.seconds();
|
let unix_timestamp = timestamp.seconds();
|
||||||
let timezone_offset = timestamp.offset_minutes();
|
let timezone_offset = timestamp.offset_minutes();
|
||||||
|
@ -201,17 +219,20 @@ impl GitRepository for RealGitRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FakeGitRepository {
|
pub struct FakeGitRepository {
|
||||||
state: Arc<Mutex<FakeGitRepositoryState>>,
|
state: Arc<Mutex<FakeGitRepositoryState>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FakeGitRepositoryState {
|
pub struct FakeGitRepositoryState {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub event_emitter: smol::channel::Sender<PathBuf>,
|
||||||
pub index_contents: HashMap<PathBuf, String>,
|
pub index_contents: HashMap<PathBuf, String>,
|
||||||
pub blames: HashMap<PathBuf, Blame>,
|
pub blames: HashMap<PathBuf, Blame>,
|
||||||
pub worktree_statuses: HashMap<RepoPath, GitFileStatus>,
|
pub worktree_statuses: HashMap<RepoPath, GitFileStatus>,
|
||||||
pub branch_name: Option<String>,
|
pub current_branch_name: Option<String>,
|
||||||
|
pub branches: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FakeGitRepository {
|
impl FakeGitRepository {
|
||||||
|
@ -220,6 +241,20 @@ impl FakeGitRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FakeGitRepositoryState {
|
||||||
|
pub fn new(path: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self {
|
||||||
|
FakeGitRepositoryState {
|
||||||
|
path,
|
||||||
|
event_emitter,
|
||||||
|
index_contents: Default::default(),
|
||||||
|
blames: Default::default(),
|
||||||
|
worktree_statuses: Default::default(),
|
||||||
|
current_branch_name: Default::default(),
|
||||||
|
branches: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl GitRepository for FakeGitRepository {
|
impl GitRepository for FakeGitRepository {
|
||||||
fn reload_index(&self) {}
|
fn reload_index(&self) {}
|
||||||
|
|
||||||
|
@ -234,7 +269,7 @@ impl GitRepository for FakeGitRepository {
|
||||||
|
|
||||||
fn branch_name(&self) -> Option<String> {
|
fn branch_name(&self) -> Option<String> {
|
||||||
let state = self.state.lock();
|
let state = self.state.lock();
|
||||||
state.branch_name.clone()
|
state.current_branch_name.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn head_sha(&self) -> Option<String> {
|
fn head_sha(&self) -> Option<String> {
|
||||||
|
@ -264,18 +299,41 @@ impl GitRepository for FakeGitRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn branches(&self) -> Result<Vec<Branch>> {
|
fn branches(&self) -> Result<Vec<Branch>> {
|
||||||
Ok(vec![])
|
let state = self.state.lock();
|
||||||
|
let current_branch = &state.current_branch_name;
|
||||||
|
Ok(state
|
||||||
|
.branches
|
||||||
|
.iter()
|
||||||
|
.map(|branch_name| Branch {
|
||||||
|
is_head: Some(branch_name) == current_branch.as_ref(),
|
||||||
|
name: branch_name.into(),
|
||||||
|
unix_timestamp: None,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn branch_exits(&self, name: &str) -> Result<bool> {
|
||||||
|
let state = self.state.lock();
|
||||||
|
Ok(state.branches.contains(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn change_branch(&self, name: &str) -> Result<()> {
|
fn change_branch(&self, name: &str) -> Result<()> {
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
state.branch_name = Some(name.to_owned());
|
state.current_branch_name = Some(name.to_owned());
|
||||||
|
state
|
||||||
|
.event_emitter
|
||||||
|
.try_send(state.path.clone())
|
||||||
|
.expect("Dropped repo change event");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_branch(&self, name: &str) -> Result<()> {
|
fn create_branch(&self, name: &str) -> Result<()> {
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
state.branch_name = Some(name.to_owned());
|
state.branches.insert(name.to_owned());
|
||||||
|
state
|
||||||
|
.event_emitter
|
||||||
|
.try_send(state.path.clone())
|
||||||
|
.expect("Dropped repo change event");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -256,6 +256,9 @@ pub struct AppContext {
|
||||||
pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
|
pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
|
||||||
pub(crate) propagate_event: bool,
|
pub(crate) propagate_event: bool,
|
||||||
pub(crate) prompt_builder: Option<PromptBuilder>,
|
pub(crate) prompt_builder: Option<PromptBuilder>,
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support", debug_assertions))]
|
||||||
|
pub(crate) name: Option<&'static str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppContext {
|
impl AppContext {
|
||||||
|
@ -309,6 +312,9 @@ impl AppContext {
|
||||||
layout_id_buffer: Default::default(),
|
layout_id_buffer: Default::default(),
|
||||||
propagate_event: true,
|
propagate_event: true,
|
||||||
prompt_builder: Some(PromptBuilder::Default),
|
prompt_builder: Some(PromptBuilder::Default),
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support", debug_assertions))]
|
||||||
|
name: None,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -988,6 +994,7 @@ impl AppContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move the global of the given type to the stack.
|
/// Move the global of the given type to the stack.
|
||||||
|
#[track_caller]
|
||||||
pub(crate) fn lease_global<G: Global>(&mut self) -> GlobalLease<G> {
|
pub(crate) fn lease_global<G: Global>(&mut self) -> GlobalLease<G> {
|
||||||
GlobalLease::new(
|
GlobalLease::new(
|
||||||
self.globals_by_type
|
self.globals_by_type
|
||||||
|
@ -1319,6 +1326,12 @@ impl AppContext {
|
||||||
|
|
||||||
(task, is_first)
|
(task, is_first)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the name for this App.
|
||||||
|
#[cfg(any(test, feature = "test-support", debug_assertions))]
|
||||||
|
pub fn get_name(&self) -> &'static str {
|
||||||
|
self.name.as_ref().unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context for AppContext {
|
impl Context for AppContext {
|
||||||
|
|
|
@ -536,6 +536,15 @@ impl AnyWeakModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for AnyWeakModel {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct(type_name::<Self>())
|
||||||
|
.field("entity_id", &self.entity_id)
|
||||||
|
.field("entity_type", &self.entity_type)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> From<WeakModel<T>> for AnyWeakModel {
|
impl<T> From<WeakModel<T>> for AnyWeakModel {
|
||||||
fn from(model: WeakModel<T>) -> Self {
|
fn from(model: WeakModel<T>) -> Self {
|
||||||
model.any_model
|
model.any_model
|
||||||
|
|
|
@ -478,6 +478,12 @@ impl TestAppContext {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set a name for this App.
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn set_name(&mut self, name: &'static str) {
|
||||||
|
self.update(|cx| cx.name = Some(name))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: 'static> Model<T> {
|
impl<T: 'static> Model<T> {
|
||||||
|
|
|
@ -57,6 +57,7 @@ pub trait UpdateGlobal {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Global> UpdateGlobal for T {
|
impl<T: Global> UpdateGlobal for T {
|
||||||
|
#[track_caller]
|
||||||
fn update_global<C, F, R>(cx: &mut C, update: F) -> R
|
fn update_global<C, F, R>(cx: &mut C, update: F) -> R
|
||||||
where
|
where
|
||||||
C: BorrowAppContext,
|
C: BorrowAppContext,
|
||||||
|
|
|
@ -306,6 +306,7 @@ where
|
||||||
self.borrow_mut().set_global(global)
|
self.borrow_mut().set_global(global)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
|
fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
|
||||||
where
|
where
|
||||||
G: Global,
|
G: Global,
|
||||||
|
|
|
@ -288,6 +288,13 @@ impl ProjectPath {
|
||||||
path: self.path.to_string_lossy().to_string(),
|
path: self.path.to_string_lossy().to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn root_path(worktree_id: WorktreeId) -> Self {
|
||||||
|
Self {
|
||||||
|
worktree_id,
|
||||||
|
path: Path::new("").into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
@ -701,7 +708,7 @@ impl Project {
|
||||||
|
|
||||||
let ssh_proto = ssh.read(cx).proto_client();
|
let ssh_proto = ssh.read(cx).proto_client();
|
||||||
let worktree_store =
|
let worktree_store =
|
||||||
cx.new_model(|_| WorktreeStore::remote(false, ssh_proto.clone(), 0));
|
cx.new_model(|_| WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID));
|
||||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
@ -3370,6 +3377,25 @@ impl Project {
|
||||||
worktree.get_local_repo(&root_entry)?.repo().clone().into()
|
worktree.get_local_repo(&root_entry)?.repo().clone().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn branches(
|
||||||
|
&self,
|
||||||
|
project_path: ProjectPath,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Task<Result<Vec<git::repository::Branch>>> {
|
||||||
|
self.worktree_store().read(cx).branches(project_path, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_or_create_branch(
|
||||||
|
&self,
|
||||||
|
repository: ProjectPath,
|
||||||
|
new_branch: String,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
self.worktree_store()
|
||||||
|
.read(cx)
|
||||||
|
.update_or_create_branch(repository, new_branch, cx)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn blame_buffer(
|
pub fn blame_buffer(
|
||||||
&self,
|
&self,
|
||||||
buffer: &Model<Buffer>,
|
buffer: &Model<Buffer>,
|
||||||
|
|
|
@ -73,6 +73,8 @@ impl WorktreeStore {
|
||||||
client.add_model_request_handler(Self::handle_copy_project_entry);
|
client.add_model_request_handler(Self::handle_copy_project_entry);
|
||||||
client.add_model_request_handler(Self::handle_delete_project_entry);
|
client.add_model_request_handler(Self::handle_delete_project_entry);
|
||||||
client.add_model_request_handler(Self::handle_expand_project_entry);
|
client.add_model_request_handler(Self::handle_expand_project_entry);
|
||||||
|
client.add_model_request_handler(Self::handle_git_branches);
|
||||||
|
client.add_model_request_handler(Self::handle_update_branch);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local(retain_worktrees: bool, fs: Arc<dyn Fs>) -> Self {
|
pub fn local(retain_worktrees: bool, fs: Arc<dyn Fs>) -> Self {
|
||||||
|
@ -127,6 +129,13 @@ impl WorktreeStore {
|
||||||
.find(|worktree| worktree.read(cx).id() == id)
|
.find(|worktree| worktree.read(cx).id() == id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn current_branch(&self, repository: ProjectPath, cx: &AppContext) -> Option<Arc<str>> {
|
||||||
|
self.worktree_for_id(repository.worktree_id, cx)?
|
||||||
|
.read(cx)
|
||||||
|
.git_entry(repository.path)?
|
||||||
|
.branch()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn worktree_for_entry(
|
pub fn worktree_for_entry(
|
||||||
&self,
|
&self,
|
||||||
entry_id: ProjectEntryId,
|
entry_id: ProjectEntryId,
|
||||||
|
@ -836,6 +845,131 @@ impl WorktreeStore {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn branches(
|
||||||
|
&self,
|
||||||
|
project_path: ProjectPath,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Task<Result<Vec<git::repository::Branch>>> {
|
||||||
|
let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else {
|
||||||
|
return Task::ready(Err(anyhow!("No worktree found for ProjectPath")));
|
||||||
|
};
|
||||||
|
|
||||||
|
match worktree.read(cx) {
|
||||||
|
Worktree::Local(local_worktree) => {
|
||||||
|
let branches = util::maybe!({
|
||||||
|
let worktree_error = |error| {
|
||||||
|
format!(
|
||||||
|
"{} for worktree {}",
|
||||||
|
error,
|
||||||
|
local_worktree.abs_path().to_string_lossy()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let entry = local_worktree
|
||||||
|
.git_entry(project_path.path)
|
||||||
|
.with_context(|| worktree_error("No git entry found"))?;
|
||||||
|
|
||||||
|
let repo = local_worktree
|
||||||
|
.get_local_repo(&entry)
|
||||||
|
.with_context(|| worktree_error("No repository found"))?
|
||||||
|
.repo()
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
repo.branches()
|
||||||
|
});
|
||||||
|
|
||||||
|
Task::ready(branches)
|
||||||
|
}
|
||||||
|
Worktree::Remote(remote_worktree) => {
|
||||||
|
let request = remote_worktree.client().request(proto::GitBranches {
|
||||||
|
project_id: remote_worktree.project_id(),
|
||||||
|
repository: Some(proto::ProjectPath {
|
||||||
|
worktree_id: project_path.worktree_id.to_proto(),
|
||||||
|
path: project_path.path.to_string_lossy().to_string(), // Root path
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
let response = request.await?;
|
||||||
|
|
||||||
|
let branches = response
|
||||||
|
.branches
|
||||||
|
.into_iter()
|
||||||
|
.map(|proto_branch| git::repository::Branch {
|
||||||
|
is_head: proto_branch.is_head,
|
||||||
|
name: proto_branch.name.into(),
|
||||||
|
unix_timestamp: proto_branch
|
||||||
|
.unix_timestamp
|
||||||
|
.map(|timestamp| timestamp as i64),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(branches)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_or_create_branch(
|
||||||
|
&self,
|
||||||
|
repository: ProjectPath,
|
||||||
|
new_branch: String,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let Some(worktree) = self.worktree_for_id(repository.worktree_id, cx) else {
|
||||||
|
return Task::ready(Err(anyhow!("No worktree found for ProjectPath")));
|
||||||
|
};
|
||||||
|
|
||||||
|
match worktree.read(cx) {
|
||||||
|
Worktree::Local(local_worktree) => {
|
||||||
|
let result = util::maybe!({
|
||||||
|
let worktree_error = |error| {
|
||||||
|
format!(
|
||||||
|
"{} for worktree {}",
|
||||||
|
error,
|
||||||
|
local_worktree.abs_path().to_string_lossy()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let entry = local_worktree
|
||||||
|
.git_entry(repository.path)
|
||||||
|
.with_context(|| worktree_error("No git entry found"))?;
|
||||||
|
|
||||||
|
let repo = local_worktree
|
||||||
|
.get_local_repo(&entry)
|
||||||
|
.with_context(|| worktree_error("No repository found"))?
|
||||||
|
.repo()
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
if !repo.branch_exits(&new_branch)? {
|
||||||
|
repo.create_branch(&new_branch)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.change_branch(&new_branch)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
Task::ready(result)
|
||||||
|
}
|
||||||
|
Worktree::Remote(remote_worktree) => {
|
||||||
|
let request = remote_worktree.client().request(proto::UpdateGitBranch {
|
||||||
|
project_id: remote_worktree.project_id(),
|
||||||
|
repository: Some(proto::ProjectPath {
|
||||||
|
worktree_id: repository.worktree_id.to_proto(),
|
||||||
|
path: repository.path.to_string_lossy().to_string(), // Root path
|
||||||
|
}),
|
||||||
|
branch_name: new_branch,
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
request.await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn filter_paths(
|
async fn filter_paths(
|
||||||
fs: &Arc<dyn Fs>,
|
fs: &Arc<dyn Fs>,
|
||||||
mut input: Receiver<MatchingEntry>,
|
mut input: Receiver<MatchingEntry>,
|
||||||
|
@ -917,6 +1051,61 @@ impl WorktreeStore {
|
||||||
.ok_or_else(|| anyhow!("invalid request"))?;
|
.ok_or_else(|| anyhow!("invalid request"))?;
|
||||||
Worktree::handle_expand_entry(worktree, envelope.payload, cx).await
|
Worktree::handle_expand_entry(worktree, envelope.payload, cx).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_git_branches(
|
||||||
|
this: Model<Self>,
|
||||||
|
branches: TypedEnvelope<proto::GitBranches>,
|
||||||
|
cx: AsyncAppContext,
|
||||||
|
) -> Result<proto::GitBranchesResponse> {
|
||||||
|
let project_path = branches
|
||||||
|
.payload
|
||||||
|
.repository
|
||||||
|
.clone()
|
||||||
|
.context("Invalid GitBranches call")?;
|
||||||
|
let project_path = ProjectPath {
|
||||||
|
worktree_id: WorktreeId::from_proto(project_path.worktree_id),
|
||||||
|
path: Path::new(&project_path.path).into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let branches = this
|
||||||
|
.read_with(&cx, |this, cx| this.branches(project_path, cx))?
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(proto::GitBranchesResponse {
|
||||||
|
branches: branches
|
||||||
|
.into_iter()
|
||||||
|
.map(|branch| proto::Branch {
|
||||||
|
is_head: branch.is_head,
|
||||||
|
name: branch.name.to_string(),
|
||||||
|
unix_timestamp: branch.unix_timestamp.map(|timestamp| timestamp as u64),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_update_branch(
|
||||||
|
this: Model<Self>,
|
||||||
|
update_branch: TypedEnvelope<proto::UpdateGitBranch>,
|
||||||
|
cx: AsyncAppContext,
|
||||||
|
) -> Result<proto::Ack> {
|
||||||
|
let project_path = update_branch
|
||||||
|
.payload
|
||||||
|
.repository
|
||||||
|
.clone()
|
||||||
|
.context("Invalid GitBranches call")?;
|
||||||
|
let project_path = ProjectPath {
|
||||||
|
worktree_id: WorktreeId::from_proto(project_path.worktree_id),
|
||||||
|
path: Path::new(&project_path.path).into(),
|
||||||
|
};
|
||||||
|
let new_branch = update_branch.payload.branch_name;
|
||||||
|
|
||||||
|
this.read_with(&cx, |this, cx| {
|
||||||
|
this.update_or_create_branch(project_path, new_branch, cx)
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(proto::Ack {})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
|
@ -281,7 +281,12 @@ message Envelope {
|
||||||
FlushBufferedMessages flush_buffered_messages = 267;
|
FlushBufferedMessages flush_buffered_messages = 267;
|
||||||
|
|
||||||
LanguageServerPromptRequest language_server_prompt_request = 268;
|
LanguageServerPromptRequest language_server_prompt_request = 268;
|
||||||
LanguageServerPromptResponse language_server_prompt_response = 269; // current max
|
LanguageServerPromptResponse language_server_prompt_response = 269;
|
||||||
|
|
||||||
|
GitBranches git_branches = 270;
|
||||||
|
GitBranchesResponse git_branches_response = 271;
|
||||||
|
|
||||||
|
UpdateGitBranch update_git_branch = 272; // current max
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -2432,3 +2437,24 @@ message LanguageServerPromptRequest {
|
||||||
message LanguageServerPromptResponse {
|
message LanguageServerPromptResponse {
|
||||||
optional uint64 action_response = 1;
|
optional uint64 action_response = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Branch {
|
||||||
|
bool is_head = 1;
|
||||||
|
string name = 2;
|
||||||
|
optional uint64 unix_timestamp = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GitBranches {
|
||||||
|
uint64 project_id = 1;
|
||||||
|
ProjectPath repository = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GitBranchesResponse {
|
||||||
|
repeated Branch branches = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateGitBranch {
|
||||||
|
uint64 project_id = 1;
|
||||||
|
string branch_name = 2;
|
||||||
|
ProjectPath repository = 3;
|
||||||
|
}
|
||||||
|
|
|
@ -357,6 +357,9 @@ messages!(
|
||||||
(FlushBufferedMessages, Foreground),
|
(FlushBufferedMessages, Foreground),
|
||||||
(LanguageServerPromptRequest, Foreground),
|
(LanguageServerPromptRequest, Foreground),
|
||||||
(LanguageServerPromptResponse, Foreground),
|
(LanguageServerPromptResponse, Foreground),
|
||||||
|
(GitBranches, Background),
|
||||||
|
(GitBranchesResponse, Background),
|
||||||
|
(UpdateGitBranch, Background)
|
||||||
);
|
);
|
||||||
|
|
||||||
request_messages!(
|
request_messages!(
|
||||||
|
@ -473,6 +476,8 @@ request_messages!(
|
||||||
(GetPermalinkToLine, GetPermalinkToLineResponse),
|
(GetPermalinkToLine, GetPermalinkToLineResponse),
|
||||||
(FlushBufferedMessages, Ack),
|
(FlushBufferedMessages, Ack),
|
||||||
(LanguageServerPromptRequest, LanguageServerPromptResponse),
|
(LanguageServerPromptRequest, LanguageServerPromptResponse),
|
||||||
|
(GitBranches, GitBranchesResponse),
|
||||||
|
(UpdateGitBranch, Ack)
|
||||||
);
|
);
|
||||||
|
|
||||||
entity_messages!(
|
entity_messages!(
|
||||||
|
@ -550,7 +555,9 @@ entity_messages!(
|
||||||
HideToast,
|
HideToast,
|
||||||
OpenServerSettings,
|
OpenServerSettings,
|
||||||
GetPermalinkToLine,
|
GetPermalinkToLine,
|
||||||
LanguageServerPromptRequest
|
LanguageServerPromptRequest,
|
||||||
|
GitBranches,
|
||||||
|
UpdateGitBranch
|
||||||
);
|
);
|
||||||
|
|
||||||
entity_messages!(
|
entity_messages!(
|
||||||
|
|
|
@ -631,7 +631,7 @@ impl SshClientDelegate {
|
||||||
|
|
||||||
self.update_status(
|
self.update_status(
|
||||||
Some(&format!(
|
Some(&format!(
|
||||||
"Building remote server binary from source for {}",
|
"Building remote server binary from source for {} with Docker",
|
||||||
&triple
|
&triple
|
||||||
)),
|
)),
|
||||||
cx,
|
cx,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext, PromptLevel};
|
use gpui::{AppContext, AsyncAppContext, Context as _, Model, ModelContext, PromptLevel};
|
||||||
use http_client::HttpClient;
|
use http_client::HttpClient;
|
||||||
use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry};
|
use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry};
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
|
|
|
@ -26,7 +26,29 @@ use std::{
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||||
let (project, _headless, fs) = init_test(cx, server_cx).await;
|
let fs = FakeFs::new(server_cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/code",
|
||||||
|
json!({
|
||||||
|
"project1": {
|
||||||
|
".git": {},
|
||||||
|
"README.md": "# project 1",
|
||||||
|
"src": {
|
||||||
|
"lib.rs": "fn one() -> usize { 1 }"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"project2": {
|
||||||
|
"README.md": "# project 2",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
fs.set_index_for_repo(
|
||||||
|
Path::new("/code/project1/.git"),
|
||||||
|
&[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
|
||||||
|
);
|
||||||
|
|
||||||
|
let (project, _headless) = init_test(&fs, cx, server_cx).await;
|
||||||
let (worktree, _) = project
|
let (worktree, _) = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.find_or_create_worktree("/code/project1", true, cx)
|
project.find_or_create_worktree("/code/project1", true, cx)
|
||||||
|
@ -128,7 +150,22 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||||
let (project, headless, _) = init_test(cx, server_cx).await;
|
let fs = FakeFs::new(server_cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/code",
|
||||||
|
json!({
|
||||||
|
"project1": {
|
||||||
|
".git": {},
|
||||||
|
"README.md": "# project 1",
|
||||||
|
"src": {
|
||||||
|
"lib.rs": "fn one() -> usize { 1 }"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (project, headless) = init_test(&fs, cx, server_cx).await;
|
||||||
|
|
||||||
project
|
project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
|
@ -193,7 +230,22 @@ async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut Tes
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||||
let (project, headless, fs) = init_test(cx, server_cx).await;
|
let fs = FakeFs::new(server_cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/code",
|
||||||
|
json!({
|
||||||
|
"project1": {
|
||||||
|
".git": {},
|
||||||
|
"README.md": "# project 1",
|
||||||
|
"src": {
|
||||||
|
"lib.rs": "fn one() -> usize { 1 }"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (project, headless) = init_test(&fs, cx, server_cx).await;
|
||||||
|
|
||||||
cx.update_global(|settings_store: &mut SettingsStore, cx| {
|
cx.update_global(|settings_store: &mut SettingsStore, cx| {
|
||||||
settings_store.set_user_settings(
|
settings_store.set_user_settings(
|
||||||
|
@ -304,7 +356,22 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||||
let (project, headless, fs) = init_test(cx, server_cx).await;
|
let fs = FakeFs::new(server_cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/code",
|
||||||
|
json!({
|
||||||
|
"project1": {
|
||||||
|
".git": {},
|
||||||
|
"README.md": "# project 1",
|
||||||
|
"src": {
|
||||||
|
"lib.rs": "fn one() -> usize { 1 }"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (project, headless) = init_test(&fs, cx, server_cx).await;
|
||||||
|
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
"/code/project1/.zed",
|
"/code/project1/.zed",
|
||||||
|
@ -463,7 +530,22 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||||
let (project, _headless, fs) = init_test(cx, server_cx).await;
|
let fs = FakeFs::new(server_cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/code",
|
||||||
|
json!({
|
||||||
|
"project1": {
|
||||||
|
".git": {},
|
||||||
|
"README.md": "# project 1",
|
||||||
|
"src": {
|
||||||
|
"lib.rs": "fn one() -> usize { 1 }"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (project, _headless) = init_test(&fs, cx, server_cx).await;
|
||||||
let (worktree, _) = project
|
let (worktree, _) = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.find_or_create_worktree("/code/project1", true, cx)
|
project.find_or_create_worktree("/code/project1", true, cx)
|
||||||
|
@ -523,7 +605,22 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_remote_resolve_file_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
async fn test_remote_resolve_file_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||||
let (project, _headless, _fs) = init_test(cx, server_cx).await;
|
let fs = FakeFs::new(server_cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/code",
|
||||||
|
json!({
|
||||||
|
"project1": {
|
||||||
|
".git": {},
|
||||||
|
"README.md": "# project 1",
|
||||||
|
"src": {
|
||||||
|
"lib.rs": "fn one() -> usize { 1 }"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (project, _headless) = init_test(&fs, cx, server_cx).await;
|
||||||
let (worktree, _) = project
|
let (worktree, _) = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.find_or_create_worktree("/code/project1", true, cx)
|
project.find_or_create_worktree("/code/project1", true, cx)
|
||||||
|
@ -566,7 +663,22 @@ async fn test_remote_resolve_file_path(cx: &mut TestAppContext, server_cx: &mut
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||||
let (project, _headless, _fs) = init_test(cx, server_cx).await;
|
let fs = FakeFs::new(server_cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/code",
|
||||||
|
json!({
|
||||||
|
"project1": {
|
||||||
|
".git": {},
|
||||||
|
"README.md": "# project 1",
|
||||||
|
"src": {
|
||||||
|
"lib.rs": "fn one() -> usize { 1 }"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (project, _headless) = init_test(&fs, cx, server_cx).await;
|
||||||
let (worktree, _) = project
|
let (worktree, _) = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.find_or_create_worktree("/code/project1", true, cx)
|
project.find_or_create_worktree("/code/project1", true, cx)
|
||||||
|
@ -597,7 +709,25 @@ async fn test_adding_then_removing_then_adding_worktrees(
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
server_cx: &mut TestAppContext,
|
server_cx: &mut TestAppContext,
|
||||||
) {
|
) {
|
||||||
let (project, _headless, _fs) = init_test(cx, server_cx).await;
|
let fs = FakeFs::new(server_cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/code",
|
||||||
|
json!({
|
||||||
|
"project1": {
|
||||||
|
".git": {},
|
||||||
|
"README.md": "# project 1",
|
||||||
|
"src": {
|
||||||
|
"lib.rs": "fn one() -> usize { 1 }"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"project2": {
|
||||||
|
"README.md": "# project 2",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (project, _headless) = init_test(&fs, cx, server_cx).await;
|
||||||
let (_worktree, _) = project
|
let (_worktree, _) = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.find_or_create_worktree("/code/project1", true, cx)
|
project.find_or_create_worktree("/code/project1", true, cx)
|
||||||
|
@ -636,9 +766,25 @@ async fn test_adding_then_removing_then_adding_worktrees(
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||||
let (project, _headless, _fs) = init_test(cx, server_cx).await;
|
let fs = FakeFs::new(server_cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/code",
|
||||||
|
json!({
|
||||||
|
"project1": {
|
||||||
|
".git": {},
|
||||||
|
"README.md": "# project 1",
|
||||||
|
"src": {
|
||||||
|
"lib.rs": "fn one() -> usize { 1 }"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (project, _headless) = init_test(&fs, cx, server_cx).await;
|
||||||
let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
|
let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
let buffer = buffer.await.unwrap();
|
let buffer = buffer.await.unwrap();
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
@ -651,7 +797,22 @@ async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut Test
|
||||||
|
|
||||||
#[gpui::test(iterations = 20)]
|
#[gpui::test(iterations = 20)]
|
||||||
async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||||
let (project, _headless, fs) = init_test(cx, server_cx).await;
|
let fs = FakeFs::new(server_cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/code",
|
||||||
|
json!({
|
||||||
|
"project1": {
|
||||||
|
".git": {},
|
||||||
|
"README.md": "# project 1",
|
||||||
|
"src": {
|
||||||
|
"lib.rs": "fn one() -> usize { 1 }"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (project, _headless) = init_test(&fs, cx, server_cx).await;
|
||||||
|
|
||||||
let (worktree, _) = project
|
let (worktree, _) = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
|
@ -690,19 +851,8 @@ async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_logger() {
|
#[gpui::test]
|
||||||
if std::env::var("RUST_LOG").is_ok() {
|
async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||||
env_logger::try_init().ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn init_test(
|
|
||||||
cx: &mut TestAppContext,
|
|
||||||
server_cx: &mut TestAppContext,
|
|
||||||
) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
|
|
||||||
init_logger();
|
|
||||||
|
|
||||||
let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
|
|
||||||
let fs = FakeFs::new(server_cx.executor());
|
let fs = FakeFs::new(server_cx.executor());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
"/code",
|
"/code",
|
||||||
|
@ -710,32 +860,109 @@ async fn init_test(
|
||||||
"project1": {
|
"project1": {
|
||||||
".git": {},
|
".git": {},
|
||||||
"README.md": "# project 1",
|
"README.md": "# project 1",
|
||||||
"src": {
|
|
||||||
"lib.rs": "fn one() -> usize { 1 }"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"project2": {
|
|
||||||
"README.md": "# project 2",
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
fs.set_index_for_repo(
|
|
||||||
Path::new("/code/project1/.git"),
|
|
||||||
&[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
|
|
||||||
);
|
|
||||||
|
|
||||||
server_cx.update(HeadlessProject::init);
|
let (project, headless_project) = init_test(&fs, cx, server_cx).await;
|
||||||
|
let branches = ["main", "dev", "feature-1"];
|
||||||
|
fs.insert_branches(Path::new("/code/project1/.git"), &branches);
|
||||||
|
|
||||||
|
let (worktree, _) = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.find_or_create_worktree("/code/project1", true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let worktree_id = cx.update(|cx| worktree.read(cx).id());
|
||||||
|
let root_path = ProjectPath::root_path(worktree_id);
|
||||||
|
// Give the worktree a bit of time to index the file system
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
let remote_branches = project
|
||||||
|
.update(cx, |project, cx| project.branches(root_path.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let new_branch = branches[2];
|
||||||
|
|
||||||
|
let remote_branches = remote_branches
|
||||||
|
.into_iter()
|
||||||
|
.map(|branch| branch.name)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(&remote_branches, &branches);
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
project.update(cx, |project, cx| {
|
||||||
|
project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
let server_branch = server_cx.update(|cx| {
|
||||||
|
headless_project.update(cx, |headless_project, cx| {
|
||||||
|
headless_project
|
||||||
|
.worktree_store
|
||||||
|
.update(cx, |worktree_store, cx| {
|
||||||
|
worktree_store
|
||||||
|
.current_branch(root_path.clone(), cx)
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(server_branch.as_ref(), branches[2]);
|
||||||
|
|
||||||
|
// Also try creating a new branch
|
||||||
|
cx.update(|cx| {
|
||||||
|
project.update(cx, |project, cx| {
|
||||||
|
project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
let server_branch = server_cx.update(|cx| {
|
||||||
|
headless_project.update(cx, |headless_project, cx| {
|
||||||
|
headless_project
|
||||||
|
.worktree_store
|
||||||
|
.update(cx, |worktree_store, cx| {
|
||||||
|
worktree_store.current_branch(root_path, cx).unwrap()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(server_branch.as_ref(), "totally-new-branch");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init_test(
|
||||||
|
server_fs: &Arc<FakeFs>,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
server_cx: &mut TestAppContext,
|
||||||
|
) -> (Model<Project>, Model<HeadlessProject>) {
|
||||||
|
let server_fs = server_fs.clone();
|
||||||
|
init_logger();
|
||||||
|
|
||||||
|
let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
|
||||||
let http_client = Arc::new(BlockedHttpClient);
|
let http_client = Arc::new(BlockedHttpClient);
|
||||||
let node_runtime = NodeRuntime::unavailable();
|
let node_runtime = NodeRuntime::unavailable();
|
||||||
let languages = Arc::new(LanguageRegistry::new(cx.executor()));
|
let languages = Arc::new(LanguageRegistry::new(cx.executor()));
|
||||||
|
server_cx.update(HeadlessProject::init);
|
||||||
let headless = server_cx.new_model(|cx| {
|
let headless = server_cx.new_model(|cx| {
|
||||||
client::init_settings(cx);
|
client::init_settings(cx);
|
||||||
|
|
||||||
HeadlessProject::new(
|
HeadlessProject::new(
|
||||||
crate::HeadlessAppState {
|
crate::HeadlessAppState {
|
||||||
session: ssh_server_client,
|
session: ssh_server_client,
|
||||||
fs: fs.clone(),
|
fs: server_fs.clone(),
|
||||||
http_client,
|
http_client,
|
||||||
node_runtime,
|
node_runtime,
|
||||||
languages,
|
languages,
|
||||||
|
@ -752,13 +979,21 @@ async fn init_test(
|
||||||
|_, cx| cx.on_release(|_, _| drop(headless))
|
|_, cx| cx.on_release(|_, _| drop(headless))
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
(project, headless, fs)
|
(project, headless)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_logger() {
|
||||||
|
if std::env::var("RUST_LOG").is_ok() {
|
||||||
|
env_logger::try_init().ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_project(ssh: Model<SshRemoteClient>, cx: &mut TestAppContext) -> Model<Project> {
|
fn build_project(ssh: Model<SshRemoteClient>, cx: &mut TestAppContext) -> Model<Project> {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let settings_store = SettingsStore::test(cx);
|
if !cx.has_global::<SettingsStore>() {
|
||||||
cx.set_global(settings_store);
|
let settings_store = SettingsStore::test(cx);
|
||||||
|
cx.set_global(settings_store);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let client = cx.update(|cx| {
|
let client = cx.update(|cx| {
|
||||||
|
@ -773,6 +1008,7 @@ fn build_project(ssh: Model<SshRemoteClient>, cx: &mut TestAppContext) -> Model<
|
||||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||||
let languages = Arc::new(LanguageRegistry::test(cx.executor()));
|
let languages = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
Project::init(&client, cx);
|
Project::init(&client, cx);
|
||||||
language::init(cx);
|
language::init(cx);
|
||||||
|
|
|
@ -123,7 +123,6 @@ impl ProtoMessageHandlerSet {
|
||||||
let extract_entity_id = *this.entity_id_extractors.get(&payload_type_id)?;
|
let extract_entity_id = *this.entity_id_extractors.get(&payload_type_id)?;
|
||||||
let entity_type_id = *this.entity_types_by_message_type.get(&payload_type_id)?;
|
let entity_type_id = *this.entity_types_by_message_type.get(&payload_type_id)?;
|
||||||
let entity_id = (extract_entity_id)(message.as_ref());
|
let entity_id = (extract_entity_id)(message.as_ref());
|
||||||
|
|
||||||
match this
|
match this
|
||||||
.entities_by_type_and_remote_id
|
.entities_by_type_and_remote_id
|
||||||
.get_mut(&(entity_type_id, entity_id))?
|
.get_mut(&(entity_type_id, entity_id))?
|
||||||
|
@ -145,6 +144,26 @@ pub enum EntityMessageSubscriber {
|
||||||
Pending(Vec<Box<dyn AnyTypedEnvelope>>),
|
Pending(Vec<Box<dyn AnyTypedEnvelope>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for EntityMessageSubscriber {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
EntityMessageSubscriber::Entity { handle } => f
|
||||||
|
.debug_struct("EntityMessageSubscriber::Entity")
|
||||||
|
.field("handle", handle)
|
||||||
|
.finish(),
|
||||||
|
EntityMessageSubscriber::Pending(vec) => f
|
||||||
|
.debug_struct("EntityMessageSubscriber::Pending")
|
||||||
|
.field(
|
||||||
|
"envelopes",
|
||||||
|
&vec.iter()
|
||||||
|
.map(|envelope| envelope.payload_type_name())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.finish(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> From<Arc<T>> for AnyProtoClient
|
impl<T> From<Arc<T>> for AnyProtoClient
|
||||||
where
|
where
|
||||||
T: ProtoClient + 'static,
|
T: ProtoClient + 'static,
|
||||||
|
|
|
@ -61,6 +61,7 @@ pub trait Settings: 'static + Send + Sync {
|
||||||
anyhow::anyhow!("missing default")
|
anyhow::anyhow!("missing default")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
fn register(cx: &mut AppContext)
|
fn register(cx: &mut AppContext)
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
|
@ -271,6 +272,7 @@ impl SettingsStore {
|
||||||
pub fn register_setting<T: Settings>(&mut self, cx: &mut AppContext) {
|
pub fn register_setting<T: Settings>(&mut self, cx: &mut AppContext) {
|
||||||
let setting_type_id = TypeId::of::<T>();
|
let setting_type_id = TypeId::of::<T>();
|
||||||
let entry = self.setting_values.entry(setting_type_id);
|
let entry = self.setting_values.entry(setting_type_id);
|
||||||
|
|
||||||
if matches!(entry, hash_map::Entry::Occupied(_)) {
|
if matches!(entry, hash_map::Entry::Occupied(_)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -447,7 +447,7 @@ impl TitleBar {
|
||||||
})
|
})
|
||||||
.on_click(move |_, cx| {
|
.on_click(move |_, cx| {
|
||||||
let _ = workspace.update(cx, |this, cx| {
|
let _ = workspace.update(cx, |this, cx| {
|
||||||
BranchList::open(this, &Default::default(), cx)
|
BranchList::open(this, &Default::default(), cx);
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
@ -75,6 +75,12 @@ impl From<String> for ArcCow<'_, str> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&String> for ArcCow<'_, str> {
|
||||||
|
fn from(value: &String) -> Self {
|
||||||
|
Self::Owned(value.clone().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> From<Cow<'a, str>> for ArcCow<'a, str> {
|
impl<'a> From<Cow<'a, str>> for ArcCow<'a, str> {
|
||||||
fn from(value: Cow<'a, str>) -> Self {
|
fn from(value: Cow<'a, str>) -> Self {
|
||||||
match value {
|
match value {
|
||||||
|
|
|
@ -14,6 +14,7 @@ fuzzy.workspace = true
|
||||||
git.workspace = true
|
git.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
|
project.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
|
|
@ -2,24 +2,23 @@ use anyhow::{Context, Result};
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use git::repository::Branch;
|
use git::repository::Branch;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, rems, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
actions, rems, AnyElement, AppContext, AsyncAppContext, DismissEvent, EventEmitter,
|
||||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
FocusHandle, FocusableView, InteractiveElement, IntoElement, ParentElement, Render,
|
||||||
Task, View, ViewContext, VisualContext, WindowContext,
|
SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WindowContext,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
|
use project::ProjectPath;
|
||||||
use std::{ops::Not, sync::Arc};
|
use std::{ops::Not, sync::Arc};
|
||||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::notifications::NotificationId;
|
use workspace::notifications::DetachAndPromptErr;
|
||||||
use workspace::{ModalView, Toast, Workspace};
|
use workspace::{ModalView, Workspace};
|
||||||
|
|
||||||
actions!(branches, [OpenRecent]);
|
actions!(branches, [OpenRecent]);
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.observe_new_views(|workspace: &mut Workspace, _| {
|
cx.observe_new_views(|workspace: &mut Workspace, _| {
|
||||||
workspace.register_action(|workspace, action, cx| {
|
workspace.register_action(BranchList::open);
|
||||||
BranchList::open(workspace, action, cx).log_err();
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
@ -31,6 +30,21 @@ pub struct BranchList {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BranchList {
|
impl BranchList {
|
||||||
|
pub fn open(_: &mut Workspace, _: &OpenRecent, cx: &mut ViewContext<Workspace>) {
|
||||||
|
let this = cx.view().clone();
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
// Modal branch picker has a longer trailoff than a popover one.
|
||||||
|
let delegate = BranchListDelegate::new(this.clone(), 70, &cx).await?;
|
||||||
|
|
||||||
|
this.update(&mut cx, |workspace, cx| {
|
||||||
|
workspace.toggle_modal(cx, |cx| BranchList::new(delegate, 34., cx))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_prompt_err("Failed to read branches", cx, |_, _| None)
|
||||||
|
}
|
||||||
|
|
||||||
fn new(delegate: BranchListDelegate, rem_width: f32, cx: &mut ViewContext<Self>) -> Self {
|
fn new(delegate: BranchListDelegate, rem_width: f32, cx: &mut ViewContext<Self>) -> Self {
|
||||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||||
let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
|
let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
|
||||||
|
@ -40,17 +54,6 @@ impl BranchList {
|
||||||
_subscription,
|
_subscription,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn open(
|
|
||||||
workspace: &mut Workspace,
|
|
||||||
_: &OpenRecent,
|
|
||||||
cx: &mut ViewContext<Workspace>,
|
|
||||||
) -> Result<()> {
|
|
||||||
// Modal branch picker has a longer trailoff than a popover one.
|
|
||||||
let delegate = BranchListDelegate::new(workspace, cx.view().clone(), 70, cx)?;
|
|
||||||
workspace.toggle_modal(cx, |cx| BranchList::new(delegate, 34., cx));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
impl ModalView for BranchList {}
|
impl ModalView for BranchList {}
|
||||||
impl EventEmitter<DismissEvent> for BranchList {}
|
impl EventEmitter<DismissEvent> for BranchList {}
|
||||||
|
@ -100,36 +103,32 @@ pub struct BranchListDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BranchListDelegate {
|
impl BranchListDelegate {
|
||||||
fn new(
|
async fn new(
|
||||||
workspace: &Workspace,
|
workspace: View<Workspace>,
|
||||||
handle: View<Workspace>,
|
|
||||||
branch_name_trailoff_after: usize,
|
branch_name_trailoff_after: usize,
|
||||||
cx: &AppContext,
|
cx: &AsyncAppContext,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let project = workspace.project().read(cx);
|
let all_branches_request = cx.update(|cx| {
|
||||||
let repo = project
|
let project = workspace.read(cx).project().read(cx);
|
||||||
.get_first_worktree_root_repo(cx)
|
let first_worktree = project
|
||||||
.context("failed to get root repository for first worktree")?;
|
.visible_worktrees(cx)
|
||||||
|
.next()
|
||||||
|
.context("No worktrees found")?;
|
||||||
|
let project_path = ProjectPath::root_path(first_worktree.read(cx).id());
|
||||||
|
anyhow::Ok(project.branches(project_path, cx))
|
||||||
|
})??;
|
||||||
|
|
||||||
|
let all_branches = all_branches_request.await?;
|
||||||
|
|
||||||
let all_branches = repo.branches()?;
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
matches: vec![],
|
matches: vec![],
|
||||||
workspace: handle,
|
workspace,
|
||||||
all_branches,
|
all_branches,
|
||||||
selected_index: 0,
|
selected_index: 0,
|
||||||
last_query: Default::default(),
|
last_query: Default::default(),
|
||||||
branch_name_trailoff_after,
|
branch_name_trailoff_after,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display_error_toast(&self, message: String, cx: &mut WindowContext<'_>) {
|
|
||||||
self.workspace.update(cx, |model, ctx| {
|
|
||||||
struct GitCheckoutFailure;
|
|
||||||
let id = NotificationId::unique::<GitCheckoutFailure>();
|
|
||||||
|
|
||||||
model.show_toast(Toast::new(id, message), ctx)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PickerDelegate for BranchListDelegate {
|
impl PickerDelegate for BranchListDelegate {
|
||||||
|
@ -235,40 +234,32 @@ impl PickerDelegate for BranchListDelegate {
|
||||||
cx.spawn({
|
cx.spawn({
|
||||||
let branch = branch.clone();
|
let branch = branch.clone();
|
||||||
|picker, mut cx| async move {
|
|picker, mut cx| async move {
|
||||||
picker
|
let branch_change_task = picker.update(&mut cx, |this, cx| {
|
||||||
.update(&mut cx, |this, cx| {
|
let project = this.delegate.workspace.read(cx).project().read(cx);
|
||||||
let project = this.delegate.workspace.read(cx).project().read(cx);
|
|
||||||
let repo = project
|
|
||||||
.get_first_worktree_root_repo(cx)
|
|
||||||
.context("failed to get root repository for first worktree")?;
|
|
||||||
|
|
||||||
let branch_to_checkout = match branch {
|
let branch_to_checkout = match branch {
|
||||||
BranchEntry::Branch(branch) => branch.string,
|
BranchEntry::Branch(branch) => branch.string,
|
||||||
BranchEntry::NewBranch { name: branch_name } => {
|
BranchEntry::NewBranch { name: branch_name } => branch_name,
|
||||||
let status = repo.create_branch(&branch_name);
|
};
|
||||||
if status.is_err() {
|
let worktree = project
|
||||||
this.delegate.display_error_toast(format!("Failed to create branch '{branch_name}', check for conflicts or unstashed files"), cx);
|
.worktrees(cx)
|
||||||
status?;
|
.next()
|
||||||
}
|
.context("worktree disappeared")?;
|
||||||
|
let repository = ProjectPath::root_path(worktree.read(cx).id());
|
||||||
|
|
||||||
branch_name
|
anyhow::Ok(project.update_or_create_branch(repository, branch_to_checkout, cx))
|
||||||
}
|
})??;
|
||||||
};
|
|
||||||
|
|
||||||
let status = repo.change_branch(&branch_to_checkout);
|
branch_change_task.await?;
|
||||||
if status.is_err() {
|
|
||||||
this.delegate.display_error_toast(format!("Failed to checkout branch '{branch_to_checkout}', check for conflicts or unstashed files"), cx);
|
|
||||||
status?;
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.emit(DismissEvent);
|
picker.update(&mut cx, |_, cx| {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
|
||||||
Ok::<(), anyhow::Error>(())
|
Ok::<(), anyhow::Error>(())
|
||||||
})
|
})
|
||||||
.log_err();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach_and_prompt_err("Failed to change branch", cx, |_, _| None);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
|
|
@ -2385,6 +2385,12 @@ impl Snapshot {
|
||||||
.map(|entry| entry.to_owned())
|
.map(|entry| entry.to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn git_entry(&self, work_directory_path: Arc<Path>) -> Option<RepositoryEntry> {
|
||||||
|
self.repository_entries
|
||||||
|
.get(&RepositoryWorkDirectory(work_directory_path))
|
||||||
|
.map(|entry| entry.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn git_entries(&self) -> impl Iterator<Item = &RepositoryEntry> {
|
pub fn git_entries(&self) -> impl Iterator<Item = &RepositoryEntry> {
|
||||||
self.repository_entries.values()
|
self.repository_entries.values()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue