use crate::error::{Error, Result};
use crate::ident::committer_unix_seconds_for_ordering;
use crate::objects::{parse_commit, parse_tag, CommitData, ObjectId, ObjectKind};
use crate::refs;
use crate::repo::Repository;
use std::collections::{HashMap, VecDeque};
const MERGE_TRAVERSAL_WEIGHT: u32 = 65_535;
#[derive(Clone, Debug)]
struct RevName {
tip_name: String,
taggerdate: i64,
generation: u32,
distance: u32,
from_tag: bool,
}
#[derive(Debug, Default, Clone)]
pub struct NameRevOptions {
pub tags_only: bool,
pub shorten_tags: bool,
pub ref_filters: Vec<String>,
pub exclude_filters: Vec<String>,
}
pub fn build_name_map(
repo: &Repository,
options: &NameRevOptions,
) -> Result<HashMap<ObjectId, String>> {
let tips = collect_tips(repo, options)?;
let mut names: HashMap<ObjectId, RevName> = HashMap::new();
let mut commit_cache: HashMap<ObjectId, CommitData> = HashMap::new();
for tip in &tips {
let Some(commit_oid) = tip.commit_oid else {
continue;
};
name_from_tip(
repo,
&mut names,
&mut commit_cache,
commit_oid,
&tip.display_name,
tip.taggerdate,
tip.from_tag,
tip.deref,
)?;
}
Ok(names
.into_iter()
.map(|(oid, name)| (oid, format_name(&name)))
.collect())
}
fn format_name(name: &RevName) -> String {
if name.generation == 0 {
return name.tip_name.clone();
}
let base = name.tip_name.strip_suffix("^0").unwrap_or(&name.tip_name);
format!("{}~{}", base, name.generation)
}
fn effective_distance(distance: u32, generation: u32) -> u32 {
distance.saturating_add(if generation > 0 {
MERGE_TRAVERSAL_WEIGHT
} else {
0
})
}
fn is_better_name(
existing: &RevName,
taggerdate: i64,
generation: u32,
distance: u32,
from_tag: bool,
) -> bool {
let existing_eff = effective_distance(existing.distance, existing.generation);
let new_eff = effective_distance(distance, generation);
if from_tag && existing.from_tag {
return existing_eff > new_eff;
}
if existing.from_tag != from_tag {
return from_tag;
}
if existing_eff != new_eff {
return existing_eff > new_eff;
}
if existing.taggerdate != taggerdate {
return existing.taggerdate > taggerdate;
}
false
}
fn get_parent_name(current: &RevName, parent_number: u32) -> String {
let base = current
.tip_name
.strip_suffix("^0")
.unwrap_or(¤t.tip_name);
if current.generation > 0 {
format!("{}~{}^{}", base, current.generation, parent_number)
} else {
format!("{}^{}", base, parent_number)
}
}
fn name_from_tip(
repo: &Repository,
names: &mut HashMap<ObjectId, RevName>,
commit_cache: &mut HashMap<ObjectId, CommitData>,
start_oid: ObjectId,
tip_name: &str,
taggerdate: i64,
from_tag: bool,
deref: bool,
) -> Result<()> {
let actual_tip_name = if deref {
format!("{}^0", tip_name)
} else {
tip_name.to_owned()
};
let should_start = match names.get(&start_oid) {
None => true,
Some(existing) => is_better_name(existing, taggerdate, 0, 0, from_tag),
};
if !should_start {
return Ok(());
}
names.insert(
start_oid,
RevName {
tip_name: actual_tip_name,
taggerdate,
generation: 0,
distance: 0,
from_tag,
},
);
let mut stack: Vec<ObjectId> = vec![start_oid];
while let Some(oid) = stack.pop() {
let current = match names.get(&oid) {
Some(n) => n.clone(),
None => continue,
};
let commit = match load_commit_cached(repo, commit_cache, oid) {
Ok(c) => c,
Err(_) => continue,
};
let parents = commit.parents.clone();
let mut to_push: Vec<ObjectId> = Vec::new();
for (idx, parent_oid) in parents.iter().enumerate() {
let parent_number = (idx + 1) as u32;
let (parent_gen, parent_dist) = if parent_number > 1 {
(
0u32,
current.distance.saturating_add(MERGE_TRAVERSAL_WEIGHT),
)
} else {
(
current.generation.saturating_add(1),
current.distance.saturating_add(1),
)
};
let should_update = match names.get(parent_oid) {
None => true,
Some(existing) => {
is_better_name(existing, taggerdate, parent_gen, parent_dist, from_tag)
}
};
if should_update {
let parent_tip_name = if parent_number > 1 {
get_parent_name(¤t, parent_number)
} else {
current.tip_name.clone()
};
names.insert(
*parent_oid,
RevName {
tip_name: parent_tip_name,
taggerdate,
generation: parent_gen,
distance: parent_dist,
from_tag,
},
);
to_push.push(*parent_oid);
}
}
for parent in to_push.into_iter().rev() {
stack.push(parent);
}
}
Ok(())
}
struct TipEntry {
display_name: String,
commit_oid: Option<ObjectId>,
taggerdate: i64,
from_tag: bool,
deref: bool,
}
fn collect_tips(repo: &Repository, options: &NameRevOptions) -> Result<Vec<TipEntry>> {
let all_refs = refs::list_refs(&repo.git_dir, "refs/")?;
let mut tips: Vec<TipEntry> = Vec::new();
for (refname, oid) in all_refs {
if options.tags_only && !refname.starts_with("refs/tags/") {
continue;
}
if options
.exclude_filters
.iter()
.any(|pat| subpath_matches(&refname, pat))
{
continue;
}
let can_abbreviate = if !options.ref_filters.is_empty() {
let mut matched = false;
let mut subpath_match = false;
for pat in &options.ref_filters {
match subpath_match_kind(&refname, pat) {
SubpathMatch::Full => matched = true,
SubpathMatch::Sub => {
matched = true;
subpath_match = true;
}
SubpathMatch::None => {}
}
}
if !matched {
continue;
}
subpath_match
} else {
false
};
let from_tag = refname.starts_with("refs/tags/");
let display_name = shorten_refname(&refname, can_abbreviate || options.shorten_tags);
let (commit_oid, taggerdate, deref) = peel_to_commit(repo, oid)?;
tips.push(TipEntry {
display_name,
commit_oid,
taggerdate,
from_tag,
deref,
});
}
tips.sort_by(|a, b| {
let tag_cmp = b.from_tag.cmp(&a.from_tag); if tag_cmp != std::cmp::Ordering::Equal {
return tag_cmp;
}
a.taggerdate.cmp(&b.taggerdate)
});
Ok(tips)
}
fn peel_to_commit(repo: &Repository, mut oid: ObjectId) -> Result<(Option<ObjectId>, i64, bool)> {
let mut deref = false;
let mut taggerdate: Option<i64> = None;
loop {
let obj = match repo.odb.read(&oid) {
Ok(o) => o,
Err(_) => return Ok((None, taggerdate.unwrap_or(0), deref)),
};
match obj.kind {
ObjectKind::Commit => {
let ts = if let Ok(c) = parse_commit(&obj.data) {
parse_signature_time(&c.committer)
} else {
0
};
let date = taggerdate.unwrap_or(ts);
return Ok((Some(oid), date, deref));
}
ObjectKind::Tag => {
let tag = match parse_tag(&obj.data) {
Ok(t) => t,
Err(_) => return Ok((None, taggerdate.unwrap_or(0), deref)),
};
if taggerdate.is_none() {
taggerdate = Some(tag.tagger.as_deref().map(parse_signature_time).unwrap_or(0));
}
oid = tag.object;
deref = true;
}
_ => return Ok((None, taggerdate.unwrap_or(0), deref)),
}
}
}
pub fn all_reachable_commits(repo: &Repository) -> Result<Vec<ObjectId>> {
let all_refs = refs::list_refs(&repo.git_dir, "refs/")?;
let mut seen: std::collections::HashSet<ObjectId> = std::collections::HashSet::new();
let mut queue: VecDeque<ObjectId> = VecDeque::new();
for (_, oid) in all_refs {
let (commit_oid, _, _) = peel_to_commit(repo, oid)?;
if let Some(c) = commit_oid {
if seen.insert(c) {
queue.push_back(c);
}
}
}
while let Some(oid) = queue.pop_front() {
let commit = match load_commit(repo, oid) {
Ok(c) => c,
Err(_) => continue,
};
for parent in commit.parents {
if seen.insert(parent) {
queue.push_back(parent);
}
}
}
let mut result: Vec<ObjectId> = seen.into_iter().collect();
result.sort();
Ok(result)
}
fn shorten_refname(refname: &str, can_abbreviate: bool) -> String {
if can_abbreviate {
if let Some(rest) = refname.strip_prefix("refs/heads/") {
return rest.to_owned();
}
if let Some(rest) = refname.strip_prefix("refs/tags/") {
return rest.to_owned();
}
if let Some(rest) = refname.strip_prefix("refs/") {
return rest.to_owned();
}
return refname.to_owned();
}
if let Some(rest) = refname.strip_prefix("refs/heads/") {
return rest.to_owned();
}
if let Some(rest) = refname.strip_prefix("refs/") {
return rest.to_owned();
}
refname.to_owned()
}
#[derive(PartialEq, Eq)]
enum SubpathMatch {
Full,
Sub,
None,
}
fn subpath_match_kind(path: &str, pattern: &str) -> SubpathMatch {
if glob_matches(pattern, path) {
return SubpathMatch::Full;
}
let mut rest = path;
while let Some(pos) = rest.find('/') {
rest = &rest[pos + 1..];
if glob_matches(pattern, rest) {
return SubpathMatch::Sub;
}
}
SubpathMatch::None
}
fn subpath_matches(path: &str, pattern: &str) -> bool {
subpath_match_kind(path, pattern) != SubpathMatch::None
}
fn glob_matches(pattern: &str, text: &str) -> bool {
let pat: Vec<char> = pattern.chars().collect();
let txt: Vec<char> = text.chars().collect();
glob_match_inner(&pat, &txt)
}
fn glob_match_inner(pat: &[char], txt: &[char]) -> bool {
match (pat.first(), txt.first()) {
(None, None) => true,
(None, Some(_)) => false,
(Some('*'), _) => {
glob_match_inner(&pat[1..], txt)
|| (!txt.is_empty() && glob_match_inner(pat, &txt[1..]))
}
(Some('?'), Some(_)) => glob_match_inner(&pat[1..], &txt[1..]),
(Some('?'), None) => false,
(Some(p), Some(t)) => p == t && glob_match_inner(&pat[1..], &txt[1..]),
(Some(_), None) => false,
}
}
pub(crate) fn parse_signature_time(sig: &str) -> i64 {
committer_unix_seconds_for_ordering(sig)
}
fn load_commit_cached<'c>(
repo: &Repository,
cache: &'c mut HashMap<ObjectId, CommitData>,
oid: ObjectId,
) -> Result<&'c CommitData> {
if let std::collections::hash_map::Entry::Vacant(e) = cache.entry(oid) {
let obj = repo.odb.read(&oid)?;
if obj.kind != ObjectKind::Commit {
return Err(Error::CorruptObject(format!(
"object {oid} is not a commit"
)));
}
let commit = parse_commit(&obj.data)?;
e.insert(commit);
}
Ok(cache.get(&oid).unwrap())
}
fn load_commit(repo: &Repository, oid: ObjectId) -> Result<CommitData> {
let obj = repo.odb.read(&oid)?;
if obj.kind != ObjectKind::Commit {
return Err(Error::CorruptObject(format!(
"object {oid} is not a commit"
)));
}
parse_commit(&obj.data)
}
pub fn resolve_oid(repo: &Repository, spec: &str) -> Result<ObjectId> {
crate::rev_parse::resolve_revision(repo, spec)
}
pub fn lookup_name<'m>(
repo: &Repository,
name_map: &'m HashMap<ObjectId, String>,
oid: ObjectId,
) -> Result<Option<&'m String>> {
if let Some(name) = name_map.get(&oid) {
return Ok(Some(name));
}
let obj = match repo.odb.read(&oid) {
Ok(o) => o,
Err(_) => return Ok(None),
};
if obj.kind == ObjectKind::Tag {
let (commit_oid, _, _) = peel_to_commit(repo, oid)?;
if let Some(c) = commit_oid {
return Ok(name_map.get(&c));
}
}
Ok(None)
}
pub fn annotate_line(
repo: &Repository,
name_map: &HashMap<ObjectId, String>,
line: &str,
name_only: bool,
) -> Result<String> {
let mut out = String::with_capacity(line.len() + 32);
let chars: Vec<char> = line.chars().collect();
let hex_len = 40usize;
let mut i = 0usize;
let mut flush_start = 0usize;
while i + hex_len <= chars.len() {
let slice: String = chars[i..i + hex_len].iter().collect();
let after_is_hex = chars
.get(i + hex_len)
.map(|c| c.is_ascii_hexdigit())
.unwrap_or(false);
if !after_is_hex && slice.chars().all(|c| c.is_ascii_hexdigit()) {
if let Ok(oid) = slice.parse::<ObjectId>() {
if let Ok(Some(name)) = lookup_name(repo, name_map, oid) {
let prefix: String = chars[flush_start..i].iter().collect();
out.push_str(&prefix);
if name_only {
out.push_str(name);
} else {
out.push_str(&slice);
out.push_str(" (");
out.push_str(name);
out.push(')');
}
flush_start = i + hex_len;
i += hex_len;
continue;
}
}
}
i += 1;
}
let tail: String = chars[flush_start..].iter().collect();
out.push_str(&tail);
Ok(out)
}
#[must_use]
pub fn abbrev_oid(oid: ObjectId, len: usize) -> String {
let hex = oid.to_hex();
let n = len.clamp(4, 40).min(hex.len());
hex[..n].to_owned()
}
pub use self::all_reachable_commits as walk_all_commits;
pub fn object_exists(repo: &Repository, oid: ObjectId) -> bool {
repo.odb.exists(&oid)
}