Detailed changes
@@ -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();
+}
@@ -2849,93 +2849,15 @@ impl GitPanel {
}
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();
- cx.spawn_in(window, async move |this, cx| {
- let mut paths = path.await.ok()?.ok()??;
- let mut path = paths.pop()?;
- let repo_name = repo.split("/").last()?.strip_suffix(".git")?.to_owned();
-
- let fs = this.read_with(cx, |this, _| this.fs.clone()).ok()?;
-
- 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();
+ crate::clone::clone_and_open(
+ repo.into(),
+ workspace,
+ window,
+ cx,
+ Arc::new(|_workspace: &mut workspace::Workspace, _window, _cx| {}),
+ );
}
pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -10,6 +10,7 @@ use ui::{
};
mod blame_ui;
+pub mod clone;
use git::{
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
@@ -18,11 +18,13 @@ use extension::ExtensionHostProxy;
use fs::{Fs, RealFs};
use futures::{StreamExt, channel::oneshot, future};
use git::GitHostingProviderRegistry;
+use git_ui::clone::clone_and_open;
use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, QuitMode, UpdateGlobal as _};
use gpui_tokio::Tokio;
use language::LanguageRegistry;
use onboarding::{FIRST_OPEN, show_onboarding_view};
+use project_panel::ProjectPanel;
use prompt_store::PromptBuilder;
use remote::RemoteConnectionOptions;
use reqwest_client::ReqwestClient;
@@ -36,10 +38,12 @@ use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use session::{AppSession, Session};
use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file};
use std::{
+ cell::RefCell,
env,
io::{self, IsTerminal},
path::{Path, PathBuf},
process,
+ rc::Rc,
sync::{Arc, OnceLock},
time::Instant,
};
@@ -896,6 +900,41 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
})
.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;
+ 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 } => {
cx.spawn(async move |cx| {
let paths_with_position =
@@ -25,6 +25,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
+use ui::SharedString;
use util::ResultExt;
use util::paths::PathWithPosition;
use workspace::PathList;
@@ -58,6 +59,9 @@ pub enum OpenRequestKind {
/// `None` opens settings without navigating to a specific path.
setting_path: Option<String>,
},
+ GitClone {
+ repo_url: SharedString,
+ },
GitCommit {
sha: String,
},
@@ -113,6 +117,8 @@ impl OpenRequest {
this.kind = Some(OpenRequestKind::Setting {
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/") {
this.parse_git_commit_url(commit_path)?
} 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<()> {
// Format: <sha>?repo=<path>
let (sha, query) = commit_path
@@ -1087,4 +1113,80 @@ mod tests {
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"),
+ }
+ }
}