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, ObjectId};
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(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>> {
let odb = repo.odb();
let mut out = Vec::with_capacity(oids.len());
for oid in oids {
let obj = odb.read(oid)?;
let data = parse_commit(&obj.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_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 = 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 = 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 = peeled_commit_hex(odb, &oid);
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 = peeled_commit_hex(odb, &oid);
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 = peeled_commit_hex(odb, &oid);
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 = peeled_commit_hex(odb, &oid);
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 = peeled_commit_hex(odb, &oid);
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()?;
odb.peel_to_commit(oid)
.ok()
.flatten()
.map(|commit| commit.to_hex())
}
fn peeled_commit_hex(odb: &Odb, oid: &ObjectId) -> String {
odb.peel_to_commit(*oid)
.ok()
.flatten()
.unwrap_or(*oid)
.to_hex()
}
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))
}