Skip to main content

cadvm_core/
revision.rs

1//! Revision resolution: `HEAD`, `HEAD~N`, branch names, full and short hashes.
2
3use cadvm_store::{Category, ObjectId};
4
5use crate::error::{CoreError, Result};
6use crate::repo::Repository;
7
8/// Resolve a revision spec to a concrete commit id.
9///
10/// Supported forms, tried in this order:
11/// * `HEAD`, `HEAD~N` — current commit, walking `N` first-parents back
12/// * a branch name
13/// * a full 64-char hash (with or without the `blake3:` prefix)
14/// * an unambiguous short hash prefix
15pub fn resolve(repo: &Repository, spec: &str) -> Result<ObjectId> {
16    let spec = spec.trim();
17
18    // HEAD / HEAD~N.
19    if spec == "HEAD" || spec.starts_with("HEAD~") || spec.starts_with("HEAD^") {
20        return resolve_head_relative(repo, spec);
21    }
22
23    // Branch name.
24    if repo.branch_exists(spec) {
25        return repo
26            .read_ref(spec)?
27            .ok_or_else(|| CoreError::EmptyBranch(spec.to_string()));
28    }
29
30    // Full or short hash.
31    resolve_hash(repo, spec)
32}
33
34fn resolve_head_relative(repo: &Repository, spec: &str) -> Result<ObjectId> {
35    let n: usize = if spec == "HEAD" {
36        0
37    } else if let Some(rest) = spec.strip_prefix("HEAD~") {
38        rest.parse()
39            .map_err(|_| CoreError::UnknownRevision(spec.to_string()))?
40    } else if let Some(rest) = spec.strip_prefix("HEAD^") {
41        // `HEAD^` == `HEAD~1`; `HEAD^N` is treated the same way for V1.
42        if rest.is_empty() {
43            1
44        } else {
45            rest.parse()
46                .map_err(|_| CoreError::UnknownRevision(spec.to_string()))?
47        }
48    } else {
49        return Err(CoreError::UnknownRevision(spec.to_string()));
50    };
51
52    let mut current = repo
53        .head_commit_id()?
54        .ok_or_else(|| CoreError::UnknownRevision("HEAD".to_string()))?;
55    for _ in 0..n {
56        let commit = repo.read_commit(&current)?;
57        current = commit
58            .parents
59            .into_iter()
60            .next()
61            .ok_or(CoreError::NoParent)?;
62    }
63    Ok(current)
64}
65
66fn resolve_hash(repo: &Repository, spec: &str) -> Result<ObjectId> {
67    // Strip an optional `blake3:` prefix for short-hash matching.
68    let hex = spec.strip_prefix("blake3:").unwrap_or(spec);
69    let hex_lower = hex.to_ascii_lowercase();
70
71    if !hex_lower.bytes().all(|b| b.is_ascii_hexdigit()) || hex_lower.is_empty() {
72        return Err(CoreError::UnknownRevision(spec.to_string()));
73    }
74
75    // Exact full hash.
76    if hex_lower.len() == 64 {
77        if let Ok(id) = spec.parse::<ObjectId>() {
78            if repo.store().has(Category::Commit, &id) {
79                return Ok(id);
80            }
81        }
82        return Err(CoreError::UnknownRevision(spec.to_string()));
83    }
84
85    // Short-hash prefix match over all commits.
86    let matches: Vec<ObjectId> = repo
87        .store()
88        .list(Category::Commit)?
89        .into_iter()
90        .filter(|id| id.hex().starts_with(&hex_lower))
91        .collect();
92
93    match matches.len() {
94        0 => Err(CoreError::UnknownRevision(spec.to_string())),
95        1 => Ok(matches.into_iter().next().unwrap()),
96        count => Err(CoreError::AmbiguousRevision {
97            prefix: spec.to_string(),
98            count,
99        }),
100    }
101}
102
103/// Walk the first-parent commit chain starting at `head` (newest first).
104pub fn commit_history(repo: &Repository, head: &ObjectId) -> Result<Vec<crate::model::Commit>> {
105    let mut out = Vec::new();
106    let mut current = Some(head.clone());
107    while let Some(id) = current {
108        let commit = repo.read_commit(&id)?;
109        current = commit.parents.first().cloned();
110        out.push(commit);
111    }
112    Ok(out)
113}