Cargo.lock 🔗
@@ -5298,6 +5298,7 @@ dependencies = [
"pretty_assertions",
"regex",
"rope",
+ "schemars",
"serde",
"serde_json",
"smol",
Mikayla Maki and Michael Sloan created
Release Notes:
- N/A
---------
Co-authored-by: Michael Sloan <mgsloan@gmail.com>
Cargo.lock | 1
crates/collab/src/rpc.rs | 4
crates/editor/src/editor.rs | 7
crates/editor/src/test/editor_test_context.rs | 1
crates/git/Cargo.toml | 1
crates/git/src/git.rs | 12
crates/git/src/repository.rs | 267 +++++++++++--
crates/git_ui/src/branch_picker.rs | 2
crates/git_ui/src/commit_modal.rs | 7
crates/git_ui/src/git_panel.rs | 408 ++++++++++++++++++--
crates/git_ui/src/git_ui.rs | 1
crates/git_ui/src/picker_prompt.rs | 235 ++++++++++++
crates/project/src/git.rs | 340 +++++++++++++++--
crates/project/src/worktree_store.rs | 17
crates/proto/proto/zed.proto | 51 ++
crates/proto/src/proto.rs | 13
crates/worktree/src/worktree.rs | 47 +
17 files changed, 1,238 insertions(+), 176 deletions(-)
@@ -5298,6 +5298,7 @@ dependencies = [
"pretty_assertions",
"regex",
"rope",
+ "schemars",
"serde",
"serde_json",
"smol",
@@ -392,9 +392,13 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::OpenContext>)
.add_request_handler(forward_mutating_project_request::<proto::CreateContext>)
.add_request_handler(forward_mutating_project_request::<proto::SynchronizeContexts>)
+ .add_request_handler(forward_mutating_project_request::<proto::Push>)
+ .add_request_handler(forward_mutating_project_request::<proto::Pull>)
+ .add_request_handler(forward_mutating_project_request::<proto::Fetch>)
.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_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>)
.add_request_handler(forward_read_only_project_request::<proto::GitCheckoutFiles>)
@@ -12979,7 +12979,12 @@ impl Editor {
.update(cx, |buffer_store, cx| buffer_store.save_buffer(buffer, cx))
.detach_and_log_err(cx);
- let _ = repo.read(cx).set_index_text(&path, new_index_text);
+ cx.background_spawn(
+ repo.read(cx)
+ .set_index_text(&path, new_index_text)
+ .log_err(),
+ )
+ .detach();
}
pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context<Self>) {
@@ -298,6 +298,7 @@ impl EditorTestContext {
self.cx.run_until_parked();
}
+ #[track_caller]
pub fn assert_index_text(&mut self, expected: Option<&str>) {
let fs = self.update_editor(|editor, _, cx| {
editor.project.as_ref().unwrap().read(cx).fs().as_fake()
@@ -26,6 +26,7 @@ log.workspace = true
parking_lot.workspace = true
regex.workspace = true
rope.workspace = true
+schemars.workspace = true
serde.workspace = true
smol.workspace = true
sum_tree.workspace = true
@@ -8,6 +8,9 @@ pub mod status;
use anyhow::{anyhow, Context as _, Result};
use gpui::action_with_deprecated_aliases;
use gpui::actions;
+use gpui::impl_actions;
+use repository::PushOptions;
+use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
use std::fmt;
@@ -27,6 +30,13 @@ pub static COMMIT_MESSAGE: LazyLock<&'static OsStr> =
LazyLock::new(|| OsStr::new("COMMIT_EDITMSG"));
pub static INDEX_LOCK: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("index.lock"));
+#[derive(Debug, Copy, Clone, PartialEq, Deserialize, JsonSchema)]
+pub struct Push {
+ pub options: Option<PushOptions>,
+}
+
+impl_actions!(git, [Push]);
+
actions!(
git,
[
@@ -43,6 +53,8 @@ actions!(
RestoreTrackedFiles,
TrashUntrackedFiles,
Uncommit,
+ Pull,
+ Fetch,
Commit,
]
);
@@ -7,6 +7,8 @@ use git2::BranchType;
use gpui::SharedString;
use parking_lot::Mutex;
use rope::Rope;
+use schemars::JsonSchema;
+use serde::Deserialize;
use std::borrow::Borrow;
use std::io::Write as _;
use std::process::Stdio;
@@ -29,6 +31,12 @@ pub struct Branch {
}
impl Branch {
+ pub fn tracking_status(&self) -> Option<UpstreamTrackingStatus> {
+ self.upstream
+ .as_ref()
+ .and_then(|upstream| upstream.tracking.status())
+ }
+
pub fn priority_key(&self) -> (bool, Option<i64>) {
(
self.is_head,
@@ -42,11 +50,32 @@ impl Branch {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Upstream {
pub ref_name: SharedString,
- pub tracking: Option<UpstreamTracking>,
+ pub tracking: UpstreamTracking,
}
-#[derive(Clone, Debug, Hash, PartialEq, Eq)]
-pub struct UpstreamTracking {
+#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
+pub enum UpstreamTracking {
+ /// Remote ref not present in local repository.
+ Gone,
+ /// Remote ref present in local repository (fetched from remote).
+ Tracked(UpstreamTrackingStatus),
+}
+
+impl UpstreamTracking {
+ pub fn is_gone(&self) -> bool {
+ matches!(self, UpstreamTracking::Gone)
+ }
+
+ pub fn status(&self) -> Option<UpstreamTrackingStatus> {
+ match self {
+ UpstreamTracking::Gone => None,
+ UpstreamTracking::Tracked(status) => Some(*status),
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
+pub struct UpstreamTrackingStatus {
pub ahead: u32,
pub behind: u32,
}
@@ -68,6 +97,11 @@ pub struct CommitDetails {
pub committer_name: SharedString,
}
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
+pub struct Remote {
+ pub name: SharedString,
+}
+
pub enum ResetMode {
// reset the branch pointer, leave index and worktree unchanged
// (this will make it look like things that were committed are now
@@ -139,6 +173,22 @@ pub trait GitRepository: Send + Sync {
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>;
+
+ fn push(
+ &self,
+ branch_name: &str,
+ upstream_name: &str,
+ options: Option<PushOptions>,
+ ) -> Result<()>;
+ fn pull(&self, branch_name: &str, upstream_name: &str) -> Result<()>;
+ fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
+ fn fetch(&self) -> Result<()>;
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
+pub enum PushOptions {
+ SetUpstream,
+ Force,
}
impl std::fmt::Debug for dyn GitRepository {
@@ -165,6 +215,14 @@ impl RealGitRepository {
hosting_provider_registry,
}
}
+
+ fn working_directory(&self) -> Result<PathBuf> {
+ self.repository
+ .lock()
+ .workdir()
+ .context("failed to read git work directory")
+ .map(Path::to_path_buf)
+ }
}
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
@@ -209,12 +267,7 @@ impl GitRepository for RealGitRepository {
}
fn reset(&self, commit: &str, mode: ResetMode) -> Result<()> {
- let working_directory = self
- .repository
- .lock()
- .workdir()
- .context("failed to read git work directory")?
- .to_path_buf();
+ let working_directory = self.working_directory()?;
let mode_flag = match mode {
ResetMode::Mixed => "--mixed",
@@ -238,12 +291,7 @@ impl GitRepository for RealGitRepository {
if paths.is_empty() {
return Ok(());
}
- let working_directory = self
- .repository
- .lock()
- .workdir()
- .context("failed to read git work directory")?
- .to_path_buf();
+ let working_directory = self.working_directory()?;
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
@@ -296,12 +344,7 @@ impl GitRepository for RealGitRepository {
}
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
- let working_directory = self
- .repository
- .lock()
- .workdir()
- .context("failed to read git work directory")?
- .to_path_buf();
+ let working_directory = self.working_directory()?;
if let Some(content) = content {
let mut child = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
@@ -485,12 +528,7 @@ impl GitRepository for RealGitRepository {
}
fn stage_paths(&self, paths: &[RepoPath]) -> Result<()> {
- let working_directory = self
- .repository
- .lock()
- .workdir()
- .context("failed to read git work directory")?
- .to_path_buf();
+ let working_directory = self.working_directory()?;
if !paths.is_empty() {
let output = new_std_command(&self.git_binary_path)
@@ -498,6 +536,8 @@ impl GitRepository for RealGitRepository {
.args(["update-index", "--add", "--remove", "--"])
.args(paths.iter().map(|p| p.as_ref()))
.output()?;
+
+ // TODO: Get remote response out of this and show it to the user
if !output.status.success() {
return Err(anyhow!(
"Failed to stage paths:\n{}",
@@ -509,12 +549,7 @@ impl GitRepository for RealGitRepository {
}
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()> {
- let working_directory = self
- .repository
- .lock()
- .workdir()
- .context("failed to read git work directory")?
- .to_path_buf();
+ let working_directory = self.working_directory()?;
if !paths.is_empty() {
let output = new_std_command(&self.git_binary_path)
@@ -522,6 +557,8 @@ impl GitRepository for RealGitRepository {
.args(["reset", "--quiet", "--"])
.args(paths.iter().map(|p| p.as_ref()))
.output()?;
+
+ // TODO: Get remote response out of this and show it to the user
if !output.status.success() {
return Err(anyhow!(
"Failed to unstage:\n{}",
@@ -533,32 +570,141 @@ impl GitRepository for RealGitRepository {
}
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> {
- let working_directory = self
- .repository
- .lock()
- .workdir()
- .context("failed to read git work directory")?
- .to_path_buf();
- let mut args = vec!["commit", "--quiet", "-m", message, "--cleanup=strip"];
- let author = name_and_email.map(|(name, email)| format!("{name} <{email}>"));
- if let Some(author) = author.as_deref() {
- args.push("--author");
- args.push(author);
+ let working_directory = self.working_directory()?;
+
+ let mut cmd = new_std_command(&self.git_binary_path);
+ cmd.current_dir(&working_directory)
+ .args(["commit", "--quiet", "-m"])
+ .arg(message)
+ .arg("--cleanup=strip");
+
+ if let Some((name, email)) = name_and_email {
+ cmd.arg("--author").arg(&format!("{name} <{email}>"));
}
+ let output = cmd.output()?;
+
+ // TODO: Get remote response out of this and show it to the user
+ if !output.status.success() {
+ return Err(anyhow!(
+ "Failed to commit:\n{}",
+ String::from_utf8_lossy(&output.stderr)
+ ));
+ }
+ Ok(())
+ }
+
+ fn push(
+ &self,
+ branch_name: &str,
+ remote_name: &str,
+ options: Option<PushOptions>,
+ ) -> Result<()> {
+ let working_directory = self.working_directory()?;
+
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
- .args(args)
+ .args(["push", "--quiet"])
+ .args(options.map(|option| match option {
+ PushOptions::SetUpstream => "--set-upstream",
+ PushOptions::Force => "--force-with-lease",
+ }))
+ .arg(remote_name)
+ .arg(format!("{}:{}", branch_name, branch_name))
.output()?;
if !output.status.success() {
return Err(anyhow!(
- "Failed to commit:\n{}",
+ "Failed to push:\n{}",
String::from_utf8_lossy(&output.stderr)
));
}
+
+ // TODO: Get remote response out of this and show it to the user
Ok(())
}
+
+ fn pull(&self, branch_name: &str, remote_name: &str) -> Result<()> {
+ let working_directory = self.working_directory()?;
+
+ let output = new_std_command(&self.git_binary_path)
+ .current_dir(&working_directory)
+ .args(["pull", "--quiet"])
+ .arg(remote_name)
+ .arg(branch_name)
+ .output()?;
+
+ if !output.status.success() {
+ return Err(anyhow!(
+ "Failed to pull:\n{}",
+ String::from_utf8_lossy(&output.stderr)
+ ));
+ }
+
+ // TODO: Get remote response out of this and show it to the user
+ Ok(())
+ }
+
+ fn fetch(&self) -> Result<()> {
+ let working_directory = self.working_directory()?;
+
+ let output = new_std_command(&self.git_binary_path)
+ .current_dir(&working_directory)
+ .args(["fetch", "--quiet", "--all"])
+ .output()?;
+
+ if !output.status.success() {
+ return Err(anyhow!(
+ "Failed to fetch:\n{}",
+ String::from_utf8_lossy(&output.stderr)
+ ));
+ }
+
+ // TODO: Get remote response out of this and show it to the user
+ Ok(())
+ }
+
+ fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>> {
+ let working_directory = self.working_directory()?;
+
+ if let Some(branch_name) = branch_name {
+ let output = new_std_command(&self.git_binary_path)
+ .current_dir(&working_directory)
+ .args(["config", "--get"])
+ .arg(format!("branch.{}.remote", branch_name))
+ .output()?;
+
+ if output.status.success() {
+ let remote_name = String::from_utf8_lossy(&output.stdout);
+
+ return Ok(vec![Remote {
+ name: remote_name.trim().to_string().into(),
+ }]);
+ }
+ }
+
+ let output = new_std_command(&self.git_binary_path)
+ .current_dir(&working_directory)
+ .args(["remote"])
+ .output()?;
+
+ if output.status.success() {
+ let remote_names = String::from_utf8_lossy(&output.stdout)
+ .split('\n')
+ .filter(|name| !name.is_empty())
+ .map(|name| Remote {
+ name: name.trim().to_string().into(),
+ })
+ .collect();
+
+ return Ok(remote_names);
+ } else {
+ return Err(anyhow!(
+ "Failed to get remotes:\n{}",
+ String::from_utf8_lossy(&output.stderr)
+ ));
+ }
+ }
}
#[derive(Debug, Clone)]
@@ -743,6 +889,22 @@ impl GitRepository for FakeGitRepository {
fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
unimplemented!()
}
+
+ fn push(&self, _branch: &str, _remote: &str, _options: Option<PushOptions>) -> Result<()> {
+ unimplemented!()
+ }
+
+ fn pull(&self, _branch: &str, _remote: &str) -> Result<()> {
+ unimplemented!()
+ }
+
+ fn fetch(&self) -> Result<()> {
+ unimplemented!()
+ }
+
+ fn get_remotes(&self, _branch: Option<&str>) -> Result<Vec<Remote>> {
+ unimplemented!()
+ }
}
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
@@ -911,9 +1073,9 @@ fn parse_branch_input(input: &str) -> Result<Vec<Branch>> {
Ok(branches)
}
-fn parse_upstream_track(upstream_track: &str) -> Result<Option<UpstreamTracking>> {
+fn parse_upstream_track(upstream_track: &str) -> Result<UpstreamTracking> {
if upstream_track == "" {
- return Ok(Some(UpstreamTracking {
+ return Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
ahead: 0,
behind: 0,
}));
@@ -929,7 +1091,7 @@ fn parse_upstream_track(upstream_track: &str) -> Result<Option<UpstreamTracking>
let mut behind: u32 = 0;
for component in upstream_track.split(", ") {
if component == "gone" {
- return Ok(None);
+ return Ok(UpstreamTracking::Gone);
}
if let Some(ahead_num) = component.strip_prefix("ahead ") {
ahead = ahead_num.parse::<u32>()?;
@@ -938,7 +1100,10 @@ fn parse_upstream_track(upstream_track: &str) -> Result<Option<UpstreamTracking>
behind = behind_num.parse::<u32>()?;
}
}
- Ok(Some(UpstreamTracking { ahead, behind }))
+ Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
+ ahead,
+ behind,
+ }))
}
#[test]
@@ -953,7 +1118,7 @@ fn test_branches_parsing() {
name: "zed-patches".into(),
upstream: Some(Upstream {
ref_name: "refs/remotes/origin/zed-patches".into(),
- tracking: Some(UpstreamTracking {
+ tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus {
ahead: 0,
behind: 0
})
@@ -261,7 +261,7 @@ impl PickerDelegate for BranchListDelegate {
.project()
.read(cx)
.active_repository(cx)
- .and_then(|repo| repo.read(cx).branch())
+ .and_then(|repo| repo.read(cx).current_branch())
.map(|branch| branch.name.to_string())
})
.ok()
@@ -159,7 +159,12 @@ impl CommitModal {
let branch = git_panel
.active_repository
.as_ref()
- .and_then(|repo| repo.read(cx).branch().map(|b| b.name.clone()))
+ .and_then(|repo| {
+ repo.read(cx)
+ .repository_entry
+ .branch()
+ .map(|b| b.name.clone())
+ })
.unwrap_or_else(|| "<no branch>".into());
let tooltip = if git_panel.has_staged_changes() {
"Commit staged changes"
@@ -4,7 +4,7 @@ use crate::repository_selector::RepositorySelectorPopoverMenu;
use crate::{
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
};
-use crate::{project_diff, ProjectDiff};
+use crate::{picker_prompt, project_diff, ProjectDiff};
use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use editor::commit_tooltip::CommitTooltip;
@@ -12,9 +12,9 @@ use editor::{
scroll::ScrollbarAutoHide, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer,
ShowScrollbar,
};
-use git::repository::{CommitDetails, ResetMode};
+use git::repository::{Branch, CommitDetails, PushOptions, Remote, ResetMode, UpstreamTracking};
use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
-use git::{RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
+use git::{Push, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
use gpui::*;
use itertools::Itertools;
use language::{Buffer, File};
@@ -27,6 +27,9 @@ use project::{
};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
+use std::cell::RefCell;
+use std::future::Future;
+use std::rc::Rc;
use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize};
use strum::{IntoEnumIterator, VariantNames};
use time::OffsetDateTime;
@@ -34,7 +37,7 @@ use ui::{
prelude::*, ButtonLike, Checkbox, ContextMenu, Divider, DividerColor, ElevationIndex, ListItem,
ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,
};
-use util::{maybe, ResultExt, TryFutureExt};
+use util::{maybe, post_inc, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, NotificationId},
@@ -174,7 +177,11 @@ struct PendingOperation {
op_id: usize,
}
+type RemoteOperations = Rc<RefCell<HashSet<u32>>>;
+
pub struct GitPanel {
+ remote_operation_id: u32,
+ pending_remote_operations: RemoteOperations,
pub(crate) active_repository: Option<Entity<Repository>>,
commit_editor: Entity<Editor>,
conflicted_count: usize,
@@ -206,6 +213,17 @@ pub struct GitPanel {
modal_open: bool,
}
+struct RemoteOperationGuard {
+ id: u32,
+ pending_remote_operations: RemoteOperations,
+}
+
+impl Drop for RemoteOperationGuard {
+ fn drop(&mut self) {
+ self.pending_remote_operations.borrow_mut().remove(&self.id);
+ }
+}
+
pub(crate) fn commit_message_editor(
commit_message_buffer: Entity<Buffer>,
project: Entity<Project>,
@@ -286,6 +304,8 @@ impl GitPanel {
cx.new(|cx| RepositorySelector::new(project.clone(), window, cx));
let mut git_panel = Self {
+ pending_remote_operations: Default::default(),
+ remote_operation_id: 0,
active_repository,
commit_editor,
conflicted_count: 0,
@@ -341,6 +361,16 @@ impl GitPanel {
cx.notify();
}
+ fn start_remote_operation(&mut self) -> RemoteOperationGuard {
+ let id = post_inc(&mut self.remote_operation_id);
+ self.pending_remote_operations.borrow_mut().insert(id);
+
+ RemoteOperationGuard {
+ id,
+ pending_remote_operations: self.pending_remote_operations.clone(),
+ }
+ }
+
fn serialize(&mut self, cx: &mut Context<Self>) {
let width = self.width;
self.pending_serialization = cx.background_spawn(
@@ -1121,23 +1151,44 @@ impl GitPanel {
let Some(repo) = self.active_repository.clone() else {
return;
};
+
+ // TODO: Use git merge-base to find the upstream and main branch split
+ let confirmation = Task::ready(true);
+ // let confirmation = if self.commit_editor.read(cx).is_empty(cx) {
+ // Task::ready(true)
+ // } else {
+ // let prompt = window.prompt(
+ // PromptLevel::Warning,
+ // "Uncomitting will replace the current commit message with the previous commit's message",
+ // None,
+ // &["Ok", "Cancel"],
+ // cx,
+ // );
+ // cx.spawn(|_, _| async move { prompt.await.is_ok_and(|i| i == 0) })
+ // };
+
let prior_head = self.load_commit_details("HEAD", cx);
- let task = cx.spawn(|_, mut cx| async move {
- let prior_head = prior_head.await?;
+ let task = cx.spawn_in(window, |this, mut cx| async move {
+ let result = maybe!(async {
+ if !confirmation.await {
+ Ok(None)
+ } else {
+ let prior_head = prior_head.await?;
- repo.update(&mut cx, |repo, _| repo.reset("HEAD^", ResetMode::Soft))?
- .await??;
+ repo.update(&mut cx, |repo, _| repo.reset("HEAD^", ResetMode::Soft))?
+ .await??;
- Ok(prior_head)
- });
+ Ok(Some(prior_head))
+ }
+ })
+ .await;
- let task = cx.spawn_in(window, |this, mut cx| async move {
- let result = task.await;
this.update_in(&mut cx, |this, window, cx| {
this.pending_commit.take();
match result {
- Ok(prior_commit) => {
+ Ok(None) => {}
+ Ok(Some(prior_commit)) => {
this.commit_editor.update(cx, |editor, cx| {
editor.set_text(prior_commit.message, window, cx)
});
@@ -1151,6 +1202,125 @@ impl GitPanel {
self.pending_commit = Some(task);
}
+ fn fetch(&mut self, _: &git::Fetch, _window: &mut Window, cx: &mut Context<Self>) {
+ let Some(repo) = self.active_repository.clone() else {
+ return;
+ };
+ let guard = self.start_remote_operation();
+ let fetch = repo.read(cx).fetch();
+ cx.spawn(|_, _| async move {
+ fetch.await??;
+ drop(guard);
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+
+ fn pull(&mut self, _: &git::Pull, window: &mut Window, cx: &mut Context<Self>) {
+ let guard = self.start_remote_operation();
+ let remote = self.get_current_remote(window, cx);
+ cx.spawn(move |this, mut cx| async move {
+ let remote = remote.await?;
+
+ this.update(&mut cx, |this, cx| {
+ let Some(repo) = this.active_repository.clone() else {
+ return Err(anyhow::anyhow!("No active repository"));
+ };
+
+ let Some(branch) = repo.read(cx).current_branch() else {
+ return Err(anyhow::anyhow!("No active branch"));
+ };
+
+ Ok(repo.read(cx).pull(branch.name.clone(), remote.name))
+ })??
+ .await??;
+
+ drop(guard);
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+
+ fn push(&mut self, action: &git::Push, window: &mut Window, cx: &mut Context<Self>) {
+ let guard = self.start_remote_operation();
+ let options = action.options;
+ let remote = self.get_current_remote(window, cx);
+ cx.spawn(move |this, mut cx| async move {
+ let remote = remote.await?;
+
+ this.update(&mut cx, |this, cx| {
+ let Some(repo) = this.active_repository.clone() else {
+ return Err(anyhow::anyhow!("No active repository"));
+ };
+
+ let Some(branch) = repo.read(cx).current_branch() else {
+ return Err(anyhow::anyhow!("No active branch"));
+ };
+
+ Ok(repo
+ .read(cx)
+ .push(branch.name.clone(), remote.name, options))
+ })??
+ .await??;
+
+ drop(guard);
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+
+ fn get_current_remote(
+ &mut self,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> impl Future<Output = Result<Remote>> {
+ let repo = self.active_repository.clone();
+ let workspace = self.workspace.clone();
+ let mut cx = window.to_async(cx);
+
+ async move {
+ let Some(repo) = repo else {
+ return Err(anyhow::anyhow!("No active repository"));
+ };
+
+ let mut current_remotes: Vec<Remote> = repo
+ .update(&mut cx, |repo, cx| {
+ let Some(current_branch) = repo.current_branch() else {
+ return Err(anyhow::anyhow!("No active branch"));
+ };
+
+ Ok(repo.get_remotes(Some(current_branch.name.to_string()), cx))
+ })??
+ .await?;
+
+ if current_remotes.len() == 0 {
+ return Err(anyhow::anyhow!("No active remote"));
+ } else if current_remotes.len() == 1 {
+ return Ok(current_remotes.pop().unwrap());
+ } else {
+ let current_remotes: Vec<_> = current_remotes
+ .into_iter()
+ .map(|remotes| remotes.name)
+ .collect();
+ let selection = cx
+ .update(|window, cx| {
+ picker_prompt::prompt(
+ "Pick which remote to push to",
+ current_remotes.clone(),
+ workspace,
+ window,
+ cx,
+ )
+ })?
+ .await?;
+
+ return Ok(Remote {
+ name: current_remotes[selection].clone(),
+ });
+ }
+ }
+ }
+
fn potential_co_authors(&self, cx: &App) -> Vec<(String, String)> {
let mut new_co_authors = Vec::new();
let project = self.project.read(cx);
@@ -1591,15 +1761,23 @@ impl GitPanel {
.color(Color::Muted),
)
.child(self.render_repository_selector(cx))
- .child(div().flex_grow())
+ .child(div().flex_grow()) // spacer
.child(
- Button::new("diff", "+/-")
- .tooltip(Tooltip::for_action_title("Open diff", &Diff))
- .on_click(|_, _, cx| {
- cx.defer(|cx| {
- cx.dispatch_action(&Diff);
- })
- }),
+ div()
+ .h_flex()
+ .gap_1()
+ .children(self.render_spinner(cx))
+ .children(self.render_sync_button(cx))
+ .children(self.render_pull_button(cx))
+ .child(
+ Button::new("diff", "+/-")
+ .tooltip(Tooltip::for_action_title("Open diff", &Diff))
+ .on_click(|_, _, cx| {
+ cx.defer(|cx| {
+ cx.dispatch_action(&Diff);
+ })
+ }),
+ ),
),
)
} else {
@@ -1607,6 +1785,74 @@ impl GitPanel {
}
}
+ pub fn render_spinner(&self, _cx: &mut Context<Self>) -> Option<impl IntoElement> {
+ (!self.pending_remote_operations.borrow().is_empty()).then(|| {
+ Icon::new(IconName::ArrowCircle)
+ .size(IconSize::XSmall)
+ .color(Color::Info)
+ .with_animation(
+ "arrow-circle",
+ Animation::new(Duration::from_secs(2)).repeat(),
+ |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
+ )
+ .into_any_element()
+ })
+ }
+
+ pub fn render_sync_button(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
+ let active_repository = self.project.read(cx).active_repository(cx);
+ active_repository.as_ref().map(|_| {
+ panel_filled_button("Fetch")
+ .icon(IconName::ArrowCircle)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .tooltip(Tooltip::for_action_title("git fetch", &git::Fetch))
+ .on_click(
+ cx.listener(move |this, _, window, cx| this.fetch(&git::Fetch, window, cx)),
+ )
+ .into_any_element()
+ })
+ }
+
+ pub fn render_pull_button(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
+ let active_repository = self.project.read(cx).active_repository(cx);
+ active_repository
+ .as_ref()
+ .and_then(|repo| repo.read(cx).current_branch())
+ .and_then(|branch| {
+ branch.upstream.as_ref().map(|upstream| {
+ let status = &upstream.tracking;
+
+ let disabled = status.is_gone();
+
+ panel_filled_button(match status {
+ git::repository::UpstreamTracking::Tracked(status) if status.behind > 0 => {
+ format!("Pull ({})", status.behind)
+ }
+ _ => "Pull".to_string(),
+ })
+ .icon(IconName::ArrowDown)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .disabled(status.is_gone())
+ .tooltip(move |window, cx| {
+ if disabled {
+ Tooltip::simple("Upstream is gone", cx)
+ } else {
+ // TODO: Add <origin> and <branch> argument substitutions to this
+ Tooltip::for_action("git pull", &git::Pull, window, cx)
+ }
+ })
+ .on_click(
+ cx.listener(move |this, _, window, cx| this.pull(&git::Pull, window, cx)),
+ )
+ .into_any_element()
+ })
+ })
+ }
+
pub fn render_repository_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
let active_repository = self.project.read(cx).active_repository(cx);
let repository_display_name = active_repository
@@ -1679,18 +1925,19 @@ impl GitPanel {
&& self.pending_commit.is_none()
&& !editor.read(cx).is_empty(cx)
&& self.has_write_access(cx);
+
let panel_editor_style = panel_editor_style(true, window, cx);
let enable_coauthors = self.render_co_authors(cx);
let tooltip = if self.has_staged_changes() {
- "Commit staged changes"
+ "git commit"
} else {
- "Commit changes to tracked files"
+ "git commit --all"
};
let title = if self.has_staged_changes() {
"Commit"
} else {
- "Commit All"
+ "Commit Tracked"
};
let editor_focus_handle = self.commit_editor.focus_handle(cx);
@@ -1706,7 +1953,7 @@ impl GitPanel {
let branch = self
.active_repository
.as_ref()
- .and_then(|repo| repo.read(cx).branch().map(|b| b.name.clone()))
+ .and_then(|repo| repo.read(cx).current_branch().map(|b| b.name.clone()))
.unwrap_or_else(|| "<no branch>".into());
let branch_selector = Button::new("branch-selector", branch)
@@ -1769,24 +2016,9 @@ impl GitPanel {
fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
let active_repository = self.active_repository.as_ref()?;
- let branch = active_repository.read(cx).branch()?;
+ let branch = active_repository.read(cx).current_branch()?;
let commit = branch.most_recent_commit.as_ref()?.clone();
- if branch.upstream.as_ref().is_some_and(|upstream| {
- if let Some(tracking) = &upstream.tracking {
- tracking.ahead == 0
- } else {
- true
- }
- }) {
- return None;
- }
- let tooltip = if self.has_staged_changes() {
- "git reset HEAD^ --soft"
- } else {
- "git reset HEAD^"
- };
-
let this = cx.entity();
Some(
h_flex()
@@ -1826,9 +2058,17 @@ impl GitPanel {
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
- .tooltip(Tooltip::for_action_title(tooltip, &git::Uncommit))
+ .tooltip(Tooltip::for_action_title(
+ if self.has_staged_changes() {
+ "git reset HEAD^ --soft"
+ } else {
+ "git reset HEAD^"
+ },
+ &git::Uncommit,
+ ))
.on_click(cx.listener(|this, _, window, cx| this.uncommit(window, cx))),
- ),
+ )
+ .child(self.render_push_button(branch, cx)),
)
}
@@ -2249,6 +2489,69 @@ impl GitPanel {
.into_any_element()
}
+ fn render_push_button(&self, branch: &Branch, cx: &Context<Self>) -> AnyElement {
+ let mut disabled = false;
+
+ // TODO: Add <origin> and <branch> argument substitutions to this
+ let button: SharedString;
+ let tooltip: SharedString;
+ let action: Option<Push>;
+ if let Some(upstream) = &branch.upstream {
+ match upstream.tracking {
+ UpstreamTracking::Gone => {
+ button = "Republish".into();
+ tooltip = "git push --set-upstream".into();
+ action = Some(git::Push {
+ options: Some(PushOptions::SetUpstream),
+ });
+ }
+ UpstreamTracking::Tracked(tracking) => {
+ if tracking.behind > 0 {
+ disabled = true;
+ button = "Push".into();
+ tooltip = "Upstream is ahead of local branch".into();
+ action = None;
+ } else if tracking.ahead > 0 {
+ button = format!("Push ({})", tracking.ahead).into();
+ tooltip = "git push".into();
+ action = Some(git::Push { options: None });
+ } else {
+ disabled = true;
+ button = "Push".into();
+ tooltip = "Upstream matches local branch".into();
+ action = None;
+ }
+ }
+ }
+ } else {
+ button = "Publish".into();
+ tooltip = "git push --set-upstream".into();
+ action = Some(git::Push {
+ options: Some(PushOptions::SetUpstream),
+ });
+ };
+
+ panel_filled_button(button)
+ .icon(IconName::ArrowUp)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .disabled(disabled)
+ .when_some(action, |this, action| {
+ this.on_click(
+ cx.listener(move |this, _, window, cx| this.push(&action, window, cx)),
+ )
+ })
+ .tooltip(move |window, cx| {
+ if let Some(action) = action.as_ref() {
+ Tooltip::for_action(tooltip.clone(), action, window, cx)
+ } else {
+ Tooltip::simple(tooltip.clone(), cx)
+ }
+ })
+ .into_any_element()
+ }
+
fn has_write_access(&self, cx: &App) -> bool {
!self.project.read(cx).is_read_only(cx)
}
@@ -2298,6 +2601,9 @@ impl Render for GitPanel {
.on_action(cx.listener(Self::unstage_all))
.on_action(cx.listener(Self::discard_tracked_changes))
.on_action(cx.listener(Self::clean_all))
+ .on_action(cx.listener(Self::fetch))
+ .on_action(cx.listener(Self::pull))
+ .on_action(cx.listener(Self::push))
.when(has_write_access && has_co_authors, |git_panel| {
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
})
@@ -2314,17 +2620,21 @@ impl Render for GitPanel {
.size_full()
.overflow_hidden()
.bg(ElevationIndex::Surface.bg(cx))
- .child(if has_entries {
+ .child(
v_flex()
.size_full()
.children(self.render_panel_header(window, cx))
- .child(self.render_entries(has_write_access, window, cx))
+ .map(|this| {
+ if has_entries {
+ this.child(self.render_entries(has_write_access, window, cx))
+ } else {
+ this.child(self.render_empty_state(cx).into_any_element())
+ }
+ })
.children(self.render_previous_commit(cx))
.child(self.render_commit_editor(window, cx))
- .into_any_element()
- } else {
- self.render_empty_state(cx).into_any_element()
- })
+ .into_any_element(),
+ )
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
deferred(
anchored()
@@ -9,6 +9,7 @@ pub mod branch_picker;
mod commit_modal;
pub mod git_panel;
mod git_panel_settings;
+pub mod picker_prompt;
pub mod project_diff;
pub mod repository_selector;
@@ -0,0 +1,235 @@
+use anyhow::{anyhow, Result};
+use futures::channel::oneshot;
+use fuzzy::{StringMatch, StringMatchCandidate};
+
+use core::cmp;
+use gpui::{
+ rems, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
+ InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
+ Task, WeakEntity, Window,
+};
+use picker::{Picker, PickerDelegate};
+use std::sync::Arc;
+use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
+use util::ResultExt;
+use workspace::{ModalView, Workspace};
+
+pub struct PickerPrompt {
+ pub picker: Entity<Picker<PickerPromptDelegate>>,
+ rem_width: f32,
+ _subscription: Subscription,
+}
+
+pub fn prompt(
+ prompt: &str,
+ options: Vec<SharedString>,
+ workspace: WeakEntity<Workspace>,
+ window: &mut Window,
+ cx: &mut App,
+) -> Task<Result<usize>> {
+ if options.is_empty() {
+ return Task::ready(Err(anyhow!("No options")));
+ }
+ let prompt = prompt.to_string().into();
+
+ window.spawn(cx, |mut cx| async move {
+ // Modal branch picker has a longer trailoff than a popover one.
+ 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)
+ })
+ })?;
+
+ rx.await?
+ })
+}
+
+impl PickerPrompt {
+ fn new(
+ delegate: PickerPromptDelegate,
+ rem_width: f32,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
+ let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
+ Self {
+ picker,
+ rem_width,
+ _subscription,
+ }
+ }
+}
+impl ModalView for PickerPrompt {}
+impl EventEmitter<DismissEvent> for PickerPrompt {}
+
+impl Focusable for PickerPrompt {
+ fn focus_handle(&self, cx: &App) -> FocusHandle {
+ self.picker.focus_handle(cx)
+ }
+}
+
+impl Render for PickerPrompt {
+ fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ v_flex()
+ .w(rems(self.rem_width))
+ .child(self.picker.clone())
+ .on_mouse_down_out(cx.listener(|this, _, window, cx| {
+ this.picker.update(cx, |this, cx| {
+ this.cancel(&Default::default(), window, cx);
+ })
+ }))
+ }
+}
+
+pub struct PickerPromptDelegate {
+ prompt: Arc<str>,
+ matches: Vec<StringMatch>,
+ all_options: Vec<SharedString>,
+ selected_index: usize,
+ max_match_length: usize,
+ tx: Option<oneshot::Sender<Result<usize>>>,
+}
+
+impl PickerPromptDelegate {
+ pub fn new(
+ prompt: Arc<str>,
+ options: Vec<SharedString>,
+ tx: oneshot::Sender<Result<usize>>,
+ max_chars: usize,
+ ) -> Self {
+ Self {
+ prompt,
+ all_options: options,
+ matches: vec![],
+ selected_index: 0,
+ max_match_length: max_chars,
+ tx: Some(tx),
+ }
+ }
+}
+
+impl PickerDelegate for PickerPromptDelegate {
+ type ListItem = ListItem;
+
+ fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
+ self.prompt.clone()
+ }
+
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(
+ &mut self,
+ ix: usize,
+ _window: &mut Window,
+ _: &mut Context<Picker<Self>>,
+ ) {
+ self.selected_index = ix;
+ }
+
+ fn update_matches(
+ &mut self,
+ query: String,
+ window: &mut Window,
+ cx: &mut Context<Picker<Self>>,
+ ) -> Task<()> {
+ cx.spawn_in(window, move |picker, mut cx| async move {
+ let candidates = picker.update(&mut cx, |picker, _| {
+ picker
+ .delegate
+ .all_options
+ .iter()
+ .enumerate()
+ .map(|(ix, option)| StringMatchCandidate::new(ix, &option))
+ .collect::<Vec<StringMatchCandidate>>()
+ });
+ let Some(candidates) = candidates.log_err() else {
+ return;
+ };
+ let matches: Vec<StringMatch> = if query.is_empty() {
+ candidates
+ .into_iter()
+ .enumerate()
+ .map(|(index, candidate)| StringMatch {
+ candidate_id: index,
+ string: candidate.string,
+ positions: Vec::new(),
+ score: 0.0,
+ })
+ .collect()
+ } else {
+ fuzzy::match_strings(
+ &candidates,
+ &query,
+ true,
+ 10000,
+ &Default::default(),
+ cx.background_executor().clone(),
+ )
+ .await
+ };
+ picker
+ .update(&mut cx, |picker, _| {
+ let delegate = &mut picker.delegate;
+ delegate.matches = matches;
+ if delegate.matches.is_empty() {
+ delegate.selected_index = 0;
+ } else {
+ delegate.selected_index =
+ cmp::min(delegate.selected_index, delegate.matches.len() - 1);
+ }
+ })
+ .log_err();
+ })
+ }
+
+ fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
+ let Some(option) = self.matches.get(self.selected_index()) else {
+ return;
+ };
+
+ self.tx.take().map(|tx| tx.send(Ok(option.candidate_id)));
+ cx.emit(DismissEvent);
+ }
+
+ fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
+ cx.emit(DismissEvent);
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ _window: &mut Window,
+ _cx: &mut Context<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ let hit = &self.matches[ix];
+ let shortened_option = util::truncate_and_trailoff(&hit.string, self.max_match_length);
+
+ Some(
+ ListItem::new(SharedString::from(format!("picker-prompt-menu-{ix}")))
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .toggle_state(selected)
+ .map(|el| {
+ let highlights: Vec<_> = hit
+ .positions
+ .iter()
+ .filter(|index| index < &&self.max_match_length)
+ .copied()
+ .collect();
+
+ el.child(HighlightedLabel::new(shortened_option, highlights))
+ }),
+ )
+ }
+}
@@ -5,7 +5,7 @@ use anyhow::{Context as _, Result};
use client::ProjectId;
use futures::channel::{mpsc, oneshot};
use futures::StreamExt as _;
-use git::repository::{Branch, CommitDetails, ResetMode};
+use git::repository::{Branch, CommitDetails, PushOptions, Remote, ResetMode};
use git::{
repository::{GitRepository, RepoPath},
status::{GitSummary, TrackedSummary},
@@ -74,6 +74,18 @@ pub enum Message {
Stage(GitRepo, Vec<RepoPath>),
Unstage(GitRepo, Vec<RepoPath>),
SetIndexText(GitRepo, RepoPath, Option<String>),
+ Push {
+ repo: GitRepo,
+ branch_name: SharedString,
+ remote_name: SharedString,
+ options: Option<PushOptions>,
+ },
+ Pull {
+ repo: GitRepo,
+ branch_name: SharedString,
+ remote_name: SharedString,
+ },
+ Fetch(GitRepo),
}
pub enum GitEvent {
@@ -107,6 +119,10 @@ impl GitStore {
}
pub fn init(client: &AnyProtoClient) {
+ client.add_entity_request_handler(Self::handle_get_remotes);
+ client.add_entity_request_handler(Self::handle_push);
+ client.add_entity_request_handler(Self::handle_pull);
+ client.add_entity_request_handler(Self::handle_fetch);
client.add_entity_request_handler(Self::handle_stage);
client.add_entity_request_handler(Self::handle_unstage);
client.add_entity_request_handler(Self::handle_commit);
@@ -242,8 +258,10 @@ impl GitStore {
mpsc::unbounded::<(Message, oneshot::Sender<Result<()>>)>();
cx.spawn(|_, cx| async move {
while let Some((msg, respond)) = update_receiver.next().await {
- let result = cx.background_spawn(Self::process_git_msg(msg)).await;
- respond.send(result).ok();
+ if !respond.is_canceled() {
+ let result = cx.background_spawn(Self::process_git_msg(msg)).await;
+ respond.send(result).ok();
+ }
}
})
.detach();
@@ -252,6 +270,94 @@ impl GitStore {
async fn process_git_msg(msg: Message) -> Result<()> {
match msg {
+ Message::Fetch(repo) => {
+ match repo {
+ GitRepo::Local(git_repository) => git_repository.fetch()?,
+ GitRepo::Remote {
+ project_id,
+ client,
+ worktree_id,
+ work_directory_id,
+ } => {
+ client
+ .request(proto::Fetch {
+ project_id: project_id.0,
+ worktree_id: worktree_id.to_proto(),
+ work_directory_id: work_directory_id.to_proto(),
+ })
+ .await
+ .context("sending fetch request")?;
+ }
+ }
+ Ok(())
+ }
+
+ Message::Pull {
+ repo,
+ branch_name,
+ remote_name,
+ } => {
+ match repo {
+ GitRepo::Local(git_repository) => {
+ git_repository.pull(&branch_name, &remote_name)?
+ }
+ GitRepo::Remote {
+ project_id,
+ client,
+ worktree_id,
+ work_directory_id,
+ } => {
+ client
+ .request(proto::Pull {
+ project_id: project_id.0,
+ worktree_id: worktree_id.to_proto(),
+ work_directory_id: work_directory_id.to_proto(),
+ branch_name: branch_name.to_string(),
+ remote_name: remote_name.to_string(),
+ })
+ .await
+ .context("sending pull request")?;
+ }
+ }
+ Ok(())
+ }
+ Message::Push {
+ repo,
+ branch_name,
+ remote_name,
+ options,
+ } => {
+ match repo {
+ GitRepo::Local(git_repository) => {
+ git_repository.push(&branch_name, &remote_name, options)?
+ }
+ GitRepo::Remote {
+ project_id,
+ client,
+ worktree_id,
+ work_directory_id,
+ } => {
+ client
+ .request(proto::Push {
+ project_id: project_id.0,
+ worktree_id: worktree_id.to_proto(),
+ work_directory_id: work_directory_id.to_proto(),
+ branch_name: branch_name.to_string(),
+ remote_name: remote_name.to_string(),
+ options: options.map(|options| match options {
+ PushOptions::Force => proto::push::PushOptions::Force,
+ PushOptions::SetUpstream => {
+ proto::push::PushOptions::SetUpstream
+ }
+ }
+ as i32),
+ })
+ .await
+ .context("sending push request")?;
+ }
+ }
+ Ok(())
+ }
Message::Stage(repo, paths) => {
match repo {
GitRepo::Local(repo) => repo.stage_paths(&paths)?,
@@ -413,6 +519,73 @@ impl GitStore {
}
}
+ async fn handle_fetch(
+ this: Entity<Self>,
+ envelope: TypedEnvelope<proto::Fetch>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::Ack> {
+ let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+ let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
+ let repository_handle =
+ Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+
+ repository_handle
+ .update(&mut cx, |repository_handle, _cx| repository_handle.fetch())?
+ .await??;
+ Ok(proto::Ack {})
+ }
+
+ async fn handle_push(
+ this: Entity<Self>,
+ envelope: TypedEnvelope<proto::Push>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::Ack> {
+ let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+ let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
+ let repository_handle =
+ Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+
+ let options = envelope
+ .payload
+ .options
+ .as_ref()
+ .map(|_| match envelope.payload.options() {
+ proto::push::PushOptions::SetUpstream => git::repository::PushOptions::SetUpstream,
+ proto::push::PushOptions::Force => git::repository::PushOptions::Force,
+ });
+
+ let branch_name = envelope.payload.branch_name.into();
+ let remote_name = envelope.payload.remote_name.into();
+
+ repository_handle
+ .update(&mut cx, |repository_handle, _cx| {
+ repository_handle.push(branch_name, remote_name, options)
+ })?
+ .await??;
+ Ok(proto::Ack {})
+ }
+
+ async fn handle_pull(
+ this: Entity<Self>,
+ envelope: TypedEnvelope<proto::Pull>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::Ack> {
+ let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+ let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
+ let repository_handle =
+ Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+
+ let branch_name = envelope.payload.branch_name.into();
+ let remote_name = envelope.payload.remote_name.into();
+
+ repository_handle
+ .update(&mut cx, |repository_handle, _cx| {
+ repository_handle.pull(branch_name, remote_name)
+ })?
+ .await??;
+ Ok(proto::Ack {})
+ }
+
async fn handle_stage(
this: Entity<Self>,
envelope: TypedEnvelope<proto::Stage>,
@@ -509,6 +682,34 @@ impl GitStore {
Ok(proto::Ack {})
}
+ async fn handle_get_remotes(
+ this: Entity<Self>,
+ envelope: TypedEnvelope<proto::GetRemotes>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::GetRemotesResponse> {
+ let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+ let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
+ let repository_handle =
+ Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
+
+ let branch_name = envelope.payload.branch_name;
+
+ let remotes = repository_handle
+ .update(&mut cx, |repository_handle, cx| {
+ repository_handle.get_remotes(branch_name, cx)
+ })?
+ .await?;
+
+ Ok(proto::GetRemotesResponse {
+ remotes: remotes
+ .into_iter()
+ .map(|remotes| proto::get_remotes_response::Remote {
+ name: remotes.name.to_string(),
+ })
+ .collect::<Vec<_>>(),
+ })
+ }
+
async fn handle_show(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GitShow>,
@@ -648,7 +849,7 @@ impl Repository {
(self.worktree_id, self.repository_entry.work_directory_id())
}
- pub fn branch(&self) -> Option<&Branch> {
+ pub fn current_branch(&self) -> Option<&Branch> {
self.repository_entry.branch()
}
@@ -802,35 +1003,19 @@ impl Repository {
commit: &str,
paths: Vec<RepoPath>,
) -> oneshot::Receiver<Result<()>> {
- let (result_tx, result_rx) = futures::channel::oneshot::channel();
- let commit = commit.to_string().into();
- self.update_sender
- .unbounded_send((
- Message::CheckoutFiles {
- repo: self.git_repo.clone(),
- commit,
- paths,
- },
- result_tx,
- ))
- .ok();
- result_rx
+ self.send_message(Message::CheckoutFiles {
+ repo: self.git_repo.clone(),
+ commit: commit.to_string().into(),
+ paths,
+ })
}
pub fn reset(&self, commit: &str, reset_mode: ResetMode) -> oneshot::Receiver<Result<()>> {
- let (result_tx, result_rx) = futures::channel::oneshot::channel();
- let commit = commit.to_string().into();
- self.update_sender
- .unbounded_send((
- Message::Reset {
- repo: self.git_repo.clone(),
- commit,
- reset_mode,
- },
- result_tx,
- ))
- .ok();
- result_rx
+ self.send_message(Message::Reset {
+ repo: self.git_repo.clone(),
+ commit: commit.to_string().into(),
+ reset_mode,
+ })
}
pub fn show(&self, commit: &str, cx: &Context<Self>) -> Task<Result<CommitDetails>> {
@@ -987,18 +1172,41 @@ impl Repository {
message: SharedString,
name_and_email: Option<(SharedString, SharedString)>,
) -> oneshot::Receiver<Result<()>> {
- let (result_tx, result_rx) = futures::channel::oneshot::channel();
- self.update_sender
- .unbounded_send((
- Message::Commit {
- git_repo: self.git_repo.clone(),
- message,
- name_and_email,
- },
- result_tx,
- ))
- .ok();
- result_rx
+ self.send_message(Message::Commit {
+ git_repo: self.git_repo.clone(),
+ message,
+ name_and_email,
+ })
+ }
+
+ pub fn fetch(&self) -> oneshot::Receiver<Result<()>> {
+ self.send_message(Message::Fetch(self.git_repo.clone()))
+ }
+
+ pub fn push(
+ &self,
+ branch: SharedString,
+ remote: SharedString,
+ options: Option<PushOptions>,
+ ) -> oneshot::Receiver<Result<()>> {
+ self.send_message(Message::Push {
+ repo: self.git_repo.clone(),
+ branch_name: branch,
+ remote_name: remote,
+ options,
+ })
+ }
+
+ pub fn pull(
+ &self,
+ branch: SharedString,
+ remote: SharedString,
+ ) -> oneshot::Receiver<Result<()>> {
+ self.send_message(Message::Pull {
+ repo: self.git_repo.clone(),
+ branch_name: branch,
+ remote_name: remote,
+ })
}
pub fn set_index_text(
@@ -1006,13 +1214,49 @@ impl Repository {
path: &RepoPath,
content: Option<String>,
) -> oneshot::Receiver<anyhow::Result<()>> {
+ self.send_message(Message::SetIndexText(
+ self.git_repo.clone(),
+ path.clone(),
+ content,
+ ))
+ }
+
+ pub fn get_remotes(&self, branch_name: Option<String>, cx: &App) -> Task<Result<Vec<Remote>>> {
+ match self.git_repo.clone() {
+ GitRepo::Local(git_repository) => {
+ cx.background_spawn(
+ async move { git_repository.get_remotes(branch_name.as_deref()) },
+ )
+ }
+ GitRepo::Remote {
+ project_id,
+ client,
+ worktree_id,
+ work_directory_id,
+ } => cx.background_spawn(async move {
+ let response = client
+ .request(proto::GetRemotes {
+ project_id: project_id.0,
+ worktree_id: worktree_id.to_proto(),
+ work_directory_id: work_directory_id.to_proto(),
+ branch_name,
+ })
+ .await?;
+
+ Ok(response
+ .remotes
+ .into_iter()
+ .map(|remotes| git::repository::Remote {
+ name: remotes.name.into(),
+ })
+ .collect())
+ }),
+ }
+ }
+
+ fn send_message(&self, message: Message) -> oneshot::Receiver<anyhow::Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
- self.update_sender
- .unbounded_send((
- Message::SetIndexText(self.git_repo.clone(), path.clone(), content),
- result_tx,
- ))
- .ok();
+ self.update_sender.unbounded_send((message, result_tx)).ok();
result_rx
}
}
@@ -946,12 +946,17 @@ impl WorktreeStore {
upstream: proto_branch.upstream.map(|upstream| {
git::repository::Upstream {
ref_name: upstream.ref_name.into(),
- tracking: upstream.tracking.map(|tracking| {
- git::repository::UpstreamTracking {
- ahead: tracking.ahead as u32,
- behind: tracking.behind as u32,
- }
- }),
+ tracking: upstream
+ .tracking
+ .map(|tracking| {
+ git::repository::UpstreamTracking::Tracked(
+ git::repository::UpstreamTrackingStatus {
+ ahead: tracking.ahead as u32,
+ behind: tracking.behind as u32,
+ },
+ )
+ })
+ .unwrap_or(git::repository::UpstreamTracking::Gone),
}
}),
most_recent_commit: proto_branch.most_recent_commit.map(|commit| {
@@ -321,7 +321,13 @@ message Envelope {
GitCommitDetails git_commit_details = 302;
SetIndexText set_index_text = 299;
- GitCheckoutFiles git_checkout_files = 303; // current max
+ GitCheckoutFiles git_checkout_files = 303;
+
+ Push push = 304;
+ Fetch fetch = 305;
+ GetRemotes get_remotes = 306;
+ GetRemotesResponse get_remotes_response = 307;
+ Pull pull = 308; // current max
}
reserved 87 to 88;
@@ -2772,3 +2778,46 @@ message OpenCommitMessageBuffer {
uint64 worktree_id = 2;
uint64 work_directory_id = 3;
}
+
+message Push {
+ uint64 project_id = 1;
+ uint64 worktree_id = 2;
+ uint64 work_directory_id = 3;
+ string remote_name = 4;
+ string branch_name = 5;
+ optional PushOptions options = 6;
+
+ enum PushOptions {
+ SET_UPSTREAM = 0;
+ FORCE = 1;
+ }
+}
+
+message Fetch {
+ uint64 project_id = 1;
+ uint64 worktree_id = 2;
+ uint64 work_directory_id = 3;
+}
+
+message GetRemotes {
+ uint64 project_id = 1;
+ uint64 worktree_id = 2;
+ uint64 work_directory_id = 3;
+ optional string branch_name = 4;
+}
+
+message GetRemotesResponse {
+ repeated Remote remotes = 1;
+
+ message Remote {
+ string name = 1;
+ }
+}
+
+message Pull {
+ uint64 project_id = 1;
+ uint64 worktree_id = 2;
+ uint64 work_directory_id = 3;
+ string remote_name = 4;
+ string branch_name = 5;
+}
@@ -445,6 +445,11 @@ messages!(
(GitShow, Background),
(GitCommitDetails, Background),
(SetIndexText, Background),
+ (Push, Background),
+ (Fetch, Background),
+ (GetRemotes, Background),
+ (GetRemotesResponse, Background),
+ (Pull, Background),
);
request_messages!(
@@ -582,6 +587,10 @@ request_messages!(
(GitReset, Ack),
(GitCheckoutFiles, Ack),
(SetIndexText, Ack),
+ (Push, Ack),
+ (Fetch, Ack),
+ (GetRemotes, GetRemotesResponse),
+ (Pull, Ack),
);
entity_messages!(
@@ -678,6 +687,10 @@ entity_messages!(
GitReset,
GitCheckoutFiles,
SetIndexText,
+ Push,
+ Fetch,
+ GetRemotes,
+ Pull,
);
entity_messages!(
@@ -20,7 +20,7 @@ use futures::{
};
use fuzzy::CharBag;
use git::{
- repository::{Branch, GitRepository, RepoPath},
+ repository::{Branch, GitRepository, RepoPath, UpstreamTrackingStatus},
status::{
FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
},
@@ -202,7 +202,7 @@ pub struct RepositoryEntry {
pub(crate) statuses_by_path: SumTree<StatusEntry>,
work_directory_id: ProjectEntryId,
pub work_directory: WorkDirectory,
- pub(crate) branch: Option<Branch>,
+ pub(crate) current_branch: Option<Branch>,
pub current_merge_conflicts: TreeSet<RepoPath>,
}
@@ -216,7 +216,7 @@ impl Deref for RepositoryEntry {
impl RepositoryEntry {
pub fn branch(&self) -> Option<&Branch> {
- self.branch.as_ref()
+ self.current_branch.as_ref()
}
pub fn work_directory_id(&self) -> ProjectEntryId {
@@ -244,8 +244,11 @@ impl RepositoryEntry {
pub fn initial_update(&self) -> proto::RepositoryEntry {
proto::RepositoryEntry {
work_directory_id: self.work_directory_id.to_proto(),
- branch: self.branch.as_ref().map(|branch| branch.name.to_string()),
- branch_summary: self.branch.as_ref().map(branch_to_proto),
+ branch: self
+ .current_branch
+ .as_ref()
+ .map(|branch| branch.name.to_string()),
+ branch_summary: self.current_branch.as_ref().map(branch_to_proto),
updated_statuses: self
.statuses_by_path
.iter()
@@ -304,8 +307,11 @@ impl RepositoryEntry {
proto::RepositoryEntry {
work_directory_id: self.work_directory_id.to_proto(),
- branch: self.branch.as_ref().map(|branch| branch.name.to_string()),
- branch_summary: self.branch.as_ref().map(branch_to_proto),
+ branch: self
+ .current_branch
+ .as_ref()
+ .map(|branch| branch.name.to_string()),
+ branch_summary: self.current_branch.as_ref().map(branch_to_proto),
updated_statuses,
removed_statuses,
current_merge_conflicts: self
@@ -329,7 +335,7 @@ pub fn branch_to_proto(branch: &git::repository::Branch) -> proto::Branch {
ref_name: upstream.ref_name.to_string(),
tracking: upstream
.tracking
- .as_ref()
+ .status()
.map(|upstream| proto::UpstreamTracking {
ahead: upstream.ahead as u64,
behind: upstream.behind as u64,
@@ -355,12 +361,16 @@ pub fn proto_to_branch(proto: &proto::Branch) -> git::repository::Branch {
.as_ref()
.map(|upstream| git::repository::Upstream {
ref_name: upstream.ref_name.to_string().into(),
- tracking: upstream.tracking.as_ref().map(|tracking| {
- git::repository::UpstreamTracking {
- ahead: tracking.ahead as u32,
- behind: tracking.behind as u32,
- }
- }),
+ tracking: upstream
+ .tracking
+ .as_ref()
+ .map(|tracking| {
+ git::repository::UpstreamTracking::Tracked(UpstreamTrackingStatus {
+ ahead: tracking.ahead as u32,
+ behind: tracking.behind as u32,
+ })
+ })
+ .unwrap_or(git::repository::UpstreamTracking::Gone),
}),
most_recent_commit: proto.most_recent_commit.as_ref().map(|commit| {
git::repository::CommitSummary {
@@ -2682,7 +2692,8 @@ impl Snapshot {
self.repositories
.update(&PathKey(work_dir_entry.path.clone()), &(), |repo| {
- repo.branch = repository.branch_summary.as_ref().map(proto_to_branch);
+ repo.current_branch =
+ repository.branch_summary.as_ref().map(proto_to_branch);
repo.statuses_by_path.edit(edits, &());
repo.current_merge_conflicts = conflicted_paths
});
@@ -2704,7 +2715,7 @@ impl Snapshot {
work_directory: WorkDirectory::InProject {
relative_path: work_dir_entry.path.clone(),
},
- branch: repository.branch_summary.as_ref().map(proto_to_branch),
+ current_branch: repository.branch_summary.as_ref().map(proto_to_branch),
statuses_by_path: statuses,
current_merge_conflicts: conflicted_paths,
},
@@ -3506,7 +3517,7 @@ impl BackgroundScannerState {
RepositoryEntry {
work_directory_id: work_dir_id,
work_directory: work_directory.clone(),
- branch: None,
+ current_branch: None,
statuses_by_path: Default::default(),
current_merge_conflicts: Default::default(),
},
@@ -5580,7 +5591,7 @@ fn update_branches(
let mut repository = snapshot
.repository(repository.work_directory.path_key())
.context("Missing repository")?;
- repository.branch = branches.into_iter().find(|branch| branch.is_head);
+ repository.current_branch = branches.into_iter().find(|branch| branch.is_head);
let mut state = state.lock();
state