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:
Coenen Benjamin
2025-12-04 14:23:36 +01:00
committed by GitHub
parent 0d80b452fb
commit 4c51fffbb5
12 changed files with 1355 additions and 194 deletions
+2
View File
@@ -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)
+37 -6
View File
@@ -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)]
+2 -1
View File
@@ -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> {
+60 -10
View File
@@ -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()
}
File diff suppressed because it is too large Load Diff
-2
View File
@@ -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
View File
@@ -1,4 +1,5 @@
use anyhow::Context as _;
use git::repository::{Remote, RemoteCommandOutput};
use linkify::{LinkFinder, LinkKind};
use ui::SharedString;
+95 -1
View File
@@ -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();
+13
View File
@@ -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;
+6 -1
View File
@@ -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;
+6
View File
@@ -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,
+4
View File
@@ -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,