Add a registry for GitHostingProviders (#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
This commit is contained in:
Marshall Bowers 2024-05-06 21:24:48 -04:00 committed by GitHub
parent a64e20ed96
commit 88c4e0b2d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 405 additions and 229 deletions

25
Cargo.lock generated
View File

@ -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",

View File

@ -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" }

View File

@ -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"] }

View File

@ -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));

View File

@ -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,

View File

@ -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;

View File

@ -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(),
)))
})
}

View File

@ -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]

View File

@ -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);

View File

@ -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;

View File

@ -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))
})
}

View File

@ -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(),
)
}
}

View File

@ -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

View File

@ -0,0 +1 @@
../../LICENSE-GPL

View File

@ -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));
}

View File

@ -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");

View File

@ -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)
}
}

View File

@ -1,8 +1,6 @@
use url::Url;
use crate::{
BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
};
use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote};
pub struct Gitee;

View File

@ -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)
}
}

View File

@ -1,8 +1,6 @@
use url::Url;
use crate::{
BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
};
use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote};
pub struct Gitlab;

View File

@ -1,8 +1,6 @@
use url::Url;
use crate::{
BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
};
use git::{BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote};
pub struct Sourcehut;

View File

@ -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")
}

View File

@ -1,5 +0,0 @@
/// Represents the common denominator of most git hosting authors
#[derive(Debug)]
pub struct GitAuthor {
pub avatar_url: String,
}

View File

@ -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,

View File

@ -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;

View File

@ -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

View File

@ -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);