//! The repository list and one repository's summary.
use anyhow::Result;
use crate::api::models::Repository;
use crate::cli::{RepoArgs, RepoListArgs, RepoSort};
use crate::commands::Ctx;
use crate::output::{
Table, Tone, ago, ci_rank, heading, note, paint, status_badge, status_cell, truncate,
};
pub async fn list(ctx: &Ctx, args: &RepoListArgs) -> Result<()> {
let res = ctx.api.repos().await?;
if ctx.emitted(&res.raw)? {
return Ok(());
}
let needle = args.search.as_deref().map(str::to_lowercase);
let mut repos: Vec<&Repository> = res
.repositories
.iter()
.filter(|r| args.archived || !r.archived)
.filter(|r| match &needle {
None => true,
Some(needle) => {
r.slug().to_lowercase().contains(needle)
|| r.description
.as_deref()
.is_some_and(|d| d.to_lowercase().contains(needle))
}
})
.collect();
// Sorting happens here because `/repos` returns everything the token
// can reach in one response — there's no pagination to respect, so
// re-ordering costs no round-trip.
sort(&mut repos, args.sort);
if repos.is_empty() {
note("No repositories.");
return Ok(());
}
let mut table = Table::new(["", "REPOSITORY", "CI", "PUSHED", "DESCRIPTION"]);
for repo in &repos {
let visibility = match repo.visibility.as_deref() {
Some("private") => paint("•", Tone::Busy),
_ => " ".to_string(),
};
let ci = match repo.ci_status.as_deref() {
Some(status) => status_cell(status, None),
None => paint("—", Tone::Muted),
};
table.row([
visibility,
repo.slug(),
ci,
ago(repo.pushed_at.as_deref()),
truncate(repo.description.as_deref().unwrap_or(""), 48),
]);
}
table.print();
let orgs = res.namespaces.iter().filter(|n| n.kind == "org").count();
if orgs > 0 {
note(&format!(
"\n{} repositories across {} namespace(s), {orgs} of them organizations.",
repos.len(),
res.namespaces.len()
));
}
Ok(())
}
fn sort(repos: &mut [&Repository], order: RepoSort) {
match order {
RepoSort::Name => repos.sort_by_key(|r| r.slug().to_lowercase()),
// Newest push first, never-pushed repos last: an empty repo has no
// work on it to be recent about. Timestamps compare as strings —
// they're ISO-8601 UTC from one server with one formatter, so
// lexicographic order *is* chronological order and no parse can
// fail halfway through a sort.
RepoSort::Pushed => repos.sort_by(|a, b| {
by_pushed(a, b).then_with(|| a.slug().to_lowercase().cmp(&b.slug().to_lowercase()))
}),
RepoSort::Ci => repos.sort_by(|a, b| {
ci_rank(a.ci_status.as_deref())
.cmp(&ci_rank(b.ci_status.as_deref()))
.then_with(|| by_pushed(a, b))
.then_with(|| a.slug().to_lowercase().cmp(&b.slug().to_lowercase()))
}),
}
}
fn by_pushed(a: &Repository, b: &Repository) -> std::cmp::Ordering {
match (a.pushed_at.as_deref(), b.pushed_at.as_deref()) {
(Some(x), Some(y)) => y.cmp(x),
(None, Some(_)) => std::cmp::Ordering::Greater,
(Some(_), None) => std::cmp::Ordering::Less,
(None, None) => std::cmp::Ordering::Equal,
}
}
pub async fn show(ctx: &Ctx, args: &RepoArgs) -> Result<()> {
let repo = match &args.target {
Some(raw) => crate::api::RepoRef::parse(raw)?,
None => ctx.repo()?,
};
let res = ctx.api.repo(&repo).await?;
if ctx.emitted(&res.raw)? {
return Ok(());
}
let r = &res.repository;
heading(&r.slug());
if let Some(description) = r.description.as_deref().filter(|d| !d.is_empty()) {
println!("{description}");
}
println!();
let visibility = r.visibility.as_deref().unwrap_or("unknown");
let archived = if r.archived { ", archived" } else { "" };
println!("visibility {visibility}{archived}");
println!(
"branch {}",
r.default_branch.as_deref().unwrap_or("—")
);
println!("your access {}", r.permission.as_deref().unwrap_or("read"));
println!("pushed {}", ago(r.pushed_at.as_deref()));
println!(
"open {} issues, {} pulls, {} wiki pages",
res.counts.open_issues, res.counts.open_pulls, res.counts.wiki_pages
);
match &res.latest_run {
Some(run) => println!(
"latest run #{} {} {} on {}",
run.number,
status_badge(run.display_status()),
run.workflow_label(),
run.short_ref().unwrap_or("—")
),
None => println!("latest run {}", paint("none yet", Tone::Muted)),
}
let p = &res.permissions;
let mut may: Vec<&str> = Vec::new();
if p.ci {
may.push("manage CI");
}
if p.issues {
may.push("write issues");
}
if p.pulls {
may.push("write pulls");
}
if p.merge {
may.push("merge");
}
println!(
"you may {}",
if may.is_empty() {
"read".to_string()
} else {
may.join(", ")
}
);
if let Some(url) = &r.html_url {
note(&format!("\n{url}"));
} else {
note(&format!("\n{}", ctx.repo_web_url(&repo, "")));
}
Ok(())
}
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
5.5 KiB · text
History
d1406da