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
|
@ -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,24 +570,21 @@ 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 = new_std_command(&self.git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.args(args)
|
||||
.output()?;
|
||||
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{}",
|
||||
|
@ -559,6 +593,118 @@ impl GitRepository for RealGitRepository {
|
|||
}
|
||||
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)]
|
||||
|
@ -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
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue