use async_trait::async_trait;
use gix::bstr::ByteSlice;
use crate::payload::{Body, Payload, TextData};
use crate::render::Shape;
use super::super::github::common::slug_from_url;
use super::super::{FetchContext, FetchError, Fetcher, Safety};
use super::{payload, repo_cache_key, text_body};
pub struct GitRepoName;
#[async_trait]
impl Fetcher for GitRepoName {
fn name(&self) -> &str {
"git_repo_name"
}
fn safety(&self) -> Safety {
Safety::Safe
}
fn description(&self) -> &'static str {
"Repository name as a single line, suitable for a hero header. Derived from the `origin` remote (parsed as `owner/name`); falls back to the working directory's basename when no remote is configured."
}
fn refresh_interval(&self) -> u64 {
60 * 60
}
fn shapes(&self) -> &[Shape] {
&[Shape::Text]
}
fn cache_key(&self, ctx: &FetchContext) -> String {
repo_cache_key(self.name(), ctx)
}
fn sample_body(&self, _shape: Shape) -> Option<Body> {
Some(Body::Text(TextData {
value: "splashboard".into(),
}))
}
async fn fetch(&self, _ctx: &FetchContext) -> Result<Payload, FetchError> {
Ok(payload(text_body(
detect_name().unwrap_or_else(|| "project".into()),
)))
}
}
fn detect_name() -> Option<String> {
let cwd = std::env::current_dir().ok()?;
remote_name(&cwd).or_else(|| cwd.file_name().map(|s| s.to_string_lossy().into_owned()))
}
fn remote_name(cwd: &std::path::Path) -> Option<String> {
let repo = gix::discover(cwd).ok()?;
let names: Vec<_> = repo.remote_names().into_iter().collect();
let preferred = names
.iter()
.find(|n| n.as_bstr() == "origin")
.or_else(|| names.first())?;
let remote = repo.find_remote(preferred).ok()?;
let url = remote.url(gix::remote::Direction::Fetch)?;
slug_from_url(&url.to_bstring().to_string()).map(|s| s.name)
}
#[cfg(test)]
mod tests {
use std::future::Future;
use std::path::Path;
use std::process::Command;
use std::time::Duration;
use super::*;
use crate::fetcher::git::test_support::make_repo;
fn run_git(dir: &Path, args: &[&str]) {
let output = Command::new("git")
.args(args)
.current_dir(dir)
.output()
.unwrap();
assert!(
output.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn ctx() -> FetchContext {
FetchContext {
widget_id: "repo-name".into(),
format: None,
timeout: Duration::from_secs(1),
file_format: None,
shape: Some(Shape::Text),
options: None,
timezone: None,
locale: None,
}
}
fn run_async<T>(future: impl Future<Output = T>) -> T {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(future)
}
#[test]
fn sample_is_non_empty() {
let sample = GitRepoName.sample_body(Shape::Text).unwrap();
match sample {
Body::Text(d) => assert!(!d.value.is_empty()),
_ => panic!("expected text sample"),
}
}
#[test]
fn fetcher_metadata_matches_repo_name_contract() {
let fetcher = GitRepoName;
assert_eq!(fetcher.name(), "git_repo_name");
assert_eq!(fetcher.safety(), Safety::Safe);
assert!(fetcher.description().contains("hero header"));
assert_eq!(fetcher.shapes(), &[Shape::Text]);
assert_eq!(fetcher.default_shape(), Shape::Text);
assert!(fetcher.cache_key(&ctx()).starts_with("git_repo_name-"));
}
#[test]
fn remote_name_prefers_origin_when_multiple_remotes_exist() {
let (_tmp, repo) = make_repo();
let workdir = repo.workdir().unwrap();
run_git(
workdir,
&[
"remote",
"add",
"upstream",
"https://github.com/acme/first.git",
],
);
run_git(
workdir,
&[
"remote",
"add",
"origin",
"git@github.com:acme/preferred.git",
],
);
assert_eq!(remote_name(workdir), Some("preferred".into()));
}
#[test]
fn remote_name_uses_first_remote_when_origin_is_missing() {
let (_tmp, repo) = make_repo();
let workdir = repo.workdir().unwrap();
run_git(
workdir,
&[
"remote",
"add",
"upstream",
"ssh://git@github.com/acme/fallback.git",
],
);
assert_eq!(remote_name(workdir), Some("fallback".into()));
}
#[test]
fn remote_name_returns_none_for_non_git_repos_and_non_github_remotes() {
let tmp = tempfile::tempdir().unwrap();
assert!(remote_name(tmp.path()).is_none());
let (_repo_tmp, repo) = make_repo();
let workdir = repo.workdir().unwrap();
run_git(
workdir,
&[
"remote",
"add",
"origin",
"https://gitlab.com/acme/project.git",
],
);
assert!(remote_name(workdir).is_none());
}
#[test]
fn detect_name_picks_up_origin_remote_from_cwd_repo() {
let _lock = crate::paths::TEST_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let (_tmp, repo) = make_repo();
let workdir = repo.workdir().unwrap().to_path_buf();
run_git(
&workdir,
&[
"remote",
"add",
"origin",
"https://github.com/acme/isolated.git",
],
);
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&workdir).unwrap();
let detected = detect_name();
std::env::set_current_dir(prev).unwrap();
assert_eq!(detected, Some("isolated".into()));
}
#[cfg(unix)]
#[test]
fn fetch_falls_back_to_project_when_cwd_has_no_basename() {
let _lock = crate::paths::TEST_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir("/").unwrap();
let payload = run_async(GitRepoName.fetch(&ctx()));
std::env::set_current_dir(prev).unwrap();
let Body::Text(text) = payload.unwrap().body else {
panic!("expected text payload");
};
assert_eq!(text.value, "project");
}
#[test]
fn fetch_returns_repo_name_from_cwd_repo() {
let _lock = crate::paths::TEST_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let (_tmp, repo) = make_repo();
let workdir = repo.workdir().unwrap().to_path_buf();
run_git(
&workdir,
&[
"remote",
"add",
"origin",
"git@github.com:acme/isolated.git",
],
);
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(&workdir).unwrap();
let payload = run_async(GitRepoName.fetch(&ctx()));
std::env::set_current_dir(prev).unwrap();
let Body::Text(text) = payload.unwrap().body else {
panic!("expected text payload");
};
assert_eq!(text.value, "isolated");
}
}