Compare commits
9 Commits
devcontain
...
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>) {
|
||||
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},
|
||||
|
||||
@@ -15,11 +15,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;
|
||||
@@ -33,10 +35,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,
|
||||
};
|
||||
@@ -893,6 +897,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.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 } => {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user