feat: show local branch count in the Repos table

A repo that keeps several branches (e.g. one theme per branch) only ever
showed its checked-out branch, hiding that the others exist. The Repos
table now renders the active branch plus `+N` for the other local
branches (cyan when >1), the BRANCH column widened to fit, and
`vulp status` prints "(+N more local)". The full list stays in the
detail Branches tab; RepoSummary.branch_count is in --json too.
This commit is contained in:
dualfroz committed 2026-07-08 16:21:22 +02:00
1 parent 212c55d138
commit 3426a28c48
3 files changed
+19 -4

No files matched your search

+2 -1
View File
@@ -92,7 +92,8 @@ pub fn status(cfg: &AppConfig, json: bool, repo: &str) -> Result<()> {
} }
println!("name: {}", r.name); println!("name: {}", r.name);
println!("path: {}", r.path.display()); println!("path: {}", r.path.display());
println!("branch: {}", r.branch); println!("branch: {}{}", r.branch,
if r.branch_count > 1 { format!(" (+{} more local)", r.branch_count - 1) } else { String::new() });
println!("dirty: {}", r.is_dirty); println!("dirty: {}", r.is_dirty);
println!("ahead: {}", r.ahead); println!("ahead: {}", r.ahead);
println!("behind: {}", r.behind); println!("behind: {}", r.behind);
+10 -2
View File
@@ -78,6 +78,14 @@ pub fn render_repos_table(f: &mut Frame, state: &HubState, area: Rect) {
}; };
let stars_str = r.stars.map(|s| format!("{}", s)).unwrap_or_else(|| "—".into()); let stars_str = r.stars.map(|s| format!("{}", s)).unwrap_or_else(|| "—".into());
let stars_col = if r.stars.is_some() { Color::Yellow } else { Color::DarkGray }; let stars_col = if r.stars.is_some() { Color::Yellow } else { Color::DarkGray };
// Active branch, plus `+N` when the repo keeps other local
// branches (e.g. a theme per branch) — so the count is visible
// without opening the Branches tab.
let branch_cell = if r.branch_count > 1 {
format!("{} +{}", r.branch.chars().take(9).collect::<String>(), r.branch_count - 1)
} else {
r.branch.chars().take(13).collect::<String>()
};
let is_pinned = state.pins.contains(&r.path); let is_pinned = state.pins.contains(&r.path);
let prefix = match (is_pinned, r.bookmarked) { let prefix = match (is_pinned, r.bookmarked) {
(true, true) => "◆★", (true, true) => "◆★",
@@ -89,7 +97,7 @@ pub fn render_repos_table(f: &mut Frame, state: &HubState, area: Rect) {
Some(Row::new(vec![ Some(Row::new(vec![
Cell::from(format!("{} {}", if is_cur { "▶" } else { " " }, if is_sel { "[✓]" } else { "[ ]" })).style(check_sty), Cell::from(format!("{} {}", if is_cur { "▶" } else { " " }, if is_sel { "[✓]" } else { "[ ]" })).style(check_sty),
Cell::from(format!("{}{}{}", prefix, if is_external { "⧉ " } else { "" }, r.name)).style(Style::default().fg(name_col)), Cell::from(format!("{}{}{}", prefix, if is_external { "⧉ " } else { "" }, r.name)).style(Style::default().fg(name_col)),
Cell::from(r.branch.chars().take(9).collect::<String>()), Cell::from(branch_cell).style(Style::default().fg(if r.branch_count > 1 { Color::Cyan } else { Color::Reset })),
Cell::from(proj_name.chars().take(21).collect::<String>()).style(Style::default().fg(proj_color)), Cell::from(proj_name.chars().take(21).collect::<String>()).style(Style::default().fg(proj_color)),
Cell::from(vis).style(match vis { "public" => Style::default().fg(Color::Green), "private" => Style::default().fg(Color::Yellow), _ => Style::default().fg(Color::DarkGray) }), Cell::from(vis).style(match vis { "public" => Style::default().fg(Color::Green), "private" => Style::default().fg(Color::Yellow), _ => Style::default().fg(Color::DarkGray) }),
Cell::from(r.last_commit_age.clone()).style(Style::default().fg(age_color(r.last_commit_ts))), Cell::from(r.last_commit_age.clone()).style(Style::default().fg(age_color(r.last_commit_ts))),
@@ -107,7 +115,7 @@ pub fn render_repos_table(f: &mut Frame, state: &HubState, area: Rect) {
let widths = [ let widths = [
Constraint::Length(6), Constraint::Length(6),
Constraint::Fill(1), Constraint::Fill(1),
Constraint::Length(10), Constraint::Length(14),
Constraint::Length(22), Constraint::Length(22),
Constraint::Length(9), Constraint::Length(9),
Constraint::Length(9), Constraint::Length(9),
+7 -1
View File
@@ -28,6 +28,10 @@ pub struct RepoSummary {
pub last_commit_ts: i64, pub last_commit_ts: i64,
pub last_tag: Option<String>, pub last_tag: Option<String>,
pub visibility: Option<String>, pub visibility: Option<String>,
/// Number of local branches. Surfaced in the Repos table as `+N` next to
/// the active branch so a repo that keeps several branches (e.g. one theme
/// per branch) shows its branch count at a glance, not just the checkout.
pub branch_count: usize,
pub owner: Option<String>, // parsed from git remote URL pub owner: Option<String>, // parsed from git remote URL
/// The provider-side repo name parsed from the git remote URL — not /// The provider-side repo name parsed from the git remote URL — not
/// necessarily the local folder name (see `parse_repo_name_from_url`). /// necessarily the local folder name (see `parse_repo_name_from_url`).
@@ -404,6 +408,8 @@ pub fn repo_summary(path: &Path) -> Result<RepoSummary> {
let owner = extract_owner(&repo); let owner = extract_owner(&repo);
let remote_repo_name = extract_remote_repo_name(&repo); let remote_repo_name = extract_remote_repo_name(&repo);
let branch_count = repo.branches(Some(git2::BranchType::Local))
.map(|bs| bs.count()).unwrap_or(0);
let has_license = detect_license(path); let has_license = detect_license(path);
let t_lang = std::time::Instant::now(); let t_lang = std::time::Instant::now();
@@ -431,7 +437,7 @@ pub fn repo_summary(path: &Path) -> Result<RepoSummary> {
Ok(RepoSummary { Ok(RepoSummary {
name, path: path.to_path_buf(), branch, ahead, behind, is_dirty, name, path: path.to_path_buf(), branch, ahead, behind, is_dirty,
last_commit_msg: msg, last_commit_age: age, last_commit_ts: ts, last_commit_msg: msg, last_commit_age: age, last_commit_ts: ts,
last_tag, visibility: None, owner, remote_repo_name, has_license, lang, bookmarked: false, last_tag, visibility: None, branch_count, owner, remote_repo_name, has_license, lang, bookmarked: false,
stars: None, open_issues: None, open_prs: None, stars: None, open_issues: None, open_prs: None,
stash_count, stash_count,
ci_status: None, dependabot_open: None, deploy_status: None, ci_status: None, dependabot_open: None, deploy_status: None,