git: Add ability to clone remote repositories from Zed (#35606)
This PR adds preliminary git clone support through using the new `GitClone` action. This works with SSH connections too. - [x] Get backend working - [x] Add a UI to interact with this Future follow-ups: - Polish the UI - Have the path select prompt say "Select Repository clone target" instead of “Open” - Use Zed path prompt if the user has that as a setting - Add support for cloning from a user's GitHub repositories directly Release Notes: - Add the ability to clone remote git repositories through the `git: Clone` action --------- Co-authored-by: hpmcdona <hayden_mcdonald@brown.edu>
This commit is contained in:
parent
12084b6677
commit
62270b33c2
9 changed files with 310 additions and 8 deletions
|
@ -12,7 +12,7 @@ use gpui::BackgroundExecutor;
|
|||
use gpui::Global;
|
||||
use gpui::ReadGlobal as _;
|
||||
use std::borrow::Cow;
|
||||
use util::command::new_std_command;
|
||||
use util::command::{new_smol_command, new_std_command};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::fd::{AsFd, AsRawFd};
|
||||
|
@ -134,6 +134,7 @@ pub trait Fs: Send + Sync {
|
|||
fn home_dir(&self) -> Option<PathBuf>;
|
||||
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
|
||||
fn git_init(&self, abs_work_directory: &Path, fallback_branch_name: String) -> Result<()>;
|
||||
async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()>;
|
||||
fn is_fake(&self) -> bool;
|
||||
async fn is_case_sensitive(&self) -> Result<bool>;
|
||||
|
||||
|
@ -839,6 +840,23 @@ impl Fs for RealFs {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()> {
|
||||
let output = new_smol_command("git")
|
||||
.current_dir(abs_work_directory)
|
||||
.args(&["clone", repo_url])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!(
|
||||
"git clone failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_fake(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
@ -2352,6 +2370,10 @@ impl Fs for FakeFs {
|
|||
smol::block_on(self.create_dir(&abs_work_directory_path.join(".git")))
|
||||
}
|
||||
|
||||
async fn git_clone(&self, _repo_url: &str, _abs_work_directory: &Path) -> Result<()> {
|
||||
anyhow::bail!("Git clone is not supported in fake Fs")
|
||||
}
|
||||
|
||||
fn is_fake(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
|
|
@ -93,6 +93,8 @@ actions!(
|
|||
Init,
|
||||
/// Opens all modified files in the editor.
|
||||
OpenModifiedFiles,
|
||||
/// Clones a repository.
|
||||
Clone,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -2081,6 +2081,99 @@ impl GitPanel {
|
|||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
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(std::path::MAIN_SEPARATOR_STR)
|
||||
.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,
|
||||
"Git Clone",
|
||||
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>) {
|
||||
let worktrees = self
|
||||
.project
|
||||
|
|
|
@ -3,21 +3,25 @@ use std::any::Any;
|
|||
use ::settings::Settings;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use commit_modal::CommitModal;
|
||||
use editor::{Editor, actions::DiffClipboardWithSelectionData};
|
||||
use editor::{Editor, EditorElement, EditorStyle, actions::DiffClipboardWithSelectionData};
|
||||
mod blame_ui;
|
||||
use git::{
|
||||
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||
status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
|
||||
};
|
||||
use git_panel_settings::GitPanelSettings;
|
||||
use gpui::{Action, App, Context, FocusHandle, Window, actions};
|
||||
use gpui::{
|
||||
Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, TextStyle,
|
||||
Window, actions,
|
||||
};
|
||||
use onboarding::GitOnboardingModal;
|
||||
use project_diff::ProjectDiff;
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
use workspace::{ModalView, Workspace};
|
||||
use zed_actions;
|
||||
|
||||
use crate::text_diff_view::TextDiffView;
|
||||
use crate::{git_panel::GitPanel, text_diff_view::TextDiffView};
|
||||
|
||||
mod askpass_modal;
|
||||
pub mod branch_picker;
|
||||
|
@ -169,6 +173,19 @@ pub fn init(cx: &mut App) {
|
|||
panel.git_init(window, cx);
|
||||
});
|
||||
});
|
||||
workspace.register_action(|workspace, _action: &git::Clone, window, cx| {
|
||||
let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
GitCloneModal::show(panel, window, cx)
|
||||
});
|
||||
|
||||
// panel.update(cx, |panel, cx| {
|
||||
// panel.git_clone(window, cx);
|
||||
// });
|
||||
});
|
||||
workspace.register_action(|workspace, _: &git::OpenModifiedFiles, window, cx| {
|
||||
open_modified_files(workspace, window, cx);
|
||||
});
|
||||
|
@ -613,3 +630,98 @@ impl Component for GitStatusIcon {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct GitCloneModal {
|
||||
panel: Entity<GitPanel>,
|
||||
repo_input: Entity<Editor>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl GitCloneModal {
|
||||
pub fn show(panel: Entity<GitPanel>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let repo_input = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
editor.set_placeholder_text("Enter repository", cx);
|
||||
editor
|
||||
});
|
||||
let focus_handle = repo_input.focus_handle(cx);
|
||||
|
||||
window.focus(&focus_handle);
|
||||
|
||||
Self {
|
||||
panel,
|
||||
repo_input,
|
||||
focus_handle,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_editor(&self, window: &Window, cx: &App) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let theme = cx.theme();
|
||||
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
background_color: Some(theme.colors().editor_background),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let element = EditorElement::new(
|
||||
&self.repo_input,
|
||||
EditorStyle {
|
||||
background: theme.colors().editor_background,
|
||||
local_player: theme.players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
div()
|
||||
.rounded_md()
|
||||
.p_1()
|
||||
.border_1()
|
||||
.border_color(theme.colors().border_variant)
|
||||
.when(
|
||||
self.repo_input
|
||||
.focus_handle(cx)
|
||||
.contains_focused(window, cx),
|
||||
|this| this.border_color(theme.colors().border_focused),
|
||||
)
|
||||
.child(element)
|
||||
.bg(theme.colors().editor_background)
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for GitCloneModal {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for GitCloneModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.w(rems(34.))
|
||||
.elevation_3(cx)
|
||||
.child(self.render_editor(window, cx))
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
|
||||
let repo = this.repo_input.read(cx).text(cx);
|
||||
this.panel.update(cx, |panel, cx| {
|
||||
panel.git_clone(repo, window, cx);
|
||||
});
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for GitCloneModal {}
|
||||
|
||||
impl ModalView for GitCloneModal {}
|
||||
|
|
|
@ -987,7 +987,7 @@ pub struct InlayHintSettings {
|
|||
/// Default: false
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
/// Global switch to toggle inline values on and off.
|
||||
/// Global switch to toggle inline values on and off when debugging.
|
||||
///
|
||||
/// Default: true
|
||||
#[serde(default = "default_true")]
|
||||
|
|
|
@ -441,6 +441,7 @@ impl GitStore {
|
|||
client.add_entity_request_handler(Self::handle_blame_buffer);
|
||||
client.add_entity_message_handler(Self::handle_update_repository);
|
||||
client.add_entity_message_handler(Self::handle_remove_repository);
|
||||
client.add_entity_request_handler(Self::handle_git_clone);
|
||||
}
|
||||
|
||||
pub fn is_local(&self) -> bool {
|
||||
|
@ -1464,6 +1465,45 @@ impl GitStore {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn git_clone(
|
||||
&self,
|
||||
repo: String,
|
||||
path: impl Into<Arc<std::path::Path>>,
|
||||
cx: &App,
|
||||
) -> Task<Result<()>> {
|
||||
let path = path.into();
|
||||
match &self.state {
|
||||
GitStoreState::Local { fs, .. } => {
|
||||
let fs = fs.clone();
|
||||
cx.background_executor()
|
||||
.spawn(async move { fs.git_clone(&repo, &path).await })
|
||||
}
|
||||
GitStoreState::Ssh {
|
||||
upstream_client,
|
||||
upstream_project_id,
|
||||
..
|
||||
} => {
|
||||
let request = upstream_client.request(proto::GitClone {
|
||||
project_id: upstream_project_id.0,
|
||||
abs_path: path.to_string_lossy().to_string(),
|
||||
remote_repo: repo,
|
||||
});
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let result = request.await?;
|
||||
|
||||
match result.success {
|
||||
true => Ok(()),
|
||||
false => Err(anyhow!("Git Clone failed")),
|
||||
}
|
||||
})
|
||||
}
|
||||
GitStoreState::Remote { .. } => {
|
||||
Task::ready(Err(anyhow!("Git Clone isn't supported for remote users")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_update_repository(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateRepository>,
|
||||
|
@ -1550,6 +1590,22 @@ impl GitStore {
|
|||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_git_clone(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::GitClone>,
|
||||
cx: AsyncApp,
|
||||
) -> Result<proto::GitCloneResponse> {
|
||||
let path: Arc<Path> = PathBuf::from(envelope.payload.abs_path).into();
|
||||
let repo_name = envelope.payload.remote_repo;
|
||||
let result = cx
|
||||
.update(|cx| this.read(cx).git_clone(repo_name, path, cx))?
|
||||
.await;
|
||||
|
||||
Ok(proto::GitCloneResponse {
|
||||
success: result.is_ok(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_fetch(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::Fetch>,
|
||||
|
|
|
@ -202,6 +202,16 @@ message GitInit {
|
|||
string fallback_branch_name = 3;
|
||||
}
|
||||
|
||||
message GitClone {
|
||||
uint64 project_id = 1;
|
||||
string abs_path = 2;
|
||||
string remote_repo = 3;
|
||||
}
|
||||
|
||||
message GitCloneResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message CheckForPushedCommits {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
|
|
|
@ -399,7 +399,10 @@ message Envelope {
|
|||
GetDefaultBranchResponse get_default_branch_response = 360;
|
||||
|
||||
GetCrashFiles get_crash_files = 361;
|
||||
GetCrashFilesResponse get_crash_files_response = 362; // current max
|
||||
GetCrashFilesResponse get_crash_files_response = 362;
|
||||
|
||||
GitClone git_clone = 363;
|
||||
GitCloneResponse git_clone_response = 364; // current max
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
|
|
|
@ -316,6 +316,8 @@ messages!(
|
|||
(PullWorkspaceDiagnostics, Background),
|
||||
(GetDefaultBranch, Background),
|
||||
(GetDefaultBranchResponse, Background),
|
||||
(GitClone, Background),
|
||||
(GitCloneResponse, Background)
|
||||
);
|
||||
|
||||
request_messages!(
|
||||
|
@ -484,6 +486,7 @@ request_messages!(
|
|||
(GetDocumentDiagnostics, GetDocumentDiagnosticsResponse),
|
||||
(PullWorkspaceDiagnostics, Ack),
|
||||
(GetDefaultBranch, GetDefaultBranchResponse),
|
||||
(GitClone, GitCloneResponse)
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
|
@ -615,7 +618,8 @@ entity_messages!(
|
|||
LogToDebugConsole,
|
||||
GetDocumentDiagnostics,
|
||||
PullWorkspaceDiagnostics,
|
||||
GetDefaultBranch
|
||||
GetDefaultBranch,
|
||||
GitClone
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue