Add support for git remotes (#42819)
Follow up of #42486 Closes #26559 https://github.com/user-attachments/assets/e2f54dda-a78b-4d9b-a910-16d51f98a111 Release Notes: - Added support for git remotes --------- Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com>
This commit is contained in:
@@ -469,6 +469,8 @@ impl Server {
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GetBlobContent>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitCreateBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitChangeBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitCreateRemote>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitRemoveRemote>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
|
||||
.add_message_handler(update_context)
|
||||
|
||||
@@ -50,6 +50,8 @@ pub struct FakeGitRepositoryState {
|
||||
pub blames: HashMap<RepoPath, Blame>,
|
||||
pub current_branch_name: Option<String>,
|
||||
pub branches: HashSet<String>,
|
||||
/// List of remotes, keys are names and values are URLs
|
||||
pub remotes: HashMap<String, String>,
|
||||
pub simulated_index_write_error_message: Option<String>,
|
||||
pub refs: HashMap<String, String>,
|
||||
}
|
||||
@@ -68,6 +70,7 @@ impl FakeGitRepositoryState {
|
||||
refs: HashMap::from_iter([("HEAD".into(), "abc".into())]),
|
||||
merge_base_contents: Default::default(),
|
||||
oids: Default::default(),
|
||||
remotes: HashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -432,8 +435,13 @@ impl GitRepository for FakeGitRepository {
|
||||
})
|
||||
}
|
||||
|
||||
fn delete_branch(&self, _name: String) -> BoxFuture<'_, Result<()>> {
|
||||
unimplemented!()
|
||||
fn delete_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
|
||||
self.with_state_async(true, move |state| {
|
||||
if !state.branches.remove(&name) {
|
||||
bail!("no such branch: {name}");
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn blame(&self, path: RepoPath, _content: Rope) -> BoxFuture<'_, Result<git::blame::Blame>> {
|
||||
@@ -598,6 +606,19 @@ impl GitRepository for FakeGitRepository {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn get_all_remotes(&self) -> BoxFuture<'_, Result<Vec<Remote>>> {
|
||||
self.with_state_async(false, move |state| {
|
||||
let remotes = state
|
||||
.remotes
|
||||
.keys()
|
||||
.map(|r| Remote {
|
||||
name: r.clone().into(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Ok(remotes)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_push_remote(&self, _branch: String) -> BoxFuture<'_, Result<Option<Remote>>> {
|
||||
unimplemented!()
|
||||
}
|
||||
@@ -606,10 +627,6 @@ impl GitRepository for FakeGitRepository {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn get_all_remotes(&self) -> BoxFuture<'_, Result<Vec<Remote>>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<gpui::SharedString>>> {
|
||||
future::ready(Ok(Vec::new())).boxed()
|
||||
}
|
||||
@@ -683,6 +700,20 @@ impl GitRepository for FakeGitRepository {
|
||||
fn default_branch(&self) -> BoxFuture<'_, Result<Option<SharedString>>> {
|
||||
async { Ok(Some("main".into())) }.boxed()
|
||||
}
|
||||
|
||||
fn create_remote(&self, name: String, url: String) -> BoxFuture<'_, Result<()>> {
|
||||
self.with_state_async(true, move |state| {
|
||||
state.remotes.insert(name, url);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_remote(&self, name: String) -> BoxFuture<'_, Result<()>> {
|
||||
self.with_state_async(true, move |state| {
|
||||
state.remotes.remove(&name);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use derive_more::Deref;
|
||||
@@ -11,7 +12,7 @@ pub struct RemoteUrl(Url);
|
||||
static USERNAME_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^[0-9a-zA-Z\-_]+@").expect("Failed to create USERNAME_REGEX"));
|
||||
|
||||
impl std::str::FromStr for RemoteUrl {
|
||||
impl FromStr for RemoteUrl {
|
||||
type Err = url::ParseError;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
|
||||
@@ -7,13 +7,15 @@ use collections::HashMap;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::io::BufWriter;
|
||||
use futures::{AsyncWriteExt, FutureExt as _, select_biased};
|
||||
use git2::BranchType;
|
||||
use git2::{BranchType, ErrorCode};
|
||||
use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, SharedString, Task};
|
||||
use parking_lot::Mutex;
|
||||
use rope::Rope;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use smol::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::process::{ExitStatus, Stdio};
|
||||
use std::{
|
||||
@@ -55,6 +57,12 @@ impl Branch {
|
||||
self.ref_name.starts_with("refs/remotes/")
|
||||
}
|
||||
|
||||
pub fn remote_name(&self) -> Option<&str> {
|
||||
self.ref_name
|
||||
.strip_prefix("refs/remotes/")
|
||||
.and_then(|stripped| stripped.split("/").next())
|
||||
}
|
||||
|
||||
pub fn tracking_status(&self) -> Option<UpstreamTrackingStatus> {
|
||||
self.upstream
|
||||
.as_ref()
|
||||
@@ -590,6 +598,10 @@ pub trait GitRepository: Send + Sync {
|
||||
|
||||
fn get_all_remotes(&self) -> BoxFuture<'_, Result<Vec<Remote>>>;
|
||||
|
||||
fn remove_remote(&self, name: String) -> BoxFuture<'_, Result<()>>;
|
||||
|
||||
fn create_remote(&self, name: String, url: String) -> BoxFuture<'_, Result<()>>;
|
||||
|
||||
/// returns a list of remote branches that contain HEAD
|
||||
fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<SharedString>>>;
|
||||
|
||||
@@ -1385,9 +1397,19 @@ impl GitRepository for RealGitRepository {
|
||||
branch
|
||||
} else if let Ok(revision) = repo.find_branch(&name, BranchType::Remote) {
|
||||
let (_, branch_name) = name.split_once("/").context("Unexpected branch format")?;
|
||||
|
||||
let revision = revision.get();
|
||||
let branch_commit = revision.peel_to_commit()?;
|
||||
let mut branch = repo.branch(&branch_name, &branch_commit, false)?;
|
||||
let mut branch = match repo.branch(&branch_name, &branch_commit, false) {
|
||||
Ok(branch) => branch,
|
||||
Err(err) if err.code() == ErrorCode::Exists => {
|
||||
repo.find_branch(&branch_name, BranchType::Local)?
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
|
||||
branch.set_upstream(Some(&name))?;
|
||||
branch
|
||||
} else {
|
||||
@@ -1403,7 +1425,6 @@ impl GitRepository for RealGitRepository {
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let branch = branch.await?;
|
||||
|
||||
GitBinary::new(git_binary_path, working_directory?, executor)
|
||||
.run(&["checkout", &branch])
|
||||
.await?;
|
||||
@@ -1993,7 +2014,7 @@ impl GitRepository for RealGitRepository {
|
||||
let working_directory = working_directory?;
|
||||
let output = new_smol_command(&git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.args(["remote"])
|
||||
.args(["remote", "-v"])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
@@ -2002,14 +2023,43 @@ impl GitRepository for RealGitRepository {
|
||||
"Failed to get all remotes:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
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(),
|
||||
let remote_names: HashSet<Remote> = String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.filter(|line| !line.is_empty())
|
||||
.filter_map(|line| {
|
||||
let mut split_line = line.split_whitespace();
|
||||
let remote_name = split_line.next()?;
|
||||
|
||||
Some(Remote {
|
||||
name: remote_name.trim().to_string().into(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(remote_names)
|
||||
|
||||
Ok(remote_names.into_iter().collect())
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn remove_remote(&self, name: String) -> BoxFuture<'_, Result<()>> {
|
||||
let repo = self.repository.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let repo = repo.lock();
|
||||
repo.remote_delete(&name)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn create_remote(&self, name: String, url: String) -> BoxFuture<'_, Result<()>> {
|
||||
let repo = self.repository.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let repo = repo.lock();
|
||||
repo.remote(&name, url.as_ref())?;
|
||||
Ok(())
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
+1129
-173
File diff suppressed because it is too large
Load Diff
@@ -3463,7 +3463,6 @@ impl GitPanel {
|
||||
) -> Option<impl IntoElement> {
|
||||
let active_repository = self.active_repository.clone()?;
|
||||
let panel_editor_style = panel_editor_style(true, window, cx);
|
||||
|
||||
let enable_coauthors = self.render_co_authors(cx);
|
||||
|
||||
let editor_focus_handle = self.commit_editor.focus_handle(cx);
|
||||
@@ -4772,7 +4771,6 @@ impl RenderOnce for PanelRepoFooter {
|
||||
const MAX_REPO_LEN: usize = 16;
|
||||
const LABEL_CHARACTER_BUDGET: usize = MAX_BRANCH_LEN + MAX_REPO_LEN;
|
||||
const MAX_SHORT_SHA_LEN: usize = 8;
|
||||
|
||||
let branch_name = self
|
||||
.branch
|
||||
.as_ref()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Context as _;
|
||||
|
||||
use git::repository::{Remote, RemoteCommandOutput};
|
||||
use linkify::{LinkFinder, LinkKind};
|
||||
use ui::SharedString;
|
||||
|
||||
@@ -472,6 +472,8 @@ impl GitStore {
|
||||
client.add_entity_request_handler(Self::handle_change_branch);
|
||||
client.add_entity_request_handler(Self::handle_create_branch);
|
||||
client.add_entity_request_handler(Self::handle_rename_branch);
|
||||
client.add_entity_request_handler(Self::handle_create_remote);
|
||||
client.add_entity_request_handler(Self::handle_remove_remote);
|
||||
client.add_entity_request_handler(Self::handle_delete_branch);
|
||||
client.add_entity_request_handler(Self::handle_git_init);
|
||||
client.add_entity_request_handler(Self::handle_push);
|
||||
@@ -2274,6 +2276,25 @@ impl GitStore {
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_create_remote(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::GitCreateRemote>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
||||
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
||||
let remote_name = envelope.payload.remote_name;
|
||||
let remote_url = envelope.payload.remote_url;
|
||||
|
||||
repository_handle
|
||||
.update(&mut cx, |repository_handle, _| {
|
||||
repository_handle.create_remote(remote_name, remote_url)
|
||||
})?
|
||||
.await??;
|
||||
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_delete_branch(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::GitDeleteBranch>,
|
||||
@@ -2292,6 +2313,24 @@ impl GitStore {
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_remove_remote(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::GitRemoveRemote>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
||||
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
||||
let remote_name = envelope.payload.remote_name;
|
||||
|
||||
repository_handle
|
||||
.update(&mut cx, |repository_handle, _| {
|
||||
repository_handle.remove_remote(remote_name)
|
||||
})?
|
||||
.await??;
|
||||
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_show(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::GitShow>,
|
||||
@@ -4865,6 +4904,61 @@ impl Repository {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_remote(
|
||||
&mut self,
|
||||
remote_name: String,
|
||||
remote_url: String,
|
||||
) -> oneshot::Receiver<Result<()>> {
|
||||
let id = self.id;
|
||||
self.send_job(
|
||||
Some(format!("git remote add {remote_name} {remote_url}").into()),
|
||||
move |repo, _cx| async move {
|
||||
match repo {
|
||||
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
||||
backend.create_remote(remote_name, remote_url).await
|
||||
}
|
||||
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
||||
client
|
||||
.request(proto::GitCreateRemote {
|
||||
project_id: project_id.0,
|
||||
repository_id: id.to_proto(),
|
||||
remote_name,
|
||||
remote_url,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn remove_remote(&mut self, remote_name: String) -> oneshot::Receiver<Result<()>> {
|
||||
let id = self.id;
|
||||
self.send_job(
|
||||
Some(format!("git remove remote {remote_name}").into()),
|
||||
move |repo, _cx| async move {
|
||||
match repo {
|
||||
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
||||
backend.remove_remote(remote_name).await
|
||||
}
|
||||
RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => {
|
||||
client
|
||||
.request(proto::GitRemoveRemote {
|
||||
project_id: project_id.0,
|
||||
repository_id: id.to_proto(),
|
||||
remote_name,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_remotes(
|
||||
&mut self,
|
||||
branch_name: Option<String>,
|
||||
@@ -4902,7 +4996,7 @@ impl Repository {
|
||||
let remotes = response
|
||||
.remotes
|
||||
.into_iter()
|
||||
.map(|remotes| git::repository::Remote {
|
||||
.map(|remotes| Remote {
|
||||
name: remotes.name.into(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -190,6 +190,19 @@ message GitRenameBranch {
|
||||
string new_name = 4;
|
||||
}
|
||||
|
||||
message GitCreateRemote {
|
||||
uint64 project_id = 1;
|
||||
uint64 repository_id = 2;
|
||||
string remote_name = 3;
|
||||
string remote_url = 4;
|
||||
}
|
||||
|
||||
message GitRemoveRemote {
|
||||
uint64 project_id = 1;
|
||||
uint64 repository_id = 2;
|
||||
string remote_name = 3;
|
||||
}
|
||||
|
||||
message GitDeleteBranch {
|
||||
uint64 project_id = 1;
|
||||
uint64 repository_id = 2;
|
||||
|
||||
@@ -437,13 +437,18 @@ message Envelope {
|
||||
OpenImageResponse open_image_response = 392;
|
||||
CreateImageForPeer create_image_for_peer = 393;
|
||||
|
||||
|
||||
GitFileHistory git_file_history = 397;
|
||||
GitFileHistoryResponse git_file_history_response = 398;
|
||||
|
||||
RunGitHook run_git_hook = 399;
|
||||
|
||||
GitDeleteBranch git_delete_branch = 400;
|
||||
ExternalExtensionAgentsUpdated external_extension_agents_updated = 401; // current max
|
||||
|
||||
ExternalExtensionAgentsUpdated external_extension_agents_updated = 401;
|
||||
|
||||
GitCreateRemote git_create_remote = 402;
|
||||
GitRemoveRemote git_remove_remote = 403;// current max
|
||||
}
|
||||
|
||||
reserved 87 to 88, 396;
|
||||
|
||||
@@ -305,6 +305,8 @@ messages!(
|
||||
(RemoteMessageResponse, Background),
|
||||
(AskPassRequest, Background),
|
||||
(AskPassResponse, Background),
|
||||
(GitCreateRemote, Background),
|
||||
(GitRemoveRemote, Background),
|
||||
(GitCreateBranch, Background),
|
||||
(GitChangeBranch, Background),
|
||||
(GitRenameBranch, Background),
|
||||
@@ -504,6 +506,8 @@ request_messages!(
|
||||
(GetRemotes, GetRemotesResponse),
|
||||
(Pull, RemoteMessageResponse),
|
||||
(AskPassRequest, AskPassResponse),
|
||||
(GitCreateRemote, Ack),
|
||||
(GitRemoveRemote, Ack),
|
||||
(GitCreateBranch, Ack),
|
||||
(GitChangeBranch, Ack),
|
||||
(GitRenameBranch, Ack),
|
||||
@@ -676,6 +680,8 @@ entity_messages!(
|
||||
GitChangeBranch,
|
||||
GitRenameBranch,
|
||||
GitCreateBranch,
|
||||
GitCreateRemote,
|
||||
GitRemoveRemote,
|
||||
CheckForPushedCommits,
|
||||
GitDiff,
|
||||
GitInit,
|
||||
|
||||
@@ -215,6 +215,10 @@ pub mod git {
|
||||
Switch,
|
||||
/// Selects a different repository.
|
||||
SelectRepo,
|
||||
/// Filter remotes.
|
||||
FilterRemotes,
|
||||
/// Create a git remote.
|
||||
CreateRemote,
|
||||
/// Opens the git branch selector.
|
||||
#[action(deprecated_aliases = ["branches::OpenRecent"])]
|
||||
Branch,
|
||||
|
||||
Reference in New Issue
Block a user