From 88c4e0b2d8932951b3dc7db6571a9861f94aa61e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 6 May 2024 21:24:48 -0400 Subject: [PATCH] Add a registry for `GitHostingProvider`s (#11470) This PR adds a registry for `GitHostingProvider`s. The intent here is to help decouple these provider-specific concerns from the lower-level `git` crate. Similar to languages, the Git hosting providers live in the new `git_hosting_providers` crate. This work also lays the foundation for if we wanted to allow defining a `GitHostingProvider` from within an extension. This could be useful if we wanted to extend the support to work with self-hosted Git providers (like GitHub Enterprise). I also took the opportunity to move some of the provider-specific code out of the `util` crate, since it had leaked into there. Release Notes: - N/A --- Cargo.lock | 25 ++++- Cargo.toml | 2 + crates/collab/Cargo.toml | 1 + crates/collab/src/tests/test_server.rs | 6 ++ crates/editor/src/editor.rs | 7 +- crates/editor/src/git/blame.rs | 19 +++- crates/fs/src/fs.rs | 13 ++- crates/git/Cargo.toml | 9 +- crates/git/src/blame.rs | 8 +- crates/git/src/git.rs | 2 - crates/git/src/hosting_provider.rs | 91 ++++++++++++++++--- crates/git/src/repository.rs | 10 +- crates/git_hosting_providers/Cargo.toml | 30 ++++++ crates/git_hosting_providers/LICENSE-GPL | 1 + .../src/git_hosting_providers.rs | 26 ++++++ .../src/providers.rs} | 0 .../src/providers}/bitbucket.rs | 20 ++-- .../src/providers}/codeberg.rs | 87 ++++++++++++++++-- .../src/providers}/gitee.rs | 4 +- .../src/providers}/github.rs | 91 ++++++++++++++++--- .../src/providers}/gitlab.rs | 4 +- .../src/providers}/sourcehut.rs | 4 +- crates/util/src/codeberg.rs | 78 ---------------- crates/util/src/git_author.rs | 5 - crates/util/src/github.rs | 72 +-------------- crates/util/src/util.rs | 2 - crates/zed/Cargo.toml | 2 + crates/zed/src/main.rs | 15 ++- 28 files changed, 405 insertions(+), 229 deletions(-) create mode 100644 crates/git_hosting_providers/Cargo.toml create mode 120000 crates/git_hosting_providers/LICENSE-GPL create mode 100644 crates/git_hosting_providers/src/git_hosting_providers.rs rename crates/{git/src/hosting_providers.rs => git_hosting_providers/src/providers.rs} (100%) rename crates/{git/src/hosting_providers => git_hosting_providers/src/providers}/bitbucket.rs (84%) rename crates/{git/src/hosting_providers => git_hosting_providers/src/providers}/codeberg.rs (75%) rename crates/{git/src/hosting_providers => git_hosting_providers/src/providers}/gitee.rs (98%) rename crates/{git/src/hosting_providers => git_hosting_providers/src/providers}/github.rs (79%) rename crates/{git/src/hosting_providers => git_hosting_providers/src/providers}/gitlab.rs (98%) rename crates/{git/src/hosting_providers => git_hosting_providers/src/providers}/sourcehut.rs (98%) delete mode 100644 crates/util/src/codeberg.rs delete mode 100644 crates/util/src/git_author.rs diff --git a/Cargo.lock b/Cargo.lock index eee04e4eb1..a4ee355c46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2286,6 +2286,7 @@ dependencies = [ "fs", "futures 0.3.28", "git", + "git_hosting_providers", "google_ai", "gpui", "headless", @@ -4394,12 +4395,13 @@ dependencies = [ "async-trait", "clock", "collections", + "derive_more", "git2", + "gpui", "lazy_static", "log", "parking_lot", "pretty_assertions", - "regex", "rope", "serde", "serde_json", @@ -4426,6 +4428,25 @@ dependencies = [ "url", ] +[[package]] +name = "git_hosting_providers" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "futures 0.3.28", + "git", + "gpui", + "isahc", + "pretty_assertions", + "regex", + "serde", + "serde_json", + "unindent", + "url", + "util", +] + [[package]] name = "glob" version = "0.3.1" @@ -12766,6 +12787,8 @@ dependencies = [ "file_icons", "fs", "futures 0.3.28", + "git", + "git_hosting_providers", "go_to_line", "gpui", "headless", diff --git a/Cargo.toml b/Cargo.toml index 11f58039c0..fa7b399865 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "crates/fsevent", "crates/fuzzy", "crates/git", + "crates/git_hosting_providers", "crates/go_to_line", "crates/google_ai", "crates/gpui", @@ -174,6 +175,7 @@ fs = { path = "crates/fs" } fsevent = { path = "crates/fsevent" } fuzzy = { path = "crates/fuzzy" } git = { path = "crates/git" } +git_hosting_providers = { path = "crates/git_hosting_providers" } go_to_line = { path = "crates/go_to_line" } google_ai = { path = "crates/google_ai" } gpui = { path = "crates/gpui" } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 5e719739ae..b3aeffb2ef 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -83,6 +83,7 @@ env_logger.workspace = true file_finder.workspace = true fs = { workspace = true, features = ["test-support"] } git = { workspace = true, features = ["test-support"] } +git_hosting_providers.workspace = true gpui = { workspace = true, features = ["test-support"] } indoc.workspace = true language = { workspace = true, features = ["test-support"] } diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 3a456a328e..650495ef8e 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -17,6 +17,7 @@ use collab_ui::channel_view::ChannelView; use collections::{HashMap, HashSet}; use fs::FakeFs; use futures::{channel::oneshot, StreamExt as _}; +use git::GitHostingProviderRegistry; use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, View, VisualTestContext}; use language::LanguageRegistry; use node_runtime::FakeNodeRuntime; @@ -257,6 +258,11 @@ impl TestServer { }) }); + let git_hosting_provider_registry = + cx.update(|cx| GitHostingProviderRegistry::default_global(cx)); + git_hosting_provider_registry + .register_hosting_provider(Arc::new(git_hosting_providers::Github)); + let fs = FakeFs::new(cx.executor()); let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx)); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1a151cd4f2..b9756364ec 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -41,7 +41,7 @@ mod editor_tests; #[cfg(any(test, feature = "test-support"))] pub mod test; use ::git::diff::{DiffHunk, DiffHunkStatus}; -use ::git::{parse_git_remote_url, BuildPermalinkParams}; +use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry}; pub(crate) use actions::*; use aho_corasick::AhoCorasick; use anyhow::{anyhow, Context as _, Result}; @@ -9548,8 +9548,9 @@ impl Editor { let selections = self.selections.all::(cx); let selection = selections.iter().peekable().next(); - let (provider, remote) = parse_git_remote_url(&origin_url) - .ok_or_else(|| anyhow!("failed to parse Git remote URL"))?; + let (provider, remote) = + parse_git_remote_url(GitHostingProviderRegistry::default_global(cx), &origin_url) + .ok_or_else(|| anyhow!("failed to parse Git remote URL"))?; Ok(provider.build_permalink( remote, diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index deca89c393..83c2b727f5 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -4,7 +4,7 @@ use anyhow::Result; use collections::HashMap; use git::{ blame::{Blame, BlameEntry}, - parse_git_remote_url, GitHostingProvider, Oid, PullRequest, + parse_git_remote_url, GitHostingProvider, GitHostingProviderRegistry, Oid, PullRequest, }; use gpui::{Model, ModelContext, Subscription, Task}; use language::{markdown, Bias, Buffer, BufferSnapshot, Edit, LanguageRegistry, ParsedMarkdown}; @@ -330,6 +330,7 @@ impl GitBlame { let snapshot = self.buffer.read(cx).snapshot(); let blame = self.project.read(cx).blame_buffer(&self.buffer, None, cx); let languages = self.project.read(cx).languages().clone(); + let provider_registry = GitHostingProviderRegistry::default_global(cx); self.task = cx.spawn(|this, mut cx| async move { let result = cx @@ -345,9 +346,14 @@ impl GitBlame { } = blame.await?; let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row); - let commit_details = - parse_commit_messages(messages, remote_url, &permalinks, &languages) - .await; + let commit_details = parse_commit_messages( + messages, + remote_url, + &permalinks, + provider_registry, + &languages, + ) + .await; anyhow::Ok((entries, commit_details)) } @@ -438,11 +444,14 @@ async fn parse_commit_messages( messages: impl IntoIterator, remote_url: Option, deprecated_permalinks: &HashMap, + provider_registry: Arc, languages: &Arc, ) -> HashMap { let mut commit_details = HashMap::default(); - let parsed_remote_url = remote_url.as_deref().and_then(parse_git_remote_url); + let parsed_remote_url = remote_url + .as_deref() + .and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url)); for (oid, message) in messages { let parsed_message = parse_markdown(&message, &languages).await; diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 7110614229..6b4af4bd97 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -1,4 +1,5 @@ use anyhow::{anyhow, Result}; +use git::GitHostingProviderRegistry; #[cfg(unix)] use std::os::unix::fs::MetadataExt; @@ -117,12 +118,19 @@ pub struct Metadata { #[derive(Default)] pub struct RealFs { + git_hosting_provider_registry: Arc, git_binary_path: Option, } impl RealFs { - pub fn new(git_binary_path: Option) -> Self { - Self { git_binary_path } + pub fn new( + git_hosting_provider_registry: Arc, + git_binary_path: Option, + ) -> Self { + Self { + git_hosting_provider_registry, + git_binary_path, + } } } @@ -474,6 +482,7 @@ impl Fs for RealFs { Arc::new(Mutex::new(RealGitRepository::new( libgit_repository, self.git_binary_path.clone(), + self.git_hosting_provider_registry.clone(), ))) }) } diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index 72ef4a7254..971ca6606b 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -16,19 +16,20 @@ anyhow.workspace = true async-trait.workspace = true clock.workspace = true collections.workspace = true +derive_more.workspace = true git2.workspace = true +gpui.workspace = true lazy_static.workspace = true log.workspace = true +parking_lot.workspace = true +rope.workspace = true +serde.workspace = true smol.workspace = true sum_tree.workspace = true text.workspace = true time.workspace = true url.workspace = true util.workspace = true -serde.workspace = true -regex.workspace = true -rope.workspace = true -parking_lot.workspace = true windows.workspace = true [dev-dependencies] diff --git a/crates/git/src/blame.rs b/crates/git/src/blame.rs index 57a5963092..e6c76aa0b3 100644 --- a/crates/git/src/blame.rs +++ b/crates/git/src/blame.rs @@ -1,10 +1,11 @@ use crate::commit::get_messages; -use crate::{parse_git_remote_url, BuildCommitPermalinkParams, Oid}; +use crate::{parse_git_remote_url, BuildCommitPermalinkParams, GitHostingProviderRegistry, Oid}; use anyhow::{anyhow, Context, Result}; use collections::{HashMap, HashSet}; use serde::{Deserialize, Serialize}; use std::io::Write; use std::process::{Command, Stdio}; +use std::sync::Arc; use std::{ops::Range, path::Path}; use text::Rope; use time; @@ -33,6 +34,7 @@ impl Blame { path: &Path, content: &Rope, remote_url: Option, + provider_registry: Arc, ) -> Result { let output = run_git_blame(git_binary, working_directory, path, &content)?; let mut entries = parse_git_blame(&output)?; @@ -40,7 +42,9 @@ impl Blame { let mut permalinks = HashMap::default(); let mut unique_shas = HashSet::default(); - let parsed_remote_url = remote_url.as_deref().and_then(parse_git_remote_url); + let parsed_remote_url = remote_url + .as_deref() + .and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url)); for entry in entries.iter_mut() { unique_shas.insert(entry.sha); diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index e7b7611ba1..3664a065a1 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -1,5 +1,4 @@ mod hosting_provider; -mod hosting_providers; use anyhow::{anyhow, Context, Result}; use serde::{Deserialize, Serialize}; @@ -11,7 +10,6 @@ pub use git2 as libgit; pub use lazy_static::lazy_static; pub use crate::hosting_provider::*; -pub use crate::hosting_providers::*; pub mod blame; pub mod commit; diff --git a/crates/git/src/hosting_provider.rs b/crates/git/src/hosting_provider.rs index e618fc83e0..ad0c37c425 100644 --- a/crates/git/src/hosting_provider.rs +++ b/crates/git/src/hosting_provider.rs @@ -2,10 +2,13 @@ use std::{ops::Range, sync::Arc}; use anyhow::Result; use async_trait::async_trait; +use collections::BTreeMap; +use derive_more::{Deref, DerefMut}; +use gpui::{AppContext, Global}; +use parking_lot::RwLock; use url::Url; use util::http::HttpClient; -use crate::hosting_providers::{Bitbucket, Codeberg, Gitee, Github, Gitlab, Sourcehut}; use crate::Oid; #[derive(Debug, PartialEq, Eq, Clone)] @@ -87,6 +90,69 @@ pub trait GitHostingProvider { } } +#[derive(Default, Deref, DerefMut)] +struct GlobalGitHostingProviderRegistry(Arc); + +impl Global for GlobalGitHostingProviderRegistry {} + +#[derive(Default)] +struct GitHostingProviderRegistryState { + providers: BTreeMap>, +} + +#[derive(Default)] +pub struct GitHostingProviderRegistry { + state: RwLock, +} + +impl GitHostingProviderRegistry { + /// Returns the global [`GitHostingProviderRegistry`]. + pub fn global(cx: &AppContext) -> Arc { + cx.global::().0.clone() + } + + /// Returns the global [`GitHostingProviderRegistry`]. + /// + /// Inserts a default [`GitHostingProviderRegistry`] if one does not yet exist. + pub fn default_global(cx: &mut AppContext) -> Arc { + cx.default_global::() + .0 + .clone() + } + + /// Sets the global [`GitHostingProviderRegistry`]. + pub fn set_global(registry: Arc, cx: &mut AppContext) { + cx.set_global(GlobalGitHostingProviderRegistry(registry)); + } + + /// Returns a new [`GitHostingProviderRegistry`]. + pub fn new() -> Self { + Self { + state: RwLock::new(GitHostingProviderRegistryState { + providers: BTreeMap::default(), + }), + } + } + + /// Returns the list of all [`GitHostingProvider`]s in the registry. + pub fn list_hosting_providers( + &self, + ) -> Vec> { + self.state.read().providers.values().cloned().collect() + } + + /// Adds the provided [`GitHostingProvider`] to the registry. + pub fn register_hosting_provider( + &self, + provider: Arc, + ) { + self.state + .write() + .providers + .insert(provider.name(), provider); + } +} + #[derive(Debug)] pub struct ParsedGitRemote<'a> { pub owner: &'a str, @@ -94,23 +160,18 @@ pub struct ParsedGitRemote<'a> { } pub fn parse_git_remote_url( + provider_registry: Arc, url: &str, ) -> Option<( Arc, ParsedGitRemote, )> { - let providers: Vec> = vec![ - Arc::new(Github), - Arc::new(Gitlab), - Arc::new(Bitbucket), - Arc::new(Codeberg), - Arc::new(Gitee), - Arc::new(Sourcehut), - ]; - - providers.into_iter().find_map(|provider| { - provider - .parse_remote_url(&url) - .map(|parsed_remote| (provider, parsed_remote)) - }) + provider_registry + .list_hosting_providers() + .into_iter() + .find_map(|provider| { + provider + .parse_remote_url(&url) + .map(|parsed_remote| (provider, parsed_remote)) + }) } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 0b479162e0..f9125724a9 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -1,4 +1,5 @@ use crate::blame::Blame; +use crate::GitHostingProviderRegistry; use anyhow::{Context, Result}; use collections::HashMap; use git2::{BranchType, StatusShow}; @@ -71,13 +72,19 @@ impl std::fmt::Debug for dyn GitRepository { pub struct RealGitRepository { pub repository: LibGitRepository, pub git_binary_path: PathBuf, + hosting_provider_registry: Arc, } impl RealGitRepository { - pub fn new(repository: LibGitRepository, git_binary_path: Option) -> Self { + pub fn new( + repository: LibGitRepository, + git_binary_path: Option, + hosting_provider_registry: Arc, + ) -> Self { Self { repository, git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")), + hosting_provider_registry, } } } @@ -246,6 +253,7 @@ impl GitRepository for RealGitRepository { path, &content, remote_url, + self.hosting_provider_registry.clone(), ) } } diff --git a/crates/git_hosting_providers/Cargo.toml b/crates/git_hosting_providers/Cargo.toml new file mode 100644 index 0000000000..46ca632e86 --- /dev/null +++ b/crates/git_hosting_providers/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "git_hosting_providers" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/git_hosting_providers.rs" + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +futures.workspace = true +git.workspace = true +gpui.workspace = true +isahc.workspace = true +regex.workspace = true +serde.workspace = true +serde_json.workspace = true +url.workspace = true +util.workspace = true + +[dev-dependencies] +unindent.workspace = true +serde_json.workspace = true +pretty_assertions.workspace = true diff --git a/crates/git_hosting_providers/LICENSE-GPL b/crates/git_hosting_providers/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/git_hosting_providers/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/git_hosting_providers/src/git_hosting_providers.rs b/crates/git_hosting_providers/src/git_hosting_providers.rs new file mode 100644 index 0000000000..7bb77c0307 --- /dev/null +++ b/crates/git_hosting_providers/src/git_hosting_providers.rs @@ -0,0 +1,26 @@ +mod providers; + +use std::sync::Arc; + +use git::GitHostingProviderRegistry; +use gpui::AppContext; + +pub use crate::providers::*; + +/// Initializes the Git hosting providers. +pub fn init(cx: &mut AppContext) { + let provider_registry = GitHostingProviderRegistry::global(cx); + + // The providers are stored in a `BTreeMap`, so insertion order matters. + // GitHub comes first. + provider_registry.register_hosting_provider(Arc::new(Github)); + + // Then GitLab. + provider_registry.register_hosting_provider(Arc::new(Gitlab)); + + // Then the other providers, in the order they were added. + provider_registry.register_hosting_provider(Arc::new(Gitee)); + provider_registry.register_hosting_provider(Arc::new(Bitbucket)); + provider_registry.register_hosting_provider(Arc::new(Sourcehut)); + provider_registry.register_hosting_provider(Arc::new(Codeberg)); +} diff --git a/crates/git/src/hosting_providers.rs b/crates/git_hosting_providers/src/providers.rs similarity index 100% rename from crates/git/src/hosting_providers.rs rename to crates/git_hosting_providers/src/providers.rs diff --git a/crates/git/src/hosting_providers/bitbucket.rs b/crates/git_hosting_providers/src/providers/bitbucket.rs similarity index 84% rename from crates/git/src/hosting_providers/bitbucket.rs rename to crates/git_hosting_providers/src/providers/bitbucket.rs index e09dc52c25..50c453442f 100644 --- a/crates/git/src/hosting_providers/bitbucket.rs +++ b/crates/git_hosting_providers/src/providers/bitbucket.rs @@ -1,8 +1,6 @@ use url::Url; -use crate::{ - BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, -}; +use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote}; pub struct Bitbucket; @@ -77,14 +75,18 @@ impl GitHostingProvider for Bitbucket { #[cfg(test)] mod tests { - use crate::parse_git_remote_url; + use std::sync::Arc; + + use git::{parse_git_remote_url, GitHostingProviderRegistry}; use super::*; #[test] fn test_parse_git_remote_url_bitbucket_https_with_username() { + let provider_registry = Arc::new(GitHostingProviderRegistry::new()); + provider_registry.register_hosting_provider(Arc::new(Bitbucket)); let url = "https://thorstenballzed@bitbucket.org/thorstenzed/testingrepo.git"; - let (provider, parsed) = parse_git_remote_url(url).unwrap(); + let (provider, parsed) = parse_git_remote_url(provider_registry, url).unwrap(); assert_eq!(provider.name(), "Bitbucket"); assert_eq!(parsed.owner, "thorstenzed"); assert_eq!(parsed.repo, "testingrepo"); @@ -92,8 +94,10 @@ mod tests { #[test] fn test_parse_git_remote_url_bitbucket_https_without_username() { + let provider_registry = Arc::new(GitHostingProviderRegistry::new()); + provider_registry.register_hosting_provider(Arc::new(Bitbucket)); let url = "https://bitbucket.org/thorstenzed/testingrepo.git"; - let (provider, parsed) = parse_git_remote_url(url).unwrap(); + let (provider, parsed) = parse_git_remote_url(provider_registry, url).unwrap(); assert_eq!(provider.name(), "Bitbucket"); assert_eq!(parsed.owner, "thorstenzed"); assert_eq!(parsed.repo, "testingrepo"); @@ -101,8 +105,10 @@ mod tests { #[test] fn test_parse_git_remote_url_bitbucket_git() { + let provider_registry = Arc::new(GitHostingProviderRegistry::new()); + provider_registry.register_hosting_provider(Arc::new(Bitbucket)); let url = "git@bitbucket.org:thorstenzed/testingrepo.git"; - let (provider, parsed) = parse_git_remote_url(url).unwrap(); + let (provider, parsed) = parse_git_remote_url(provider_registry, url).unwrap(); assert_eq!(provider.name(), "Bitbucket"); assert_eq!(parsed.owner, "thorstenzed"); assert_eq!(parsed.repo, "testingrepo"); diff --git a/crates/git/src/hosting_providers/codeberg.rs b/crates/git_hosting_providers/src/providers/codeberg.rs similarity index 75% rename from crates/git/src/hosting_providers/codeberg.rs rename to crates/git_hosting_providers/src/providers/codeberg.rs index 01c133e3f8..abea1f8ed2 100644 --- a/crates/git/src/hosting_providers/codeberg.rs +++ b/crates/git_hosting_providers/src/providers/codeberg.rs @@ -1,17 +1,88 @@ use std::sync::Arc; -use anyhow::Result; +use anyhow::{bail, Context, Result}; use async_trait::async_trait; +use futures::AsyncReadExt; +use isahc::config::Configurable; +use isahc::{AsyncBody, Request}; +use serde::Deserialize; use url::Url; -use util::codeberg; use util::http::HttpClient; -use crate::{ +use git::{ BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, Oid, ParsedGitRemote, }; +#[derive(Debug, Deserialize)] +struct CommitDetails { + commit: Commit, + author: Option, +} + +#[derive(Debug, Deserialize)] +struct Commit { + author: Author, +} + +#[derive(Debug, Deserialize)] +struct Author { + name: String, + email: String, + date: String, +} + +#[derive(Debug, Deserialize)] +struct User { + pub login: String, + pub id: u64, + pub avatar_url: String, +} + pub struct Codeberg; +impl Codeberg { + async fn fetch_codeberg_commit_author( + &self, + repo_owner: &str, + repo: &str, + commit: &str, + client: &Arc, + ) -> Result> { + let url = + format!("https://codeberg.org/api/v1/repos/{repo_owner}/{repo}/git/commits/{commit}"); + + let mut request = Request::get(&url) + .redirect_policy(isahc::config::RedirectPolicy::Follow) + .header("Content-Type", "application/json"); + + if let Ok(codeberg_token) = std::env::var("CODEBERG_TOKEN") { + request = request.header("Authorization", format!("Bearer {}", codeberg_token)); + } + + let mut response = client + .send(request.body(AsyncBody::default())?) + .await + .with_context(|| format!("error fetching Codeberg commit details at {:?}", url))?; + + let mut body = Vec::new(); + response.body_mut().read_to_end(&mut body).await?; + + if response.status().is_client_error() { + let text = String::from_utf8_lossy(body.as_slice()); + bail!( + "status error {}, response: {text:?}", + response.status().as_u16() + ); + } + + let body_str = std::str::from_utf8(&body)?; + + serde_json::from_str::(body_str) + .map(|commit| commit.author) + .context("failed to deserialize Codeberg commit details") + } +} + #[async_trait] impl GitHostingProvider for Codeberg { fn name(&self) -> String { @@ -90,11 +161,11 @@ impl GitHostingProvider for Codeberg { http_client: Arc, ) -> Result> { let commit = commit.to_string(); - let avatar_url = - codeberg::fetch_codeberg_commit_author(repo_owner, repo, &commit, &http_client) - .await? - .map(|author| Url::parse(&author.avatar_url)) - .transpose()?; + let avatar_url = self + .fetch_codeberg_commit_author(repo_owner, repo, &commit, &http_client) + .await? + .map(|author| Url::parse(&author.avatar_url)) + .transpose()?; Ok(avatar_url) } } diff --git a/crates/git/src/hosting_providers/gitee.rs b/crates/git_hosting_providers/src/providers/gitee.rs similarity index 98% rename from crates/git/src/hosting_providers/gitee.rs rename to crates/git_hosting_providers/src/providers/gitee.rs index 76fb92e566..34d1da262d 100644 --- a/crates/git/src/hosting_providers/gitee.rs +++ b/crates/git_hosting_providers/src/providers/gitee.rs @@ -1,8 +1,6 @@ use url::Url; -use crate::{ - BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, -}; +use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote}; pub struct Gitee; diff --git a/crates/git/src/hosting_providers/github.rs b/crates/git_hosting_providers/src/providers/github.rs similarity index 79% rename from crates/git/src/hosting_providers/github.rs rename to crates/git_hosting_providers/src/providers/github.rs index c3219278d9..137dca69c1 100644 --- a/crates/git/src/hosting_providers/github.rs +++ b/crates/git_hosting_providers/src/providers/github.rs @@ -1,13 +1,16 @@ use std::sync::{Arc, OnceLock}; -use anyhow::Result; +use anyhow::{bail, Context, Result}; use async_trait::async_trait; +use futures::AsyncReadExt; +use isahc::config::Configurable; +use isahc::{AsyncBody, Request}; use regex::Regex; +use serde::Deserialize; use url::Url; -use util::github; use util::http::HttpClient; -use crate::{ +use git::{ BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, Oid, ParsedGitRemote, PullRequest, }; @@ -18,8 +21,72 @@ fn pull_request_number_regex() -> &'static Regex { PULL_REQUEST_NUMBER_REGEX.get_or_init(|| Regex::new(r"\(#(\d+)\)$").unwrap()) } +#[derive(Debug, Deserialize)] +struct CommitDetails { + commit: Commit, + author: Option, +} + +#[derive(Debug, Deserialize)] +struct Commit { + author: Author, +} + +#[derive(Debug, Deserialize)] +struct Author { + email: String, +} + +#[derive(Debug, Deserialize)] +struct User { + pub id: u64, + pub avatar_url: String, +} + pub struct Github; +impl Github { + async fn fetch_github_commit_author( + &self, + repo_owner: &str, + repo: &str, + commit: &str, + client: &Arc, + ) -> Result> { + let url = format!("https://api.github.com/repos/{repo_owner}/{repo}/commits/{commit}"); + + let mut request = Request::get(&url) + .redirect_policy(isahc::config::RedirectPolicy::Follow) + .header("Content-Type", "application/json"); + + if let Ok(github_token) = std::env::var("GITHUB_TOKEN") { + request = request.header("Authorization", format!("Bearer {}", github_token)); + } + + let mut response = client + .send(request.body(AsyncBody::default())?) + .await + .with_context(|| format!("error fetching GitHub commit details at {:?}", url))?; + + let mut body = Vec::new(); + response.body_mut().read_to_end(&mut body).await?; + + if response.status().is_client_error() { + let text = String::from_utf8_lossy(body.as_slice()); + bail!( + "status error {}, response: {text:?}", + response.status().as_u16() + ); + } + + let body_str = std::str::from_utf8(&body)?; + + serde_json::from_str::(body_str) + .map(|commit| commit.author) + .context("failed to deserialize GitHub commit details") + } +} + #[async_trait] impl GitHostingProvider for Github { fn name(&self) -> String { @@ -110,15 +177,15 @@ impl GitHostingProvider for Github { http_client: Arc, ) -> Result> { let commit = commit.to_string(); - let avatar_url = - github::fetch_github_commit_author(repo_owner, repo, &commit, &http_client) - .await? - .map(|author| -> Result { - let mut url = Url::parse(&author.avatar_url)?; - url.set_query(Some("size=128")); - Ok(url) - }) - .transpose()?; + let avatar_url = self + .fetch_github_commit_author(repo_owner, repo, &commit, &http_client) + .await? + .map(|author| -> Result { + let mut url = Url::parse(&author.avatar_url)?; + url.set_query(Some("size=128")); + Ok(url) + }) + .transpose()?; Ok(avatar_url) } } diff --git a/crates/git/src/hosting_providers/gitlab.rs b/crates/git_hosting_providers/src/providers/gitlab.rs similarity index 98% rename from crates/git/src/hosting_providers/gitlab.rs rename to crates/git_hosting_providers/src/providers/gitlab.rs index 8ca99f3a61..ccb8a7280a 100644 --- a/crates/git/src/hosting_providers/gitlab.rs +++ b/crates/git_hosting_providers/src/providers/gitlab.rs @@ -1,8 +1,6 @@ use url::Url; -use crate::{ - BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, -}; +use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote}; pub struct Gitlab; diff --git a/crates/git/src/hosting_providers/sourcehut.rs b/crates/git_hosting_providers/src/providers/sourcehut.rs similarity index 98% rename from crates/git/src/hosting_providers/sourcehut.rs rename to crates/git_hosting_providers/src/providers/sourcehut.rs index 93bd8fbf7a..623b23ab6c 100644 --- a/crates/git/src/hosting_providers/sourcehut.rs +++ b/crates/git_hosting_providers/src/providers/sourcehut.rs @@ -1,8 +1,6 @@ use url::Url; -use crate::{ - BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, -}; +use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote}; pub struct Sourcehut; diff --git a/crates/util/src/codeberg.rs b/crates/util/src/codeberg.rs deleted file mode 100644 index 67cf76aaa8..0000000000 --- a/crates/util/src/codeberg.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::{git_author::GitAuthor, http::HttpClient}; -use anyhow::{bail, Context, Result}; -use futures::AsyncReadExt; -use isahc::{config::Configurable, AsyncBody, Request}; -use serde::Deserialize; -use std::sync::Arc; - -#[derive(Debug, Deserialize)] -struct CommitDetails { - commit: Commit, - author: Option, -} - -#[derive(Debug, Deserialize)] -struct Commit { - author: Author, -} - -#[derive(Debug, Deserialize)] -struct Author { - name: String, - email: String, - date: String, -} - -#[derive(Debug, Deserialize)] -struct User { - pub login: String, - pub id: u64, - pub avatar_url: String, -} - -pub async fn fetch_codeberg_commit_author( - repo_owner: &str, - repo: &str, - commit: &str, - client: &Arc, -) -> Result> { - let url = format!("https://codeberg.org/api/v1/repos/{repo_owner}/{repo}/git/commits/{commit}"); - - let mut request = Request::get(&url) - .redirect_policy(isahc::config::RedirectPolicy::Follow) - .header("Content-Type", "application/json"); - - if let Ok(codeberg_token) = std::env::var("CODEBERG_TOKEN") { - request = request.header("Authorization", format!("Bearer {}", codeberg_token)); - } - - let mut response = client - .send(request.body(AsyncBody::default())?) - .await - .with_context(|| format!("error fetching Codeberg commit details at {:?}", url))?; - - let mut body = Vec::new(); - response.body_mut().read_to_end(&mut body).await?; - - if response.status().is_client_error() { - let text = String::from_utf8_lossy(body.as_slice()); - bail!( - "status error {}, response: {text:?}", - response.status().as_u16() - ); - } - - let body_str = std::str::from_utf8(&body)?; - - serde_json::from_str::(body_str) - .map(|codeberg_commit| { - if let Some(author) = codeberg_commit.author { - Some(GitAuthor { - avatar_url: author.avatar_url, - }) - } else { - None - } - }) - .context("deserializing Codeberg commit details failed") -} diff --git a/crates/util/src/git_author.rs b/crates/util/src/git_author.rs deleted file mode 100644 index b7172e6fcd..0000000000 --- a/crates/util/src/git_author.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// Represents the common denominator of most git hosting authors -#[derive(Debug)] -pub struct GitAuthor { - pub avatar_url: String, -} diff --git a/crates/util/src/github.rs b/crates/util/src/github.rs index f1ecce4732..f6d768d066 100644 --- a/crates/util/src/github.rs +++ b/crates/util/src/github.rs @@ -1,7 +1,6 @@ -use crate::{git_author::GitAuthor, http::HttpClient}; +use crate::http::HttpClient; use anyhow::{anyhow, bail, Context, Result}; use futures::AsyncReadExt; -use isahc::{config::Configurable, AsyncBody, Request}; use serde::Deserialize; use std::sync::Arc; use url::Url; @@ -27,75 +26,6 @@ pub struct GithubReleaseAsset { pub browser_download_url: String, } -#[derive(Debug, Deserialize)] -struct CommitDetails { - commit: Commit, - author: Option, -} - -#[derive(Debug, Deserialize)] -struct Commit { - author: Author, -} - -#[derive(Debug, Deserialize)] -struct Author { - email: String, -} - -#[derive(Debug, Deserialize)] -struct User { - pub id: u64, - pub avatar_url: String, -} - -pub async fn fetch_github_commit_author( - repo_owner: &str, - repo: &str, - commit: &str, - client: &Arc, -) -> Result> { - let url = format!("https://api.github.com/repos/{repo_owner}/{repo}/commits/{commit}"); - - let mut request = Request::get(&url) - .redirect_policy(isahc::config::RedirectPolicy::Follow) - .header("Content-Type", "application/json"); - - if let Ok(github_token) = std::env::var("GITHUB_TOKEN") { - request = request.header("Authorization", format!("Bearer {}", github_token)); - } - - let mut response = client - .send(request.body(AsyncBody::default())?) - .await - .with_context(|| format!("error fetching GitHub commit details at {:?}", url))?; - - let mut body = Vec::new(); - response.body_mut().read_to_end(&mut body).await?; - - if response.status().is_client_error() { - let text = String::from_utf8_lossy(body.as_slice()); - bail!( - "status error {}, response: {text:?}", - response.status().as_u16() - ); - } - - let body_str = std::str::from_utf8(&body)?; - - serde_json::from_str::(body_str) - .map(|github_commit| { - if let Some(author) = github_commit.author { - Some(GitAuthor { - avatar_url: author.avatar_url, - }) - } else { - None - } - }) - .context("deserializing GitHub commit details failed") -} - pub async fn latest_github_release( repo_name_with_owner: &str, require_assets: bool, diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 9ff5087e89..f3dbc0fc43 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -1,7 +1,5 @@ pub mod arc_cow; -pub mod codeberg; pub mod fs; -mod git_author; pub mod github; pub mod http; pub mod paths; diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index a8130fe5df..033dfc348f 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -46,6 +46,8 @@ file_icons.workspace = true file_finder.workspace = true fs.workspace = true futures.workspace = true +git.workspace = true +git_hosting_providers.workspace = true go_to_line.workspace = true gpui.workspace = true headless.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f03cd0d639..ba7d879a1d 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -16,6 +16,7 @@ use editor::Editor; use env_logger::Builder; use fs::RealFs; use futures::{future, StreamExt}; +use git::GitHostingProviderRegistry; use gpui::{App, AppContext, AsyncAppContext, Context, Task, VisualContext}; use image_viewer; use language::LanguageRegistry; @@ -119,6 +120,7 @@ fn init_headless(dev_server_token: DevServerToken) { project::Project::init(&client, cx); client::init(&client, cx); + let git_hosting_provider_registry = GitHostingProviderRegistry::default_global(cx); let git_binary_path = if option_env!("ZED_BUNDLE").as_deref() == Some("true") { cx.path_for_auxiliary_executable("git") .context("could not find git binary path") @@ -126,7 +128,9 @@ fn init_headless(dev_server_token: DevServerToken) { } else { None }; - let fs = Arc::new(RealFs::new(git_binary_path)); + let fs = Arc::new(RealFs::new(git_hosting_provider_registry, git_binary_path)); + + git_hosting_providers::init(cx); let mut languages = LanguageRegistry::new(Task::ready(()), cx.background_executor().clone()); @@ -186,6 +190,7 @@ fn init_ui(args: Args) { let session_id = Uuid::new_v4().to_string(); reliability::init_panic_hook(&app, installation_id.clone(), session_id.clone()); + let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new()); let git_binary_path = if option_env!("ZED_BUNDLE").as_deref() == Some("true") { app.path_for_auxiliary_executable("git") .context("could not find git binary path") @@ -195,7 +200,10 @@ fn init_ui(args: Args) { }; log::info!("Using git binary path: {:?}", git_binary_path); - let fs = Arc::new(RealFs::new(git_binary_path)); + let fs = Arc::new(RealFs::new( + git_hosting_provider_registry.clone(), + git_binary_path, + )); let user_settings_file_rx = watch_config_file( &app.background_executor(), fs.clone(), @@ -236,6 +244,9 @@ fn init_ui(args: Args) { AppCommitSha::set_global(AppCommitSha(build_sha.into()), cx); } + GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx); + git_hosting_providers::init(cx); + SystemAppearance::init(cx); OpenListener::set_global(listener.clone(), cx);