Add git init button (#26522)

Mikayla Maki created

Because why not

Release Notes:

- N/A

Change summary

assets/settings/default.json                 |   6 
crates/collab/src/rpc.rs                     |   1 
crates/fs/src/fs.rs                          |  34 +++
crates/git/src/git.rs                        |   3 
crates/git_ui/src/git_panel.rs               | 192 +++++++++++++++++----
crates/git_ui/src/git_panel_settings.rs      |   9 
crates/git_ui/src/git_ui.rs                  |   8 
crates/git_ui/src/picker_prompt.rs           |  25 +-
crates/project/src/git.rs                    | 182 +++++++++++++++++++-
crates/project/src/project.rs                |  29 ++-
crates/proto/proto/zed.proto                 |  10 +
crates/proto/src/proto.rs                    |   3 
crates/remote_server/src/headless_project.rs |   6 
13 files changed, 427 insertions(+), 81 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -555,6 +555,12 @@
     //
     // Default: icon
     "status_style": "icon",
+    // What branch name to use if init.defaultBranch
+    // is not set
+    //
+    // Default: main
+    "fallback_branch_name": "main",
+
     "scrollbar": {
       // When to show the scrollbar in the git panel.
       //

crates/collab/src/rpc.rs 🔗

@@ -396,6 +396,7 @@ impl Server {
             .add_request_handler(forward_mutating_project_request::<proto::Stage>)
             .add_request_handler(forward_mutating_project_request::<proto::Unstage>)
             .add_request_handler(forward_mutating_project_request::<proto::Commit>)
+            .add_request_handler(forward_mutating_project_request::<proto::GitInit>)
             .add_request_handler(forward_read_only_project_request::<proto::GetRemotes>)
             .add_request_handler(forward_read_only_project_request::<proto::GitShow>)
             .add_request_handler(forward_read_only_project_request::<proto::GitReset>)

crates/fs/src/fs.rs 🔗

@@ -16,12 +16,14 @@ use git::{repository::RepoPath, status::FileStatus};
 
 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
 use ashpd::desktop::trash;
+use std::borrow::Cow;
 #[cfg(any(test, feature = "test-support"))]
 use std::collections::HashSet;
 #[cfg(unix)]
 use std::os::fd::AsFd;
 #[cfg(unix)]
 use std::os::fd::AsRawFd;
+use util::command::new_std_command;
 
 #[cfg(unix)]
 use std::os::unix::fs::MetadataExt;
@@ -136,6 +138,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<()>;
     fn is_fake(&self) -> bool;
     async fn is_case_sensitive(&self) -> Result<bool>;
 
@@ -765,6 +768,29 @@ impl Fs for RealFs {
         )))
     }
 
+    fn git_init(&self, abs_work_directory_path: &Path, fallback_branch_name: String) -> Result<()> {
+        let config = new_std_command("git")
+            .current_dir(abs_work_directory_path)
+            .args(&["config", "--global", "--get", "init.defaultBranch"])
+            .output()?;
+
+        let branch_name;
+
+        if config.status.success() && !config.stdout.is_empty() {
+            branch_name = String::from_utf8_lossy(&config.stdout);
+        } else {
+            branch_name = Cow::Borrowed(fallback_branch_name.as_str());
+        }
+
+        new_std_command("git")
+            .current_dir(abs_work_directory_path)
+            .args(&["init", "-b"])
+            .arg(branch_name.trim())
+            .output()?;
+
+        Ok(())
+    }
+
     fn is_fake(&self) -> bool {
         false
     }
@@ -2075,6 +2101,14 @@ impl Fs for FakeFs {
         }
     }
 
+    fn git_init(
+        &self,
+        abs_work_directory_path: &Path,
+        _fallback_branch_name: String,
+    ) -> Result<()> {
+        smol::block_on(self.create_dir(&abs_work_directory_path.join(".git")))
+    }
+
     fn is_fake(&self) -> bool {
         true
     }

crates/git/src/git.rs 🔗

@@ -50,7 +50,8 @@ actions!(
         Fetch,
         Commit,
         ExpandCommitEditor,
-        GenerateCommitMessage
+        GenerateCommitMessage,
+        Init,
     ]
 );
 action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);

crates/git_ui/src/git_panel.rs 🔗

@@ -1767,6 +1767,88 @@ impl GitPanel {
             .detach_and_log_err(cx);
     }
 
+    pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let worktrees = self
+            .project
+            .read(cx)
+            .visible_worktrees(cx)
+            .collect::<Vec<_>>();
+
+        let worktree = if worktrees.len() == 1 {
+            Task::ready(Some(worktrees.first().unwrap().clone()))
+        } else if worktrees.len() == 0 {
+            let result = window.prompt(
+                PromptLevel::Warning,
+                "Unable to initialize a git repository",
+                Some("Open a directory first"),
+                &["Ok"],
+                cx,
+            );
+            cx.background_executor()
+                .spawn(async move {
+                    result.await.ok();
+                })
+                .detach();
+            return;
+        } else {
+            let worktree_directories = worktrees
+                .iter()
+                .map(|worktree| worktree.read(cx).abs_path())
+                .map(|worktree_abs_path| {
+                    if let Ok(path) = worktree_abs_path.strip_prefix(util::paths::home_dir()) {
+                        Path::new("~")
+                            .join(path)
+                            .to_string_lossy()
+                            .to_string()
+                            .into()
+                    } else {
+                        worktree_abs_path.to_string_lossy().to_string().into()
+                    }
+                })
+                .collect_vec();
+            let prompt = picker_prompt::prompt(
+                "Where would you like to initialize this git repository?",
+                worktree_directories,
+                self.workspace.clone(),
+                window,
+                cx,
+            );
+
+            cx.spawn(|_, _| async move { prompt.await.map(|ix| worktrees[ix].clone()) })
+        };
+
+        cx.spawn_in(window, |this, mut cx| async move {
+            let worktree = match worktree.await {
+                Some(worktree) => worktree,
+                None => {
+                    return;
+                }
+            };
+
+            let Ok(result) = this.update(&mut cx, |this, cx| {
+                let fallback_branch_name = GitPanelSettings::get_global(cx)
+                    .fallback_branch_name
+                    .clone();
+                this.project.read(cx).git_init(
+                    worktree.read(cx).abs_path(),
+                    fallback_branch_name,
+                    cx,
+                )
+            }) else {
+                return;
+            };
+
+            let result = result.await;
+
+            this.update_in(&mut cx, |this, _, cx| match result {
+                Ok(()) => {}
+                Err(e) => this.show_error_toast("init", e, cx),
+            })
+            .ok();
+        })
+        .detach();
+    }
+
     pub(crate) fn pull(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         if !self.can_push_and_pull(cx) {
             return;
@@ -1971,7 +2053,7 @@ impl GitPanel {
                             cx,
                         )
                     })?
-                    .await?;
+                    .await;
 
                 Ok(selection.map(|selection| Remote {
                     name: current_remotes[selection].clone(),
@@ -2677,7 +2759,13 @@ impl GitPanel {
         })
     }
 
-    fn render_panel_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render_panel_header(
+        &self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Option<impl IntoElement> {
+        self.active_repository.as_ref()?;
+
         let text;
         let action;
         let tooltip;
@@ -2697,39 +2785,42 @@ impl GitPanel {
             _ => format!("{} Changes", self.entry_count),
         };
 
-        self.panel_header_container(window, cx)
-            .px_2()
-            .child(
-                panel_button(change_string)
-                    .color(Color::Muted)
-                    .tooltip(Tooltip::for_action_title_in(
-                        "Open diff",
-                        &Diff,
-                        &self.focus_handle,
-                    ))
-                    .on_click(|_, _, cx| {
-                        cx.defer(|cx| {
-                            cx.dispatch_action(&Diff);
-                        })
-                    }),
-            )
-            .child(div().flex_grow()) // spacer
-            .child(self.render_overflow_menu("overflow_menu"))
-            .child(
-                panel_filled_button(text)
-                    .tooltip(Tooltip::for_action_title_in(
-                        tooltip,
-                        action.as_ref(),
-                        &self.focus_handle,
-                    ))
-                    .disabled(self.entry_count == 0)
-                    .on_click(move |_, _, cx| {
-                        let action = action.boxed_clone();
-                        cx.defer(move |cx| {
-                            cx.dispatch_action(action.as_ref());
-                        })
-                    }),
-            )
+        Some(
+            self.panel_header_container(window, cx)
+                .px_2()
+                .child(
+                    panel_button(change_string)
+                        .color(Color::Muted)
+                        .tooltip(Tooltip::for_action_title_in(
+                            "Open diff",
+                            &Diff,
+                            &self.focus_handle,
+                        ))
+                        .on_click(|_, _, cx| {
+                            cx.defer(|cx| {
+                                cx.dispatch_action(&Diff);
+                            })
+                        }),
+                )
+                .child(div().flex_grow()) // spacer
+                .child(self.render_overflow_menu("overflow_menu"))
+                .child(div().w_2()) // another spacer
+                .child(
+                    panel_filled_button(text)
+                        .tooltip(Tooltip::for_action_title_in(
+                            tooltip,
+                            action.as_ref(),
+                            &self.focus_handle,
+                        ))
+                        .disabled(self.entry_count == 0)
+                        .on_click(move |_, _, cx| {
+                            let action = action.boxed_clone();
+                            cx.defer(move |cx| {
+                                cx.dispatch_action(action.as_ref());
+                            })
+                        }),
+                ),
+        )
     }
 
     pub fn render_footer(
@@ -2949,12 +3040,29 @@ impl GitPanel {
             .items_center()
             .child(
                 v_flex()
-                    .gap_3()
-                    .child(if self.active_repository.is_some() {
-                        "No changes to commit"
-                    } else {
-                        "No Git repositories"
-                    })
+                    .gap_2()
+                    .child(h_flex().w_full().justify_around().child(
+                        if self.active_repository.is_some() {
+                            "No changes to commit"
+                        } else {
+                            "No Git repositories"
+                        },
+                    ))
+                    .children(self.active_repository.is_none().then(|| {
+                        h_flex().w_full().justify_around().child(
+                            panel_filled_button("Initialize Repository")
+                                .tooltip(Tooltip::for_action_title_in(
+                                    "git init",
+                                    &git::Init,
+                                    &self.focus_handle,
+                                ))
+                                .on_click(move |_, _, cx| {
+                                    cx.defer(move |cx| {
+                                        cx.dispatch_action(&git::Init);
+                                    })
+                                }),
+                        )
+                    }))
                     .text_ui_sm(cx)
                     .mx_auto()
                     .text_color(Color::Placeholder.color(cx)),
@@ -3674,7 +3782,7 @@ impl Render for GitPanel {
             .child(
                 v_flex()
                     .size_full()
-                    .child(self.render_panel_header(window, cx))
+                    .children(self.render_panel_header(window, cx))
                     .map(|this| {
                         if has_entries {
                             this.child(self.render_entries(has_write_access, window, cx))

crates/git_ui/src/git_panel_settings.rs 🔗

@@ -58,15 +58,22 @@ pub struct GitPanelSettingsContent {
     ///
     /// Default: inherits editor scrollbar settings
     pub scrollbar: Option<ScrollbarSettings>,
+
+    /// What the default branch name should be when
+    /// `init.defaultBranch` is not set in git
+    ///
+    /// Default: main
+    pub fallback_branch_name: Option<String>,
 }
 
-#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
+#[derive(Deserialize, Debug, Clone, PartialEq)]
 pub struct GitPanelSettings {
     pub button: bool,
     pub dock: DockPosition,
     pub default_width: Pixels,
     pub status_style: StatusStyle,
     pub scrollbar: ScrollbarSettings,
+    pub fallback_branch_name: String,
 }
 
 impl Settings for GitPanelSettings {

crates/git_ui/src/git_ui.rs 🔗

@@ -82,6 +82,14 @@ pub fn init(cx: &mut App) {
                 panel.unstage_all(action, window, cx);
             });
         });
+        workspace.register_action(|workspace, _action: &git::Init, window, cx| {
+            let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
+                return;
+            };
+            panel.update(cx, |panel, cx| {
+                panel.git_init(window, cx);
+            });
+        });
     })
     .detach();
 }

crates/git_ui/src/picker_prompt.rs 🔗

@@ -1,4 +1,3 @@
-use anyhow::{anyhow, Result};
 use futures::channel::oneshot;
 use fuzzy::{StringMatch, StringMatchCandidate};
 
@@ -26,9 +25,9 @@ pub fn prompt(
     workspace: WeakEntity<Workspace>,
     window: &mut Window,
     cx: &mut App,
-) -> Task<Result<Option<usize>>> {
+) -> Task<Option<usize>> {
     if options.is_empty() {
-        return Task::ready(Err(anyhow!("No options")));
+        return Task::ready(None);
     }
     let prompt = prompt.to_string().into();
 
@@ -37,15 +36,17 @@ pub fn prompt(
         let (tx, rx) = oneshot::channel();
         let delegate = PickerPromptDelegate::new(prompt, options, tx, 70);
 
-        workspace.update_in(&mut cx, |workspace, window, cx| {
-            workspace.toggle_modal(window, cx, |window, cx| {
-                PickerPrompt::new(delegate, 34., window, cx)
+        workspace
+            .update_in(&mut cx, |workspace, window, cx| {
+                workspace.toggle_modal(window, cx, |window, cx| {
+                    PickerPrompt::new(delegate, 34., window, cx)
+                })
             })
-        })?;
+            .ok();
 
         match rx.await {
-            Ok(selection) => Some(selection).transpose(),
-            Err(_) => anyhow::Ok(None), // User cancelled
+            Ok(selection) => Some(selection),
+            Err(_) => None, // User cancelled
         }
     })
 }
@@ -94,14 +95,14 @@ pub struct PickerPromptDelegate {
     all_options: Vec<SharedString>,
     selected_index: usize,
     max_match_length: usize,
-    tx: Option<oneshot::Sender<Result<usize>>>,
+    tx: Option<oneshot::Sender<usize>>,
 }
 
 impl PickerPromptDelegate {
     pub fn new(
         prompt: Arc<str>,
         options: Vec<SharedString>,
-        tx: oneshot::Sender<Result<usize>>,
+        tx: oneshot::Sender<usize>,
         max_chars: usize,
     ) -> Self {
         Self {
@@ -200,7 +201,7 @@ impl PickerDelegate for PickerPromptDelegate {
             return;
         };
 
-        self.tx.take().map(|tx| tx.send(Ok(option.candidate_id)));
+        self.tx.take().map(|tx| tx.send(option.candidate_id));
         cx.emit(DismissEvent);
     }
 

crates/project/src/git.rs 🔗

@@ -8,6 +8,7 @@ use askpass::{AskPassDelegate, AskPassSession};
 use buffer_diff::BufferDiffEvent;
 use client::ProjectId;
 use collections::HashMap;
+use fs::Fs;
 use futures::{
     channel::{mpsc, oneshot},
     future::OptionFuture,
@@ -38,21 +39,37 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
+
 use text::BufferId;
 use util::{debug_panic, maybe, ResultExt};
 use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry, WorkDirectory};
 
 pub struct GitStore {
+    state: GitStoreState,
     buffer_store: Entity<BufferStore>,
-    environment: Option<Entity<ProjectEnvironment>>,
-    pub(super) project_id: Option<ProjectId>,
-    pub(super) client: AnyProtoClient,
     repositories: Vec<Entity<Repository>>,
     active_index: Option<usize>,
     update_sender: mpsc::UnboundedSender<GitJob>,
     _subscriptions: [Subscription; 2],
 }
 
+enum GitStoreState {
+    Local {
+        client: AnyProtoClient,
+        environment: Entity<ProjectEnvironment>,
+        fs: Arc<dyn Fs>,
+    },
+    Ssh {
+        environment: Entity<ProjectEnvironment>,
+        upstream_client: AnyProtoClient,
+        project_id: ProjectId,
+    },
+    Remote {
+        upstream_client: AnyProtoClient,
+        project_id: ProjectId,
+    },
+}
+
 pub struct Repository {
     commit_message_buffer: Option<Entity<Buffer>>,
     git_store: WeakEntity<GitStore>,
@@ -101,12 +118,12 @@ enum GitJobKey {
 impl EventEmitter<GitEvent> for GitStore {}
 
 impl GitStore {
-    pub fn new(
+    pub fn local(
         worktree_store: &Entity<WorktreeStore>,
         buffer_store: Entity<BufferStore>,
-        environment: Option<Entity<ProjectEnvironment>>,
+        environment: Entity<ProjectEnvironment>,
+        fs: Arc<dyn Fs>,
         client: AnyProtoClient,
-        project_id: Option<ProjectId>,
         cx: &mut Context<'_, Self>,
     ) -> Self {
         let update_sender = Self::spawn_git_worker(cx);
@@ -115,11 +132,73 @@ impl GitStore {
             cx.subscribe(&buffer_store, Self::on_buffer_store_event),
         ];
 
+        let state = GitStoreState::Local {
+            client,
+            environment,
+            fs,
+        };
+
         GitStore {
+            state,
+            buffer_store,
+            repositories: Vec::new(),
+            active_index: None,
+            update_sender,
+            _subscriptions,
+        }
+    }
+
+    pub fn remote(
+        worktree_store: &Entity<WorktreeStore>,
+        buffer_store: Entity<BufferStore>,
+        upstream_client: AnyProtoClient,
+        project_id: ProjectId,
+        cx: &mut Context<'_, Self>,
+    ) -> Self {
+        let update_sender = Self::spawn_git_worker(cx);
+        let _subscriptions = [
+            cx.subscribe(worktree_store, Self::on_worktree_store_event),
+            cx.subscribe(&buffer_store, Self::on_buffer_store_event),
+        ];
+
+        let state = GitStoreState::Remote {
+            upstream_client,
             project_id,
-            client,
+        };
+
+        GitStore {
+            state,
             buffer_store,
+            repositories: Vec::new(),
+            active_index: None,
+            update_sender,
+            _subscriptions,
+        }
+    }
+
+    pub fn ssh(
+        worktree_store: &Entity<WorktreeStore>,
+        buffer_store: Entity<BufferStore>,
+        environment: Entity<ProjectEnvironment>,
+        upstream_client: AnyProtoClient,
+        project_id: ProjectId,
+        cx: &mut Context<'_, Self>,
+    ) -> Self {
+        let update_sender = Self::spawn_git_worker(cx);
+        let _subscriptions = [
+            cx.subscribe(worktree_store, Self::on_worktree_store_event),
+            cx.subscribe(&buffer_store, Self::on_buffer_store_event),
+        ];
+
+        let state = GitStoreState::Ssh {
+            upstream_client,
+            project_id,
             environment,
+        };
+
+        GitStore {
+            state,
+            buffer_store,
             repositories: Vec::new(),
             active_index: None,
             update_sender,
@@ -132,6 +211,7 @@ impl GitStore {
         client.add_entity_request_handler(Self::handle_get_branches);
         client.add_entity_request_handler(Self::handle_change_branch);
         client.add_entity_request_handler(Self::handle_create_branch);
+        client.add_entity_request_handler(Self::handle_git_init);
         client.add_entity_request_handler(Self::handle_push);
         client.add_entity_request_handler(Self::handle_pull);
         client.add_entity_request_handler(Self::handle_fetch);
@@ -153,6 +233,34 @@ impl GitStore {
             .map(|index| self.repositories[index].clone())
     }
 
+    fn client(&self) -> AnyProtoClient {
+        match &self.state {
+            GitStoreState::Local { client, .. } => client.clone(),
+            GitStoreState::Ssh {
+                upstream_client, ..
+            } => upstream_client.clone(),
+            GitStoreState::Remote {
+                upstream_client, ..
+            } => upstream_client.clone(),
+        }
+    }
+
+    fn project_environment(&self) -> Option<Entity<ProjectEnvironment>> {
+        match &self.state {
+            GitStoreState::Local { environment, .. } => Some(environment.clone()),
+            GitStoreState::Ssh { environment, .. } => Some(environment.clone()),
+            GitStoreState::Remote { .. } => None,
+        }
+    }
+
+    fn project_id(&self) -> Option<ProjectId> {
+        match &self.state {
+            GitStoreState::Local { .. } => None,
+            GitStoreState::Ssh { project_id, .. } => Some(*project_id),
+            GitStoreState::Remote { project_id, .. } => Some(*project_id),
+        }
+    }
+
     fn on_worktree_store_event(
         &mut self,
         worktree_store: Entity<WorktreeStore>,
@@ -162,8 +270,8 @@ impl GitStore {
         let mut new_repositories = Vec::new();
         let mut new_active_index = None;
         let this = cx.weak_entity();
-        let client = self.client.clone();
-        let project_id = self.project_id;
+        let client = self.client();
+        let project_id = self.project_id();
 
         worktree_store.update(cx, |worktree_store, cx| {
             for worktree in worktree_store.worktrees() {
@@ -229,9 +337,9 @@ impl GitStore {
                             });
                             existing_handle
                         } else {
+                            let environment = self.project_environment();
                             cx.new(|_| Repository {
-                                project_environment: self
-                                    .environment
+                                project_environment: environment
                                     .as_ref()
                                     .map(|env| env.downgrade()),
                                 git_store: this.clone(),
@@ -382,6 +490,56 @@ impl GitStore {
         job_tx
     }
 
+    pub fn git_init(
+        &self,
+        path: Arc<Path>,
+        fallback_branch_name: String,
+        cx: &App,
+    ) -> Task<Result<()>> {
+        match &self.state {
+            GitStoreState::Local { fs, .. } => {
+                let fs = fs.clone();
+                cx.background_executor()
+                    .spawn(async move { fs.git_init(&path, fallback_branch_name) })
+            }
+            GitStoreState::Ssh {
+                upstream_client,
+                project_id,
+                ..
+            }
+            | GitStoreState::Remote {
+                upstream_client,
+                project_id,
+            } => {
+                let client = upstream_client.clone();
+                let project_id = *project_id;
+                cx.background_executor().spawn(async move {
+                    client
+                        .request(proto::GitInit {
+                            project_id: project_id.0,
+                            abs_path: path.to_string_lossy().to_string(),
+                            fallback_branch_name,
+                        })
+                        .await?;
+                    Ok(())
+                })
+            }
+        }
+    }
+
+    async fn handle_git_init(
+        this: Entity<Self>,
+        envelope: TypedEnvelope<proto::GitInit>,
+        cx: AsyncApp,
+    ) -> Result<proto::Ack> {
+        let path: Arc<Path> = PathBuf::from(envelope.payload.abs_path).into();
+        let name = envelope.payload.fallback_branch_name;
+        cx.update(|cx| this.read(cx).git_init(path, name, cx))?
+            .await?;
+
+        Ok(proto::Ack {})
+    }
+
     async fn handle_fetch(
         this: Entity<Self>,
         envelope: TypedEnvelope<proto::Fetch>,
@@ -889,7 +1047,7 @@ fn make_remote_delegate(
 ) -> AskPassDelegate {
     AskPassDelegate::new(cx, move |prompt, tx, cx| {
         this.update(cx, |this, cx| {
-            let response = this.client.request(proto::AskPassRequest {
+            let response = this.client().request(proto::AskPassRequest {
                 project_id,
                 worktree_id: worktree_id.to_proto(),
                 work_directory_id: work_directory_id.to_proto(),

crates/project/src/project.rs 🔗

@@ -841,12 +841,12 @@ impl Project {
             });
 
             let git_store = cx.new(|cx| {
-                GitStore::new(
+                GitStore::local(
                     &worktree_store,
                     buffer_store.clone(),
-                    Some(environment.clone()),
+                    environment.clone(),
+                    fs.clone(),
                     client.clone().into(),
-                    None,
                     cx,
                 )
             });
@@ -970,12 +970,12 @@ impl Project {
             cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
 
             let git_store = cx.new(|cx| {
-                GitStore::new(
+                GitStore::ssh(
                     &worktree_store,
                     buffer_store.clone(),
-                    Some(environment.clone()),
+                    environment.clone(),
                     ssh_proto.clone(),
-                    Some(ProjectId(SSH_PROJECT_ID)),
+                    ProjectId(SSH_PROJECT_ID),
                     cx,
                 )
             });
@@ -1177,12 +1177,12 @@ impl Project {
         })?;
 
         let git_store = cx.new(|cx| {
-            GitStore::new(
+            GitStore::remote(
+                // In this remote case we pass None for the environment
                 &worktree_store,
                 buffer_store.clone(),
-                None,
                 client.clone().into(),
-                Some(ProjectId(remote_id)),
+                ProjectId(remote_id),
                 cx,
             )
         })?;
@@ -4443,6 +4443,17 @@ impl Project {
         })
     }
 
+    pub fn git_init(
+        &self,
+        path: Arc<Path>,
+        fallback_branch_name: String,
+        cx: &App,
+    ) -> Task<Result<()>> {
+        self.git_store
+            .read(cx)
+            .git_init(path, fallback_branch_name, cx)
+    }
+
     pub fn buffer_store(&self) -> &Entity<BufferStore> {
         &self.buffer_store
     }

crates/proto/proto/zed.proto 🔗

@@ -344,7 +344,9 @@ message Envelope {
         AskPassResponse ask_pass_response = 318;
 
         GitDiff git_diff = 319;
-        GitDiffResponse git_diff_response = 320; // current max
+        GitDiffResponse git_diff_response = 320;
+
+        GitInit git_init = 321; // current max
     }
 
     reserved 87 to 88;
@@ -2937,3 +2939,9 @@ message GitDiff {
 message GitDiffResponse {
     string diff = 1;
 }
+
+message GitInit {
+    uint64 project_id = 1;
+    string abs_path = 2;
+    string fallback_branch_name = 3;
+}

crates/proto/src/proto.rs 🔗

@@ -460,6 +460,7 @@ messages!(
     (CheckForPushedCommitsResponse, Background),
     (GitDiff, Background),
     (GitDiffResponse, Background),
+    (GitInit, Background),
 );
 
 request_messages!(
@@ -607,6 +608,7 @@ request_messages!(
     (GitChangeBranch, Ack),
     (CheckForPushedCommits, CheckForPushedCommitsResponse),
     (GitDiff, GitDiffResponse),
+    (GitInit, Ack),
 );
 
 entity_messages!(
@@ -713,6 +715,7 @@ entity_messages!(
     GitCreateBranch,
     CheckForPushedCommits,
     GitDiff,
+    GitInit,
 );
 
 entity_messages!(

crates/remote_server/src/headless_project.rs 🔗

@@ -89,12 +89,12 @@ impl HeadlessProject {
 
         let environment = project::ProjectEnvironment::new(&worktree_store, None, cx);
         let git_store = cx.new(|cx| {
-            GitStore::new(
+            GitStore::local(
                 &worktree_store,
                 buffer_store.clone(),
-                Some(environment.clone()),
+                environment.clone(),
+                fs.clone(),
                 session.clone().into(),
-                None,
                 cx,
             )
         });