ssh project: Handle multiple paths and worktrees correctly (#18277)
This makes SSH projects work with `ssh_connections` that have multiple paths: ```json { "ssh_connections": [ { "host": "127.0.0.1", "projects": [ { "paths": [ "/Users/thorstenball/work/projs/go-proj", "/Users/thorstenball/work/projs/rust-proj" ] } ] } ] } ``` @ConradIrwin @mikayla-maki since this wasn't really released yet, we didn't create a full-on migration, so old ssh projects that were already serialized need to either be manually deleted from the database, or the whole local DB wiped. Release Notes: - N/A --------- Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
parent
3a2f0653d1
commit
437bcc0ce6
4 changed files with 66 additions and 56 deletions
|
@ -268,7 +268,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|port| port.to_string())
|
.map(|port| port.to_string())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
ssh_project.path,
|
ssh_project.paths.join(","),
|
||||||
ssh_project
|
ssh_project
|
||||||
.user
|
.user
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -403,7 +403,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
password: None,
|
password: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let paths = vec![PathBuf::from(ssh_project.path.clone())];
|
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
|
||||||
|
|
||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
open_ssh_project(connection_options, paths, app_state, open_options, &mut cx).await
|
open_ssh_project(connection_options, paths, app_state, open_options, &mut cx).await
|
||||||
|
@ -460,9 +460,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
.filter_map(|i| paths.paths().get(*i).cloned())
|
.filter_map(|i| paths.paths().get(*i).cloned())
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
SerializedWorkspaceLocation::Ssh(ssh_project) => {
|
SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()),
|
||||||
Arc::new(vec![PathBuf::from(ssh_project.ssh_url())])
|
|
||||||
}
|
|
||||||
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
|
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
|
||||||
Arc::new(vec![PathBuf::from(format!(
|
Arc::new(vec![PathBuf::from(format!(
|
||||||
"{}:{}",
|
"{}:{}",
|
||||||
|
|
|
@ -366,6 +366,9 @@ define_connection! {
|
||||||
);
|
);
|
||||||
ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
|
ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
|
||||||
),
|
),
|
||||||
|
sql!(
|
||||||
|
ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -769,39 +772,40 @@ impl WorkspaceDb {
|
||||||
&self,
|
&self,
|
||||||
host: String,
|
host: String,
|
||||||
port: Option<u16>,
|
port: Option<u16>,
|
||||||
path: String,
|
paths: Vec<String>,
|
||||||
user: Option<String>,
|
user: Option<String>,
|
||||||
) -> Result<SerializedSshProject> {
|
) -> Result<SerializedSshProject> {
|
||||||
|
let paths = serde_json::to_string(&paths)?;
|
||||||
if let Some(project) = self
|
if let Some(project) = self
|
||||||
.get_ssh_project(host.clone(), port, path.clone(), user.clone())
|
.get_ssh_project(host.clone(), port, paths.clone(), user.clone())
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
Ok(project)
|
Ok(project)
|
||||||
} else {
|
} else {
|
||||||
self.insert_ssh_project(host, port, path, user)
|
self.insert_ssh_project(host, port, paths, user)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow!("failed to insert ssh project"))
|
.ok_or_else(|| anyhow!("failed to insert ssh project"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query! {
|
query! {
|
||||||
async fn get_ssh_project(host: String, port: Option<u16>, path: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
|
async fn get_ssh_project(host: String, port: Option<u16>, paths: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
|
||||||
SELECT id, host, port, path, user
|
SELECT id, host, port, paths, user
|
||||||
FROM ssh_projects
|
FROM ssh_projects
|
||||||
WHERE host IS ? AND port IS ? AND path IS ? AND user IS ?
|
WHERE host IS ? AND port IS ? AND paths IS ? AND user IS ?
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query! {
|
query! {
|
||||||
async fn insert_ssh_project(host: String, port: Option<u16>, path: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
|
async fn insert_ssh_project(host: String, port: Option<u16>, paths: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
|
||||||
INSERT INTO ssh_projects(
|
INSERT INTO ssh_projects(
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
path,
|
paths,
|
||||||
user
|
user
|
||||||
) VALUES (?1, ?2, ?3, ?4)
|
) VALUES (?1, ?2, ?3, ?4)
|
||||||
RETURNING id, host, port, path, user
|
RETURNING id, host, port, paths, user
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -840,7 +844,7 @@ impl WorkspaceDb {
|
||||||
|
|
||||||
query! {
|
query! {
|
||||||
fn ssh_projects() -> Result<Vec<SerializedSshProject>> {
|
fn ssh_projects() -> Result<Vec<SerializedSshProject>> {
|
||||||
SELECT id, host, port, path, user
|
SELECT id, host, port, paths, user
|
||||||
FROM ssh_projects
|
FROM ssh_projects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1656,45 +1660,45 @@ mod tests {
|
||||||
async fn test_get_or_create_ssh_project() {
|
async fn test_get_or_create_ssh_project() {
|
||||||
let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project").await);
|
let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project").await);
|
||||||
|
|
||||||
let (host, port, path, user) = (
|
let (host, port, paths, user) = (
|
||||||
"example.com".to_string(),
|
"example.com".to_string(),
|
||||||
Some(22_u16),
|
Some(22_u16),
|
||||||
"/home/user".to_string(),
|
vec!["/home/user".to_string(), "/etc/nginx".to_string()],
|
||||||
Some("user".to_string()),
|
Some("user".to_string()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let project = db
|
let project = db
|
||||||
.get_or_create_ssh_project(host.clone(), port, path.clone(), user.clone())
|
.get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(project.host, host);
|
assert_eq!(project.host, host);
|
||||||
assert_eq!(project.path, path);
|
assert_eq!(project.paths, paths);
|
||||||
assert_eq!(project.user, user);
|
assert_eq!(project.user, user);
|
||||||
|
|
||||||
// Test that calling the function again with the same parameters returns the same project
|
// Test that calling the function again with the same parameters returns the same project
|
||||||
let same_project = db
|
let same_project = db
|
||||||
.get_or_create_ssh_project(host.clone(), port, path.clone(), user.clone())
|
.get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(project.id, same_project.id);
|
assert_eq!(project.id, same_project.id);
|
||||||
|
|
||||||
// Test with different parameters
|
// Test with different parameters
|
||||||
let (host2, path2, user2) = (
|
let (host2, paths2, user2) = (
|
||||||
"otherexample.com".to_string(),
|
"otherexample.com".to_string(),
|
||||||
"/home/otheruser".to_string(),
|
vec!["/home/otheruser".to_string()],
|
||||||
Some("otheruser".to_string()),
|
Some("otheruser".to_string()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let different_project = db
|
let different_project = db
|
||||||
.get_or_create_ssh_project(host2.clone(), None, path2.clone(), user2.clone())
|
.get_or_create_ssh_project(host2.clone(), None, paths2.clone(), user2.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_ne!(project.id, different_project.id);
|
assert_ne!(project.id, different_project.id);
|
||||||
assert_eq!(different_project.host, host2);
|
assert_eq!(different_project.host, host2);
|
||||||
assert_eq!(different_project.path, path2);
|
assert_eq!(different_project.paths, paths2);
|
||||||
assert_eq!(different_project.user, user2);
|
assert_eq!(different_project.user, user2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1702,25 +1706,25 @@ mod tests {
|
||||||
async fn test_get_or_create_ssh_project_with_null_user() {
|
async fn test_get_or_create_ssh_project_with_null_user() {
|
||||||
let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project_with_null_user").await);
|
let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project_with_null_user").await);
|
||||||
|
|
||||||
let (host, port, path, user) = (
|
let (host, port, paths, user) = (
|
||||||
"example.com".to_string(),
|
"example.com".to_string(),
|
||||||
None,
|
None,
|
||||||
"/home/user".to_string(),
|
vec!["/home/user".to_string()],
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let project = db
|
let project = db
|
||||||
.get_or_create_ssh_project(host.clone(), port, path.clone(), None)
|
.get_or_create_ssh_project(host.clone(), port, paths.clone(), None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(project.host, host);
|
assert_eq!(project.host, host);
|
||||||
assert_eq!(project.path, path);
|
assert_eq!(project.paths, paths);
|
||||||
assert_eq!(project.user, None);
|
assert_eq!(project.user, None);
|
||||||
|
|
||||||
// Test that calling the function again with the same parameters returns the same project
|
// Test that calling the function again with the same parameters returns the same project
|
||||||
let same_project = db
|
let same_project = db
|
||||||
.get_or_create_ssh_project(host.clone(), port, path.clone(), user.clone())
|
.get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -1735,32 +1739,32 @@ mod tests {
|
||||||
(
|
(
|
||||||
"example.com".to_string(),
|
"example.com".to_string(),
|
||||||
None,
|
None,
|
||||||
"/home/user".to_string(),
|
vec!["/home/user".to_string()],
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"anotherexample.com".to_string(),
|
"anotherexample.com".to_string(),
|
||||||
Some(123_u16),
|
Some(123_u16),
|
||||||
"/home/user2".to_string(),
|
vec!["/home/user2".to_string()],
|
||||||
Some("user2".to_string()),
|
Some("user2".to_string()),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"yetanother.com".to_string(),
|
"yetanother.com".to_string(),
|
||||||
Some(345_u16),
|
Some(345_u16),
|
||||||
"/home/user3".to_string(),
|
vec!["/home/user3".to_string(), "/proc/1234/exe".to_string()],
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (host, port, path, user) in projects.iter() {
|
for (host, port, paths, user) in projects.iter() {
|
||||||
let project = db
|
let project = db
|
||||||
.get_or_create_ssh_project(host.clone(), *port, path.clone(), user.clone())
|
.get_or_create_ssh_project(host.clone(), *port, paths.clone(), user.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(&project.host, host);
|
assert_eq!(&project.host, host);
|
||||||
assert_eq!(&project.port, port);
|
assert_eq!(&project.port, port);
|
||||||
assert_eq!(&project.path, path);
|
assert_eq!(&project.paths, paths);
|
||||||
assert_eq!(&project.user, user);
|
assert_eq!(&project.user, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,24 +26,29 @@ pub struct SerializedSshProject {
|
||||||
pub id: SshProjectId,
|
pub id: SshProjectId,
|
||||||
pub host: String,
|
pub host: String,
|
||||||
pub port: Option<u16>,
|
pub port: Option<u16>,
|
||||||
pub path: String,
|
pub paths: Vec<String>,
|
||||||
pub user: Option<String>,
|
pub user: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SerializedSshProject {
|
impl SerializedSshProject {
|
||||||
pub fn ssh_url(&self) -> String {
|
pub fn ssh_urls(&self) -> Vec<PathBuf> {
|
||||||
let mut result = String::from("ssh://");
|
self.paths
|
||||||
if let Some(user) = &self.user {
|
.iter()
|
||||||
result.push_str(user);
|
.map(|path| {
|
||||||
result.push('@');
|
let mut result = String::new();
|
||||||
}
|
if let Some(user) = &self.user {
|
||||||
result.push_str(&self.host);
|
result.push_str(user);
|
||||||
if let Some(port) = &self.port {
|
result.push('@');
|
||||||
result.push(':');
|
}
|
||||||
result.push_str(&port.to_string());
|
result.push_str(&self.host);
|
||||||
}
|
if let Some(port) = &self.port {
|
||||||
result.push_str(&self.path);
|
result.push(':');
|
||||||
result
|
result.push_str(&port.to_string());
|
||||||
|
}
|
||||||
|
result.push_str(path);
|
||||||
|
PathBuf::from(result)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +63,8 @@ impl Bind for &SerializedSshProject {
|
||||||
let next_index = statement.bind(&self.id.0, start_index)?;
|
let next_index = statement.bind(&self.id.0, start_index)?;
|
||||||
let next_index = statement.bind(&self.host, next_index)?;
|
let next_index = statement.bind(&self.host, next_index)?;
|
||||||
let next_index = statement.bind(&self.port, next_index)?;
|
let next_index = statement.bind(&self.port, next_index)?;
|
||||||
let next_index = statement.bind(&self.path, next_index)?;
|
let raw_paths = serde_json::to_string(&self.paths)?;
|
||||||
|
let next_index = statement.bind(&raw_paths, next_index)?;
|
||||||
statement.bind(&self.user, next_index)
|
statement.bind(&self.user, next_index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +74,9 @@ impl Column for SerializedSshProject {
|
||||||
let id = statement.column_int64(start_index)?;
|
let id = statement.column_int64(start_index)?;
|
||||||
let host = statement.column_text(start_index + 1)?.to_string();
|
let host = statement.column_text(start_index + 1)?.to_string();
|
||||||
let (port, _) = Option::<u16>::column(statement, start_index + 2)?;
|
let (port, _) = Option::<u16>::column(statement, start_index + 2)?;
|
||||||
let path = statement.column_text(start_index + 3)?.to_string();
|
let raw_paths = statement.column_text(start_index + 3)?.to_string();
|
||||||
|
let paths: Vec<String> = serde_json::from_str(&raw_paths)?;
|
||||||
|
|
||||||
let (user, _) = Option::<String>::column(statement, start_index + 4)?;
|
let (user, _) = Option::<String>::column(statement, start_index + 4)?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
|
@ -76,7 +84,7 @@ impl Column for SerializedSshProject {
|
||||||
id: SshProjectId(id as u64),
|
id: SshProjectId(id as u64),
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
path,
|
paths,
|
||||||
user,
|
user,
|
||||||
},
|
},
|
||||||
start_index + 5,
|
start_index + 5,
|
||||||
|
|
|
@ -5516,14 +5516,14 @@ pub fn open_ssh_project(
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
// TODO: Handle multiple paths
|
|
||||||
let path = paths.iter().next().cloned().unwrap_or_default();
|
|
||||||
|
|
||||||
let serialized_ssh_project = persistence::DB
|
let serialized_ssh_project = persistence::DB
|
||||||
.get_or_create_ssh_project(
|
.get_or_create_ssh_project(
|
||||||
connection_options.host.clone(),
|
connection_options.host.clone(),
|
||||||
connection_options.port,
|
connection_options.port,
|
||||||
path.to_string_lossy().to_string(),
|
paths
|
||||||
|
.iter()
|
||||||
|
.map(|path| path.to_string_lossy().to_string())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
connection_options.username.clone(),
|
connection_options.username.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue