Add avatar support for codeberg in git blame (#10991)
Release Notes: - Added support for avatars in git blame for repositories hosted on codeberg <img width="1144" alt="Screenshot 2024-04-25 at 16 45 22" src="https://github.com/zed-industries/zed/assets/43210583/d44770d8-44ea-4c6b-a1c0-ac2d1d49408f"> Questions: - Should we move git stuff like `Commit`, `Author`, etc outside of hosting-specific files (I don't think so, as other hostings can have different stuff) - Should we also add support for self hosted forgejo instances or should it be a different PR?
This commit is contained in:
parent
8152e0676f
commit
f96cab286c
5 changed files with 107 additions and 28 deletions
|
@ -3,7 +3,7 @@ use std::{ops::Range, sync::Arc};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use util::{github, http::HttpClient};
|
use util::{codeberg, github, http::HttpClient};
|
||||||
|
|
||||||
use crate::Oid;
|
use crate::Oid;
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ impl HostingProvider {
|
||||||
|
|
||||||
pub fn supports_avatars(&self) -> bool {
|
pub fn supports_avatars(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
HostingProvider::Github => true,
|
HostingProvider::Github | HostingProvider::Codeberg => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,24 +71,27 @@ impl HostingProvider {
|
||||||
commit: Oid,
|
commit: Oid,
|
||||||
client: Arc<dyn HttpClient>,
|
client: Arc<dyn HttpClient>,
|
||||||
) -> Result<Option<Url>> {
|
) -> Result<Option<Url>> {
|
||||||
match self {
|
Ok(match self {
|
||||||
HostingProvider::Github => {
|
HostingProvider::Github => {
|
||||||
let commit = commit.to_string();
|
let commit = commit.to_string();
|
||||||
|
github::fetch_github_commit_author(repo_owner, repo, &commit, &client)
|
||||||
let author =
|
.await?
|
||||||
github::fetch_github_commit_author(repo_owner, repo, &commit, &client).await?;
|
.map(|author| -> Result<Url, url::ParseError> {
|
||||||
|
let mut url = Url::parse(&author.avatar_url)?;
|
||||||
let url = if let Some(author) = author {
|
url.set_query(Some("size=128"));
|
||||||
let mut url = Url::parse(&author.avatar_url)?;
|
Ok(url)
|
||||||
url.set_query(Some("size=128"));
|
})
|
||||||
Some(url)
|
.transpose()
|
||||||
} else {
|
}
|
||||||
None
|
HostingProvider::Codeberg => {
|
||||||
};
|
let commit = commit.to_string();
|
||||||
Ok(url)
|
codeberg::fetch_codeberg_commit_author(repo_owner, repo, &commit, &client)
|
||||||
|
.await?
|
||||||
|
.map(|author| Url::parse(&author.avatar_url))
|
||||||
|
.transpose()
|
||||||
}
|
}
|
||||||
_ => Ok(None),
|
_ => Ok(None),
|
||||||
}
|
}?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
78
crates/util/src/codeberg.rs
Normal file
78
crates/util/src/codeberg.rs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
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")
|
||||||
|
}
|
5
crates/util/src/git_author.rs
Normal file
5
crates/util/src/git_author.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/// Represents the common denominator of most git hosting authors
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct GitAuthor {
|
||||||
|
pub avatar_url: String,
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::http::HttpClient;
|
use crate::{git_author::GitAuthor, http::HttpClient};
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use futures::AsyncReadExt;
|
use futures::AsyncReadExt;
|
||||||
use isahc::{config::Configurable, AsyncBody, Request};
|
use isahc::{config::Configurable, AsyncBody, Request};
|
||||||
|
@ -49,19 +49,12 @@ struct User {
|
||||||
pub avatar_url: String,
|
pub avatar_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct GitHubAuthor {
|
|
||||||
pub id: u64,
|
|
||||||
pub email: String,
|
|
||||||
pub avatar_url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_github_commit_author(
|
pub async fn fetch_github_commit_author(
|
||||||
repo_owner: &str,
|
repo_owner: &str,
|
||||||
repo: &str,
|
repo: &str,
|
||||||
commit: &str,
|
commit: &str,
|
||||||
client: &Arc<dyn HttpClient>,
|
client: &Arc<dyn HttpClient>,
|
||||||
) -> Result<Option<GitHubAuthor>> {
|
) -> Result<Option<GitAuthor>> {
|
||||||
let url = format!("https://api.github.com/repos/{repo_owner}/{repo}/commits/{commit}");
|
let url = format!("https://api.github.com/repos/{repo_owner}/{repo}/commits/{commit}");
|
||||||
|
|
||||||
let mut request = Request::get(&url)
|
let mut request = Request::get(&url)
|
||||||
|
@ -93,10 +86,8 @@ pub async fn fetch_github_commit_author(
|
||||||
serde_json::from_str::<CommitDetails>(body_str)
|
serde_json::from_str::<CommitDetails>(body_str)
|
||||||
.map(|github_commit| {
|
.map(|github_commit| {
|
||||||
if let Some(author) = github_commit.author {
|
if let Some(author) = github_commit.author {
|
||||||
Some(GitHubAuthor {
|
Some(GitAuthor {
|
||||||
id: author.id,
|
|
||||||
avatar_url: author.avatar_url,
|
avatar_url: author.avatar_url,
|
||||||
email: github_commit.commit.author.email,
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
pub mod arc_cow;
|
pub mod arc_cow;
|
||||||
|
pub mod codeberg;
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
|
mod git_author;
|
||||||
pub mod github;
|
pub mod github;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
pub mod paths;
|
pub mod paths;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue