Detailed changes
@@ -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!(