use std::collections::{HashMap, HashSet};
use super::{BranchCategory, CompletionBranch, LocalBranch, RemoteBranch, Repository};
#[derive(Debug, Default)]
pub(in crate::git) struct LocalBranchInventory {
entries: Vec<LocalBranch>,
by_name: HashMap<String, usize>,
}
impl LocalBranchInventory {
fn new(entries: Vec<LocalBranch>) -> Self {
let by_name = entries
.iter()
.enumerate()
.map(|(i, b)| (b.name.clone(), i))
.collect();
Self { entries, by_name }
}
fn entries(&self) -> &[LocalBranch] {
&self.entries
}
fn get(&self, name: &str) -> Option<&LocalBranch> {
self.by_name.get(name).map(|&i| &self.entries[i])
}
}
const FIELD_SEP: char = '\0';
const LOCAL_BRANCH_FORMAT: &str = "--format=%(refname:lstrip=2)%00%(objectname)%00%(committerdate:unix)%00%(upstream:short)%00%(upstream:track)";
const REMOTE_BRANCH_FORMAT: &str =
"--format=%(refname:lstrip=2)%00%(objectname)%00%(committerdate:unix)";
impl Repository {
pub fn ref_exists(&self, reference: &str) -> anyhow::Result<bool> {
Ok(self
.run_command(&[
"rev-parse",
"--verify",
&format!("{}^{{commit}}", reference),
])
.is_ok())
}
pub fn local_branches(&self) -> anyhow::Result<&[LocalBranch]> {
Ok(self.local_branch_inventory()?.entries())
}
pub(super) fn local_branch(&self, name: &str) -> anyhow::Result<Option<&LocalBranch>> {
Ok(self.local_branch_inventory()?.get(name))
}
fn local_branch_inventory(&self) -> anyhow::Result<&LocalBranchInventory> {
self.cache
.local_branches
.get_or_try_init(|| self.scan_local_branches())
}
pub fn remote_branches(&self) -> anyhow::Result<&[RemoteBranch]> {
self.cache
.remote_branches
.get_or_try_init(|| self.scan_remote_branches())
.map(Vec::as_slice)
}
fn scan_local_branches(&self) -> anyhow::Result<LocalBranchInventory> {
let output = self.run_command(&["for-each-ref", LOCAL_BRANCH_FORMAT, "refs/heads/"])?;
let mut branches: Vec<LocalBranch> =
output.lines().filter_map(parse_local_branch_line).collect();
branches.sort_by_key(|b| std::cmp::Reverse(b.committer_ts));
for branch in &branches {
let qualified = format!("refs/heads/{}", branch.name);
self.cache
.resolved_refs
.insert(branch.name.clone(), qualified.clone());
self.cache
.commit_shas
.insert(qualified, branch.commit_sha.clone());
self.cache
.commit_shas
.insert(branch.name.clone(), branch.commit_sha.clone());
}
Ok(LocalBranchInventory::new(branches))
}
fn scan_remote_branches(&self) -> anyhow::Result<Vec<RemoteBranch>> {
let output = self.run_command(&["for-each-ref", REMOTE_BRANCH_FORMAT, "refs/remotes/"])?;
let mut branches: Vec<RemoteBranch> = output
.lines()
.filter_map(parse_remote_branch_line)
.collect();
branches.sort_by_key(|b| std::cmp::Reverse(b.committer_ts));
Ok(branches)
}
pub fn all_branches(&self) -> anyhow::Result<Vec<String>> {
Ok(self
.local_branches()?
.iter()
.map(|b| b.name.clone())
.collect())
}
pub fn available_branches(&self) -> anyhow::Result<Vec<String>> {
let worktrees = self.list_worktrees()?;
let branches_with_worktrees: HashSet<String> = worktrees
.iter()
.filter_map(|wt| wt.branch.clone())
.collect();
Ok(self
.local_branches()?
.iter()
.filter(|b| !branches_with_worktrees.contains(&b.name))
.map(|b| b.name.clone())
.collect())
}
pub fn branches_for_completion(&self) -> anyhow::Result<Vec<CompletionBranch>> {
let worktrees = self.list_worktrees()?;
let worktree_branches: HashSet<String> = worktrees
.iter()
.filter_map(|wt| wt.branch.clone())
.collect();
let locals = self.local_branches()?;
let local_names: HashSet<&str> = locals.iter().map(|b| b.name.as_str()).collect();
let mut branch_remotes: HashMap<String, (Vec<String>, i64)> = HashMap::new();
for remote in self.remote_branches()? {
if local_names.contains(remote.local_name.as_str()) {
continue;
}
branch_remotes
.entry(remote.local_name.clone())
.and_modify(|(remotes, ts)| {
remotes.push(remote.remote_name.clone());
*ts = (*ts).max(remote.committer_ts);
})
.or_insert_with(|| (vec![remote.remote_name.clone()], remote.committer_ts));
}
let mut remote_only: Vec<(String, Vec<String>, i64)> = branch_remotes
.into_iter()
.map(|(name, (mut remotes, ts))| {
remotes.sort(); (name, remotes, ts)
})
.collect();
remote_only.sort_by_key(|b| std::cmp::Reverse(b.2));
let mut result = Vec::with_capacity(locals.len() + remote_only.len());
for branch in locals {
if worktree_branches.contains(&branch.name) {
result.push(CompletionBranch {
name: branch.name.clone(),
timestamp: branch.committer_ts,
category: BranchCategory::Worktree,
});
}
}
for branch in locals {
if !worktree_branches.contains(&branch.name) {
result.push(CompletionBranch {
name: branch.name.clone(),
timestamp: branch.committer_ts,
category: BranchCategory::Local,
});
}
}
for (name, remotes, timestamp) in remote_only {
result.push(CompletionBranch {
name,
timestamp,
category: BranchCategory::Remote(remotes),
});
}
Ok(result)
}
}
fn parse_local_branch_line(line: &str) -> Option<LocalBranch> {
let mut parts = line.split(FIELD_SEP);
let name = parts.next()?.to_string();
let commit_sha = parts.next()?.to_string();
let committer_ts: i64 = parts.next()?.parse().ok()?;
let upstream_short_raw = parts.next()?;
let upstream_track = parts.next()?;
let upstream_short = if upstream_short_raw.is_empty() || upstream_track == "[gone]" {
None
} else {
Some(upstream_short_raw.to_string())
};
Some(LocalBranch {
name,
commit_sha,
committer_ts,
upstream_short,
})
}
fn parse_remote_branch_line(line: &str) -> Option<RemoteBranch> {
let mut parts = line.split(FIELD_SEP);
let short_name = parts.next()?;
let commit_sha = parts.next()?.to_string();
let committer_ts: i64 = parts.next()?.parse().ok()?;
let (remote_name, local_name) = short_name.split_once('/')?;
if local_name == "HEAD" {
return None;
}
Some(RemoteBranch {
short_name: short_name.to_string(),
commit_sha,
committer_ts,
remote_name: remote_name.to_string(),
local_name: local_name.to_string(),
})
}