use std::collections::{HashMap, HashSet};
use crate::change_id::{resolved_change_id, ChangeId};
use crate::config::{parse_bool, ConfigSet};
use crate::error::Result;
use crate::objects::{parse_commit, CommitData, ObjectId, ObjectKind, OidHashMap, OidHashSet};
use crate::odb::Odb;
use crate::refs::{PackedPeel, PackedRefPeels};
use crate::repo::Repository;
use crate::state::{resolve_head, HeadState};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum DecorationKind {
Branch,
RemoteBranch,
Tag,
Stash,
Head,
Grafted,
Other,
}
#[derive(Clone, Debug)]
pub struct DecorationItem {
pub refname: Option<String>,
pub display: String,
pub kind: DecorationKind,
}
pub type DecorationMap = HashMap<String, Vec<DecorationItem>>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CommitRefDecoration {
pub name: String,
pub kind: DecorationKind,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LoadedCommit {
pub oid: ObjectId,
pub parents: Vec<ObjectId>,
pub author: String,
pub committer: String,
pub message: String,
pub refs: Vec<CommitRefDecoration>,
pub change_id: ChangeId,
pub change_id_stored: bool,
}
#[derive(Default, Clone)]
pub struct DecorationFilter {
pub include: Vec<String>,
pub exclude: Vec<String>,
pub exclude_config: Vec<String>,
}
impl DecorationFilter {
pub fn is_empty(&self) -> bool {
self.include.is_empty() && self.exclude.is_empty() && self.exclude_config.is_empty()
}
pub fn matches(&self, refname: &str) -> bool {
for pat in &self.exclude {
if decoration_pattern_matches(pat, refname) {
return false;
}
}
if !self.include.is_empty() {
for pat in &self.include {
if decoration_pattern_matches(pat, refname) {
return true;
}
}
return false;
}
for pat in &self.exclude_config {
if decoration_pattern_matches(pat, refname) {
return false;
}
}
true
}
}
pub fn normalize_glob_ref(pattern: &str) -> String {
let mut out = String::new();
if !pattern.starts_with("refs/") && pattern != "HEAD" {
out.push_str("refs/");
}
out.push_str(pattern);
if out.ends_with('/') {
out.pop();
}
out
}
fn decoration_pattern_matches(pattern: &str, refname: &str) -> bool {
let has_glob = pattern.bytes().any(|b| matches!(b, b'*' | b'?' | b'['));
if has_glob {
crate::wildmatch::wildmatch(pattern.as_bytes(), refname.as_bytes(), 0)
} else {
if let Some(rest) = refname.strip_prefix(pattern) {
rest.is_empty() || rest.starts_with('/')
} else {
false
}
}
}
fn replace_ref_base() -> String {
let mut base = crate::environment::Environment::empty()
.var("GIT_REPLACE_REF_BASE")
.unwrap_or_else(|| "refs/replace/".to_owned());
if !base.ends_with('/') {
base.push('/');
}
base
}
fn prepend_decoration(items: &mut Vec<DecorationItem>, item: DecorationItem) {
items.insert(0, item);
}
pub fn collect_decorations(repo: &Repository, full: bool) -> Result<DecorationMap> {
collect_decorations_inner(repo, full, false, &DecorationFilter::default())
}
pub fn load_commits_with_decorations(
repo: &Repository,
oids: &[ObjectId],
decorations: &DecorationMap,
) -> Result<Vec<LoadedCommit>> {
load_commits_with_decorations_reusing(repo, oids, decorations, &mut OidHashMap::default())
}
pub fn load_commits_with_decorations_reusing(
repo: &Repository,
oids: &[ObjectId],
decorations: &DecorationMap,
parsed: &mut OidHashMap<CommitData>,
) -> Result<Vec<LoadedCommit>> {
let odb = repo.odb();
let mut out = Vec::with_capacity(oids.len());
for oid in oids {
let data = match parsed.remove(oid) {
Some(data) => data,
None => parse_commit(&odb.read(oid)?.data)?,
};
let resolved = resolved_change_id(oid, &data);
let hex = oid.to_hex();
let refs = decorations
.get(&hex)
.map(|items| {
items
.iter()
.map(|item| CommitRefDecoration {
name: item.display.clone(),
kind: item.kind,
})
.collect()
})
.unwrap_or_default();
out.push(LoadedCommit {
oid: *oid,
parents: data.parents,
author: data.author,
committer: data.committer,
message: data.message,
refs,
change_id: resolved.id,
change_id_stored: resolved.stored,
});
}
Ok(out)
}
pub fn collect_decorations_for_commits(
repo: &Repository,
full: bool,
commits: &[ObjectId],
) -> Result<DecorationMap> {
let targets: OidHashSet = commits.iter().copied().collect();
collect_decorations_impl(
repo,
full,
false,
&DecorationFilter::default(),
Some(&targets),
)
}
pub fn collect_decorations_inner(
repo: &Repository,
full: bool,
clear_decorations: bool,
filter: &DecorationFilter,
) -> Result<DecorationMap> {
collect_decorations_impl(repo, full, clear_decorations, filter, None)
}
fn collect_decorations_impl(
repo: &Repository,
full: bool,
clear_decorations: bool,
filter: &DecorationFilter,
targets: Option<&OidHashSet>,
) -> Result<DecorationMap> {
let mut map: DecorationMap = HashMap::new();
let git_dir = &repo.git_dir();
let head = resolve_head(git_dir)?;
let hide_remote_update_noise = repo
.config()
.unwrap_or_else(|_| std::sync::Arc::new(ConfigSet::new()))
.get("grit.submoduleUpdateRemoteDecorations")
.as_deref()
.and_then(|value| parse_bool(value).ok())
.unwrap_or(false);
let rep_base = replace_ref_base();
let mut all_refs = match targets {
Some(targets) => {
crate::refs::list_refs_filtering_packed(git_dir, "refs/", &|name, oid, peel| {
name.starts_with(&rep_base)
|| match peel {
PackedPeel::Peeled(peeled) => targets.contains(&peeled),
PackedPeel::NotTag => targets.contains(oid),
PackedPeel::Unknown => true,
}
})?
}
None => crate::refs::list_refs(git_dir, "refs/")?,
};
all_refs.sort_by(|a, b| a.0.cmp(&b.0));
let peeler = RefTipPeeler::new(repo, targets);
for (refname, oid) in all_refs {
if !filter.is_empty() && !refname.starts_with(&rep_base) && !filter.matches(&refname) {
continue;
}
if refname.starts_with(&rep_base) {
let Some(rest) = refname.strip_prefix(&rep_base) else {
continue;
};
let rest = rest.trim();
if rest.len() != 40 {
continue;
}
let Ok(replaced) = rest.parse::<ObjectId>() else {
continue;
};
if targets.is_some_and(|t| !t.contains(&replaced)) {
continue;
}
prepend_decoration(
map.entry(rest.to_owned()).or_default(),
DecorationItem {
refname: None,
display: "replaced".to_owned(),
kind: DecorationKind::Grafted,
},
);
continue;
}
if refname == "refs/stash" || refname.starts_with("refs/stash/") {
let Some(hex) = peeler.commit_hex(&refname, &oid) else {
continue;
};
prepend_decoration(
map.entry(hex).or_default(),
DecorationItem {
refname: Some("refs/stash".to_string()),
display: "refs/stash".to_owned(),
kind: DecorationKind::Stash,
},
);
continue;
}
if let Some(rest) = refname.strip_prefix("refs/heads/") {
let Some(hex) = peeler.commit_hex(&refname, &oid) else {
continue;
};
let display = if full {
refname.clone()
} else {
rest.to_owned()
};
prepend_decoration(
map.entry(hex).or_default(),
DecorationItem {
refname: Some(refname.clone()),
display,
kind: DecorationKind::Branch,
},
);
continue;
}
if let Some(rest) = refname.strip_prefix("refs/tags/") {
let Some(peeled) = peeler.commit_hex(&refname, &oid) else {
continue;
};
let display = if full {
refname.clone()
} else {
rest.to_owned()
};
prepend_decoration(
map.entry(peeled).or_default(),
DecorationItem {
refname: Some(refname.clone()),
display,
kind: DecorationKind::Tag,
},
);
continue;
}
if let Some(rest) = refname.strip_prefix("refs/remotes/") {
let Some(peeled) = peeler.commit_hex(&refname, &oid) else {
continue;
};
let display = if full {
refname.clone()
} else {
rest.to_owned()
};
prepend_decoration(
map.entry(peeled).or_default(),
DecorationItem {
refname: Some(refname.clone()),
display,
kind: DecorationKind::RemoteBranch,
},
);
continue;
}
if clear_decorations || !filter.is_empty() {
let Some(peeled) = peeler.commit_hex(&refname, &oid) else {
continue;
};
prepend_decoration(
map.entry(peeled).or_default(),
DecorationItem {
refname: Some(refname.clone()),
display: refname.clone(),
kind: DecorationKind::Other,
},
);
}
}
if let Some(oid) = head
.oid()
.filter(|oid| targets.is_none_or(|t| t.contains(*oid)))
{
if filter.is_empty() || filter.matches("HEAD") {
let hex = oid.to_hex();
prepend_decoration(
map.entry(hex).or_default(),
DecorationItem {
refname: Some("HEAD".to_string()),
display: "HEAD".to_owned(),
kind: DecorationKind::Head,
},
);
}
}
for items in map.values_mut() {
let mut seen = HashSet::new();
items.retain(|it| seen.insert((it.kind, it.display.clone(), it.refname.clone())));
if hide_remote_update_noise {
let branch_names: HashSet<String> = items
.iter()
.filter(|it| it.kind == DecorationKind::Branch)
.map(|it| it.display.clone())
.collect();
if !branch_names.is_empty() {
let hide_detached_head = !matches!(head, HeadState::Branch { .. });
items.retain(|it| {
if hide_detached_head && it.kind == DecorationKind::Head {
return false;
}
if it.kind == DecorationKind::RemoteBranch {
let short_remote = it
.display
.split_once('/')
.map(|(_, branch)| branch)
.unwrap_or(it.display.as_str());
return !branch_names.contains(short_remote) && short_remote != "HEAD";
}
true
});
}
}
}
Ok(map)
}
pub fn peel_to_commit_hex(odb: &Odb, hex: &str) -> Option<String> {
let oid: ObjectId = hex.parse().ok()?;
odb.peel_to_commit(oid)
.ok()
.flatten()
.map(|commit| commit.to_hex())
}
struct RefTipPeeler<'a> {
repo: &'a Repository,
packed: Option<PackedRefPeels>,
graph: std::cell::OnceCell<Option<std::sync::Arc<crate::commit_graph_file::CommitGraphChain>>>,
targets: Option<&'a OidHashSet>,
}
impl<'a> RefTipPeeler<'a> {
fn new(repo: &'a Repository, targets: Option<&'a OidHashSet>) -> Self {
let packed = PackedRefPeels::load(repo.git_dir(), repo.refs().format()).ok();
Self {
repo,
packed,
graph: std::cell::OnceCell::new(),
targets,
}
}
fn is_commit(&self, oid: &ObjectId) -> bool {
let graph = self.graph.get_or_init(|| {
self.repo
.caches()
.commit_graph_chain(self.repo.odb().objects_dir())
});
if graph.as_ref().is_some_and(|g| g.find_commit(oid).is_some()) {
return true;
}
self.repo
.odb()
.read_info(oid)
.is_ok_and(|info| info.kind == ObjectKind::Commit)
}
fn commit_hex(&self, refname: &str, oid: &ObjectId) -> Option<String> {
let key = self.key(refname, oid)?;
if self.targets.is_some_and(|t| !t.contains(&key)) {
return None;
}
Some(key.to_hex())
}
fn key(&self, refname: &str, oid: &ObjectId) -> Option<ObjectId> {
match self.packed.as_ref().and_then(|p| p.peeled(refname, oid)) {
Some(Some(peeled)) => {
if let Some(targets) = self.targets {
return targets.contains(&peeled).then_some(peeled);
}
return Some(if self.is_commit(&peeled) {
peeled
} else {
*oid
});
}
Some(None) => return Some(*oid),
None => {}
}
if self.targets.is_some_and(|t| t.contains(oid)) {
return Some(*oid);
}
Some(
self.repo
.odb()
.peel_to_commit(*oid)
.ok()
.flatten()
.unwrap_or(*oid),
)
}
}
pub fn current_branch_decoration_index(
items: &[DecorationItem],
head: &HeadState,
) -> Option<usize> {
let refname = match head {
HeadState::Branch { refname, .. } => refname.as_str(),
_ => return None,
};
if !items.iter().any(|it| it.kind == DecorationKind::Head) {
return None;
}
items
.iter()
.position(|it| it.kind == DecorationKind::Branch && it.refname.as_deref() == Some(refname))
}