Git push/pull/fetch (#25445)
Release Notes: - N/A --------- Co-authored-by: Michael Sloan <mgsloan@gmail.com>
This commit is contained in:
parent
b1b6401ce7
commit
ff6844300e
17 changed files with 1242 additions and 180 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -5298,6 +5298,7 @@ dependencies = [
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"regex",
|
"regex",
|
||||||
"rope",
|
"rope",
|
||||||
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smol",
|
"smol",
|
||||||
|
|
|
@ -392,9 +392,13 @@ impl Server {
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::OpenContext>)
|
.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::CreateContext>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::SynchronizeContexts>)
|
.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::Stage>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::Unstage>)
|
.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::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::GitShow>)
|
||||||
.add_request_handler(forward_read_only_project_request::<proto::GitReset>)
|
.add_request_handler(forward_read_only_project_request::<proto::GitReset>)
|
||||||
.add_request_handler(forward_read_only_project_request::<proto::GitCheckoutFiles>)
|
.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))
|
.update(cx, |buffer_store, cx| buffer_store.save_buffer(buffer, cx))
|
||||||
.detach_and_log_err(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>) {
|
pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context<Self>) {
|
||||||
|
|
|
@ -298,6 +298,7 @@ impl EditorTestContext {
|
||||||
self.cx.run_until_parked();
|
self.cx.run_until_parked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub fn assert_index_text(&mut self, expected: Option<&str>) {
|
pub fn assert_index_text(&mut self, expected: Option<&str>) {
|
||||||
let fs = self.update_editor(|editor, _, cx| {
|
let fs = self.update_editor(|editor, _, cx| {
|
||||||
editor.project.as_ref().unwrap().read(cx).fs().as_fake()
|
editor.project.as_ref().unwrap().read(cx).fs().as_fake()
|
||||||
|
|
|
@ -26,6 +26,7 @@ log.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
rope.workspace = true
|
rope.workspace = true
|
||||||
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
sum_tree.workspace = true
|
sum_tree.workspace = true
|
||||||
|
|
|
@ -8,6 +8,9 @@ pub mod status;
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use gpui::action_with_deprecated_aliases;
|
use gpui::action_with_deprecated_aliases;
|
||||||
use gpui::actions;
|
use gpui::actions;
|
||||||
|
use gpui::impl_actions;
|
||||||
|
use repository::PushOptions;
|
||||||
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
@ -27,6 +30,13 @@ pub static COMMIT_MESSAGE: LazyLock<&'static OsStr> =
|
||||||
LazyLock::new(|| OsStr::new("COMMIT_EDITMSG"));
|
LazyLock::new(|| OsStr::new("COMMIT_EDITMSG"));
|
||||||
pub static INDEX_LOCK: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("index.lock"));
|
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!(
|
actions!(
|
||||||
git,
|
git,
|
||||||
[
|
[
|
||||||
|
@ -43,6 +53,8 @@ actions!(
|
||||||
RestoreTrackedFiles,
|
RestoreTrackedFiles,
|
||||||
TrashUntrackedFiles,
|
TrashUntrackedFiles,
|
||||||
Uncommit,
|
Uncommit,
|
||||||
|
Pull,
|
||||||
|
Fetch,
|
||||||
Commit,
|
Commit,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,6 +7,8 @@ use git2::BranchType;
|
||||||
use gpui::SharedString;
|
use gpui::SharedString;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::Deserialize;
|
||||||
use std::borrow::Borrow;
|
use std::borrow::Borrow;
|
||||||
use std::io::Write as _;
|
use std::io::Write as _;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
|
@ -29,6 +31,12 @@ pub struct Branch {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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>) {
|
pub fn priority_key(&self) -> (bool, Option<i64>) {
|
||||||
(
|
(
|
||||||
self.is_head,
|
self.is_head,
|
||||||
|
@ -42,11 +50,32 @@ impl Branch {
|
||||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||||
pub struct Upstream {
|
pub struct Upstream {
|
||||||
pub ref_name: SharedString,
|
pub ref_name: SharedString,
|
||||||
pub tracking: Option<UpstreamTracking>,
|
pub tracking: UpstreamTracking,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||||
pub struct UpstreamTracking {
|
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 ahead: u32,
|
||||||
pub behind: u32,
|
pub behind: u32,
|
||||||
}
|
}
|
||||||
|
@ -68,6 +97,11 @@ pub struct CommitDetails {
|
||||||
pub committer_name: SharedString,
|
pub committer_name: SharedString,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||||
|
pub struct Remote {
|
||||||
|
pub name: SharedString,
|
||||||
|
}
|
||||||
|
|
||||||
pub enum ResetMode {
|
pub enum ResetMode {
|
||||||
// reset the branch pointer, leave index and worktree unchanged
|
// reset the branch pointer, leave index and worktree unchanged
|
||||||
// (this will make it look like things that were committed are now
|
// (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 unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
|
||||||
|
|
||||||
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> 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 {
|
impl std::fmt::Debug for dyn GitRepository {
|
||||||
|
@ -165,6 +215,14 @@ impl RealGitRepository {
|
||||||
hosting_provider_registry,
|
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
|
// 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<()> {
|
fn reset(&self, commit: &str, mode: ResetMode) -> Result<()> {
|
||||||
let working_directory = self
|
let working_directory = self.working_directory()?;
|
||||||
.repository
|
|
||||||
.lock()
|
|
||||||
.workdir()
|
|
||||||
.context("failed to read git work directory")?
|
|
||||||
.to_path_buf();
|
|
||||||
|
|
||||||
let mode_flag = match mode {
|
let mode_flag = match mode {
|
||||||
ResetMode::Mixed => "--mixed",
|
ResetMode::Mixed => "--mixed",
|
||||||
|
@ -238,12 +291,7 @@ impl GitRepository for RealGitRepository {
|
||||||
if paths.is_empty() {
|
if paths.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let working_directory = self
|
let working_directory = self.working_directory()?;
|
||||||
.repository
|
|
||||||
.lock()
|
|
||||||
.workdir()
|
|
||||||
.context("failed to read git work directory")?
|
|
||||||
.to_path_buf();
|
|
||||||
|
|
||||||
let output = new_std_command(&self.git_binary_path)
|
let output = new_std_command(&self.git_binary_path)
|
||||||
.current_dir(&working_directory)
|
.current_dir(&working_directory)
|
||||||
|
@ -296,12 +344,7 @@ impl GitRepository for RealGitRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
|
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
|
||||||
let working_directory = self
|
let working_directory = self.working_directory()?;
|
||||||
.repository
|
|
||||||
.lock()
|
|
||||||
.workdir()
|
|
||||||
.context("failed to read git work directory")?
|
|
||||||
.to_path_buf();
|
|
||||||
if let Some(content) = content {
|
if let Some(content) = content {
|
||||||
let mut child = new_std_command(&self.git_binary_path)
|
let mut child = new_std_command(&self.git_binary_path)
|
||||||
.current_dir(&working_directory)
|
.current_dir(&working_directory)
|
||||||
|
@ -485,12 +528,7 @@ impl GitRepository for RealGitRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stage_paths(&self, paths: &[RepoPath]) -> Result<()> {
|
fn stage_paths(&self, paths: &[RepoPath]) -> Result<()> {
|
||||||
let working_directory = self
|
let working_directory = self.working_directory()?;
|
||||||
.repository
|
|
||||||
.lock()
|
|
||||||
.workdir()
|
|
||||||
.context("failed to read git work directory")?
|
|
||||||
.to_path_buf();
|
|
||||||
|
|
||||||
if !paths.is_empty() {
|
if !paths.is_empty() {
|
||||||
let output = new_std_command(&self.git_binary_path)
|
let output = new_std_command(&self.git_binary_path)
|
||||||
|
@ -498,6 +536,8 @@ impl GitRepository for RealGitRepository {
|
||||||
.args(["update-index", "--add", "--remove", "--"])
|
.args(["update-index", "--add", "--remove", "--"])
|
||||||
.args(paths.iter().map(|p| p.as_ref()))
|
.args(paths.iter().map(|p| p.as_ref()))
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
|
// TODO: Get remote response out of this and show it to the user
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"Failed to stage paths:\n{}",
|
"Failed to stage paths:\n{}",
|
||||||
|
@ -509,12 +549,7 @@ impl GitRepository for RealGitRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()> {
|
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()> {
|
||||||
let working_directory = self
|
let working_directory = self.working_directory()?;
|
||||||
.repository
|
|
||||||
.lock()
|
|
||||||
.workdir()
|
|
||||||
.context("failed to read git work directory")?
|
|
||||||
.to_path_buf();
|
|
||||||
|
|
||||||
if !paths.is_empty() {
|
if !paths.is_empty() {
|
||||||
let output = new_std_command(&self.git_binary_path)
|
let output = new_std_command(&self.git_binary_path)
|
||||||
|
@ -522,6 +557,8 @@ impl GitRepository for RealGitRepository {
|
||||||
.args(["reset", "--quiet", "--"])
|
.args(["reset", "--quiet", "--"])
|
||||||
.args(paths.iter().map(|p| p.as_ref()))
|
.args(paths.iter().map(|p| p.as_ref()))
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
|
// TODO: Get remote response out of this and show it to the user
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"Failed to unstage:\n{}",
|
"Failed to unstage:\n{}",
|
||||||
|
@ -533,24 +570,21 @@ impl GitRepository for RealGitRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> {
|
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> {
|
||||||
let working_directory = self
|
let working_directory = self.working_directory()?;
|
||||||
.repository
|
|
||||||
.lock()
|
let mut cmd = new_std_command(&self.git_binary_path);
|
||||||
.workdir()
|
cmd.current_dir(&working_directory)
|
||||||
.context("failed to read git work directory")?
|
.args(["commit", "--quiet", "-m"])
|
||||||
.to_path_buf();
|
.arg(message)
|
||||||
let mut args = vec!["commit", "--quiet", "-m", message, "--cleanup=strip"];
|
.arg("--cleanup=strip");
|
||||||
let author = name_and_email.map(|(name, email)| format!("{name} <{email}>"));
|
|
||||||
if let Some(author) = author.as_deref() {
|
if let Some((name, email)) = name_and_email {
|
||||||
args.push("--author");
|
cmd.arg("--author").arg(&format!("{name} <{email}>"));
|
||||||
args.push(author);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = new_std_command(&self.git_binary_path)
|
let output = cmd.output()?;
|
||||||
.current_dir(&working_directory)
|
|
||||||
.args(args)
|
|
||||||
.output()?;
|
|
||||||
|
|
||||||
|
// TODO: Get remote response out of this and show it to the user
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"Failed to commit:\n{}",
|
"Failed to commit:\n{}",
|
||||||
|
@ -559,6 +593,118 @@ impl GitRepository for RealGitRepository {
|
||||||
}
|
}
|
||||||
Ok(())
|
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(["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 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)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -743,6 +889,22 @@ impl GitRepository for FakeGitRepository {
|
||||||
fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
|
fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
|
||||||
unimplemented!()
|
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<()> {
|
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)
|
Ok(branches)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_upstream_track(upstream_track: &str) -> Result<Option<UpstreamTracking>> {
|
fn parse_upstream_track(upstream_track: &str) -> Result<UpstreamTracking> {
|
||||||
if upstream_track == "" {
|
if upstream_track == "" {
|
||||||
return Ok(Some(UpstreamTracking {
|
return Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
|
||||||
ahead: 0,
|
ahead: 0,
|
||||||
behind: 0,
|
behind: 0,
|
||||||
}));
|
}));
|
||||||
|
@ -929,7 +1091,7 @@ fn parse_upstream_track(upstream_track: &str) -> Result<Option<UpstreamTracking>
|
||||||
let mut behind: u32 = 0;
|
let mut behind: u32 = 0;
|
||||||
for component in upstream_track.split(", ") {
|
for component in upstream_track.split(", ") {
|
||||||
if component == "gone" {
|
if component == "gone" {
|
||||||
return Ok(None);
|
return Ok(UpstreamTracking::Gone);
|
||||||
}
|
}
|
||||||
if let Some(ahead_num) = component.strip_prefix("ahead ") {
|
if let Some(ahead_num) = component.strip_prefix("ahead ") {
|
||||||
ahead = ahead_num.parse::<u32>()?;
|
ahead = ahead_num.parse::<u32>()?;
|
||||||
|
@ -938,7 +1100,10 @@ fn parse_upstream_track(upstream_track: &str) -> Result<Option<UpstreamTracking>
|
||||||
behind = behind_num.parse::<u32>()?;
|
behind = behind_num.parse::<u32>()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Some(UpstreamTracking { ahead, behind }))
|
Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
|
||||||
|
ahead,
|
||||||
|
behind,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -953,7 +1118,7 @@ fn test_branches_parsing() {
|
||||||
name: "zed-patches".into(),
|
name: "zed-patches".into(),
|
||||||
upstream: Some(Upstream {
|
upstream: Some(Upstream {
|
||||||
ref_name: "refs/remotes/origin/zed-patches".into(),
|
ref_name: "refs/remotes/origin/zed-patches".into(),
|
||||||
tracking: Some(UpstreamTracking {
|
tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus {
|
||||||
ahead: 0,
|
ahead: 0,
|
||||||
behind: 0
|
behind: 0
|
||||||
})
|
})
|
||||||
|
|
|
@ -261,7 +261,7 @@ impl PickerDelegate for BranchListDelegate {
|
||||||
.project()
|
.project()
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.active_repository(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())
|
.map(|branch| branch.name.to_string())
|
||||||
})
|
})
|
||||||
.ok()
|
.ok()
|
||||||
|
|
|
@ -159,7 +159,12 @@ impl CommitModal {
|
||||||
let branch = git_panel
|
let branch = git_panel
|
||||||
.active_repository
|
.active_repository
|
||||||
.as_ref()
|
.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());
|
.unwrap_or_else(|| "<no branch>".into());
|
||||||
let tooltip = if git_panel.has_staged_changes() {
|
let tooltip = if git_panel.has_staged_changes() {
|
||||||
"Commit staged changes"
|
"Commit staged changes"
|
||||||
|
|
|
@ -4,7 +4,7 @@ use crate::repository_selector::RepositorySelectorPopoverMenu;
|
||||||
use crate::{
|
use crate::{
|
||||||
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
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 collections::HashMap;
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::commit_tooltip::CommitTooltip;
|
use editor::commit_tooltip::CommitTooltip;
|
||||||
|
@ -12,9 +12,9 @@ use editor::{
|
||||||
scroll::ScrollbarAutoHide, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer,
|
scroll::ScrollbarAutoHide, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer,
|
||||||
ShowScrollbar,
|
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::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
|
||||||
use git::{RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
|
use git::{Push, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{Buffer, File};
|
use language::{Buffer, File};
|
||||||
|
@ -27,6 +27,9 @@ use project::{
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::Settings as _;
|
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 std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize};
|
||||||
use strum::{IntoEnumIterator, VariantNames};
|
use strum::{IntoEnumIterator, VariantNames};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
@ -34,7 +37,7 @@ use ui::{
|
||||||
prelude::*, ButtonLike, Checkbox, ContextMenu, Divider, DividerColor, ElevationIndex, ListItem,
|
prelude::*, ButtonLike, Checkbox, ContextMenu, Divider, DividerColor, ElevationIndex, ListItem,
|
||||||
ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,
|
ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,
|
||||||
};
|
};
|
||||||
use util::{maybe, ResultExt, TryFutureExt};
|
use util::{maybe, post_inc, ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
notifications::{DetachAndPromptErr, NotificationId},
|
notifications::{DetachAndPromptErr, NotificationId},
|
||||||
|
@ -174,7 +177,11 @@ struct PendingOperation {
|
||||||
op_id: usize,
|
op_id: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RemoteOperations = Rc<RefCell<HashSet<u32>>>;
|
||||||
|
|
||||||
pub struct GitPanel {
|
pub struct GitPanel {
|
||||||
|
remote_operation_id: u32,
|
||||||
|
pending_remote_operations: RemoteOperations,
|
||||||
pub(crate) active_repository: Option<Entity<Repository>>,
|
pub(crate) active_repository: Option<Entity<Repository>>,
|
||||||
commit_editor: Entity<Editor>,
|
commit_editor: Entity<Editor>,
|
||||||
conflicted_count: usize,
|
conflicted_count: usize,
|
||||||
|
@ -206,6 +213,17 @@ pub struct GitPanel {
|
||||||
modal_open: bool,
|
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(
|
pub(crate) fn commit_message_editor(
|
||||||
commit_message_buffer: Entity<Buffer>,
|
commit_message_buffer: Entity<Buffer>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
|
@ -286,6 +304,8 @@ impl GitPanel {
|
||||||
cx.new(|cx| RepositorySelector::new(project.clone(), window, cx));
|
cx.new(|cx| RepositorySelector::new(project.clone(), window, cx));
|
||||||
|
|
||||||
let mut git_panel = Self {
|
let mut git_panel = Self {
|
||||||
|
pending_remote_operations: Default::default(),
|
||||||
|
remote_operation_id: 0,
|
||||||
active_repository,
|
active_repository,
|
||||||
commit_editor,
|
commit_editor,
|
||||||
conflicted_count: 0,
|
conflicted_count: 0,
|
||||||
|
@ -341,6 +361,16 @@ impl GitPanel {
|
||||||
cx.notify();
|
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>) {
|
fn serialize(&mut self, cx: &mut Context<Self>) {
|
||||||
let width = self.width;
|
let width = self.width;
|
||||||
self.pending_serialization = cx.background_spawn(
|
self.pending_serialization = cx.background_spawn(
|
||||||
|
@ -1121,23 +1151,44 @@ impl GitPanel {
|
||||||
let Some(repo) = self.active_repository.clone() else {
|
let Some(repo) = self.active_repository.clone() else {
|
||||||
return;
|
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 prior_head = self.load_commit_details("HEAD", cx);
|
||||||
|
|
||||||
let task = cx.spawn(|_, mut cx| async move {
|
|
||||||
let prior_head = prior_head.await?;
|
|
||||||
|
|
||||||
repo.update(&mut cx, |repo, _| repo.reset("HEAD^", ResetMode::Soft))?
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
Ok(prior_head)
|
|
||||||
});
|
|
||||||
|
|
||||||
let task = cx.spawn_in(window, |this, mut cx| async move {
|
let task = cx.spawn_in(window, |this, mut cx| async move {
|
||||||
let result = task.await;
|
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??;
|
||||||
|
|
||||||
|
Ok(Some(prior_head))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
this.update_in(&mut cx, |this, window, cx| {
|
this.update_in(&mut cx, |this, window, cx| {
|
||||||
this.pending_commit.take();
|
this.pending_commit.take();
|
||||||
match result {
|
match result {
|
||||||
Ok(prior_commit) => {
|
Ok(None) => {}
|
||||||
|
Ok(Some(prior_commit)) => {
|
||||||
this.commit_editor.update(cx, |editor, cx| {
|
this.commit_editor.update(cx, |editor, cx| {
|
||||||
editor.set_text(prior_commit.message, window, cx)
|
editor.set_text(prior_commit.message, window, cx)
|
||||||
});
|
});
|
||||||
|
@ -1151,6 +1202,125 @@ impl GitPanel {
|
||||||
self.pending_commit = Some(task);
|
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)> {
|
fn potential_co_authors(&self, cx: &App) -> Vec<(String, String)> {
|
||||||
let mut new_co_authors = Vec::new();
|
let mut new_co_authors = Vec::new();
|
||||||
let project = self.project.read(cx);
|
let project = self.project.read(cx);
|
||||||
|
@ -1591,15 +1761,23 @@ impl GitPanel {
|
||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
)
|
)
|
||||||
.child(self.render_repository_selector(cx))
|
.child(self.render_repository_selector(cx))
|
||||||
.child(div().flex_grow())
|
.child(div().flex_grow()) // spacer
|
||||||
.child(
|
.child(
|
||||||
Button::new("diff", "+/-")
|
div()
|
||||||
.tooltip(Tooltip::for_action_title("Open diff", &Diff))
|
.h_flex()
|
||||||
.on_click(|_, _, cx| {
|
.gap_1()
|
||||||
cx.defer(|cx| {
|
.children(self.render_spinner(cx))
|
||||||
cx.dispatch_action(&Diff);
|
.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 {
|
} 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 {
|
pub fn render_repository_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let active_repository = self.project.read(cx).active_repository(cx);
|
let active_repository = self.project.read(cx).active_repository(cx);
|
||||||
let repository_display_name = active_repository
|
let repository_display_name = active_repository
|
||||||
|
@ -1679,18 +1925,19 @@ impl GitPanel {
|
||||||
&& self.pending_commit.is_none()
|
&& self.pending_commit.is_none()
|
||||||
&& !editor.read(cx).is_empty(cx)
|
&& !editor.read(cx).is_empty(cx)
|
||||||
&& self.has_write_access(cx);
|
&& self.has_write_access(cx);
|
||||||
|
|
||||||
let panel_editor_style = panel_editor_style(true, window, cx);
|
let panel_editor_style = panel_editor_style(true, window, cx);
|
||||||
let enable_coauthors = self.render_co_authors(cx);
|
let enable_coauthors = self.render_co_authors(cx);
|
||||||
|
|
||||||
let tooltip = if self.has_staged_changes() {
|
let tooltip = if self.has_staged_changes() {
|
||||||
"Commit staged changes"
|
"git commit"
|
||||||
} else {
|
} else {
|
||||||
"Commit changes to tracked files"
|
"git commit --all"
|
||||||
};
|
};
|
||||||
let title = if self.has_staged_changes() {
|
let title = if self.has_staged_changes() {
|
||||||
"Commit"
|
"Commit"
|
||||||
} else {
|
} else {
|
||||||
"Commit All"
|
"Commit Tracked"
|
||||||
};
|
};
|
||||||
let editor_focus_handle = self.commit_editor.focus_handle(cx);
|
let editor_focus_handle = self.commit_editor.focus_handle(cx);
|
||||||
|
|
||||||
|
@ -1706,7 +1953,7 @@ impl GitPanel {
|
||||||
let branch = self
|
let branch = self
|
||||||
.active_repository
|
.active_repository
|
||||||
.as_ref()
|
.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());
|
.unwrap_or_else(|| "<no branch>".into());
|
||||||
|
|
||||||
let branch_selector = Button::new("branch-selector", branch)
|
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> {
|
fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
|
||||||
let active_repository = self.active_repository.as_ref()?;
|
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();
|
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();
|
let this = cx.entity();
|
||||||
Some(
|
Some(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
@ -1826,9 +2058,17 @@ impl GitPanel {
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.icon_position(IconPosition::Start)
|
.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))),
|
.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()
|
.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 {
|
fn has_write_access(&self, cx: &App) -> bool {
|
||||||
!self.project.read(cx).is_read_only(cx)
|
!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::unstage_all))
|
||||||
.on_action(cx.listener(Self::discard_tracked_changes))
|
.on_action(cx.listener(Self::discard_tracked_changes))
|
||||||
.on_action(cx.listener(Self::clean_all))
|
.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| {
|
.when(has_write_access && has_co_authors, |git_panel| {
|
||||||
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
|
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
|
||||||
})
|
})
|
||||||
|
@ -2314,17 +2620,21 @@ impl Render for GitPanel {
|
||||||
.size_full()
|
.size_full()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.bg(ElevationIndex::Surface.bg(cx))
|
.bg(ElevationIndex::Surface.bg(cx))
|
||||||
.child(if has_entries {
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.children(self.render_panel_header(window, cx))
|
.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))
|
.children(self.render_previous_commit(cx))
|
||||||
.child(self.render_commit_editor(window, cx))
|
.child(self.render_commit_editor(window, cx))
|
||||||
.into_any_element()
|
.into_any_element(),
|
||||||
} else {
|
)
|
||||||
self.render_empty_state(cx).into_any_element()
|
|
||||||
})
|
|
||||||
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
|
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
|
||||||
deferred(
|
deferred(
|
||||||
anchored()
|
anchored()
|
||||||
|
|
|
@ -9,6 +9,7 @@ pub mod branch_picker;
|
||||||
mod commit_modal;
|
mod commit_modal;
|
||||||
pub mod git_panel;
|
pub mod git_panel;
|
||||||
mod git_panel_settings;
|
mod git_panel_settings;
|
||||||
|
pub mod picker_prompt;
|
||||||
pub mod project_diff;
|
pub mod project_diff;
|
||||||
pub mod repository_selector;
|
pub mod repository_selector;
|
||||||
|
|
||||||
|
|
235
crates/git_ui/src/picker_prompt.rs
Normal file
235
crates/git_ui/src/picker_prompt.rs
Normal file
|
@ -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 client::ProjectId;
|
||||||
use futures::channel::{mpsc, oneshot};
|
use futures::channel::{mpsc, oneshot};
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
use git::repository::{Branch, CommitDetails, ResetMode};
|
use git::repository::{Branch, CommitDetails, PushOptions, Remote, ResetMode};
|
||||||
use git::{
|
use git::{
|
||||||
repository::{GitRepository, RepoPath},
|
repository::{GitRepository, RepoPath},
|
||||||
status::{GitSummary, TrackedSummary},
|
status::{GitSummary, TrackedSummary},
|
||||||
|
@ -74,6 +74,18 @@ pub enum Message {
|
||||||
Stage(GitRepo, Vec<RepoPath>),
|
Stage(GitRepo, Vec<RepoPath>),
|
||||||
Unstage(GitRepo, Vec<RepoPath>),
|
Unstage(GitRepo, Vec<RepoPath>),
|
||||||
SetIndexText(GitRepo, RepoPath, Option<String>),
|
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 {
|
pub enum GitEvent {
|
||||||
|
@ -107,6 +119,10 @@ impl GitStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(client: &AnyProtoClient) {
|
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_stage);
|
||||||
client.add_entity_request_handler(Self::handle_unstage);
|
client.add_entity_request_handler(Self::handle_unstage);
|
||||||
client.add_entity_request_handler(Self::handle_commit);
|
client.add_entity_request_handler(Self::handle_commit);
|
||||||
|
@ -242,8 +258,10 @@ impl GitStore {
|
||||||
mpsc::unbounded::<(Message, oneshot::Sender<Result<()>>)>();
|
mpsc::unbounded::<(Message, oneshot::Sender<Result<()>>)>();
|
||||||
cx.spawn(|_, cx| async move {
|
cx.spawn(|_, cx| async move {
|
||||||
while let Some((msg, respond)) = update_receiver.next().await {
|
while let Some((msg, respond)) = update_receiver.next().await {
|
||||||
let result = cx.background_spawn(Self::process_git_msg(msg)).await;
|
if !respond.is_canceled() {
|
||||||
respond.send(result).ok();
|
let result = cx.background_spawn(Self::process_git_msg(msg)).await;
|
||||||
|
respond.send(result).ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -252,6 +270,94 @@ impl GitStore {
|
||||||
|
|
||||||
async fn process_git_msg(msg: Message) -> Result<()> {
|
async fn process_git_msg(msg: Message) -> Result<()> {
|
||||||
match msg {
|
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) => {
|
Message::Stage(repo, paths) => {
|
||||||
match repo {
|
match repo {
|
||||||
GitRepo::Local(repo) => repo.stage_paths(&paths)?,
|
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(
|
async fn handle_stage(
|
||||||
this: Entity<Self>,
|
this: Entity<Self>,
|
||||||
envelope: TypedEnvelope<proto::Stage>,
|
envelope: TypedEnvelope<proto::Stage>,
|
||||||
|
@ -509,6 +682,34 @@ impl GitStore {
|
||||||
Ok(proto::Ack {})
|
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(
|
async fn handle_show(
|
||||||
this: Entity<Self>,
|
this: Entity<Self>,
|
||||||
envelope: TypedEnvelope<proto::GitShow>,
|
envelope: TypedEnvelope<proto::GitShow>,
|
||||||
|
@ -648,7 +849,7 @@ impl Repository {
|
||||||
(self.worktree_id, self.repository_entry.work_directory_id())
|
(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()
|
self.repository_entry.branch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -802,35 +1003,19 @@ impl Repository {
|
||||||
commit: &str,
|
commit: &str,
|
||||||
paths: Vec<RepoPath>,
|
paths: Vec<RepoPath>,
|
||||||
) -> oneshot::Receiver<Result<()>> {
|
) -> oneshot::Receiver<Result<()>> {
|
||||||
let (result_tx, result_rx) = futures::channel::oneshot::channel();
|
self.send_message(Message::CheckoutFiles {
|
||||||
let commit = commit.to_string().into();
|
repo: self.git_repo.clone(),
|
||||||
self.update_sender
|
commit: commit.to_string().into(),
|
||||||
.unbounded_send((
|
paths,
|
||||||
Message::CheckoutFiles {
|
})
|
||||||
repo: self.git_repo.clone(),
|
|
||||||
commit,
|
|
||||||
paths,
|
|
||||||
},
|
|
||||||
result_tx,
|
|
||||||
))
|
|
||||||
.ok();
|
|
||||||
result_rx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&self, commit: &str, reset_mode: ResetMode) -> oneshot::Receiver<Result<()>> {
|
pub fn reset(&self, commit: &str, reset_mode: ResetMode) -> oneshot::Receiver<Result<()>> {
|
||||||
let (result_tx, result_rx) = futures::channel::oneshot::channel();
|
self.send_message(Message::Reset {
|
||||||
let commit = commit.to_string().into();
|
repo: self.git_repo.clone(),
|
||||||
self.update_sender
|
commit: commit.to_string().into(),
|
||||||
.unbounded_send((
|
reset_mode,
|
||||||
Message::Reset {
|
})
|
||||||
repo: self.git_repo.clone(),
|
|
||||||
commit,
|
|
||||||
reset_mode,
|
|
||||||
},
|
|
||||||
result_tx,
|
|
||||||
))
|
|
||||||
.ok();
|
|
||||||
result_rx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show(&self, commit: &str, cx: &Context<Self>) -> Task<Result<CommitDetails>> {
|
pub fn show(&self, commit: &str, cx: &Context<Self>) -> Task<Result<CommitDetails>> {
|
||||||
|
@ -987,18 +1172,41 @@ impl Repository {
|
||||||
message: SharedString,
|
message: SharedString,
|
||||||
name_and_email: Option<(SharedString, SharedString)>,
|
name_and_email: Option<(SharedString, SharedString)>,
|
||||||
) -> oneshot::Receiver<Result<()>> {
|
) -> oneshot::Receiver<Result<()>> {
|
||||||
let (result_tx, result_rx) = futures::channel::oneshot::channel();
|
self.send_message(Message::Commit {
|
||||||
self.update_sender
|
git_repo: self.git_repo.clone(),
|
||||||
.unbounded_send((
|
message,
|
||||||
Message::Commit {
|
name_and_email,
|
||||||
git_repo: self.git_repo.clone(),
|
})
|
||||||
message,
|
}
|
||||||
name_and_email,
|
|
||||||
},
|
pub fn fetch(&self) -> oneshot::Receiver<Result<()>> {
|
||||||
result_tx,
|
self.send_message(Message::Fetch(self.git_repo.clone()))
|
||||||
))
|
}
|
||||||
.ok();
|
|
||||||
result_rx
|
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(
|
pub fn set_index_text(
|
||||||
|
@ -1006,13 +1214,49 @@ impl Repository {
|
||||||
path: &RepoPath,
|
path: &RepoPath,
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
) -> oneshot::Receiver<anyhow::Result<()>> {
|
) -> 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();
|
let (result_tx, result_rx) = futures::channel::oneshot::channel();
|
||||||
self.update_sender
|
self.update_sender.unbounded_send((message, result_tx)).ok();
|
||||||
.unbounded_send((
|
|
||||||
Message::SetIndexText(self.git_repo.clone(), path.clone(), content),
|
|
||||||
result_tx,
|
|
||||||
))
|
|
||||||
.ok();
|
|
||||||
result_rx
|
result_rx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -946,12 +946,17 @@ impl WorktreeStore {
|
||||||
upstream: proto_branch.upstream.map(|upstream| {
|
upstream: proto_branch.upstream.map(|upstream| {
|
||||||
git::repository::Upstream {
|
git::repository::Upstream {
|
||||||
ref_name: upstream.ref_name.into(),
|
ref_name: upstream.ref_name.into(),
|
||||||
tracking: upstream.tracking.map(|tracking| {
|
tracking: upstream
|
||||||
git::repository::UpstreamTracking {
|
.tracking
|
||||||
ahead: tracking.ahead as u32,
|
.map(|tracking| {
|
||||||
behind: tracking.behind as u32,
|
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| {
|
most_recent_commit: proto_branch.most_recent_commit.map(|commit| {
|
||||||
|
|
|
@ -321,7 +321,13 @@ message Envelope {
|
||||||
GitCommitDetails git_commit_details = 302;
|
GitCommitDetails git_commit_details = 302;
|
||||||
|
|
||||||
SetIndexText set_index_text = 299;
|
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;
|
reserved 87 to 88;
|
||||||
|
@ -2772,3 +2778,46 @@ message OpenCommitMessageBuffer {
|
||||||
uint64 worktree_id = 2;
|
uint64 worktree_id = 2;
|
||||||
uint64 work_directory_id = 3;
|
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),
|
(GitShow, Background),
|
||||||
(GitCommitDetails, Background),
|
(GitCommitDetails, Background),
|
||||||
(SetIndexText, Background),
|
(SetIndexText, Background),
|
||||||
|
(Push, Background),
|
||||||
|
(Fetch, Background),
|
||||||
|
(GetRemotes, Background),
|
||||||
|
(GetRemotesResponse, Background),
|
||||||
|
(Pull, Background),
|
||||||
);
|
);
|
||||||
|
|
||||||
request_messages!(
|
request_messages!(
|
||||||
|
@ -582,6 +587,10 @@ request_messages!(
|
||||||
(GitReset, Ack),
|
(GitReset, Ack),
|
||||||
(GitCheckoutFiles, Ack),
|
(GitCheckoutFiles, Ack),
|
||||||
(SetIndexText, Ack),
|
(SetIndexText, Ack),
|
||||||
|
(Push, Ack),
|
||||||
|
(Fetch, Ack),
|
||||||
|
(GetRemotes, GetRemotesResponse),
|
||||||
|
(Pull, Ack),
|
||||||
);
|
);
|
||||||
|
|
||||||
entity_messages!(
|
entity_messages!(
|
||||||
|
@ -678,6 +687,10 @@ entity_messages!(
|
||||||
GitReset,
|
GitReset,
|
||||||
GitCheckoutFiles,
|
GitCheckoutFiles,
|
||||||
SetIndexText,
|
SetIndexText,
|
||||||
|
Push,
|
||||||
|
Fetch,
|
||||||
|
GetRemotes,
|
||||||
|
Pull,
|
||||||
);
|
);
|
||||||
|
|
||||||
entity_messages!(
|
entity_messages!(
|
||||||
|
|
|
@ -20,7 +20,7 @@ use futures::{
|
||||||
};
|
};
|
||||||
use fuzzy::CharBag;
|
use fuzzy::CharBag;
|
||||||
use git::{
|
use git::{
|
||||||
repository::{Branch, GitRepository, RepoPath},
|
repository::{Branch, GitRepository, RepoPath, UpstreamTrackingStatus},
|
||||||
status::{
|
status::{
|
||||||
FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
|
FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
|
||||||
},
|
},
|
||||||
|
@ -202,7 +202,7 @@ pub struct RepositoryEntry {
|
||||||
pub(crate) statuses_by_path: SumTree<StatusEntry>,
|
pub(crate) statuses_by_path: SumTree<StatusEntry>,
|
||||||
work_directory_id: ProjectEntryId,
|
work_directory_id: ProjectEntryId,
|
||||||
pub work_directory: WorkDirectory,
|
pub work_directory: WorkDirectory,
|
||||||
pub(crate) branch: Option<Branch>,
|
pub(crate) current_branch: Option<Branch>,
|
||||||
pub current_merge_conflicts: TreeSet<RepoPath>,
|
pub current_merge_conflicts: TreeSet<RepoPath>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,7 +216,7 @@ impl Deref for RepositoryEntry {
|
||||||
|
|
||||||
impl RepositoryEntry {
|
impl RepositoryEntry {
|
||||||
pub fn branch(&self) -> Option<&Branch> {
|
pub fn branch(&self) -> Option<&Branch> {
|
||||||
self.branch.as_ref()
|
self.current_branch.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn work_directory_id(&self) -> ProjectEntryId {
|
pub fn work_directory_id(&self) -> ProjectEntryId {
|
||||||
|
@ -244,8 +244,11 @@ impl RepositoryEntry {
|
||||||
pub fn initial_update(&self) -> proto::RepositoryEntry {
|
pub fn initial_update(&self) -> proto::RepositoryEntry {
|
||||||
proto::RepositoryEntry {
|
proto::RepositoryEntry {
|
||||||
work_directory_id: self.work_directory_id.to_proto(),
|
work_directory_id: self.work_directory_id.to_proto(),
|
||||||
branch: self.branch.as_ref().map(|branch| branch.name.to_string()),
|
branch: self
|
||||||
branch_summary: self.branch.as_ref().map(branch_to_proto),
|
.current_branch
|
||||||
|
.as_ref()
|
||||||
|
.map(|branch| branch.name.to_string()),
|
||||||
|
branch_summary: self.current_branch.as_ref().map(branch_to_proto),
|
||||||
updated_statuses: self
|
updated_statuses: self
|
||||||
.statuses_by_path
|
.statuses_by_path
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -304,8 +307,11 @@ impl RepositoryEntry {
|
||||||
|
|
||||||
proto::RepositoryEntry {
|
proto::RepositoryEntry {
|
||||||
work_directory_id: self.work_directory_id.to_proto(),
|
work_directory_id: self.work_directory_id.to_proto(),
|
||||||
branch: self.branch.as_ref().map(|branch| branch.name.to_string()),
|
branch: self
|
||||||
branch_summary: self.branch.as_ref().map(branch_to_proto),
|
.current_branch
|
||||||
|
.as_ref()
|
||||||
|
.map(|branch| branch.name.to_string()),
|
||||||
|
branch_summary: self.current_branch.as_ref().map(branch_to_proto),
|
||||||
updated_statuses,
|
updated_statuses,
|
||||||
removed_statuses,
|
removed_statuses,
|
||||||
current_merge_conflicts: self
|
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(),
|
ref_name: upstream.ref_name.to_string(),
|
||||||
tracking: upstream
|
tracking: upstream
|
||||||
.tracking
|
.tracking
|
||||||
.as_ref()
|
.status()
|
||||||
.map(|upstream| proto::UpstreamTracking {
|
.map(|upstream| proto::UpstreamTracking {
|
||||||
ahead: upstream.ahead as u64,
|
ahead: upstream.ahead as u64,
|
||||||
behind: upstream.behind as u64,
|
behind: upstream.behind as u64,
|
||||||
|
@ -355,12 +361,16 @@ pub fn proto_to_branch(proto: &proto::Branch) -> git::repository::Branch {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|upstream| git::repository::Upstream {
|
.map(|upstream| git::repository::Upstream {
|
||||||
ref_name: upstream.ref_name.to_string().into(),
|
ref_name: upstream.ref_name.to_string().into(),
|
||||||
tracking: upstream.tracking.as_ref().map(|tracking| {
|
tracking: upstream
|
||||||
git::repository::UpstreamTracking {
|
.tracking
|
||||||
ahead: tracking.ahead as u32,
|
.as_ref()
|
||||||
behind: tracking.behind as u32,
|
.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| {
|
most_recent_commit: proto.most_recent_commit.as_ref().map(|commit| {
|
||||||
git::repository::CommitSummary {
|
git::repository::CommitSummary {
|
||||||
|
@ -2682,7 +2692,8 @@ impl Snapshot {
|
||||||
|
|
||||||
self.repositories
|
self.repositories
|
||||||
.update(&PathKey(work_dir_entry.path.clone()), &(), |repo| {
|
.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.statuses_by_path.edit(edits, &());
|
||||||
repo.current_merge_conflicts = conflicted_paths
|
repo.current_merge_conflicts = conflicted_paths
|
||||||
});
|
});
|
||||||
|
@ -2704,7 +2715,7 @@ impl Snapshot {
|
||||||
work_directory: WorkDirectory::InProject {
|
work_directory: WorkDirectory::InProject {
|
||||||
relative_path: work_dir_entry.path.clone(),
|
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,
|
statuses_by_path: statuses,
|
||||||
current_merge_conflicts: conflicted_paths,
|
current_merge_conflicts: conflicted_paths,
|
||||||
},
|
},
|
||||||
|
@ -3506,7 +3517,7 @@ impl BackgroundScannerState {
|
||||||
RepositoryEntry {
|
RepositoryEntry {
|
||||||
work_directory_id: work_dir_id,
|
work_directory_id: work_dir_id,
|
||||||
work_directory: work_directory.clone(),
|
work_directory: work_directory.clone(),
|
||||||
branch: None,
|
current_branch: None,
|
||||||
statuses_by_path: Default::default(),
|
statuses_by_path: Default::default(),
|
||||||
current_merge_conflicts: Default::default(),
|
current_merge_conflicts: Default::default(),
|
||||||
},
|
},
|
||||||
|
@ -5580,7 +5591,7 @@ fn update_branches(
|
||||||
let mut repository = snapshot
|
let mut repository = snapshot
|
||||||
.repository(repository.work_directory.path_key())
|
.repository(repository.work_directory.path_key())
|
||||||
.context("Missing repository")?;
|
.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();
|
let mut state = state.lock();
|
||||||
state
|
state
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue