Compare commits
9 Commits
screenshot
...
git-clone
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cc3b3a04f | ||
|
|
422dc4f307 | ||
|
|
b1aa0e2efd | ||
|
|
3dbfee1c47 | ||
|
|
4930d3aa80 | ||
|
|
5d633a3968 | ||
|
|
6967ea41e5 | ||
|
|
5068581b39 | ||
|
|
f3bd6b88db |
155
crates/git_ui/src/clone.rs
Normal file
155
crates/git_ui/src/clone.rs
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
use gpui::{App, Context, WeakEntity, Window};
|
||||||
|
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use ui::{Color, IconName, SharedString};
|
||||||
|
use util::ResultExt;
|
||||||
|
use workspace::{self, Workspace};
|
||||||
|
|
||||||
|
pub fn clone_and_open(
|
||||||
|
repo_url: SharedString,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
on_success: Arc<
|
||||||
|
dyn Fn(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + Sync + 'static,
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
let destination_prompt = cx.prompt_for_paths(gpui::PathPromptOptions {
|
||||||
|
files: false,
|
||||||
|
directories: true,
|
||||||
|
multiple: false,
|
||||||
|
prompt: Some("Select as Repository Destination".into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
window
|
||||||
|
.spawn(cx, async move |cx| {
|
||||||
|
let mut paths = destination_prompt.await.ok()?.ok()??;
|
||||||
|
let mut destination_dir = paths.pop()?;
|
||||||
|
|
||||||
|
let repo_name = repo_url
|
||||||
|
.split('/')
|
||||||
|
.next_back()
|
||||||
|
.map(|name| name.strip_suffix(".git").unwrap_or(name))
|
||||||
|
.unwrap_or("repository")
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
let clone_task = workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
let fs = workspace.app_state().fs.clone();
|
||||||
|
let destination_dir = destination_dir.clone();
|
||||||
|
let repo_url = repo_url.clone();
|
||||||
|
cx.spawn(async move |_workspace, _cx| {
|
||||||
|
fs.git_clone(&repo_url, destination_dir.as_path()).await
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
if let Err(error) = clone_task.await {
|
||||||
|
workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
let toast = StatusToast::new(error.to_string(), cx, |this, _| {
|
||||||
|
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
||||||
|
.dismiss_button(true)
|
||||||
|
});
|
||||||
|
workspace.toggle_status_toast(toast, cx);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_worktrees = workspace
|
||||||
|
.read_with(cx, |workspace, cx| {
|
||||||
|
workspace.project().read(cx).worktrees(cx).next().is_some()
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let prompt_answer = if has_worktrees {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
window.prompt(
|
||||||
|
gpui::PromptLevel::Info,
|
||||||
|
&format!("Git Clone: {}", repo_name),
|
||||||
|
None,
|
||||||
|
&["Add repo to project", "Open repo in new project"],
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.ok()?
|
||||||
|
.await
|
||||||
|
.ok()?
|
||||||
|
} else {
|
||||||
|
// Don't ask if project is empty
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
destination_dir.push(&repo_name);
|
||||||
|
|
||||||
|
match prompt_answer {
|
||||||
|
0 => {
|
||||||
|
workspace
|
||||||
|
.update_in(cx, |workspace, window, cx| {
|
||||||
|
let create_task = workspace.project().update(cx, |project, cx| {
|
||||||
|
project.create_worktree(destination_dir.as_path(), true, cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
let workspace_weak = cx.weak_entity();
|
||||||
|
let on_success = on_success.clone();
|
||||||
|
cx.spawn_in(window, async move |_window, cx| {
|
||||||
|
if create_task.await.log_err().is_some() {
|
||||||
|
workspace_weak
|
||||||
|
.update_in(cx, |workspace, window, cx| {
|
||||||
|
(on_success)(workspace, window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
workspace
|
||||||
|
.update(cx, move |workspace, cx| {
|
||||||
|
let app_state = workspace.app_state().clone();
|
||||||
|
let destination_path = destination_dir.clone();
|
||||||
|
let on_success = on_success.clone();
|
||||||
|
|
||||||
|
workspace::open_new(
|
||||||
|
Default::default(),
|
||||||
|
app_state,
|
||||||
|
cx,
|
||||||
|
move |workspace, window, cx| {
|
||||||
|
cx.activate(true);
|
||||||
|
|
||||||
|
let create_task =
|
||||||
|
workspace.project().update(cx, |project, cx| {
|
||||||
|
project.create_worktree(
|
||||||
|
destination_path.as_path(),
|
||||||
|
true,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let workspace_weak = cx.weak_entity();
|
||||||
|
cx.spawn_in(window, async move |_window, cx| {
|
||||||
|
if create_task.await.log_err().is_some() {
|
||||||
|
workspace_weak
|
||||||
|
.update_in(cx, |workspace, window, cx| {
|
||||||
|
(on_success)(workspace, window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
@@ -2848,93 +2848,15 @@ impl GitPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn git_clone(&mut self, repo: String, window: &mut Window, cx: &mut Context<Self>) {
|
pub(crate) fn git_clone(&mut self, repo: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let path = cx.prompt_for_paths(gpui::PathPromptOptions {
|
|
||||||
files: false,
|
|
||||||
directories: true,
|
|
||||||
multiple: false,
|
|
||||||
prompt: Some("Select as Repository Destination".into()),
|
|
||||||
});
|
|
||||||
|
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
crate::clone::clone_and_open(
|
||||||
let mut paths = path.await.ok()?.ok()??;
|
repo.into(),
|
||||||
let mut path = paths.pop()?;
|
workspace,
|
||||||
let repo_name = repo.split("/").last()?.strip_suffix(".git")?.to_owned();
|
window,
|
||||||
|
cx,
|
||||||
let fs = this.read_with(cx, |this, _| this.fs.clone()).ok()?;
|
Arc::new(|_workspace: &mut workspace::Workspace, _window, _cx| {}),
|
||||||
|
);
|
||||||
let prompt_answer = match fs.git_clone(&repo, path.as_path()).await {
|
|
||||||
Ok(_) => cx.update(|window, cx| {
|
|
||||||
window.prompt(
|
|
||||||
PromptLevel::Info,
|
|
||||||
&format!("Git Clone: {}", repo_name),
|
|
||||||
None,
|
|
||||||
&["Add repo to project", "Open repo in new project"],
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
Err(e) => {
|
|
||||||
this.update(cx, |this: &mut GitPanel, cx| {
|
|
||||||
let toast = StatusToast::new(e.to_string(), cx, |this, _| {
|
|
||||||
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
|
||||||
.dismiss_button(true)
|
|
||||||
});
|
|
||||||
|
|
||||||
this.workspace
|
|
||||||
.update(cx, |workspace, cx| {
|
|
||||||
workspace.toggle_status_toast(toast, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
path.push(repo_name);
|
|
||||||
match prompt_answer.await.ok()? {
|
|
||||||
0 => {
|
|
||||||
workspace
|
|
||||||
.update(cx, |workspace, cx| {
|
|
||||||
workspace
|
|
||||||
.project()
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.create_worktree(path.as_path(), true, cx)
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
workspace
|
|
||||||
.update(cx, move |workspace, cx| {
|
|
||||||
workspace::open_new(
|
|
||||||
Default::default(),
|
|
||||||
workspace.app_state().clone(),
|
|
||||||
cx,
|
|
||||||
move |workspace, _, cx| {
|
|
||||||
cx.activate(true);
|
|
||||||
workspace
|
|
||||||
.project()
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.create_worktree(&path, true, cx)
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.detach();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(())
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use ui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
mod blame_ui;
|
mod blame_ui;
|
||||||
|
pub mod clone;
|
||||||
|
|
||||||
use git::{
|
use git::{
|
||||||
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ use extension::ExtensionHostProxy;
|
|||||||
use fs::{Fs, RealFs};
|
use fs::{Fs, RealFs};
|
||||||
use futures::{StreamExt, channel::oneshot, future};
|
use futures::{StreamExt, channel::oneshot, future};
|
||||||
use git::GitHostingProviderRegistry;
|
use git::GitHostingProviderRegistry;
|
||||||
|
use git_ui::clone::clone_and_open;
|
||||||
use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, QuitMode, UpdateGlobal as _};
|
use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, QuitMode, UpdateGlobal as _};
|
||||||
|
|
||||||
use gpui_tokio::Tokio;
|
use gpui_tokio::Tokio;
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use onboarding::{FIRST_OPEN, show_onboarding_view};
|
use onboarding::{FIRST_OPEN, show_onboarding_view};
|
||||||
|
use project_panel::ProjectPanel;
|
||||||
use prompt_store::PromptBuilder;
|
use prompt_store::PromptBuilder;
|
||||||
use remote::RemoteConnectionOptions;
|
use remote::RemoteConnectionOptions;
|
||||||
use reqwest_client::ReqwestClient;
|
use reqwest_client::ReqwestClient;
|
||||||
@@ -33,10 +35,12 @@ use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
|||||||
use session::{AppSession, Session};
|
use session::{AppSession, Session};
|
||||||
use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file};
|
use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file};
|
||||||
use std::{
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
env,
|
env,
|
||||||
io::{self, IsTerminal},
|
io::{self, IsTerminal},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process,
|
process,
|
||||||
|
rc::Rc,
|
||||||
sync::{Arc, OnceLock},
|
sync::{Arc, OnceLock},
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
@@ -893,6 +897,41 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
|
|||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
OpenRequestKind::GitClone { repo_url } => {
|
||||||
|
workspace::with_active_or_new_workspace(cx, |_workspace, window, cx| {
|
||||||
|
if window.is_window_active() {
|
||||||
|
clone_and_open(
|
||||||
|
repo_url,
|
||||||
|
cx.weak_entity(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
Arc::new(|workspace: &mut workspace::Workspace, window, cx| {
|
||||||
|
workspace.focus_panel::<ProjectPanel>(window, cx);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let subscription = Rc::new(RefCell::new(None));
|
||||||
|
subscription.replace(Some(cx.observe_in(&cx.entity(), window, {
|
||||||
|
let subscription = subscription.clone();
|
||||||
|
let repo_url = repo_url.clone();
|
||||||
|
move |_, workspace_entity, window, cx| {
|
||||||
|
if window.is_window_active() && subscription.take().is_some() {
|
||||||
|
clone_and_open(
|
||||||
|
repo_url.clone(),
|
||||||
|
workspace_entity.downgrade(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
Arc::new(|workspace: &mut workspace::Workspace, window, cx| {
|
||||||
|
workspace.focus_panel::<ProjectPanel>(window, cx);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
}
|
||||||
OpenRequestKind::GitCommit { sha } => {
|
OpenRequestKind::GitCommit { sha } => {
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let paths_with_position =
|
let paths_with_position =
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use ui::SharedString;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use util::paths::PathWithPosition;
|
use util::paths::PathWithPosition;
|
||||||
use workspace::PathList;
|
use workspace::PathList;
|
||||||
@@ -58,6 +59,9 @@ pub enum OpenRequestKind {
|
|||||||
/// `None` opens settings without navigating to a specific path.
|
/// `None` opens settings without navigating to a specific path.
|
||||||
setting_path: Option<String>,
|
setting_path: Option<String>,
|
||||||
},
|
},
|
||||||
|
GitClone {
|
||||||
|
repo_url: SharedString,
|
||||||
|
},
|
||||||
GitCommit {
|
GitCommit {
|
||||||
sha: String,
|
sha: String,
|
||||||
},
|
},
|
||||||
@@ -113,6 +117,8 @@ impl OpenRequest {
|
|||||||
this.kind = Some(OpenRequestKind::Setting {
|
this.kind = Some(OpenRequestKind::Setting {
|
||||||
setting_path: Some(setting_path.to_string()),
|
setting_path: Some(setting_path.to_string()),
|
||||||
});
|
});
|
||||||
|
} else if let Some(clone_path) = url.strip_prefix("zed://git/clone") {
|
||||||
|
this.parse_git_clone_url(clone_path)?
|
||||||
} else if let Some(commit_path) = url.strip_prefix("zed://git/commit/") {
|
} else if let Some(commit_path) = url.strip_prefix("zed://git/commit/") {
|
||||||
this.parse_git_commit_url(commit_path)?
|
this.parse_git_commit_url(commit_path)?
|
||||||
} else if url.starts_with("ssh://") {
|
} else if url.starts_with("ssh://") {
|
||||||
@@ -143,6 +149,26 @@ impl OpenRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_git_clone_url(&mut self, clone_path: &str) -> Result<()> {
|
||||||
|
// Format: /?repo=<url> or ?repo=<url>
|
||||||
|
let clone_path = clone_path.strip_prefix('/').unwrap_or(clone_path);
|
||||||
|
|
||||||
|
let query = clone_path
|
||||||
|
.strip_prefix('?')
|
||||||
|
.context("invalid git clone url: missing query string")?;
|
||||||
|
|
||||||
|
let repo_url = url::form_urlencoded::parse(query.as_bytes())
|
||||||
|
.find_map(|(key, value)| (key == "repo").then_some(value))
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.context("invalid git clone url: missing repo query parameter")?
|
||||||
|
.to_string()
|
||||||
|
.into();
|
||||||
|
|
||||||
|
self.kind = Some(OpenRequestKind::GitClone { repo_url });
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_git_commit_url(&mut self, commit_path: &str) -> Result<()> {
|
fn parse_git_commit_url(&mut self, commit_path: &str) -> Result<()> {
|
||||||
// Format: <sha>?repo=<path>
|
// Format: <sha>?repo=<path>
|
||||||
let (sha, query) = commit_path
|
let (sha, query) = commit_path
|
||||||
@@ -1087,4 +1113,80 @@ mod tests {
|
|||||||
|
|
||||||
assert!(!errored_reuse);
|
assert!(!errored_reuse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_parse_git_clone_url(cx: &mut TestAppContext) {
|
||||||
|
let _app_state = init_test(cx);
|
||||||
|
|
||||||
|
let request = cx.update(|cx| {
|
||||||
|
OpenRequest::parse(
|
||||||
|
RawOpenRequest {
|
||||||
|
urls: vec![
|
||||||
|
"zed://git/clone/?repo=https://github.com/zed-industries/zed.git".into(),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
match request.kind {
|
||||||
|
Some(OpenRequestKind::GitClone { repo_url }) => {
|
||||||
|
assert_eq!(repo_url, "https://github.com/zed-industries/zed.git");
|
||||||
|
}
|
||||||
|
_ => panic!("Expected GitClone kind"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_parse_git_clone_url_without_slash(cx: &mut TestAppContext) {
|
||||||
|
let _app_state = init_test(cx);
|
||||||
|
|
||||||
|
let request = cx.update(|cx| {
|
||||||
|
OpenRequest::parse(
|
||||||
|
RawOpenRequest {
|
||||||
|
urls: vec![
|
||||||
|
"zed://git/clone?repo=https://github.com/zed-industries/zed.git".into(),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
match request.kind {
|
||||||
|
Some(OpenRequestKind::GitClone { repo_url }) => {
|
||||||
|
assert_eq!(repo_url, "https://github.com/zed-industries/zed.git");
|
||||||
|
}
|
||||||
|
_ => panic!("Expected GitClone kind"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_parse_git_clone_url_with_encoding(cx: &mut TestAppContext) {
|
||||||
|
let _app_state = init_test(cx);
|
||||||
|
|
||||||
|
let request = cx.update(|cx| {
|
||||||
|
OpenRequest::parse(
|
||||||
|
RawOpenRequest {
|
||||||
|
urls: vec![
|
||||||
|
"zed://git/clone/?repo=https%3A%2F%2Fgithub.com%2Fzed-industries%2Fzed.git"
|
||||||
|
.into(),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
match request.kind {
|
||||||
|
Some(OpenRequestKind::GitClone { repo_url }) => {
|
||||||
|
assert_eq!(repo_url, "https://github.com/zed-industries/zed.git");
|
||||||
|
}
|
||||||
|
_ => panic!("Expected GitClone kind"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user