Detailed changes
@@ -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",
@@ -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" }
@@ -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"] }
@@ -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));
@@ -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::<Point>(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,
@@ -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<Item = (Oid, String)>,
remote_url: Option<String>,
deprecated_permalinks: &HashMap<Oid, Url>,
+ provider_registry: Arc<GitHostingProviderRegistry>,
languages: &Arc<LanguageRegistry>,
) -> HashMap<Oid, CommitDetails> {
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;
@@ -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<GitHostingProviderRegistry>,
git_binary_path: Option<PathBuf>,
}
impl RealFs {
- pub fn new(git_binary_path: Option<PathBuf>) -> Self {
- Self { git_binary_path }
+ pub fn new(
+ git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
+ git_binary_path: Option<PathBuf>,
+ ) -> 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(),
)))
})
}
@@ -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]
@@ -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<String>,
+ provider_registry: Arc<GitHostingProviderRegistry>,
) -> Result<Self> {
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);
@@ -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;
@@ -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<GitHostingProviderRegistry>);
+
+impl Global for GlobalGitHostingProviderRegistry {}
+
+#[derive(Default)]
+struct GitHostingProviderRegistryState {
+ providers: BTreeMap<String, Arc<dyn GitHostingProvider + Send + Sync + 'static>>,
+}
+
+#[derive(Default)]
+pub struct GitHostingProviderRegistry {
+ state: RwLock<GitHostingProviderRegistryState>,
+}
+
+impl GitHostingProviderRegistry {
+ /// Returns the global [`GitHostingProviderRegistry`].
+ pub fn global(cx: &AppContext) -> Arc<Self> {
+ cx.global::<GlobalGitHostingProviderRegistry>().0.clone()
+ }
+
+ /// Returns the global [`GitHostingProviderRegistry`].
+ ///
+ /// Inserts a default [`GitHostingProviderRegistry`] if one does not yet exist.
+ pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
+ cx.default_global::<GlobalGitHostingProviderRegistry>()
+ .0
+ .clone()
+ }
+
+ /// Sets the global [`GitHostingProviderRegistry`].
+ pub fn set_global(registry: Arc<GitHostingProviderRegistry>, 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<Arc<dyn GitHostingProvider + Send + Sync + 'static>> {
+ self.state.read().providers.values().cloned().collect()
+ }
+
+ /// Adds the provided [`GitHostingProvider`] to the registry.
+ pub fn register_hosting_provider(
+ &self,
+ provider: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
+ ) {
+ 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<GitHostingProviderRegistry>,
url: &str,
) -> Option<(
Arc<dyn GitHostingProvider + Send + Sync + 'static>,
ParsedGitRemote,
)> {
- let providers: Vec<Arc<dyn GitHostingProvider + Send + Sync + 'static>> = 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))
+ })
}
@@ -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<GitHostingProviderRegistry>,
}
impl RealGitRepository {
- pub fn new(repository: LibGitRepository, git_binary_path: Option<PathBuf>) -> Self {
+ pub fn new(
+ repository: LibGitRepository,
+ git_binary_path: Option<PathBuf>,
+ hosting_provider_registry: Arc<GitHostingProviderRegistry>,
+ ) -> 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(),
)
}
}
@@ -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
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -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));
+}
@@ -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");
@@ -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<User>,
+}
+
+#[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<dyn HttpClient>,
+ ) -> Result<Option<User>> {
+ 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::<CommitDetails>(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<dyn HttpClient>,
) -> Result<Option<Url>> {
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)
}
}
@@ -1,8 +1,6 @@
use url::Url;
-use crate::{
- BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
-};
+use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote};
pub struct Gitee;
@@ -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<User>,
+}
+
+#[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<dyn HttpClient>,
+ ) -> Result<Option<User>> {
+ 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::<CommitDetails>(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<dyn HttpClient>,
) -> Result<Option<Url>> {
let commit = commit.to_string();
- let avatar_url =
- github::fetch_github_commit_author(repo_owner, repo, &commit, &http_client)
- .await?
- .map(|author| -> Result<Url, url::ParseError> {
- 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<Url, url::ParseError> {
+ let mut url = Url::parse(&author.avatar_url)?;
+ url.set_query(Some("size=128"));
+ Ok(url)
+ })
+ .transpose()?;
Ok(avatar_url)
}
}
@@ -1,8 +1,6 @@
use url::Url;
-use crate::{
- BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
-};
+use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote};
pub struct Gitlab;
@@ -1,8 +1,6 @@
use url::Url;
-use crate::{
- BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
-};
+use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote};
pub struct Sourcehut;
@@ -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<User>,
-}
-
-#[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<dyn HttpClient>,
-) -> Result<Option<GitAuthor>> {
- 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::<CommitDetails>(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")
-}
@@ -1,5 +0,0 @@
-/// Represents the common denominator of most git hosting authors
-#[derive(Debug)]
-pub struct GitAuthor {
- pub avatar_url: String,
-}
@@ -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<User>,
-}
-
-#[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<dyn HttpClient>,
-) -> Result<Option<GitAuthor>> {
- 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::<CommitDetails>(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,
@@ -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;
@@ -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
@@ -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);