use std::collections::{HashMap, HashSet};
use crate::config::{parse_bool, ConfigSet};
use crate::error::Result;
use crate::objects::{ObjectId, ObjectKind};
use crate::odb::Odb;
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(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 =
std::env::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 collect_decorations_inner(
repo: &Repository,
full: bool,
clear_decorations: bool,
filter: &DecorationFilter,
) -> Result<DecorationMap> {
let mut map: DecorationMap = HashMap::new();
let git_dir = &repo.git_dir;
let odb = &repo.odb;
let head = resolve_head(git_dir)?;
let hide_remote_update_noise = ConfigSet::load(Some(git_dir), true)
.unwrap_or_default()
.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 = crate::refs::list_refs(git_dir, "refs/")?;
all_refs.sort_by(|a, b| a.0.cmp(&b.0));
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 || rest.parse::<ObjectId>().is_err() {
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 hex = peel_to_commit_hex(odb, &oid.to_hex()).unwrap_or_else(|| oid.to_hex());
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 display = if full {
refname.clone()
} else {
rest.to_owned()
};
let hex = peel_to_commit_hex(odb, &oid.to_hex()).unwrap_or_else(|| oid.to_hex());
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 display = if full {
refname.clone()
} else {
rest.to_owned()
};
let peeled = peel_to_commit_hex(odb, &oid.to_hex()).unwrap_or_else(|| oid.to_hex());
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 display = if full {
refname.clone()
} else {
rest.to_owned()
};
let peeled = peel_to_commit_hex(odb, &oid.to_hex()).unwrap_or_else(|| oid.to_hex());
prepend_decoration(
map.entry(peeled).or_default(),
DecorationItem {
refname: Some(refname.clone()),
display,
kind: DecorationKind::RemoteBranch,
},
);
continue;
}
if clear_decorations || !filter.is_empty() {
let peeled = peel_to_commit_hex(odb, &oid.to_hex()).unwrap_or_else(|| oid.to_hex());
prepend_decoration(
map.entry(peeled).or_default(),
DecorationItem {
refname: Some(refname.clone()),
display: refname.clone(),
kind: DecorationKind::Other,
},
);
}
}
if let Some(oid) = head.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()?;
let obj = odb.read(&oid).ok()?;
match obj.kind {
ObjectKind::Commit => Some(hex.to_owned()),
ObjectKind::Tag => {
let text = std::str::from_utf8(&obj.data).ok()?;
for line in text.lines() {
if let Some(target) = line.strip_prefix("object ") {
let target_hex = target.trim();
return peel_to_commit_hex(odb, target_hex);
}
}
None
}
_ => None,
}
}
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))
}