use crate::{
CommitMetadata, CommitRecord, RevisionRange, RevisionSelection, RevisionSelectionItem,
parse_revision_range,
};
use sley_core::{GitError, ObjectFormat, ObjectId, Result};
use sley_object::{Commit, ObjectType};
use sley_odb::{FileObjectDatabase, ObjectReader};
use std::cmp::Reverse;
use std::collections::{HashMap, HashSet, VecDeque};
pub fn parse_rev_list_blob_limit(value: &str) -> Result<usize> {
git_parse_blob_limit(value)
.and_then(|limit| usize::try_from(limit).ok())
.ok_or_else(|| {
eprintln!("fatal: invalid filter-spec 'blob:limit={value}'");
GitError::Exit(128)
})
}
pub fn git_parse_blob_limit(value: &str) -> Option<u64> {
if value.is_empty() || value.contains('-') {
return None;
}
let (digits, factor) = match value.as_bytes()[value.len() - 1] {
b'k' | b'K' => (&value[..value.len() - 1], 1024u64),
b'm' | b'M' => (&value[..value.len() - 1], 1024 * 1024),
b'g' | b'G' => (&value[..value.len() - 1], 1024 * 1024 * 1024),
_ => (value, 1),
};
let base = if let Some(hex) = digits
.strip_prefix("0x")
.or_else(|| digits.strip_prefix("0X"))
{
u64::from_str_radix(hex, 16).ok()?
} else if digits.len() > 1 && digits.starts_with('0') {
u64::from_str_radix(&digits[1..], 8).ok()?
} else {
digits.parse::<u64>().ok()?
};
base.checked_mul(factor)
}
pub fn parse_rev_list_tree_depth(value: &str) -> Result<usize> {
value.parse::<usize>().map_err(|_| {
eprintln!("fatal: expected 'tree:<depth>'");
GitError::Exit(128)
})
}
pub fn parse_rev_list_object_type_filter(value: &str) -> Result<ObjectType> {
match value {
"blob" => Ok(ObjectType::Blob),
"tree" => Ok(ObjectType::Tree),
"commit" => Ok(ObjectType::Commit),
"tag" => Ok(ObjectType::Tag),
_ => {
eprintln!("fatal: '{value}' for 'object:type=<type>' is not a valid object type");
Err(GitError::Exit(128))
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RevListOrdering {
Default,
Topo,
Date,
AuthorDate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RevListMissingAction {
Error,
Print,
PrintInfo,
AllowAny,
AllowPromisor,
ExcludePromisor,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RevListWalkWithMissing {
pub records: Vec<CommitRecord>,
pub missing: Vec<ObjectId>,
}
pub fn rev_list_topo_order(records: Vec<&CommitRecord>) -> Result<Vec<&CommitRecord>> {
let records = rev_list_commit_date_input_order(records)?;
Ok(rev_list_topo_emit(records, None))
}
pub fn rev_list_date_order(records: Vec<&CommitRecord>) -> Result<Vec<&CommitRecord>> {
let timestamps = records
.iter()
.map(|record| commit_identity_timestamp_i64(&record.commit.committer))
.collect::<Result<Vec<_>>>()?;
Ok(rev_list_ready_order(records, |idx| {
(timestamps[idx], Reverse(idx))
}))
}
pub fn rev_list_author_date_order(records: Vec<&CommitRecord>) -> Result<Vec<&CommitRecord>> {
let records = rev_list_commit_date_input_order(records)?;
let timestamps = records
.iter()
.map(|record| commit_identity_timestamp_i64(&record.commit.author))
.collect::<Result<Vec<_>>>()?;
Ok(rev_list_prio_emit(records, ×tamps))
}
fn rev_list_prio_emit<'a>(
records: Vec<&'a CommitRecord>,
priority: &[i64],
) -> Vec<&'a CommitRecord> {
let index_by_oid = records
.iter()
.enumerate()
.map(|(idx, record)| (record.oid, idx))
.collect::<HashMap<_, _>>();
let n = records.len();
let mut indegree = vec![1usize; n];
for record in &records {
for parent in &record.parents {
if let Some(&parent_idx) = index_by_oid.get(parent) {
indegree[parent_idx] += 1;
}
}
}
let mut ctr = vec![u64::MAX; n];
let mut next_ctr = 0u64;
let mut ready: Vec<usize> = Vec::new();
for idx in 0..n {
if indegree[idx] == 1 {
ctr[idx] = next_ctr;
next_ctr += 1;
ready.push(idx);
}
}
let mut out = Vec::with_capacity(n);
while !ready.is_empty() {
let pos = ready
.iter()
.enumerate()
.max_by_key(|(_, idx)| (priority[**idx], Reverse(ctr[**idx])))
.map(|(pos, _)| pos)
.expect("ready is not empty");
let idx = ready.swap_remove(pos);
let record = records[idx];
for parent in &record.parents {
if let Some(&parent_idx) = index_by_oid.get(parent) {
if indegree[parent_idx] == 0 {
continue;
}
indegree[parent_idx] -= 1;
if indegree[parent_idx] == 1 {
ctr[parent_idx] = next_ctr;
next_ctr += 1;
ready.push(parent_idx);
}
}
}
indegree[idx] = 0;
out.push(record);
}
for (idx, record) in records.into_iter().enumerate() {
if ctr[idx] == u64::MAX {
out.push(record);
}
}
out
}
pub fn rev_list_commit_date_input_order(records: Vec<&CommitRecord>) -> Result<Vec<&CommitRecord>> {
let mut keyed = records
.into_iter()
.map(|record| {
commit_identity_timestamp_i64(&record.commit.committer).map(|ts| (ts, record))
})
.collect::<Result<Vec<_>>>()?;
keyed.sort_by(|(ta, a), (tb, b)| tb.cmp(ta).then_with(|| a.oid.cmp(&b.oid)));
Ok(keyed.into_iter().map(|(_, record)| record).collect())
}
pub fn rev_list_topo_emit<'a>(
records: Vec<&'a CommitRecord>,
priority: Option<&[i64]>,
) -> Vec<&'a CommitRecord> {
let _ = priority;
let index_by_oid = records
.iter()
.enumerate()
.map(|(idx, record)| (record.oid, idx))
.collect::<HashMap<_, _>>();
let mut indegree = vec![1usize; records.len()];
for record in &records {
for parent in &record.parents {
if let Some(&pi) = index_by_oid.get(parent) {
indegree[pi] += 1;
}
}
}
let mut queue: Vec<usize> = indegree
.iter()
.enumerate()
.filter_map(|(idx, deg)| (*deg == 1).then_some(idx))
.collect();
queue.reverse();
let mut out = Vec::with_capacity(records.len());
while let Some(idx) = queue.pop() {
let record = records[idx];
for parent in &record.parents {
if let Some(&pi) = index_by_oid.get(parent) {
if indegree[pi] == 0 {
continue;
}
indegree[pi] -= 1;
if indegree[pi] == 1 {
queue.push(pi);
}
}
}
indegree[idx] = 0;
out.push(record);
}
out
}
pub fn rev_list_ready_order<K: Ord>(
records: Vec<&CommitRecord>,
ready_key: impl Fn(usize) -> K,
) -> Vec<&CommitRecord> {
let index_by_oid = records
.iter()
.enumerate()
.map(|(idx, record)| (record.oid, idx))
.collect::<HashMap<_, _>>();
let mut remaining_children = vec![0usize; records.len()];
for record in &records {
for parent in &record.parents {
if let Some(parent_idx) = index_by_oid.get(parent).copied() {
remaining_children[parent_idx] += 1;
}
}
}
let mut ready = remaining_children
.iter()
.enumerate()
.filter_map(|(idx, child_count)| (*child_count == 0).then_some(idx))
.collect::<Vec<_>>();
let mut emitted = vec![false; records.len()];
let mut out = Vec::with_capacity(records.len());
while !ready.is_empty() {
let ready_pos = ready
.iter()
.enumerate()
.max_by_key(|(_, idx)| ready_key(**idx))
.map(|(pos, _)| pos)
.expect("ready is not empty");
let idx = ready.swap_remove(ready_pos);
if emitted[idx] {
continue;
}
emitted[idx] = true;
let record = records[idx];
out.push(record);
for parent in &record.parents {
if let Some(parent_idx) = index_by_oid.get(parent).copied() {
remaining_children[parent_idx] = remaining_children[parent_idx].saturating_sub(1);
if remaining_children[parent_idx] == 0 && !emitted[parent_idx] {
ready.push(parent_idx);
}
}
}
}
for (idx, record) in records.into_iter().enumerate() {
if !emitted[idx] {
out.push(record);
}
}
out
}
pub fn rev_list_metadata_date_order(records: Vec<CommitMetadata>) -> Vec<CommitMetadata> {
let index_by_oid = records
.iter()
.enumerate()
.map(|(idx, record)| (record.oid, idx))
.collect::<HashMap<_, _>>();
let mut remaining_children = vec![0usize; records.len()];
for record in &records {
for parent in &record.parents {
if let Some(parent_idx) = index_by_oid.get(parent).copied() {
remaining_children[parent_idx] += 1;
}
}
}
let mut ready = remaining_children
.iter()
.enumerate()
.filter_map(|(idx, child_count)| (*child_count == 0).then_some(idx))
.collect::<Vec<_>>();
let mut emitted = vec![false; records.len()];
let mut order = Vec::with_capacity(records.len());
while !ready.is_empty() {
let ready_pos = ready
.iter()
.enumerate()
.max_by_key(|(_, idx)| (records[**idx].commit_time, Reverse(**idx)))
.map(|(pos, _)| pos)
.expect("ready is not empty");
let idx = ready.swap_remove(ready_pos);
if emitted[idx] {
continue;
}
emitted[idx] = true;
order.push(idx);
for parent in &records[idx].parents {
if let Some(parent_idx) = index_by_oid.get(parent).copied() {
remaining_children[parent_idx] = remaining_children[parent_idx].saturating_sub(1);
if remaining_children[parent_idx] == 0 && !emitted[parent_idx] {
ready.push(parent_idx);
}
}
}
}
for (idx, was_emitted) in emitted.iter().enumerate() {
if !was_emitted {
order.push(idx);
}
}
let mut slots = records.into_iter().map(Some).collect::<Vec<_>>();
order
.into_iter()
.filter_map(|idx| slots[idx].take())
.collect()
}
pub fn rev_list_walk_commits(
db: &FileObjectDatabase,
format: ObjectFormat,
starts: impl IntoIterator<Item = ObjectId>,
first_parent: bool,
) -> Result<Vec<CommitRecord>> {
rev_list_walk_commits_with_missing(
db,
format,
starts,
first_parent,
RevListMissingAction::Error,
)
}
pub fn rev_list_walk_commits_with_missing(
db: &FileObjectDatabase,
format: ObjectFormat,
starts: impl IntoIterator<Item = ObjectId>,
first_parent: bool,
missing_action: RevListMissingAction,
) -> Result<Vec<CommitRecord>> {
Ok(rev_list_walk_commits_with_missing_details(
db,
format,
starts,
first_parent,
missing_action,
)?
.records)
}
pub fn rev_list_walk_commits_with_missing_details(
db: &FileObjectDatabase,
format: ObjectFormat,
starts: impl IntoIterator<Item = ObjectId>,
first_parent: bool,
missing_action: RevListMissingAction,
) -> Result<RevListWalkWithMissing> {
if !first_parent {
return rev_list_walk_commits_all_parents_with_missing(db, format, starts, missing_action);
}
validate_commit_graph_for_rev_walk(db, format)?;
let grafts = load_commit_grafts(db, format);
let mut seen = HashSet::new();
let mut missing_seen = HashSet::new();
let mut pending = starts.into_iter().collect::<VecDeque<_>>();
let mut out = Vec::new();
let mut missing = Vec::new();
while let Some(oid) = pending.pop_front() {
if !seen.insert(oid) {
continue;
}
if missing_action == RevListMissingAction::ExcludePromisor && db.is_promised_object(&oid) {
continue;
}
let object = match db.read_object(&oid) {
Ok(object) => object,
Err(err) if rev_list_missing_allowed(db, &oid, missing_action) => {
let _ = err;
if matches!(
missing_action,
RevListMissingAction::Print | RevListMissingAction::PrintInfo
) && missing_seen.insert(oid)
{
missing.push(oid);
}
continue;
}
Err(err) => return Err(err),
};
if object.object_type != ObjectType::Commit {
return Err(GitError::InvalidObject(format!(
"expected commit {oid}, found {}",
object.object_type.as_str()
)));
}
let commit = Commit::parse(format, &object.body)?;
let parents = graft_overridden_parents(db, &grafts, &oid, commit.parents.clone());
if let Some(parent) = parents.first() {
pending.push_back(*parent);
}
out.push(CommitRecord {
oid,
parents,
commit,
});
}
Ok(RevListWalkWithMissing {
records: out,
missing,
})
}
pub fn load_commit_grafts(
db: &FileObjectDatabase,
format: ObjectFormat,
) -> HashMap<ObjectId, Vec<ObjectId>> {
let Some(git_dir) = db.objects_dir().parent() else {
return HashMap::new();
};
load_commit_grafts_from_git_dir(git_dir, format)
}
pub fn load_commit_grafts_from_git_dir(
git_dir: &std::path::Path,
format: ObjectFormat,
) -> HashMap<ObjectId, Vec<ObjectId>> {
let Ok(contents) = std::fs::read_to_string(git_dir.join("info").join("grafts")) else {
return HashMap::new();
};
let mut grafts = HashMap::new();
for line in contents.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut oids = line
.split_whitespace()
.filter_map(|hex| ObjectId::from_hex(format, hex).ok());
let Some(commit) = oids.next() else {
continue;
};
grafts.insert(commit, oids.collect::<Vec<_>>());
}
grafts
}
fn graft_overridden_parents(
db: &FileObjectDatabase,
grafts: &HashMap<ObjectId, Vec<ObjectId>>,
oid: &ObjectId,
parents: Vec<ObjectId>,
) -> Vec<ObjectId> {
let overridden = match grafts.get(oid) {
Some(graft) => graft.clone(),
None => parents,
};
sley_odb::grafted_parents(db, oid, overridden)
}
fn rev_list_walk_commits_all_parents_with_missing(
db: &FileObjectDatabase,
format: ObjectFormat,
starts: impl IntoIterator<Item = ObjectId>,
missing_action: RevListMissingAction,
) -> Result<RevListWalkWithMissing> {
validate_commit_graph_for_rev_walk(db, format)?;
let grafts = load_commit_grafts(db, format);
let mut seen = HashSet::new();
let mut missing_seen = HashSet::new();
let mut pending: VecDeque<ObjectId> = starts.into_iter().collect();
let mut out = Vec::new();
let mut missing = Vec::new();
while let Some(oid) = pending.pop_front() {
if !seen.insert(oid) {
continue;
}
if missing_action == RevListMissingAction::ExcludePromisor && db.is_promised_object(&oid) {
continue;
}
let object = match db.read_object(&oid) {
Ok(object) => object,
Err(err) if rev_list_missing_allowed(db, &oid, missing_action) => {
let _ = err;
if matches!(
missing_action,
RevListMissingAction::Print | RevListMissingAction::PrintInfo
) && missing_seen.insert(oid)
{
missing.push(oid);
}
continue;
}
Err(err) => return Err(err),
};
if object.object_type != ObjectType::Commit {
return Err(GitError::InvalidObject(format!(
"expected commit {oid}, found {}",
object.object_type.as_str()
)));
}
let commit = Commit::parse(format, &object.body)?;
let parents = graft_overridden_parents(db, &grafts, &oid, commit.parents.clone());
pending.extend(parents.iter().cloned());
out.push(CommitRecord {
oid,
parents,
commit,
});
}
Ok(RevListWalkWithMissing {
records: out,
missing,
})
}
fn rev_list_missing_allowed(
db: &FileObjectDatabase,
oid: &ObjectId,
missing_action: RevListMissingAction,
) -> bool {
match missing_action {
RevListMissingAction::Error => false,
RevListMissingAction::Print
| RevListMissingAction::PrintInfo
| RevListMissingAction::AllowAny => true,
RevListMissingAction::AllowPromisor | RevListMissingAction::ExcludePromisor => {
db.is_promised_object(oid)
}
}
}
fn validate_commit_graph_for_rev_walk(db: &FileObjectDatabase, format: ObjectFormat) -> Result<()> {
let Some(git_dir) = db.objects_dir().parent() else {
return Ok(());
};
match crate::load_direct_commit_graph(git_dir, format) {
crate::DirectCommitGraph::Invalid(message) => Err(GitError::InvalidFormat(message)),
crate::DirectCommitGraph::Missing | crate::DirectCommitGraph::Raw(_) => Ok(()),
}
}
pub fn rev_list_walk_commits_all_parents(
db: &FileObjectDatabase,
format: ObjectFormat,
starts: impl IntoIterator<Item = ObjectId>,
missing_action: RevListMissingAction,
) -> Result<Vec<CommitRecord>> {
Ok(rev_list_walk_commits_all_parents_with_missing(db, format, starts, missing_action)?.records)
}
pub fn rev_list_no_walk_commits(
db: &FileObjectDatabase,
format: ObjectFormat,
starts: impl IntoIterator<Item = ObjectId>,
) -> Result<Vec<CommitRecord>> {
let mut seen = HashSet::new();
let mut out = Vec::new();
for oid in starts {
if !seen.insert(oid) {
continue;
}
out.push(read_rev_list_commit_record(db, format, oid)?);
}
Ok(out)
}
pub fn read_rev_list_commit_record(
db: &FileObjectDatabase,
format: ObjectFormat,
oid: ObjectId,
) -> Result<CommitRecord> {
let object = db.read_object(&oid)?;
if object.object_type != ObjectType::Commit {
return Err(GitError::InvalidObject(format!(
"expected commit {oid}, found {}",
object.object_type.as_str()
)));
}
let commit = Commit::parse(format, &object.body)?;
let parents = commit.parents.clone();
Ok(CommitRecord {
oid,
parents,
commit,
})
}
pub fn add_rev_list_revision_arg(
value: &str,
not: bool,
includes: &mut Vec<String>,
excludes: &mut Vec<String>,
linear_ranges: &mut Vec<(String, String, bool)>,
symmetric_ranges: &mut Vec<(String, String, bool)>,
) -> Result<()> {
if let Some(exclude) = value.strip_prefix('^')
&& !exclude.is_empty()
{
if not {
includes.push(exclude.to_string());
} else {
excludes.push(exclude.to_string());
}
return Ok(());
}
let selection = if value.contains("..") {
let Some(range) = parse_revision_range(value) else {
return Err(GitError::Command(format!(
"unsupported rev-list range {value}"
)));
};
let mut selection = RevisionSelection::new();
selection.range(range);
selection
} else {
RevisionSelection::from_specs([value])?
};
for item in selection.items() {
match item {
RevisionSelectionItem::Include(rev) => {
if not {
excludes.push(rev.clone());
} else {
includes.push(rev.clone());
}
}
RevisionSelectionItem::Exclude(rev) => {
if not {
includes.push(rev.clone());
} else {
excludes.push(rev.clone());
}
}
RevisionSelectionItem::Range(RevisionRange::Asymmetric { start, end }) => {
linear_ranges.push((start.clone(), end.clone(), not));
}
RevisionSelectionItem::Range(RevisionRange::Symmetric { left, right }) => {
symmetric_ranges.push((left.clone(), right.clone(), not));
}
}
}
Ok(())
}
pub fn commit_identity_timestamp(raw: &[u8]) -> String {
sley_core::split_ident_line(raw)
.and_then(|fields| fields.date)
.map(|date| String::from_utf8_lossy(date).into_owned())
.unwrap_or_default()
}
pub fn commit_identity_timestamp_i64(raw: &[u8]) -> Result<i64> {
Ok(commit_identity_timestamp(raw).parse::<i64>().unwrap_or(0))
}