use crate::environment::RepositoryOptions;
use std::borrow::Cow;
use std::ffi::OsStr;
use std::fs;
use std::path::{Component, Path, PathBuf};
use regex::Regex;
use std::collections::{HashMap, HashSet};
use std::ops::ControlFlow;
use std::sync::Arc;
use crate::check_ref_format::{check_refname_format, RefNameOptions};
use crate::config::ConfigSet;
use crate::error::{Error, Result};
use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectId, ObjectKind};
use crate::reflog::ReflogEntry;
use crate::refs;
use crate::refs::store::RawRef;
use crate::repo::Repository;
use crate::rev_parse_error::{AmbiguousObjectHint, RevParseError};
pub fn discover_optional(start: Option<&Path>) -> Result<Option<Repository>> {
match Repository::discover(start, &RepositoryOptions::empty()) {
Ok(repo) => Ok(Some(repo)),
Err(Error::NotARepository(msg)) => {
if msg.contains("invalid gitfile format")
|| msg.contains("gitfile does not contain 'gitdir:' line")
|| msg.contains("not a regular file")
{
return Err(Error::NotARepository(msg));
}
if let Some(start) = start {
let start = if start.is_absolute() {
start.to_path_buf()
} else {
let cwd = crate::environment::Environment::empty().cwd.clone();
cwd.join(start)
};
let dot_git = start.join(".git");
if dot_git.is_file() || dot_git.is_symlink() {
return Err(Error::NotARepository(msg));
}
}
Ok(None)
}
Err(err) => Err(err),
}
}
#[must_use]
pub fn is_inside_work_tree(repo: &Repository, cwd: &Path) -> bool {
let Some(work_tree) = &repo.work_tree() else {
return false;
};
path_is_within(cwd, work_tree)
}
#[must_use]
pub fn is_inside_git_dir(repo: &Repository, cwd: &Path) -> bool {
path_is_within(cwd, repo.git_dir())
}
#[must_use]
pub fn show_prefix(repo: &Repository, cwd: &Path) -> String {
let Some(work_tree) = repo.work_tree() else {
return String::new();
};
if !path_is_within(cwd, work_tree) {
return String::new();
}
if cwd == work_tree {
return String::new();
}
let Ok(rel) = cwd.strip_prefix(work_tree) else {
return String::new();
};
let mut out = rel
.components()
.filter_map(component_to_text)
.collect::<Vec<_>>()
.join("/");
if !out.is_empty() {
out.push('/');
}
out
}
#[must_use]
pub fn superproject_work_tree_from_nested_git_modules(git_dir: &Path) -> Option<PathBuf> {
let mut p = git_dir.to_path_buf();
while let Some(parent) = p.parent() {
if p.file_name().is_some_and(|n| n == "modules")
&& parent.file_name().is_some_and(|n| n == ".git")
{
return parent.parent().map(PathBuf::from);
}
if parent == p {
break;
}
p = parent.to_path_buf();
}
None
}
#[must_use]
pub fn symbolic_full_name(repo: &Repository, spec: &str) -> Option<String> {
if upstream_suffix_info(spec).is_some() {
return resolve_upstream_symbolic_name(repo, spec).ok();
}
if let Ok(Some(branch)) = expand_at_minus_to_branch_name(repo, spec) {
let ref_name = format!("refs/heads/{branch}");
if repo.resolve_ref_name(&ref_name).is_ok() {
return Some(ref_name);
}
return None;
}
if spec == "HEAD" {
if let Ok(Some(target)) = refs::read_symbolic_ref(repo.git_dir(), "HEAD") {
return Some(target);
}
return None;
}
if spec.starts_with("refs/") {
if repo.resolve_ref_name(spec).is_ok() {
return Some(spec.to_owned());
}
return None;
}
for prefix in &["refs/heads/", "refs/tags/", "refs/remotes/"] {
let candidate = format!("{prefix}{spec}");
if repo.resolve_ref_name(&candidate).is_ok() {
return Some(candidate);
}
}
if let Some(full) = remote_tracking_head_symbolic_target(repo, spec) {
return Some(full);
}
None
}
fn remote_tracking_head_symbolic_target(repo: &Repository, name: &str) -> Option<String> {
if name.contains('/')
|| matches!(
name,
"HEAD" | "FETCH_HEAD" | "MERGE_HEAD" | "CHERRY_PICK_HEAD" | "REVERT_HEAD"
)
{
return None;
}
let config = repo.ensure_config_arc_fast().ok()?;
let url_key = format!("remote.{name}.url");
config.get(&url_key)?;
let head_ref = format!("refs/remotes/{name}/HEAD");
let target = refs::read_symbolic_ref(repo.git_dir(), &head_ref).ok()??;
Some(target)
}
pub fn expand_at_minus_to_branch_name(repo: &Repository, spec: &str) -> Result<Option<String>> {
if !spec.starts_with("@{-") || !spec.ends_with('}') {
return Ok(None);
}
let inner = &spec[3..spec.len() - 1];
let n: usize = inner
.parse()
.map_err(|_| Error::InvalidRef(format!("invalid N in @{{-N}} for '{spec}'")))?;
if n < 1 {
return Ok(None);
}
resolve_at_minus_to_branch(repo, n).map(Some)
}
pub fn resolve_at_minus_to_oid(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
try_resolve_at_minus(repo, spec)
}
#[must_use]
pub fn abbreviate_ref_name(full_name: &str) -> String {
for prefix in &["refs/heads/", "refs/tags/", "refs/remotes/"] {
if let Some(short) = full_name.strip_prefix(prefix) {
return short.to_owned();
}
}
if let Some(short) = full_name.strip_prefix("refs/") {
return short.to_owned();
}
full_name.to_owned()
}
#[must_use]
pub fn upstream_suffix_info(spec: &str) -> Option<(&str, bool)> {
let lower = spec.to_ascii_lowercase();
if lower.ends_with("@{push}") {
let base = &spec[..spec.len() - 7];
return Some((base, true));
}
if lower.ends_with("@{upstream}") {
let base = &spec[..spec.len() - 11];
return Some((base, false));
}
if lower.ends_with("@{u}") {
let base = &spec[..spec.len() - 4];
return Some((base, false));
}
None
}
pub fn resolve_upstream_symbolic_name(repo: &Repository, spec: &str) -> Result<String> {
let Some((base, is_push)) = upstream_suffix_info(spec) else {
return Err(Error::InvalidRef(format!("not an upstream spec: {spec}")));
};
resolve_upstream_full_ref_name(repo, base, is_push)
}
fn resolve_upstream_full_ref_name(repo: &Repository, base: &str, is_push: bool) -> Result<String> {
if is_push {
return resolve_push_ref_name(repo, base);
}
let (branch_key, display_branch) = resolve_upstream_branch_context(repo, base)?;
let config_path = repo.common_git_dir().join("config");
let _ = repo.config()?;
let config_content = repo.caches().local_config_text(&config_path)?;
let Some((remote, merge)) = parse_branch_tracking(&config_content, &branch_key) else {
return Err(RevParseError::NoUpstream {
branch: display_branch,
}
.into());
};
if remote == "." {
let m = merge.trim();
if m.starts_with("refs/") {
return Ok(m.to_owned());
}
return Ok(format!("refs/heads/{m}"));
}
let merge_branch = merge
.strip_prefix("refs/heads/")
.ok_or_else(|| Error::InvalidRef(format!("invalid merge ref: {merge}")))?;
let tracking = format!("refs/remotes/{remote}/{merge_branch}");
if repo.resolve_ref_name(&tracking).is_err() {
return Err(RevParseError::UpstreamNotTracked {
merge: merge.to_owned(),
}
.into());
}
Ok(tracking)
}
pub fn resolve_push_full_ref_for_branch(repo: &Repository, branch_short: &str) -> Result<String> {
let config_path = repo.common_git_dir().join("config");
let _ = repo.config()?;
let config_content = repo.caches().local_config_text(&config_path)?;
let upstream_tracking =
parse_branch_tracking(&config_content, branch_short).and_then(|(remote, merge)| {
if remote == "." {
return None;
}
let mb = merge.strip_prefix("refs/heads/").unwrap_or(&merge);
let tr = format!("refs/remotes/{remote}/{mb}");
if repo.resolve_ref_name(&tr).is_ok() {
Some(tr)
} else {
None
}
});
let push_remote = parse_config_value(&config_content, "remote", "pushRemote")
.or_else(|| parse_config_value(&config_content, "remote", "pushDefault"))
.or_else(|| {
let section = format!("[branch \"{}\"]", branch_short);
let mut in_section = false;
for line in config_content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
in_section = trimmed == section;
continue;
}
if in_section {
if let Some(v) = trimmed
.strip_prefix("pushremote = ")
.or_else(|| trimmed.strip_prefix("pushRemote = "))
{
return Some(v.trim().to_owned());
}
}
}
None
})
.or_else(|| {
parse_branch_tracking(&config_content, branch_short)
.map(|(r, _)| r)
.filter(|r| r != ".")
});
let Some(push_remote_name) = push_remote else {
return upstream_tracking.ok_or_else(|| RevParseError::NoPushRemote.into());
};
if remote_has_push_refspec(&config_content, &push_remote_name) {
return match push_refspec_mapped_tracking(&config_content, &push_remote_name, branch_short)
{
Some(mapped) => Ok(mapped),
None => Err(RevParseError::PushRefspecMissing {
remote: push_remote_name,
branch: branch_short.to_owned(),
}
.into()),
};
}
let push_default = parse_config_value(&config_content, "push", "default");
let push_default = push_default.as_deref().unwrap_or("simple");
if push_default == "nothing" {
return Err(RevParseError::PushDefaultNothing.into());
}
let current_tracking = format!("refs/remotes/{push_remote_name}/{branch_short}");
match push_default {
"upstream" => upstream_tracking.ok_or_else(|| {
RevParseError::NoUpstreamForPush {
branch: branch_short.to_owned(),
}
.into()
}),
"simple" => {
if let Some(ref up) = upstream_tracking {
if up == ¤t_tracking && repo.resolve_ref_name(¤t_tracking).is_ok() {
return Ok(current_tracking);
}
}
Err(RevParseError::PushDefaultSimpleMismatch.into())
}
_ => {
if repo.resolve_ref_name(¤t_tracking).is_ok() {
Ok(current_tracking)
} else if let Some(up) = upstream_tracking {
Ok(up)
} else {
Err(RevParseError::NoPushTrackingRef {
branch: branch_short.to_owned(),
}
.into())
}
}
}
}
fn remote_push_refspecs(config_content: &str, remote_name: &str) -> Vec<String> {
let section = format!("[remote \"{remote_name}\"]");
let mut in_section = false;
let mut out = Vec::new();
for line in config_content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
in_section = trimmed == section;
continue;
}
if !in_section {
continue;
}
if let Some(val) = trimmed
.strip_prefix("push = ")
.or_else(|| trimmed.strip_prefix("push="))
{
if let Some(spec) = val.split_whitespace().next() {
out.push(spec.trim().to_owned());
}
}
}
out
}
fn remote_has_push_refspec(config_content: &str, remote_name: &str) -> bool {
!remote_push_refspecs(config_content, remote_name).is_empty()
}
fn match_refname_with_pattern(pattern: &str, refname: &str, replacement: &str) -> Option<String> {
match (pattern.find('*'), replacement.find('*')) {
(Some(pstar), Some(vstar)) => {
let kprefix = &pattern[..pstar];
let ksuffix = &pattern[pstar + 1..];
if !refname.starts_with(kprefix) || !refname.ends_with(ksuffix) {
return None;
}
if refname.len() < kprefix.len() + ksuffix.len() {
return None;
}
let middle = &refname[kprefix.len()..refname.len() - ksuffix.len()];
Some(format!(
"{}{}{}",
&replacement[..vstar],
middle,
&replacement[vstar + 1..]
))
}
(None, None) => {
if pattern == refname {
Some(replacement.to_owned())
} else {
None
}
}
_ => None,
}
}
fn push_refspec_mapped_tracking(
config_content: &str,
remote_name: &str,
branch_short: &str,
) -> Option<String> {
let src = format!("refs/heads/{branch_short}");
let mut dst = None;
for spec in remote_push_refspecs(config_content, remote_name) {
let spec = spec.strip_prefix('+').unwrap_or(&spec);
let Some((left, right)) = spec.split_once(':') else {
continue;
};
if let Some(mapped) = match_refname_with_pattern(left.trim(), &src, right.trim()) {
dst = Some(mapped);
break;
}
}
let dst = dst?;
map_dest_to_tracking(config_content, remote_name, &dst)
}
fn map_dest_to_tracking(config_content: &str, remote_name: &str, dst: &str) -> Option<String> {
let section = format!("[remote \"{remote_name}\"]");
let mut in_section = false;
for line in config_content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
in_section = trimmed == section;
continue;
}
if !in_section {
continue;
}
let Some(val) = trimmed
.strip_prefix("fetch = ")
.or_else(|| trimmed.strip_prefix("fetch="))
else {
continue;
};
let Some(spec) = val.split_whitespace().next() else {
continue;
};
let spec = spec.trim().strip_prefix('+').unwrap_or(spec.trim());
let Some((left, right)) = spec.split_once(':') else {
continue;
};
if let Some(mapped) = match_refname_with_pattern(left.trim(), dst, right.trim()) {
return Some(mapped);
}
}
dst.strip_prefix("refs/heads/")
.map(|rest| format!("refs/remotes/{remote_name}/{rest}"))
}
fn resolve_push_ref_name(repo: &Repository, base: &str) -> Result<String> {
let (branch_key, _display) = resolve_upstream_branch_context(repo, base)?;
resolve_push_full_ref_for_branch(repo, &branch_key)
}
fn resolve_upstream_branch_context(repo: &Repository, base: &str) -> Result<(String, String)> {
let base = if base == "HEAD" {
Cow::Borrowed("")
} else if base.starts_with("@{-") && base.ends_with('}') {
if let Ok(Some(b)) = expand_at_minus_to_branch_name(repo, base) {
Cow::Owned(b)
} else {
Cow::Borrowed(base)
}
} else {
Cow::Borrowed(base)
};
let base = base.as_ref();
let base = if base == "@" { "" } else { base };
if base.is_empty() {
let Some(head) = refs::read_head(repo.git_dir())? else {
return Err(RevParseError::HeadNotBranch.into());
};
let Some(short) = head.strip_prefix("refs/heads/") else {
return Err(RevParseError::HeadNotBranch.into());
};
return Ok((short.to_owned(), short.to_owned()));
}
let head_branch = refs::read_head(repo.git_dir())?.and_then(|h| {
h.strip_prefix("refs/heads/")
.map(std::borrow::ToOwned::to_owned)
});
if head_branch.as_deref() == Some(base) {
return Ok((base.to_owned(), base.to_owned()));
}
let refname = format!("refs/heads/{base}");
if repo.resolve_ref_name(&refname).is_err() {
return Err(RevParseError::NoSuchBranch {
branch: base.to_owned(),
}
.into());
}
Ok((base.to_owned(), base.to_owned()))
}
fn parse_config_value(config: &str, section: &str, key: &str) -> Option<String> {
let section_header = format!("[{}]", section);
let key_lower = key.to_ascii_lowercase();
let mut in_section = false;
for line in config.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
in_section = trimmed.eq_ignore_ascii_case(§ion_header);
continue;
}
if in_section {
let lower = trimmed.to_ascii_lowercase();
if lower.starts_with(&key_lower) {
let rest = lower[key_lower.len()..].trim_start().to_string();
if rest.starts_with('=') {
if let Some(eq_pos) = trimmed.find('=') {
return Some(trimmed[eq_pos + 1..].trim().to_owned());
}
}
}
}
}
None
}
fn parse_branch_tracking(config: &str, branch: &str) -> Option<(String, String)> {
let mut remote = None;
let mut merge = None;
let mut in_section = false;
let target_section = format!("[branch \"{}\"]", branch);
for line in config.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
in_section = trimmed == target_section
|| trimmed.starts_with(&format!("[branch \"{}\"", branch));
continue;
}
if !in_section {
continue;
}
if let Some(value) = trimmed.strip_prefix("remote = ") {
remote = Some(value.trim().to_owned());
} else if let Some(value) = trimmed.strip_prefix("merge = ") {
merge = Some(value.trim().to_owned());
}
if let Some(value) = trimmed.strip_prefix("remote=") {
remote = Some(value.trim().to_owned());
} else if let Some(value) = trimmed.strip_prefix("merge=") {
merge = Some(value.trim().to_owned());
}
}
match (remote, merge) {
(Some(r), Some(m)) => Some((r, m)),
_ => None,
}
}
#[must_use]
pub fn load_graft_parents(git_dir: &Path) -> HashMap<ObjectId, Vec<ObjectId>> {
let graft_path = crate::repo::common_git_dir_for_config(git_dir).join("info/grafts");
let mut grafts = HashMap::new();
let Ok(contents) = fs::read_to_string(&graft_path) else {
return grafts;
};
for raw_line in contents.lines() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut fields = line.split_whitespace();
let Some(commit_hex) = fields.next() else {
continue;
};
let Ok(commit_oid) = commit_hex.parse::<ObjectId>() else {
continue;
};
let mut parents = Vec::new();
let mut valid = true;
for parent_hex in fields {
match parent_hex.parse::<ObjectId>() {
Ok(parent_oid) => parents.push(parent_oid),
Err(_) => {
valid = false;
break;
}
}
}
if valid {
grafts.insert(commit_oid, parents);
}
}
grafts
}
struct NavParentCache<'repo> {
repo: &'repo Repository,
grafts: HashMap<ObjectId, Vec<ObjectId>>,
graph: Option<crate::commit_graph_file::CommitGraphChain>,
}
impl<'repo> NavParentCache<'repo> {
fn new(repo: &'repo Repository) -> Self {
let grafts = load_graft_parents(repo.git_dir());
let graph = repo
.caches()
.commit_graph_chain(repo.odb().objects_dir())
.map(|arc| (*arc).clone());
Self {
repo,
grafts,
graph,
}
}
fn parents_of(&self, commit_oid: ObjectId) -> Result<Vec<ObjectId>> {
if let Some(grafted) = self.grafts.get(&commit_oid) {
return Ok(grafted.clone());
}
if let Some(graph) = &self.graph {
if let Some((parents, _)) = graph.graph_commit(&commit_oid) {
return Ok(parents);
}
}
let obj = self.repo.odb().read(&commit_oid)?;
if obj.kind != ObjectKind::Commit {
return Err(Error::InvalidRef(format!(
"invalid ref: {commit_oid} is not a commit"
)));
}
Ok(crate::objects::CommitRef::parse(&obj.data)?
.parents()
.to_vec())
}
}
pub fn commit_parents_for_navigation(
repo: &Repository,
commit_oid: ObjectId,
) -> Result<Vec<ObjectId>> {
NavParentCache::new(repo).parents_of(commit_oid)
}
#[derive(Debug, Clone, Copy)]
enum ParentShorthandKind {
At,
Bang,
Minus { exclude_parent: usize },
}
#[must_use]
pub fn spec_has_parent_shorthand_suffix(spec: &str) -> bool {
find_parent_shorthand(spec).is_some()
}
fn find_parent_shorthand(spec: &str) -> Option<(usize, ParentShorthandKind)> {
let mut best: Option<(usize, ParentShorthandKind, u8)> = None;
for (idx, _) in spec.match_indices('^') {
let Some(tail) = spec.get(idx + 1..) else {
continue;
};
if tail.starts_with('@') && idx + 2 == spec.len() {
best = Some((idx, ParentShorthandKind::At, 0));
break;
}
if tail.starts_with('!') && idx + 2 == spec.len() {
let cand = (idx, ParentShorthandKind::Bang, 1);
best = Some(match best {
Some(b) if b.2 < 1 => b,
_ => cand,
});
continue;
}
if let Some(after) = tail.strip_prefix('-') {
let (exclude_parent, valid) = if after.is_empty() {
(1usize, true)
} else if after.bytes().all(|b| b.is_ascii_digit()) && !after.is_empty() {
let n: usize = after.parse().unwrap_or(0);
(n, n >= 1)
} else {
(0, false)
};
if !valid {
continue;
}
let cand = (idx, ParentShorthandKind::Minus { exclude_parent }, 2);
best = Some(match best {
Some(b) if b.2 < 2 => b,
_ => cand,
});
}
}
best.map(|(i, k, _)| (i, k))
}
pub fn expand_parent_shorthand_rev_parse_lines(
repo: &Repository,
spec: &str,
symbolic: bool,
short_len: Option<usize>,
) -> Result<Option<Vec<String>>> {
let Some((mark_idx, kind)) = find_parent_shorthand(spec) else {
return Ok(None);
};
let base_spec = &spec[..mark_idx];
let base_for_resolve = if base_spec.is_empty() {
"HEAD"
} else {
base_spec
};
let symbolic_base = if base_spec.is_empty() {
"HEAD"
} else {
base_spec
};
let tip_oid = resolve_revision_for_range_end(repo, base_for_resolve)?;
let commit_oid = peel_to_commit_for_merge_base(repo, tip_oid)?;
let parents = commit_parents_for_navigation(repo, commit_oid)?;
let mut out = Vec::new();
match kind {
ParentShorthandKind::At => {
if parents.is_empty() {
return Ok(Some(out));
}
for (i, p) in parents.iter().enumerate() {
let parent_n = i + 1;
if symbolic {
out.push(format!("{symbolic_base}^{parent_n}"));
} else if let Some(len) = short_len {
out.push(abbreviate_object_id(repo, *p, len)?);
} else {
out.push(p.to_string());
}
}
}
ParentShorthandKind::Bang => {
if parents.is_empty() {
if symbolic {
out.push(symbolic_base.to_string());
} else if let Some(len) = short_len {
out.push(abbreviate_object_id(repo, commit_oid, len)?);
} else {
out.push(commit_oid.to_string());
}
return Ok(Some(out));
}
if symbolic {
out.push(symbolic_base.to_string());
for (i, _) in parents.iter().enumerate() {
let parent_n = i + 1;
out.push(format!("^{symbolic_base}^{parent_n}"));
}
} else if let Some(len) = short_len {
out.push(abbreviate_object_id(repo, commit_oid, len)?);
for p in &parents {
out.push(format!("^{}", abbreviate_object_id(repo, *p, len)?));
}
} else {
out.push(commit_oid.to_string());
for p in &parents {
out.push(format!("^{p}"));
}
}
}
ParentShorthandKind::Minus { exclude_parent } => {
if exclude_parent > parents.len() {
return Ok(None);
}
let excluded_parent = parents[exclude_parent - 1];
if symbolic {
out.push(symbolic_base.to_string());
out.push(format!("^{symbolic_base}^{exclude_parent}"));
} else if let Some(len) = short_len {
out.push(abbreviate_object_id(repo, commit_oid, len)?);
out.push(format!(
"^{}",
abbreviate_object_id(repo, excluded_parent, len)?
));
} else {
out.push(commit_oid.to_string());
out.push(format!("^{excluded_parent}"));
}
}
}
Ok(Some(out))
}
pub fn split_double_dot_range(spec: &str) -> Option<(&str, &str)> {
if spec == ".." {
return Some(("", ""));
}
let bytes = spec.as_bytes();
let mut search = 0usize;
while let Some(rel) = spec[search..].find("..") {
let idx = search + rel;
let touches_dot_before = idx > 0 && bytes[idx - 1] == b'.';
let touches_dot_after = idx + 2 < bytes.len() && bytes[idx + 2] == b'.';
if touches_dot_before || touches_dot_after {
search = idx + 1;
continue;
}
if idx + 2 < bytes.len() && (bytes[idx + 2] == b'/' || bytes[idx + 2] == b'\\') {
search = idx + 1;
continue;
}
let left = &spec[..idx];
let right = &spec[idx + 2..];
return Some((left, right));
}
None
}
#[must_use]
pub fn split_triple_dot_range(spec: &str) -> Option<(&str, &str)> {
if spec == "..." {
return Some(("", ""));
}
let bytes = spec.as_bytes();
let mut search = 0usize;
while let Some(rel) = spec[search..].find("...") {
let idx = search + rel;
let four_before = idx >= 1 && bytes[idx - 1] == b'.';
let four_after = idx + 3 < bytes.len() && bytes[idx + 3] == b'.';
if four_before || four_after {
search = idx + 1;
continue;
}
let left = &spec[..idx];
let right = &spec[idx + 3..];
return Some((left, right));
}
None
}
pub fn resolve_revision_without_index_dwim(repo: &Repository, spec: &str) -> Result<ObjectId> {
resolve_revision_impl(repo, spec, false, false, true, false, false, false, false)
}
pub fn resolve_revision(repo: &Repository, spec: &str) -> Result<ObjectId> {
resolve_revision_impl(repo, spec, true, false, true, false, false, false, true)
}
pub fn resolve_revision_for_checkout_guess(
repo: &Repository,
spec: &str,
remote_branch_guess: bool,
) -> Result<ObjectId> {
resolve_revision_impl(
repo,
spec,
true,
false,
true,
false,
false,
false,
remote_branch_guess,
)
}
pub fn resolve_revision_for_range_end(repo: &Repository, spec: &str) -> Result<ObjectId> {
resolve_revision_impl(repo, spec, true, true, true, false, false, false, true)
}
pub fn resolve_revision_for_range_end_without_index_dwim(
repo: &Repository,
spec: &str,
) -> Result<ObjectId> {
resolve_revision_impl(repo, spec, false, true, true, false, false, false, true)
}
pub fn resolve_revision_for_verify(repo: &Repository, spec: &str) -> Result<ObjectId> {
resolve_revision_impl(repo, spec, false, true, true, false, false, false, true)
}
pub fn resolve_revision_for_commit_tree_tree(repo: &Repository, spec: &str) -> Result<ObjectId> {
resolve_revision_impl(repo, spec, true, false, true, false, true, false, true)
}
pub fn resolve_revision_for_patch_old_blob(repo: &Repository, spec: &str) -> Result<ObjectId> {
resolve_revision_impl(repo, spec, true, false, true, false, false, true, true)
}
pub fn try_parse_double_dot_log_range(
repo: &Repository,
spec: &str,
) -> Result<Option<(ObjectId, ObjectId)>> {
let Some((left, right)) = split_double_dot_range(spec) else {
return Ok(None);
};
let left_tip = if left.is_empty() {
resolve_revision_for_range_end(repo, "HEAD")?
} else {
resolve_revision_for_range_end(repo, left)?
};
let right_tip = if right.is_empty() {
resolve_revision_for_range_end(repo, "HEAD")?
} else {
resolve_revision_for_range_end(repo, right)?
};
let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
Ok(Some((left_c, right_c)))
}
fn try_parse_double_dot_log_range_without_index_dwim(
repo: &Repository,
spec: &str,
) -> Result<Option<(ObjectId, ObjectId)>> {
let Some((left, right)) = split_double_dot_range(spec) else {
return Ok(None);
};
let left_tip = if left.is_empty() {
resolve_revision_for_range_end_without_index_dwim(repo, "HEAD")?
} else {
resolve_revision_for_range_end_without_index_dwim(repo, left)?
};
let right_tip = if right.is_empty() {
resolve_revision_for_range_end_without_index_dwim(repo, "HEAD")?
} else {
resolve_revision_for_range_end_without_index_dwim(repo, right)?
};
let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
Ok(Some((left_c, right_c)))
}
#[must_use]
pub fn revision_spec_contains_ancestry_navigation(spec: &str) -> bool {
let (_, steps) = parse_nav_steps(spec);
!steps.is_empty()
}
pub fn resolve_revision_as_commit(repo: &Repository, spec: &str) -> Result<ObjectId> {
if let Some((left, right)) = split_triple_dot_range(spec) {
let left_tip = if left.is_empty() {
resolve_revision_for_range_end(repo, "HEAD")?
} else {
resolve_revision_for_range_end(repo, left)?
};
let right_tip = if right.is_empty() {
resolve_revision_for_range_end(repo, "HEAD")?
} else {
resolve_revision_for_range_end(repo, right)?
};
let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
let bases = crate::merge_base::merge_bases_first_vs_rest(repo, left_c, &[right_c])?;
return bases
.into_iter()
.next()
.ok_or_else(|| Error::ObjectNotFound(format!("no merge base for '{spec}'")));
}
if let Some((_excl, tip)) = try_parse_double_dot_log_range(repo, spec)? {
return Ok(tip);
}
let oid = resolve_revision_for_range_end(repo, spec)?;
peel_to_commit_for_merge_base(repo, oid)
}
pub fn resolve_revision_as_commit_without_index_dwim(
repo: &Repository,
spec: &str,
) -> Result<ObjectId> {
if let Some((left, right)) = split_triple_dot_range(spec) {
let left_tip = if left.is_empty() {
resolve_revision_for_range_end_without_index_dwim(repo, "HEAD")?
} else {
resolve_revision_for_range_end_without_index_dwim(repo, left)?
};
let right_tip = if right.is_empty() {
resolve_revision_for_range_end_without_index_dwim(repo, "HEAD")?
} else {
resolve_revision_for_range_end_without_index_dwim(repo, right)?
};
let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
let bases = crate::merge_base::merge_bases_first_vs_rest(repo, left_c, &[right_c])?;
return bases
.into_iter()
.next()
.ok_or_else(|| Error::ObjectNotFound(format!("no merge base for '{spec}'")));
}
if let Some((_excl, tip)) = try_parse_double_dot_log_range_without_index_dwim(repo, spec)? {
return Ok(tip);
}
let oid = resolve_revision_for_range_end_without_index_dwim(repo, spec)?;
peel_to_commit_for_merge_base(repo, oid)
}
fn resolve_ref_dwim_for_rev_parse(repo: &Repository, spec: &str) -> (usize, Option<ObjectId>) {
const RULES: &[&str] = &[
"{0}",
"refs/{0}",
"refs/tags/{0}",
"refs/heads/{0}",
"refs/remotes/{0}",
"refs/remotes/{0}/HEAD",
];
let mut count = 0usize;
let mut first = None;
let refname_opts = RefNameOptions::default();
for rule in RULES {
let candidate = rule.replace("{0}", spec);
if let Ok(Some(target)) = refs::read_symbolic_ref_for_repository(repo, &candidate) {
if check_refname_format(&target, &refname_opts).is_err()
|| repo.resolve_ref_name(&target).is_err()
{
if candidate != "HEAD" {
repo.diagnostics()
.warn(crate::diagnostics::Warning::DanglingSymref {
name: candidate.clone(),
});
}
continue;
}
}
if let Ok(oid) = repo.resolve_ref_name(&candidate) {
count += 1;
if first.is_none() {
first = Some(oid);
}
}
}
(count, first)
}
#[expect(clippy::too_many_arguments)]
fn resolve_revision_impl(
repo: &Repository,
spec: &str,
index_dwim: bool,
commit_only_hex: bool,
use_disambiguate_config: bool,
treeish_colon_lhs: bool,
implicit_tree_abbrev: bool,
implicit_blob_abbrev: bool,
remote_branch_name_guess: bool,
) -> Result<ObjectId> {
if let Some(pattern) = spec.strip_prefix(":/") {
if pattern.is_empty() {
let head = crate::state::resolve_head_for_repository(repo)
.map_err(|_| Error::ObjectNotFound(":/".to_owned()))?;
return head
.oid()
.copied()
.ok_or_else(|| Error::ObjectNotFound(":/".to_owned()));
}
return resolve_commit_message_search(repo, pattern);
}
if let Some(index_spec) = parse_index_colon_spec(spec) {
let path = normalize_colon_path_for_tree(repo, index_spec.raw_path)?;
return resolve_index_path_at_stage(repo, &path, index_spec.stage)
.map_err(|e| diagnose_index_path_error(repo, &path, index_spec.stage, e));
}
if let Some(tag_path) = spec.strip_prefix("tags/") {
if !tag_path.is_empty() {
let tag_ref = format!("refs/tags/{tag_path}");
if let Ok(oid) = repo.resolve_ref_name(&tag_ref) {
return Ok(oid);
}
}
}
if spec == "AUTO_MERGE" {
let raw = fs::read_to_string(repo.git_dir().join("AUTO_MERGE")).map_err(|e| {
Error::from(RevParseError::AutoMergeRead {
detail: e.to_string(),
})
})?;
let line = raw.lines().next().unwrap_or("").trim();
return line
.parse::<ObjectId>()
.map_err(|_| Error::InvalidRef("AUTO_MERGE: invalid object id".to_owned()));
}
if spec.starts_with("refs/") && !spec.contains(':') {
if let Ok(oid) = repo.resolve_ref_name(spec) {
return Ok(oid);
}
}
if let Some(idx) = spec.find("...") {
let left_raw = &spec[..idx];
let right_raw = &spec[idx + 3..];
if !left_raw.is_empty() || !right_raw.is_empty() {
let left_oid = peel_to_commit_for_merge_base(
repo,
if left_raw.is_empty() {
resolve_revision_impl(
repo,
"HEAD",
index_dwim,
commit_only_hex,
use_disambiguate_config,
false,
false,
false,
remote_branch_name_guess,
)?
} else {
resolve_revision_impl(
repo,
left_raw,
index_dwim,
commit_only_hex,
use_disambiguate_config,
false,
false,
false,
remote_branch_name_guess,
)?
},
)?;
let right_oid = peel_to_commit_for_merge_base(
repo,
if right_raw.is_empty() {
resolve_revision_impl(
repo,
"HEAD",
index_dwim,
commit_only_hex,
use_disambiguate_config,
false,
false,
false,
remote_branch_name_guess,
)?
} else {
resolve_revision_impl(
repo,
right_raw,
index_dwim,
commit_only_hex,
use_disambiguate_config,
false,
false,
false,
remote_branch_name_guess,
)?
},
)?;
let bases = crate::merge_base::merge_bases_first_vs_rest(repo, left_oid, &[right_oid])?;
return bases
.into_iter()
.next()
.ok_or_else(|| Error::ObjectNotFound(format!("no merge base for '{spec}'")));
}
}
if let Some((before, after)) = split_treeish_colon(spec) {
if !before.is_empty() && !spec.starts_with(":/") {
let rev_oid = match resolve_revision_impl(
repo,
before,
index_dwim,
commit_only_hex,
use_disambiguate_config,
true,
false,
false,
remote_branch_name_guess,
) {
Ok(o) => o,
Err(Error::ObjectNotFound(s)) if s == before => {
return Err(RevParseError::InvalidObjectName {
name: before.to_owned(),
}
.into());
}
Err(Error::RevParse(RevParseError::AmbiguousArgument { .. })) => {
return Err(RevParseError::InvalidObjectName {
name: before.to_owned(),
}
.into());
}
Err(e) => return Err(e),
};
let tree_oid = peel_to_tree(repo, rev_oid)?;
if after.is_empty() {
return Ok(tree_oid);
}
let clean_path = match normalize_colon_path_for_tree(repo, after) {
Ok(p) => p,
Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
let wt = repo
.work_tree()
.and_then(|p| p.canonicalize().ok())
.map(|p| p.display().to_string())
.unwrap_or_default();
return Err(RevParseError::OutsideRepository {
path: after.to_owned(),
work_tree: wt,
}
.into());
}
Err(e) => return Err(e),
};
return resolve_tree_path_rev_parse(repo, &tree_oid, &clean_path)
.map_err(|e| diagnose_tree_path_error(repo, before, after, &clean_path, e));
}
}
let (base_with_nav, peel) = parse_peel_suffix(spec);
let (base, nav_steps) = parse_nav_steps(base_with_nav);
let peel_for_hex = peel
.or(((treeish_colon_lhs || implicit_tree_abbrev) && peel.is_none()).then_some("tree"))
.or((implicit_blob_abbrev && peel.is_none()).then_some("blob"));
let mut oid = resolve_base(
repo,
base,
index_dwim,
commit_only_hex,
use_disambiguate_config,
peel_for_hex,
implicit_tree_abbrev,
implicit_blob_abbrev,
remote_branch_name_guess,
)?;
let nav_cache = NavParentCache::new(repo);
for step in nav_steps {
oid = apply_nav_step(repo, &nav_cache, oid, step).map_err(|e| {
if matches!(e, Error::ObjectNotFound(_)) {
RevParseError::AmbiguousArgument {
spec: spec.to_owned(),
}
.into()
} else {
e
}
})?;
}
apply_peel(repo, oid, peel)
}
fn normalize_path_components(path: PathBuf) -> PathBuf {
let mut out = PathBuf::new();
for c in path.components() {
match c {
Component::Prefix(_) | Component::RootDir => out.push(c),
Component::CurDir => {}
Component::ParentDir => {
let _ = out.pop();
}
Component::Normal(x) => out.push(x),
}
}
out
}
fn normalize_colon_path_for_bare_tree(raw_path: &str) -> Result<String> {
let cwd_relative = raw_path.starts_with("./") || raw_path.starts_with("../") || raw_path == ".";
if cwd_relative {
return Err(Error::InvalidRef(
"relative path syntax can't be used outside working tree".to_owned(),
));
}
let s = raw_path.trim_start_matches('/');
let mut stack: Vec<&str> = Vec::new();
for part in s.split('/') {
if part.is_empty() || part == "." {
continue;
}
if part == ".." {
let _ = stack.pop();
} else {
stack.push(part);
}
}
Ok(stack.join("/"))
}
fn normalize_colon_path_for_tree(repo: &Repository, raw_path: &str) -> Result<String> {
let Some(work_tree) = repo.work_tree() else {
return normalize_colon_path_for_bare_tree(raw_path);
};
let cwd = repo.environment().cwd.clone();
let wt_canon = work_tree.canonicalize().map_err(Error::Io)?;
let cwd_relative = raw_path.starts_with("./") || raw_path.starts_with("../") || raw_path == ".";
if cwd_relative && !path_is_within(&cwd, work_tree) {
return Err(Error::InvalidRef(
"relative path syntax can't be used outside working tree".to_owned(),
));
}
let full = if raw_path.starts_with('/') {
PathBuf::from(raw_path)
} else if cwd_relative {
cwd.join(raw_path)
} else {
work_tree.join(raw_path)
};
let full = normalize_path_components(full);
if !path_is_within(&full, &wt_canon) {
return Err(Error::InvalidRef("outside repository".to_owned()));
}
let rel = full
.strip_prefix(&wt_canon)
.map_err(|_| Error::InvalidRef("outside repository".to_owned()))?;
let s = rel.to_string_lossy().replace('\\', "/");
Ok(s.trim_end_matches('/').to_owned())
}
pub fn peel_to_commit_for_merge_base(repo: &Repository, mut oid: ObjectId) -> Result<ObjectId> {
oid = apply_peel(repo, oid, Some(""))?;
let obj = repo.read_replaced(&oid)?;
match obj.kind {
ObjectKind::Commit => Ok(oid),
ObjectKind::Tree => Err(Error::InvalidRef(format!(
"object {oid} does not name a commit"
))),
ObjectKind::Blob => Err(Error::InvalidRef(format!(
"object {oid} does not name a commit"
))),
ObjectKind::Tag => Err(Error::InvalidRef("unexpected tag after peel".to_owned())),
}
}
pub fn try_peel_to_commit_for_merge_base(
repo: &Repository,
oid: ObjectId,
) -> Result<Option<ObjectId>> {
let oid = apply_peel(repo, oid, Some(""))?;
let obj = repo.odb().read(&oid)?;
match obj.kind {
ObjectKind::Commit => Ok(Some(oid)),
ObjectKind::Tree | ObjectKind::Blob => Ok(None),
ObjectKind::Tag => Err(Error::InvalidRef("unexpected tag after peel".to_owned())),
}
}
pub fn peel_to_tree(repo: &Repository, oid: ObjectId) -> Result<ObjectId> {
let obj = repo.read_replaced(&oid)?;
match obj.kind {
crate::objects::ObjectKind::Tree => Ok(oid),
crate::objects::ObjectKind::Commit => {
let commit = crate::objects::parse_commit(&obj.data)?;
Ok(commit.tree)
}
crate::objects::ObjectKind::Tag => {
let tag = crate::objects::parse_tag(&obj.data)?;
peel_to_tree(repo, tag.object)
}
_ => Err(Error::ObjectNotFound(format!(
"cannot peel {} to tree",
oid
))),
}
}
fn resolve_tree_path(repo: &Repository, tree_oid: &ObjectId, path: &str) -> Result<ObjectId> {
resolve_treeish_path_to_object(repo, *tree_oid, path)
}
#[derive(Debug)]
enum TreePathError {
NotInTree,
Other(Error),
}
impl From<Error> for TreePathError {
fn from(err: Error) -> Self {
Self::Other(err)
}
}
fn resolve_tree_path_rev_parse(
repo: &Repository,
tree_oid: &ObjectId,
path: &str,
) -> std::result::Result<ObjectId, TreePathError> {
let obj = repo.odb().read(tree_oid)?;
let entries = crate::objects::parse_tree(&obj.data)?;
let components: Vec<&str> = path.split('/').filter(|c| !c.is_empty()).collect();
if components.is_empty() {
return Err(Error::InvalidRef(format!(
"path '{path}' does not name an object in tree {tree_oid}"
))
.into());
}
let first = components[0];
let rest: Vec<&str> = components[1..].to_vec();
for entry in entries {
let name = String::from_utf8_lossy(&entry.name);
if name == first {
if rest.is_empty() {
return Ok(entry.oid);
}
if entry.mode != crate::index::MODE_TREE {
return Err(Error::ObjectNotFound(path.to_owned()).into());
}
return resolve_tree_path_rev_parse(repo, &entry.oid, &rest.join("/"));
}
}
Err(TreePathError::NotInTree)
}
#[derive(Debug, Clone)]
pub struct TreeishBlobAtPath {
pub path: String,
pub oid: ObjectId,
pub mode: String,
}
pub fn resolve_treeish_blob_at_path(repo: &Repository, spec: &str) -> Result<TreeishBlobAtPath> {
let (before, after) = split_treeish_colon(spec)
.ok_or_else(|| Error::InvalidRef(format!("'{spec}' is not a treeish:path revision")))?;
let rev_oid =
match resolve_revision_impl(repo, before, true, false, true, true, false, false, true) {
Ok(o) => o,
Err(Error::ObjectNotFound(s)) if s == before => {
return Err(RevParseError::InvalidObjectName {
name: before.to_owned(),
}
.into());
}
Err(Error::RevParse(RevParseError::AmbiguousArgument { .. })) => {
return Err(RevParseError::InvalidObjectName {
name: before.to_owned(),
}
.into());
}
Err(e) => return Err(e),
};
let tree_oid = peel_to_tree(repo, rev_oid)?;
if after.is_empty() {
return Ok(TreeishBlobAtPath {
path: String::new(),
oid: tree_oid,
mode: "040000".to_string(),
});
}
let clean_path = match normalize_colon_path_for_tree(repo, after) {
Ok(p) => p,
Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
let wt = repo
.work_tree()
.and_then(|p| p.canonicalize().ok())
.map(|p| p.display().to_string())
.unwrap_or_default();
return Err(RevParseError::OutsideRepository {
path: after.to_owned(),
work_tree: wt,
}
.into());
}
Err(e) => return Err(e),
};
let (oid, mode_str) = walk_tree_to_blob_entry(repo, &tree_oid, &clean_path)
.map_err(|e| diagnose_tree_path_error(repo, before, after, &clean_path, e))?;
Ok(TreeishBlobAtPath {
path: clean_path,
oid,
mode: mode_str,
})
}
fn walk_tree_to_blob_entry(
repo: &Repository,
tree_oid: &ObjectId,
path: &str,
) -> std::result::Result<(ObjectId, String), TreePathError> {
let obj = repo.read_replaced(tree_oid)?;
let entries = crate::objects::parse_tree(&obj.data)?;
let components: Vec<&str> = path.split('/').filter(|c| !c.is_empty()).collect();
if components.is_empty() {
return Err(Error::InvalidRef(format!(
"path '{path}' does not name a blob in tree {tree_oid}"
))
.into());
}
let first = components[0];
let rest: Vec<&str> = components[1..].to_vec();
for entry in entries {
let name = String::from_utf8_lossy(&entry.name);
if name == first {
if rest.is_empty() {
if entry.mode == crate::index::MODE_TREE {
return Err(Error::InvalidRef(format!("'{path}' is a tree, not a blob")).into());
}
return Ok((entry.oid, entry.mode_str()));
}
if entry.mode != crate::index::MODE_TREE {
return Err(Error::ObjectNotFound(path.to_owned()).into());
}
return walk_tree_to_blob_entry(repo, &entry.oid, &rest.join("/"));
}
}
Err(TreePathError::NotInTree)
}
#[derive(Debug, Clone, Copy)]
enum NavStep {
ParentN(usize),
AncestorN(usize),
}
fn parse_nav_steps(spec: &str) -> (&str, Vec<NavStep>) {
let mut steps = Vec::new();
let mut remaining = spec;
loop {
if let Some(tilde_pos) = remaining.rfind('~') {
let after = &remaining[tilde_pos + 1..];
if after.is_empty() {
steps.push(NavStep::AncestorN(1));
remaining = &remaining[..tilde_pos];
continue;
}
if after.bytes().all(|b| b.is_ascii_digit()) {
let n: usize = after.parse().unwrap_or(1);
steps.push(NavStep::AncestorN(n));
remaining = &remaining[..tilde_pos];
continue;
}
}
if let Some(caret_pos) = remaining.rfind('^') {
let after = &remaining[caret_pos + 1..];
if after.is_empty() {
steps.push(NavStep::ParentN(1));
remaining = &remaining[..caret_pos];
continue;
}
if after.bytes().all(|b| b.is_ascii_digit()) && !after.is_empty() {
let n: usize = after.parse().unwrap_or(usize::MAX);
steps.push(NavStep::ParentN(n));
remaining = &remaining[..caret_pos];
continue;
}
}
break;
}
steps.reverse();
(remaining, steps)
}
fn peel_annotated_tag_chain(repo: &Repository, mut oid: ObjectId) -> Result<ObjectId> {
loop {
let obj = repo.read_replaced(&oid)?;
if obj.kind != ObjectKind::Tag {
return Ok(oid);
}
let tag = parse_tag(&obj.data)?;
oid = tag.object;
}
}
fn apply_nav_step(
repo: &Repository,
nav: &NavParentCache<'_>,
oid: ObjectId,
step: NavStep,
) -> Result<ObjectId> {
match step {
NavStep::ParentN(0) => Ok(oid),
NavStep::ParentN(n) => {
let oid = peel_annotated_tag_chain(repo, oid)?;
let parents = nav.parents_of(oid)?;
parents
.get(n - 1)
.copied()
.ok_or_else(|| Error::ObjectNotFound(format!("{oid}^{n}")))
}
NavStep::AncestorN(n) => {
let mut current = peel_annotated_tag_chain(repo, oid)?;
for _ in 0..n {
let parents = nav.parents_of(current)?;
current = parents
.first()
.copied()
.ok_or_else(|| Error::ObjectNotFound(format!("{current}~1")))?;
}
Ok(current)
}
}
}
pub fn abbreviate_object_id(repo: &Repository, oid: ObjectId, min_len: usize) -> Result<String> {
abbreviate_object_id_with_prefix_cache(repo, oid, min_len, &mut HashMap::new())
}
pub(crate) fn abbreviate_object_id_with_prefix_cache(
repo: &Repository,
oid: ObjectId,
min_len: usize,
prefix_ambiguous: &mut HashMap<String, bool>,
) -> Result<String> {
let min_len = min_len.clamp(4, 40);
let target = oid.to_hex();
if !repo.odb().exists(&oid) {
return Ok(target[..min_len].to_owned());
}
for len in min_len..=40 {
let prefix = &target[..len];
if prefix_is_ambiguous_in_odb(repo, prefix, prefix_ambiguous)? {
continue;
}
return Ok(prefix.to_owned());
}
Ok(target)
}
pub(crate) fn prefix_is_ambiguous_in_odb(
repo: &Repository,
prefix: &str,
cache: &mut HashMap<String, bool>,
) -> Result<bool> {
if let Some(hit) = cache.get(prefix) {
return Ok(*hit);
}
let mut matches = Vec::new();
repo.odb().lookup_prefix(prefix, 2, &mut matches)?;
let amb = matches.len() > 1;
cache.insert(prefix.to_owned(), amb);
Ok(amb)
}
#[must_use]
pub fn to_relative_path(path: &Path, cwd: &Path) -> String {
let path_components = normalize_components(path);
let cwd_components = normalize_components(cwd);
let mut common = 0usize;
let max_common = path_components.len().min(cwd_components.len());
while common < max_common && path_components[common] == cwd_components[common] {
common += 1;
}
let mut parts = Vec::new();
let up_count = cwd_components.len().saturating_sub(common);
for _ in 0..up_count {
parts.push("..".to_owned());
}
for item in path_components.iter().skip(common) {
parts.push(item.clone());
}
if parts.is_empty() {
".".to_owned()
} else {
parts.join("/")
}
}
fn disambiguate_kind_rank(kind: ObjectKind) -> u8 {
match kind {
ObjectKind::Tag => 0,
ObjectKind::Commit => 1,
ObjectKind::Tree => 2,
ObjectKind::Blob => 3,
}
}
fn oid_satisfies_peel_filter(repo: &Repository, oid: ObjectId, peel_inner: &str) -> bool {
apply_peel(repo, oid, Some(peel_inner)).is_ok()
}
pub fn ambiguous_object_hint_lines(
repo: &Repository,
short_prefix: &str,
peel_filter: Option<&str>,
) -> Result<Vec<AmbiguousObjectHint>> {
let mut typed: Vec<(u8, String, &'static str)> = Vec::new();
let mut bad_hex: Vec<String> = Vec::new();
for oid in list_all_abbrev_matches(repo, short_prefix)? {
let hex = oid.to_hex();
match repo.read_replaced(&oid) {
Ok(obj) => {
let ok = peel_filter.is_none_or(|p| oid_satisfies_peel_filter(repo, oid, p));
if ok {
typed.push((disambiguate_kind_rank(obj.kind), hex, obj.kind.as_str()));
}
}
Err(_) => bad_hex.push(hex),
}
}
if typed.is_empty() && peel_filter.is_some() {
return ambiguous_object_hint_lines(repo, short_prefix, None);
}
bad_hex.sort();
typed.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
let mut out = Vec::new();
for h in bad_hex {
out.push(AmbiguousObjectHint {
oid_hex: h,
kind: None,
});
}
for (_, hex, kind) in typed {
out.push(AmbiguousObjectHint {
oid_hex: hex,
kind: Some(kind),
});
}
Ok(out)
}
fn read_core_disambiguate(repo: &Repository) -> Option<&'static str> {
let config = repo
.ensure_config_arc_fast()
.unwrap_or_else(|_| Arc::new(ConfigSet::new()));
let v = config.get("core.disambiguate")?;
match v.to_ascii_lowercase().as_str() {
"committish" | "commit" => Some("commit"),
"treeish" | "tree" => Some("tree"),
"blob" => Some("blob"),
"tag" => Some("tag"),
"none" => None,
_ => None,
}
}
fn warn_if_branch_refname_collides_with_abbrev_hex(
repo: &Repository,
spec: &str,
object_oid: ObjectId,
) {
if spec.len() >= repo.odb().hash_algo().hex_len() {
return;
}
let branch_ref = format!("refs/heads/{spec}");
let Ok(ref_oid) = repo.resolve_ref_name(&branch_ref) else {
return;
};
if ref_oid != object_oid {
repo.warn(crate::diagnostics::Warning::AmbiguousRefname {
spec: spec.to_owned(),
});
}
}
fn warn_if_hex_ref_collides_with_objects(repo: &Repository, spec: &str, ref_oid: ObjectId) {
if spec.len() >= repo.odb().hash_algo().hex_len() || !is_hex_prefix(spec) {
return;
}
let Ok(matches) = find_abbrev_matches(repo, spec) else {
return;
};
if matches.is_empty() {
return;
}
if matches.len() > 1 || matches[0] != ref_oid {
repo.warn(crate::diagnostics::Warning::AmbiguousRefname {
spec: spec.to_owned(),
});
}
}
fn disambiguate_hex_by_peel(
repo: &Repository,
spec: &str,
matches: &[ObjectId],
peel: &str,
) -> Result<ObjectId> {
let peel_some = Some(peel);
let filtered: Vec<ObjectId> = matches
.iter()
.copied()
.filter(|oid| apply_peel(repo, *oid, peel_some).is_ok())
.collect();
if filtered.len() == 1 {
return Ok(filtered[0]);
}
if filtered.is_empty() {
return Err(RevParseError::AmbiguousShortObject {
spec: spec.to_owned(),
}
.into());
}
let mut peeled_targets: HashSet<ObjectId> = HashSet::new();
for oid in &filtered {
if let Ok(p) = apply_peel(repo, *oid, peel_some) {
peeled_targets.insert(p);
}
}
if peeled_targets.len() == 1 {
let mut sorted = filtered;
sorted.sort_by_key(|o| o.to_hex());
return Ok(sorted[0]);
}
if peel == "commit" {
let mut by_peeled: HashMap<ObjectId, Vec<ObjectId>> = HashMap::new();
for oid in &filtered {
if let Ok(c) = apply_peel(repo, *oid, Some("commit")) {
by_peeled.entry(c).or_default().push(*oid);
}
}
if by_peeled.len() == 1 {
let mut reps: Vec<ObjectId> = by_peeled.into_values().next().unwrap_or_default();
reps.sort_by_key(|o| o.to_hex());
if let Some(oid) = reps.first().copied() {
return Ok(oid);
}
}
}
Err(RevParseError::AmbiguousShortObject {
spec: spec.to_owned(),
}
.into())
}
fn commit_reachable_closure(repo: &Repository, start: ObjectId) -> Result<HashSet<ObjectId>> {
use std::collections::VecDeque;
let mut seen = HashSet::new();
let mut q = VecDeque::from([start]);
while let Some(oid) = q.pop_front() {
if !seen.insert(oid) {
continue;
}
let obj = match repo.read_replaced(&oid) {
Ok(o) => o,
Err(_) => continue,
};
if obj.kind != ObjectKind::Commit {
continue;
}
let commit = match parse_commit(&obj.data) {
Ok(c) => c,
Err(_) => continue,
};
for p in &commit.parents {
q.push_back(*p);
}
}
Ok(seen)
}
fn describe_generation_count(
repo: &Repository,
head: ObjectId,
tag_commit: ObjectId,
) -> Result<usize> {
let from_tag = commit_reachable_closure(repo, tag_commit)?;
let from_head = commit_reachable_closure(repo, head)?;
Ok(from_head.difference(&from_tag).count())
}
fn may_be_describe_name(spec: &str) -> bool {
let Some((head, tail)) = spec.rsplit_once('-') else {
return false;
};
let Some(hex) = tail.strip_prefix(['g', 'G']) else {
return false;
};
!hex.is_empty() && hex.bytes().all(|b| b.is_ascii_hexdigit()) && head.contains('-')
}
fn try_resolve_describe_name(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
if !may_be_describe_name(spec) {
return Ok(None);
}
let re = Regex::new(r"(?i)^(.+)-(\d+)-g([0-9a-fA-F]+)$")
.map_err(|_| Error::from(RevParseError::InternalDescribeRegex))?;
let Some(caps) = re.captures(spec) else {
return Ok(None);
};
let tag_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let gen: usize = caps
.get(2)
.and_then(|m| m.as_str().parse().ok())
.unwrap_or(0);
let hex_abbrev = caps.get(3).map(|m| m.as_str()).unwrap_or("");
if tag_name.is_empty() || hex_abbrev.is_empty() {
return Ok(None);
}
let hex_lower = hex_abbrev.to_ascii_lowercase();
let mut commit_candidates: Vec<ObjectId> = find_abbrev_matches(repo, &hex_lower)?
.into_iter()
.filter(|oid| {
repo.odb()
.read(oid)
.is_ok_and(|o| o.kind == ObjectKind::Commit)
})
.collect();
commit_candidates.sort_by_key(|o| o.to_hex());
commit_candidates.dedup();
if let Ok(tag_oid) = repo
.resolve_ref_name(&format!("refs/tags/{tag_name}"))
.or_else(|_| repo.resolve_ref_name(tag_name))
{
let tag_commit = peel_to_commit_for_merge_base(repo, tag_oid)?;
let mut strict_candidates = commit_candidates
.iter()
.copied()
.filter(|oid| describe_generation_count(repo, *oid, tag_commit).ok() == Some(gen))
.collect::<Vec<_>>();
strict_candidates.sort_by_key(|o| o.to_hex());
if strict_candidates.len() == 1 {
return Ok(Some(strict_candidates[0]));
}
if strict_candidates.len() > 1 {
return Err(RevParseError::AmbiguousShortObject {
spec: hex_abbrev.to_owned(),
}
.into());
}
}
match commit_candidates.len() {
0 => Err(Error::ObjectNotFound(spec.to_owned())),
1 => Ok(Some(commit_candidates[0])),
_ => Err(RevParseError::AmbiguousShortObject {
spec: hex_abbrev.to_owned(),
}
.into()),
}
}
#[expect(clippy::too_many_arguments, clippy::only_used_in_recursion)]
fn resolve_base(
repo: &Repository,
spec: &str,
index_dwim: bool,
commit_only_hex: bool,
use_disambiguate_config: bool,
peel_for_disambig: Option<&str>,
implicit_tree_abbrev: bool,
implicit_blob_abbrev: bool,
remote_branch_name_guess: bool,
) -> Result<ObjectId> {
if spec == "@" {
return resolve_base(
repo,
"HEAD",
index_dwim,
commit_only_hex,
use_disambiguate_config,
peel_for_disambig,
implicit_tree_abbrev,
implicit_blob_abbrev,
remote_branch_name_guess,
);
}
if spec == "FETCH_HEAD" {
let path = repo.git_dir().join("FETCH_HEAD");
let content = std::fs::read_to_string(&path)
.map_err(|_| Error::ObjectNotFound("FETCH_HEAD".to_owned()))?;
let mut first_oid = None;
for line in content.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let mut parts = line.split('\t');
let Some(oid_hex) = parts.next() else {
continue;
};
if ObjectId::is_full_hex(oid_hex) {
let oid = oid_hex
.parse::<ObjectId>()
.map_err(|_| Error::InvalidRef("invalid FETCH_HEAD object id".to_owned()))?;
first_oid.get_or_insert(oid);
let not_for_merge = parts.next().is_some_and(|v| v == "not-for-merge");
if !not_for_merge {
return Ok(oid);
}
}
}
return first_oid.ok_or_else(|| Error::ObjectNotFound("FETCH_HEAD".to_owned()));
}
if let Some(after_open) = spec.strip_prefix("@{-") {
if let Some(close) = after_open.find('}') {
let n_str = &after_open[..close];
if let Ok(n) = n_str.parse::<usize>() {
if n >= 1 {
let suffix = &after_open[close + 1..];
if suffix.is_empty() {
if let Some(oid) = try_resolve_at_minus(repo, spec)? {
return Ok(oid);
}
} else {
let branch = resolve_at_minus_to_branch(repo, n)?;
let new_spec = format!("{branch}{suffix}");
return resolve_base(
repo,
&new_spec,
index_dwim,
commit_only_hex,
use_disambiguate_config,
peel_for_disambig,
implicit_tree_abbrev,
implicit_blob_abbrev,
remote_branch_name_guess,
);
}
}
}
}
}
if upstream_suffix_info(spec).is_some() {
let full_ref = resolve_upstream_symbolic_name(repo, spec)?;
return repo
.resolve_ref_name(&full_ref)
.map_err(|_| Error::ObjectNotFound(spec.to_owned()));
}
if let Some(oid) = try_resolve_reflog_index(repo, spec)? {
return Ok(oid);
}
if let Some(pattern) = spec.strip_prefix(":/") {
if !pattern.is_empty() {
return resolve_commit_message_search(repo, pattern);
}
}
if let Some(rest) = spec.strip_prefix(':') {
if !rest.is_empty() && !rest.starts_with('/') {
if rest.len() >= 3 && rest.as_bytes()[1] == b':' {
if let Some(stage_char) = rest.chars().next() {
if let Some(stage) = stage_char.to_digit(10) {
if stage <= 3 {
let raw_path = &rest[2..];
let path = match normalize_colon_path_for_tree(repo, raw_path) {
Ok(p) => p,
Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
let wt = repo
.work_tree()
.and_then(|p| p.canonicalize().ok())
.map(|p| p.display().to_string())
.unwrap_or_default();
return Err(RevParseError::OutsideRepository {
path: raw_path.to_owned(),
work_tree: wt,
}
.into());
}
Err(e) => return Err(e),
};
return resolve_index_path_at_stage(repo, &path, stage as u8).map_err(
|e| diagnose_index_path_error(repo, &path, stage as u8, e),
);
}
}
}
}
let clean_rest = match normalize_colon_path_for_tree(repo, rest) {
Ok(p) => p,
Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
let wt = repo
.work_tree()
.and_then(|p| p.canonicalize().ok())
.map(|p| p.display().to_string())
.unwrap_or_default();
return Err(RevParseError::OutsideRepository {
path: rest.to_owned(),
work_tree: wt,
}
.into());
}
Err(e) => return Err(e),
};
return resolve_index_path(repo, &clean_rest)
.map_err(|e| diagnose_index_path_error(repo, &clean_rest, 0, e));
}
}
if let Some((treeish, path)) = split_treeish_spec(spec) {
let root_oid = resolve_revision_impl(
repo,
treeish,
index_dwim,
commit_only_hex,
use_disambiguate_config,
false,
false,
false,
false,
)?;
return resolve_treeish_path_to_object(repo, root_oid, path);
}
if let Ok(oid) = spec.parse::<ObjectId>() {
let rn = format!("refs/heads/{spec}");
if repo.resolve_ref_name(&rn).is_ok() {
repo.warn(crate::diagnostics::Warning::AmbiguousRefname {
spec: spec.to_owned(),
});
}
return Ok(oid);
}
match try_resolve_describe_name(repo, spec) {
Ok(Some(oid)) => return Ok(oid),
Err(e) => return Err(e),
Ok(None) => {}
}
if is_hex_prefix(spec) && spec.len() < repo.odb().hash_algo().hex_len() {
let tag_ref = format!("refs/tags/{spec}");
if let Ok(oid) = repo.resolve_ref_name(&tag_ref) {
warn_if_hex_ref_collides_with_objects(repo, spec, oid);
return Ok(oid);
}
let branch_ref = format!("refs/heads/{spec}");
if let Ok(oid) = repo.resolve_ref_name(&branch_ref) {
warn_if_hex_ref_collides_with_objects(repo, spec, oid);
return Ok(oid);
}
}
if is_hex_prefix(spec) {
let matches = find_abbrev_matches(repo, spec)?;
if matches.is_empty() {
if (4..repo.odb().hash_algo().hex_len()).contains(&spec.len()) {
return Err(Error::ObjectNotFound(spec.to_owned()));
}
} else if matches.len() == 1 {
let oid = matches[0];
warn_if_branch_refname_collides_with_abbrev_hex(repo, spec, oid);
return Ok(oid);
} else if matches.len() > 1 {
if let Some(p) = peel_for_disambig {
let oid = disambiguate_hex_by_peel(repo, spec, &matches, p)?;
warn_if_branch_refname_collides_with_abbrev_hex(repo, spec, oid);
return Ok(oid);
}
if commit_only_hex {
let oid = disambiguate_hex_by_peel(repo, spec, &matches, "commit")?;
warn_if_branch_refname_collides_with_abbrev_hex(repo, spec, oid);
return Ok(oid);
}
if use_disambiguate_config {
if let Some(pref) = read_core_disambiguate(repo) {
if let Ok(oid) = disambiguate_hex_by_peel(repo, spec, &matches, pref) {
warn_if_branch_refname_collides_with_abbrev_hex(repo, spec, oid);
return Ok(oid);
}
}
}
return Err(RevParseError::AmbiguousShortObject {
spec: spec.to_owned(),
}
.into());
}
}
let (dwim_count, dwim_oid) = resolve_ref_dwim_for_rev_parse(repo, spec);
if dwim_count > 1 {
repo.warn(crate::diagnostics::Warning::AmbiguousRefname {
spec: spec.to_owned(),
});
}
if let Some(oid) = dwim_oid {
return Ok(oid);
}
if let Some(rest) = spec.strip_prefix("remotes/") {
let full = format!("refs/remotes/{rest}");
if let Ok(oid) = repo.resolve_ref_name(&full) {
return Ok(oid);
}
}
if !spec.contains('/')
&& !spec.starts_with('.')
&& spec != "HEAD"
&& spec != "FETCH_HEAD"
&& spec != "MERGE_HEAD"
&& spec != "CHERRY_PICK_HEAD"
&& spec != "REVERT_HEAD"
&& spec != "REBASE_HEAD"
&& spec != "AUTO_MERGE"
&& spec != "stash"
{
let local_branch = format!("refs/heads/{spec}");
if repo.resolve_ref_name(&local_branch).is_err() {
let remote_head = format!("refs/remotes/{spec}/HEAD");
if let Ok(oid) = repo.resolve_ref_name(&remote_head) {
return Ok(oid);
}
}
}
if spec == "stash" {
if let Ok(oid) = repo.resolve_ref_name("refs/stash") {
return Ok(oid);
}
}
let head_ref = format!("refs/heads/{spec}");
let tag_ref = format!("refs/tags/{spec}");
let head_oid = repo.resolve_ref_name(&head_ref).ok();
let tag_oid = repo.resolve_ref_name(&tag_ref).ok();
match (head_oid, tag_oid) {
(Some(h), Some(t)) if h != t => {
repo.warn(crate::diagnostics::Warning::AmbiguousRefname {
spec: spec.to_owned(),
});
return Ok(h);
}
(Some(h), _) => return Ok(h),
(None, Some(t)) => return Ok(t),
(None, None) => {}
}
if !spec.contains('/')
&& !spec.contains(':')
&& !spec.starts_with('.')
&& spec != "HEAD"
&& spec.len() <= 255
{
let mut ref_match: Option<ObjectId> = None;
for prefix in ["refs/heads/", "refs/tags/", "refs/remotes/", "refs/notes/"] {
let full = format!("{prefix}{spec}");
if let Ok(oid) = repo.resolve_ref_name(&full) {
ref_match = Some(oid);
break;
}
}
if let Some(oid) = ref_match {
return Ok(oid);
}
}
for candidate in &[format!("refs/remotes/{spec}"), format!("refs/notes/{spec}")] {
if let Ok(oid) = repo.resolve_ref_name(candidate) {
return Ok(oid);
}
}
if let Some(head_ref) = remote_tracking_head_symbolic_target(repo, spec) {
if let Ok(oid) = repo.resolve_ref_name(&head_ref) {
return Ok(oid);
}
}
if remote_branch_name_guess
&& !spec.contains('/')
&& spec != "HEAD"
&& spec != "FETCH_HEAD"
&& spec != "MERGE_HEAD"
{
const REMOTES: &str = "refs/remotes/";
if let Ok(remote_refs) = refs::list_refs(repo.git_dir(), REMOTES) {
let matches: Vec<ObjectId> = remote_refs
.into_iter()
.filter(|(r, _)| {
r.strip_prefix(REMOTES)
.is_some_and(|rest| rest == spec || rest.ends_with(&format!("/{spec}")))
})
.map(|(_, oid)| oid)
.collect();
if matches.len() == 1 {
return Ok(matches[0]);
}
if matches.len() > 1 {
return Err(Error::InvalidRef(format!(
"ambiguous refname '{spec}': matches multiple remote-tracking branches"
)));
}
}
}
if crate::change_id::is_change_id_spec_token(spec) {
return crate::change_id::resolve_commit_by_change_id_prefix(repo, spec);
}
if !spec.contains(':') && !spec.starts_with('-') {
if index_dwim {
if resolve_index_path(repo, spec).is_ok() {
if repo.work_tree().is_some() {
return Err(RevParseError::AmbiguousArgument {
spec: spec.to_owned(),
}
.into());
}
return resolve_index_path(repo, spec);
}
return Err(RevParseError::AmbiguousArgument {
spec: spec.to_owned(),
}
.into());
}
return Err(RevParseError::InvalidObjectName {
name: spec.to_owned(),
}
.into());
}
Err(Error::ObjectNotFound(spec.to_owned()))
}
fn read_reflog_for_repo(repo: &Repository, refname: &str) -> Result<Vec<ReflogEntry>> {
let mut entries = Vec::new();
repo.refs()
.for_each_reflog_entry(refname, false, &mut |entry| {
entries.push(entry.clone());
ControlFlow::Continue(())
})?;
Ok(entries)
}
fn resolve_at_minus_to_branch(repo: &Repository, n: usize) -> Result<String> {
let entries = read_reflog_for_repo(repo, "HEAD")?;
let mut count = 0usize;
for entry in entries.iter().rev() {
let msg = &entry.message;
if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
count += 1;
if count == n {
if let Some(to_pos) = rest.find(" to ") {
return Ok(rest[..to_pos].to_string());
}
}
}
}
Err(Error::InvalidRef(format!(
"@{{-{n}}}: only {count} checkout(s) in reflog"
)))
}
fn try_resolve_at_minus(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
if !spec.starts_with("@{-") || !spec.ends_with('}') {
return Ok(None);
}
let inner = &spec[3..spec.len() - 1];
let n: usize = match inner.parse() {
Ok(n) if n >= 1 => n,
_ => return Ok(None),
};
let entries = read_reflog_for_repo(repo, "HEAD")?;
let mut count = 0usize;
for entry in entries.iter().rev() {
let msg = &entry.message;
if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
count += 1;
if count == n {
if let Some(to_pos) = rest.find(" to ") {
let from_branch = &rest[..to_pos];
let ref_name = format!("refs/heads/{from_branch}");
if let Ok(oid) = repo.resolve_ref_name(&ref_name) {
return Ok(Some(oid));
}
if let Ok(oid) = from_branch.parse::<ObjectId>() {
if repo.odb().exists(&oid) {
return Ok(Some(oid));
}
}
if is_hex_prefix(from_branch) {
if let Ok(oid) = resolve_revision_for_range_end(repo, from_branch)
.and_then(|oid| peel_to_commit_for_merge_base(repo, oid))
{
return Ok(Some(oid));
}
}
return Err(Error::InvalidRef(format!(
"cannot resolve @{{-{n}}}: branch '{}' not found",
from_branch
)));
}
}
}
}
Err(Error::InvalidRef(format!(
"@{{-{n}}}: only {count} checkout(s) in reflog"
)))
}
#[derive(Debug, Clone)]
enum AtStep {
Index(usize),
Date(i64),
Upstream,
Push,
Now,
}
fn try_parse_at_step_inner(inner: &str, now_sec: i64) -> Option<AtStep> {
if inner.eq_ignore_ascii_case("u") || inner.eq_ignore_ascii_case("upstream") {
return Some(AtStep::Upstream);
}
if inner.eq_ignore_ascii_case("push") {
return Some(AtStep::Push);
}
if inner.eq_ignore_ascii_case("now") {
return Some(AtStep::Now);
}
if let Ok(n) = inner.parse::<usize>() {
return Some(AtStep::Index(n));
}
approxidate_at(inner, now_sec).map(AtStep::Date)
}
fn next_reflog_at_open(spec: &str, mut from: usize) -> Option<usize> {
let b = spec.as_bytes();
while let Some(rel) = spec[from..].find("@{") {
let i = from + rel;
if b.get(i + 2) == Some(&b'-') {
let after_open = i + 2;
let close = spec[after_open..].find('}').map(|j| after_open + j)?;
from = close + 1;
continue;
}
return Some(i);
}
None
}
fn split_reflog_at_chain(spec: &str, now_sec: i64) -> Option<(String, Vec<AtStep>)> {
let at = next_reflog_at_open(spec, 0)?;
let prefix = spec[..at].to_owned();
let mut steps = Vec::new();
let mut pos = at;
while pos < spec.len() {
let rest = &spec[pos..];
if !rest.starts_with("@{") {
return None;
}
if rest.as_bytes().get(2) == Some(&b'-') {
return None;
}
let inner_start = pos + 2;
let close = spec[inner_start..].find('}').map(|i| inner_start + i)?;
let inner = &spec[inner_start..close];
let step = try_parse_at_step_inner(inner, now_sec)?;
steps.push(step);
pos = close + 1;
}
if steps.is_empty() {
return None;
}
Some((prefix, steps))
}
fn dwim_refname(repo: &Repository, raw: &str) -> String {
if raw.is_empty() || raw == "HEAD" || raw.starts_with("refs/") {
return raw.to_owned();
}
if raw == "stash" && repo.resolve_ref_name("refs/stash").is_ok() {
return "refs/stash".to_owned();
}
let candidate = format!("refs/heads/{raw}");
if repo.resolve_ref_name(&candidate).is_ok() {
candidate
} else {
raw.to_owned()
}
}
fn reflog_display_name(refname_raw: &str, refname: &str) -> String {
if refname_raw.is_empty() {
if let Some(b) = refname.strip_prefix("refs/heads/") {
return b.to_owned();
}
return refname.to_owned();
}
refname_raw.to_owned()
}
fn resolve_reflog_oid(
repo: &Repository,
refname: &str,
refname_raw: &str,
index_or_date: ReflogSelector,
) -> Result<ObjectId> {
let mut entries = read_reflog_for_repo(repo, refname)?;
if refname == "HEAD" {
if let ReflogSelector::Index(index) = index_or_date {
if index >= entries.len() {
if let Ok(Some(RawRef::Symbolic(branch_ref))) = repo.refs().read_raw("HEAD") {
if let Ok(branch_entries) = read_reflog_for_repo(repo, &branch_ref) {
if index < branch_entries.len() {
entries = branch_entries;
}
}
}
}
}
}
let display = reflog_display_name(refname_raw, refname);
match index_or_date {
ReflogSelector::Index(index) => {
let len = entries.len();
if index == 0 {
if len == 0 {
if !repo.refs().reflog_exists(refname).unwrap_or(false) {
return Err(RevParseError::ReflogEmpty {
ref_display: display.clone(),
}
.into());
}
return repo.resolve_ref_name(refname).map_err(|_| {
RevParseError::ReflogEmpty {
ref_display: display.clone(),
}
.into()
});
}
return Ok(entries[len - 1].new_oid);
}
if len == 0 {
return Err(RevParseError::ReflogEmpty {
ref_display: display.clone(),
}
.into());
}
if index > len {
return Err(RevParseError::ReflogInsufficientEntries {
ref_display: display.clone(),
available: len,
}
.into());
}
let oid = entries[len - index].old_oid;
if oid.is_zero() {
return Err(RevParseError::ReflogInsufficientEntries {
ref_display: display.clone(),
available: len,
}
.into());
}
Ok(oid)
}
ReflogSelector::Date(target_ts) => {
if entries.is_empty() {
return Err(RevParseError::ReflogEmpty {
ref_display: display.clone(),
}
.into());
}
for entry in entries.iter().rev() {
let ts = parse_reflog_entry_timestamp(entry);
if let Some(t) = ts {
if t <= target_ts {
return Ok(entry.new_oid);
}
}
}
Ok(entries[0].new_oid)
}
}
}
fn resolve_at_minus_token_to_branch(repo: &Repository, token: &str) -> Result<Option<String>> {
if !token.starts_with("@{-") || !token.ends_with('}') {
return Ok(None);
}
let inner = &token[3..token.len() - 1];
let n: usize = inner
.parse()
.map_err(|_| Error::InvalidRef(format!("invalid N in @{{-N}} for '{token}'")))?;
if n < 1 {
return Ok(None);
}
Ok(Some(resolve_at_minus_to_branch(repo, n)?))
}
pub fn reflog_walk_refname(repo: &Repository, spec: &str) -> Result<Option<String>> {
let now_sec = repo.wall_clock_epoch();
let Some((prefix, steps)) = split_reflog_at_chain(spec, now_sec) else {
return Ok(None);
};
let prefix_resolved = if let Some(b) = resolve_at_minus_token_to_branch(repo, &prefix)? {
b
} else {
prefix.clone()
};
let mut current_spec = if prefix_resolved.is_empty() {
if let Ok(Some(b)) = refs::read_head(repo.git_dir()) {
if let Some(short) = b.strip_prefix("refs/heads/") {
short.to_owned()
} else {
"HEAD".to_owned()
}
} else {
"HEAD".to_owned()
}
} else {
prefix_resolved
};
let last_reflog_peel = steps
.iter()
.rposition(|s| matches!(s, AtStep::Index(_) | AtStep::Date(_) | AtStep::Now));
let limit = last_reflog_peel.unwrap_or(steps.len());
for step in steps.iter().take(limit) {
match step {
AtStep::Upstream => {
let base = if current_spec == "@" {
"HEAD"
} else {
current_spec.as_str()
};
let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{u}}"))?;
current_spec = full;
}
AtStep::Push => {
let base = if current_spec == "@" {
"HEAD"
} else {
current_spec.as_str()
};
let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{push}}"))?;
current_spec = full;
}
AtStep::Now | AtStep::Index(_) | AtStep::Date(_) => {}
}
}
Ok(Some(dwim_refname(repo, current_spec.as_str())))
}
pub fn resolve_reflog_walk_log_ref(repo: &Repository, r: &str) -> Result<String> {
if let Ok(Some(w)) = reflog_walk_refname(repo, r) {
return Ok(w);
}
if r == "HEAD" || r.starts_with("refs/") {
return Ok(r.to_string());
}
if r.starts_with("@{") {
if let Some(n_str) = r.strip_prefix("@{").and_then(|s| s.strip_suffix('}')) {
if let Some(stripped) = n_str.strip_prefix('-') {
if stripped.parse::<usize>().is_ok() {
if let Ok(branch) = refs::resolve_at_n_branch(repo.git_dir(), r) {
return Ok(format!("refs/heads/{branch}"));
}
}
}
}
return Ok(r.to_string());
}
let candidate = format!("refs/heads/{r}");
if repo.resolve_ref_name(&candidate).is_ok() {
Ok(candidate)
} else {
Ok(r.to_string())
}
}
fn try_resolve_reflog_index(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
let now_sec = repo.wall_clock_epoch();
let Some((prefix, steps)) = split_reflog_at_chain(spec, now_sec) else {
return Ok(None);
};
let prefix_resolved = if let Some(b) = resolve_at_minus_token_to_branch(repo, &prefix)? {
b
} else {
prefix.clone()
};
let mut current_spec = if prefix_resolved.is_empty() {
if let Ok(Some(b)) = refs::read_head(repo.git_dir()) {
if let Some(short) = b.strip_prefix("refs/heads/") {
short.to_owned()
} else {
"HEAD".to_owned()
}
} else {
"HEAD".to_owned()
}
} else {
prefix_resolved
};
for (i, step) in steps.iter().enumerate() {
match step {
AtStep::Upstream => {
let base = if current_spec == "@" {
"HEAD"
} else {
current_spec.as_str()
};
let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{u}}"))?;
current_spec = full;
}
AtStep::Push => {
let base = if current_spec == "@" {
"HEAD"
} else {
current_spec.as_str()
};
let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{push}}"))?;
current_spec = full;
}
AtStep::Now => {
let refname_raw = current_spec.as_str();
let refname = dwim_refname(repo, refname_raw);
let oid =
resolve_reflog_oid(repo, &refname, refname_raw, ReflogSelector::Index(0))?;
if i + 1 == steps.len() {
return Ok(Some(oid));
}
current_spec = oid.to_hex();
}
AtStep::Index(n) => {
let refname_raw = current_spec.as_str();
let refname = dwim_refname(repo, refname_raw);
let oid =
resolve_reflog_oid(repo, &refname, refname_raw, ReflogSelector::Index(*n))?;
if i + 1 == steps.len() {
return Ok(Some(oid));
}
current_spec = oid.to_hex();
}
AtStep::Date(ts) => {
let refname_raw = current_spec.as_str();
let refname = dwim_refname(repo, refname_raw);
let oid =
resolve_reflog_oid(repo, &refname, refname_raw, ReflogSelector::Date(*ts))?;
if i + 1 == steps.len() {
return Ok(Some(oid));
}
current_spec = oid.to_hex();
}
}
}
let refname_raw = current_spec.as_str();
let refname = dwim_refname(repo, refname_raw);
repo.resolve_ref_name(&refname)
.map(Some)
.map_err(|_| Error::ObjectNotFound(spec.to_owned()))
}
enum ReflogSelector {
Index(usize),
Date(i64),
}
fn parse_reflog_entry_timestamp(entry: &crate::reflog::ReflogEntry) -> Option<i64> {
let parts: Vec<&str> = entry.identity.rsplitn(3, ' ').collect();
if parts.len() >= 2 {
parts[1].parse::<i64>().ok()
} else {
None
}
}
#[must_use]
pub fn reflog_date_selector_timestamp(s: &str) -> Option<i64> {
reflog_date_selector_timestamp_at(s, crate::git_date::tm::process_wall_clock_sec())
}
pub fn reflog_date_selector_timestamp_at(s: &str, now_sec: i64) -> Option<i64> {
approxidate_at(s, now_sec)
}
fn approxidate_at(s: &str, now_ts: i64) -> Option<i64> {
let lower = s.trim().to_ascii_lowercase();
if lower.split_whitespace().next() == Some("now") {
if let Some(raw) = crate::environment::Environment::empty()
.var("GIT_COMMITTER_DATE")
.or_else(|| crate::environment::Environment::empty().var("GIT_AUTHOR_DATE"))
{
let mut it = raw.split_whitespace();
if let Some(ts) = it.next().and_then(|p| p.parse::<i64>().ok()) {
return Some(ts);
}
}
return Some(now_ts);
}
if let Ok((ts, _offset)) = crate::git_date::parse::parse_date_basic(s.trim()) {
return Some(ts as i64);
}
let relative = lower.replace('.', " ");
let parts: Vec<&str> = relative.split_whitespace().collect();
if parts.len() >= 2 {
let (n_str, unit) = if (parts.len() >= 3 && parts[2] == "ago") || parts.len() == 2 {
(parts[0], parts[1])
} else {
("", "")
};
if !n_str.is_empty() {
if let Ok(n) = n_str.parse::<i64>() {
let secs: Option<i64> = match unit.trim_end_matches('s') {
"second" => Some(n),
"minute" => Some(n * 60),
"hour" => Some(n * 3600),
"day" => Some(n * 86400),
"week" => Some(n * 604800),
"month" => Some(n * 2592000),
"year" => Some(n * 31536000),
_ => None,
};
if let Some(s) = secs {
return Some(now_ts - s);
}
}
}
}
let re_like = |input: &str| -> Option<i64> {
for (i, _) in input.char_indices() {
let rest = &input[i..];
if rest.len() >= 10 {
let bytes = rest.as_bytes();
if bytes[4] == b'-'
&& bytes[7] == b'-'
&& bytes[0..4].iter().all(|b| b.is_ascii_digit())
&& bytes[5..7].iter().all(|b| b.is_ascii_digit())
&& bytes[8..10].iter().all(|b| b.is_ascii_digit())
{
let year: i32 = rest[0..4].parse().ok()?;
let month: u8 = rest[5..7].parse().ok()?;
let day: u8 = rest[8..10].parse().ok()?;
let date = time::Date::from_calendar_date(
year,
time::Month::try_from(month).ok()?,
day,
)
.ok()?;
let dt = date.with_hms(0, 0, 0).ok()?;
let odt = dt.assume_utc();
return Some(odt.unix_timestamp());
}
}
}
None
};
re_like(s)
}
fn head_tree_oid(repo: &Repository) -> Result<ObjectId> {
let head_oid = repo.resolve_ref_name("HEAD")?;
peel_to_tree(repo, head_oid)
}
fn path_in_tree(repo: &Repository, tree_oid: ObjectId, path: &str) -> bool {
resolve_tree_path(repo, &tree_oid, path).is_ok()
}
fn path_in_index(repo: &Repository, path: &str, stage: u8) -> bool {
resolve_index_path_at_stage(repo, path, stage).is_ok()
}
fn diagnose_tree_path_error(
repo: &Repository,
rev_label: &str,
raw_after_colon: &str,
clean_path: &str,
err: TreePathError,
) -> Error {
if let TreePathError::Other(err) = err {
return err;
}
let rel_display: &str =
if raw_after_colon.starts_with("./") || raw_after_colon.starts_with("../") {
clean_path
} else {
raw_after_colon
};
if let Ok(head_tree) = head_tree_oid(repo) {
if path_in_tree(repo, head_tree, clean_path) {
return RevParseError::PathOnDiskNotInRevision {
path: rel_display.to_owned(),
revision: rev_label.to_owned(),
}
.into();
}
{
let cwd = repo.environment().cwd.clone();
let prefix = show_prefix(repo, &cwd);
let pfx = prefix.trim_end_matches('/');
if !pfx.is_empty() {
let candidate = if clean_path.is_empty() {
pfx.to_owned()
} else {
format!("{pfx}/{clean_path}")
};
if path_in_tree(repo, head_tree, &candidate) {
let alternate_spec = Some(format!("{rev_label}:{candidate}"));
return RevParseError::PathPrefixExists {
candidate,
path: rel_display.to_owned(),
alternate_spec,
}
.into();
}
}
}
let on_disk = repo
.work_tree()
.map(|wt| wt.join(clean_path))
.is_some_and(|p| p.exists());
let in_index = path_in_index(repo, clean_path, 0);
if on_disk || in_index {
return RevParseError::PathOnDiskNotInRevision {
path: rel_display.to_owned(),
revision: rev_label.to_owned(),
}
.into();
}
}
RevParseError::PathNotInRevision {
path: rel_display.to_owned(),
revision: rev_label.to_owned(),
}
.into()
}
fn diagnose_index_path_error(repo: &Repository, path: &str, stage: u8, err: Error) -> Error {
let Error::ObjectNotFound(_) = err else {
return err;
};
let work_path = repo
.work_tree()
.map(|wt| wt.join(path))
.filter(|p| p.exists());
let on_disk = work_path.is_some();
let in_head = head_tree_oid(repo)
.map(|t| path_in_tree(repo, t, path))
.unwrap_or(false);
let in_index = path_in_index(repo, path, 0);
let at_stage = path_in_index(repo, path, stage);
if stage > 0 && !in_index {
{
let cwd = repo.environment().cwd.clone();
let prefix = show_prefix(repo, &cwd);
let pfx = prefix.trim_end_matches('/');
if !pfx.is_empty() {
let candidate = if path.is_empty() {
pfx.to_owned()
} else {
format!("{pfx}/{path}")
};
if path_in_index(repo, &candidate, 0) && !path_in_index(repo, &candidate, stage) {
let alternate_spec = Some(format!(":0:{candidate}"));
return RevParseError::PathIndexPrefixMismatch {
candidate,
path: path.to_owned(),
alternate_spec,
}
.into();
}
}
}
return RevParseError::PathMissingFromWorktreeAndIndex {
path: path.to_owned(),
}
.into();
}
if stage > 0 && in_index && !at_stage {
return RevParseError::PathWrongIndexStage {
path: path.to_owned(),
stage,
alternate_spec: Some(format!(":0:{path}")),
}
.into();
}
if stage == 0 {
if !on_disk && !in_index {
{
let cwd = repo.environment().cwd.clone();
let prefix = show_prefix(repo, &cwd);
let pfx = prefix.trim_end_matches('/');
if !pfx.is_empty() {
let candidate = if path.is_empty() {
pfx.to_owned()
} else {
format!("{pfx}/{path}")
};
if path_in_index(repo, &candidate, 0) {
let alternate_spec = Some(format!(":0:{candidate}"));
return RevParseError::PathIndexPrefixMismatch {
candidate,
path: path.to_owned(),
alternate_spec,
}
.into();
}
}
}
return RevParseError::PathMissingFromWorktreeAndIndex {
path: path.to_owned(),
}
.into();
}
if on_disk && !in_index && !in_head {
return RevParseError::PathOnDiskNotInIndex {
path: path.to_owned(),
}
.into();
}
}
RevParseError::PathNotInIndex {
path: path.to_owned(),
}
.into()
}
fn resolve_index_path(repo: &Repository, path: &str) -> Result<ObjectId> {
resolve_index_path_at_stage(repo, path, 0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IndexColonSpec<'a> {
pub stage: u8,
pub raw_path: &'a str,
}
#[must_use]
pub fn parse_index_colon_spec(spec: &str) -> Option<IndexColonSpec<'_>> {
if !spec.starts_with(':') || spec.starts_with(":/") || spec.len() <= 1 {
return None;
}
let rest = &spec[1..];
if rest.is_empty() {
return None;
}
if rest.len() >= 3 && rest.as_bytes()[1] == b':' {
if let Some(stage_char) = rest.chars().next() {
if let Some(stage) = stage_char.to_digit(10) {
if stage <= 3 {
return Some(IndexColonSpec {
stage: stage as u8,
raw_path: &rest[2..],
});
}
}
}
}
Some(IndexColonSpec {
stage: 0,
raw_path: rest,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexPathEntry {
pub path: String,
pub oid: ObjectId,
pub mode: u32,
}
pub fn resolve_index_path_entry(repo: &Repository, spec: &str) -> Result<Option<IndexPathEntry>> {
let Some(colon) = parse_index_colon_spec(spec) else {
return Ok(None);
};
let path = match normalize_colon_path_for_tree(repo, colon.raw_path) {
Ok(p) => p,
Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
let wt = repo
.work_tree()
.and_then(|p| p.canonicalize().ok())
.map(|p| p.display().to_string())
.unwrap_or_default();
return Err(RevParseError::OutsideRepository {
path: colon.raw_path.to_owned(),
work_tree: wt,
}
.into());
}
Err(e) => return Err(e),
};
let index_path = if let Some(raw) = repo.environment().var("GIT_INDEX_FILE") {
let p = std::path::PathBuf::from(raw);
if p.is_absolute() {
p
} else {
let cwd = repo.environment().cwd.clone();
cwd.join(p)
}
} else {
repo.index_path()
};
use crate::index::Index;
let config = repo.config()?;
let index = Index::load_expand_sparse_with_config(&index_path, repo.odb(), &config)
.map_err(|_| Error::ObjectNotFound(format!(":{}:{}", colon.stage, path)))?;
let entry = index
.get(path.as_bytes(), colon.stage)
.ok_or_else(|| Error::ObjectNotFound(format!(":{}:{}", colon.stage, path)))?;
Ok(Some(IndexPathEntry {
path,
oid: entry.oid,
mode: entry.mode,
}))
}
fn resolve_index_path_at_stage(repo: &Repository, path: &str, stage: u8) -> Result<ObjectId> {
use crate::index::Index;
let index_path = if let Some(raw) = repo.environment().var("GIT_INDEX_FILE") {
let p = std::path::PathBuf::from(raw);
if p.is_absolute() {
p
} else {
let cwd = repo.environment().cwd.clone();
cwd.join(p)
}
} else {
repo.index_path()
};
let config = repo.config()?;
let index = Index::load_expand_sparse_with_config(&index_path, repo.odb(), &config)
.map_err(|_| Error::ObjectNotFound(format!(":{stage}:{path}")))?;
let lookup_path = if repo.pathspec_precompose_enabled() {
crate::unicode_normalization::precompose_utf8_path(path).into_owned()
} else {
path.to_owned()
};
match index.get(lookup_path.as_bytes(), stage) {
Some(entry) => Ok(entry.oid),
None => Err(Error::ObjectNotFound(format!(":{stage}:{path}"))),
}
}
pub fn split_treeish_colon(spec: &str) -> Option<(&str, &str)> {
if spec.starts_with(':') {
return None;
}
let bytes = spec.as_bytes();
let mut i = 0usize;
let mut peel_depth = 0usize;
while i < bytes.len() {
if i + 1 < bytes.len() && bytes[i] == b'^' && bytes[i + 1] == b'{' {
peel_depth += 1;
i += 2;
continue;
}
if peel_depth > 0 {
if bytes[i] == b'}' {
peel_depth -= 1;
}
i += 1;
continue;
}
if bytes[i] == b':' && i > 0 {
let before = &spec[..i];
let after = &spec[i + 1..];
if !before.is_empty() {
return Some((before, after)); }
}
i += 1;
}
None
}
pub(crate) fn split_treeish_spec(spec: &str) -> Option<(&str, &str)> {
split_treeish_colon(spec)
}
pub(crate) fn resolve_treeish_path_to_object(
repo: &Repository,
treeish: ObjectId,
path: &str,
) -> Result<ObjectId> {
let object = repo.read_replaced(&treeish)?;
let mut current_tree = match object.kind {
ObjectKind::Commit => parse_commit(&object.data)?.tree,
ObjectKind::Tree => treeish,
_ => {
return Err(Error::InvalidRef(format!(
"object {treeish} does not name a tree"
)))
}
};
let parts_vec: Vec<&str> = path.split('/').filter(|p| !p.is_empty()).collect();
if parts_vec.is_empty() {
return Ok(current_tree);
}
for (idx, part) in parts_vec.iter().enumerate() {
let tree_object = repo.read_replaced(¤t_tree)?;
if tree_object.kind != ObjectKind::Tree {
return Err(Error::CorruptObject(format!(
"object {current_tree} is not a tree"
)));
}
let entries = parse_tree(&tree_object.data)?;
let Some(entry) = entries.iter().find(|entry| entry.name == part.as_bytes()) else {
return Err(Error::ObjectNotFound(path.to_owned()));
};
if idx + 1 == parts_vec.len() {
return Ok(entry.oid);
}
if entry.mode != crate::index::MODE_TREE {
return Err(Error::ObjectNotFound(path.to_owned()));
}
current_tree = entry.oid;
}
Err(Error::ObjectNotFound(path.to_owned()))
}
pub(crate) fn resolve_treeish_path(
repo: &Repository,
treeish: ObjectId,
path: &str,
) -> Result<ObjectId> {
resolve_treeish_path_to_object(repo, treeish, path)
}
fn apply_peel(repo: &Repository, mut oid: ObjectId, peel: Option<&str>) -> Result<ObjectId> {
match peel {
None => Ok(oid),
Some(search) if search.starts_with('/') => {
let pattern = &search[1..];
if pattern.is_empty() {
return Err(Error::InvalidRef(
"empty commit message search pattern".to_owned(),
));
}
resolve_commit_message_search_from(repo, oid, pattern)
}
Some("") => {
while let Ok(obj) = repo.read_replaced(&oid) {
if obj.kind != ObjectKind::Tag {
break;
}
oid = parse_tag_target(&obj.data)?;
}
Ok(oid)
}
Some("commit") => {
oid = apply_peel(repo, oid, Some(""))?;
let obj = repo.read_replaced(&oid)?;
if obj.kind == ObjectKind::Commit {
Ok(oid)
} else {
Err(Error::InvalidRef("expected commit".to_owned()))
}
}
Some("tree") => {
oid = apply_peel(repo, oid, Some(""))?;
let obj = repo.read_replaced(&oid)?;
match obj.kind {
ObjectKind::Tree => Ok(oid),
ObjectKind::Commit => Ok(parse_commit(&obj.data)?.tree),
_ => Err(Error::InvalidRef("expected tree or commit".to_owned())),
}
}
Some("blob") => {
let mut cur = oid;
loop {
let obj = repo.read_replaced(&cur)?;
match obj.kind {
ObjectKind::Blob => return Ok(cur),
ObjectKind::Tag => {
cur = parse_tag_target(&obj.data)?;
}
_ => return Err(Error::InvalidRef("expected blob".to_owned())),
}
}
}
Some("object") => Ok(oid),
Some("tag") => {
let obj = repo.read_replaced(&oid)?;
if obj.kind == ObjectKind::Tag {
Ok(oid)
} else {
Err(Error::InvalidRef("expected tag".to_owned()))
}
}
Some(other) => Err(Error::InvalidRef(format!(
"unsupported peel operator '{{{other}}}'"
))),
}
}
pub fn expand_rev_token_circ_bang(repo: &Repository, token: &str) -> Result<Vec<String>> {
let Some(base) = token.strip_suffix("^!") else {
return Ok(vec![token.to_owned()]);
};
if base.is_empty() {
return Err(RevParseError::AmbiguousArgument {
spec: token.to_owned(),
}
.into());
}
let oid = resolve_revision_for_range_end(repo, base)?;
let commit_oid = peel_to_commit_for_merge_base(repo, oid)?;
let parents = commit_parents_for_navigation(repo, commit_oid)?;
let mut out = vec![base.to_owned()];
for p in parents {
out.push(format!("^{}", p.to_hex()));
}
Ok(out)
}
#[must_use]
pub fn parse_peel_suffix(spec: &str) -> (&str, Option<&str>) {
if let Some(base) = spec.strip_suffix("^{}") {
return (base, Some(""));
}
if let Some(start) = spec.rfind("^{") {
if spec.ends_with('}') {
let base = &spec[..start];
let op = &spec[start + 2..spec.len() - 1];
return (base, Some(op));
}
}
if let Some(base) = spec.strip_suffix("^0") {
if !base.ends_with('^') {
return (base, Some("commit"));
}
}
(spec, None)
}
fn parse_tag_target(data: &[u8]) -> Result<ObjectId> {
let text = std::str::from_utf8(data)
.map_err(|_| Error::CorruptObject("invalid tag object".to_owned()))?;
let Some(line) = text.lines().find(|line| line.starts_with("object ")) else {
return Err(Error::CorruptObject("tag missing object header".to_owned()));
};
let oid_text = line.trim_start_matches("object ").trim();
oid_text.parse::<ObjectId>()
}
fn parse_oneline_pattern(pattern: &str) -> Option<(bool, &str)> {
let Some(after_bang) = pattern.strip_prefix('!') else {
return Some((false, pattern));
};
if let Some(neg) = after_bang.strip_prefix('-') {
return Some((true, neg));
}
if after_bang.starts_with('!') {
return Some((false, after_bang));
}
None
}
fn resolve_commit_message_search_from(
repo: &Repository,
start: ObjectId,
pattern: &str,
) -> Result<ObjectId> {
let Some((negate, effective_pattern)) = parse_oneline_pattern(pattern) else {
return Err(Error::ObjectNotFound(format!(":/{pattern}")));
};
let regex = Regex::new(effective_pattern).ok();
let mut visited = std::collections::HashSet::new();
let mut queue = std::collections::VecDeque::new();
queue.push_back(start);
visited.insert(start);
while let Some(oid) = queue.pop_front() {
let obj = match repo.read_replaced(&oid) {
Ok(o) => o,
Err(_) => continue,
};
if obj.kind != ObjectKind::Commit {
continue;
}
let commit = match parse_commit(&obj.data) {
Ok(c) => c,
Err(_) => continue,
};
let base_match = if let Some(re) = ®ex {
re.is_match(&commit.message)
} else {
commit.message.contains(effective_pattern)
};
let is_match = negate ^ base_match;
if is_match {
return Ok(oid);
}
for parent in &commit.parents {
if visited.insert(*parent) {
queue.push_back(*parent);
}
}
}
Err(Error::ObjectNotFound(format!(":/{pattern}")))
}
fn find_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
let max_len = repo.odb().hash_algo().hex_len();
if !is_hex_prefix(prefix) || !(4..=max_len).contains(&prefix.len()) {
return Ok(Vec::new());
}
let mut matches = Vec::new();
repo.odb().lookup_prefix(prefix, 0, &mut matches)?;
Ok(matches)
}
fn is_hex_prefix(text: &str) -> bool {
!text.is_empty() && text.chars().all(|ch| ch.is_ascii_hexdigit())
}
fn path_is_within(path: &Path, container: &Path) -> bool {
if path == container {
return true;
}
path.starts_with(container)
}
fn normalize_components(path: &Path) -> Vec<String> {
path.components()
.filter_map(|component| match component {
Component::RootDir => Some(String::from("/")),
Component::Normal(item) => Some(item.to_string_lossy().into_owned()),
_ => None,
})
.collect()
}
fn component_to_text(component: Component<'_>) -> Option<String> {
match component {
Component::Normal(item) => Some(os_to_string(item)),
_ => None,
}
}
fn os_to_string(text: &OsStr) -> String {
text.to_string_lossy().into_owned()
}
fn resolve_commit_message_search(
repo: &crate::repo::Repository,
pattern: &str,
) -> Result<ObjectId> {
let (negate, effective_pattern) = if let Some(rest) = pattern.strip_prefix('!') {
if rest.starts_with('!') {
(false, rest) } else {
(true, rest) }
} else {
(false, pattern)
};
let regex = Regex::new(effective_pattern).ok();
use crate::state::resolve_head;
let head =
resolve_head(repo.git_dir()).map_err(|_| Error::ObjectNotFound(format!(":/{pattern}")))?;
let start_oid = match head.oid() {
Some(oid) => *oid,
None => return Err(Error::ObjectNotFound(format!(":/{pattern}"))),
};
let mut visited = std::collections::HashSet::new();
let mut queue = std::collections::VecDeque::new();
queue.push_back(start_oid);
visited.insert(start_oid);
if let Ok(refs) = crate::refs::list_refs(repo.git_dir(), "refs/") {
for (_name, oid) in refs {
if visited.insert(oid) {
queue.push_back(oid);
}
}
}
while let Some(oid) = queue.pop_front() {
let obj = match repo.read_replaced(&oid) {
Ok(o) => o,
Err(_) => continue,
};
if obj.kind != ObjectKind::Commit {
continue;
}
let commit = match parse_commit(&obj.data) {
Ok(c) => c,
Err(_) => continue,
};
let base_match = if let Some(re) = ®ex {
re.is_match(&commit.message)
} else {
commit.message.contains(effective_pattern)
};
let is_match = if negate { !base_match } else { base_match };
if is_match {
return Ok(oid);
}
for parent in &commit.parents {
if visited.insert(*parent) {
queue.push_back(*parent);
}
}
}
Err(Error::ObjectNotFound(format!(":/{pattern}")))
}
pub fn list_all_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
find_abbrev_matches(repo, prefix)
}
pub fn list_loose_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
list_all_abbrev_matches(repo, prefix)
}
#[cfg(test)]
mod line_coverage_tests {
use super::*;
use crate::index::{entry_from_stat, Index, IndexEntry, MODE_REGULAR};
use crate::objects::ObjectId;
use crate::objects::ObjectKind;
use crate::odb::Odb;
use crate::ref_storage::RefStorageFormat;
use crate::repo::{init_repository, InitOptions, Repository};
use crate::write_tree::write_tree_from_index;
use std::fs;
use tempfile::tempdir;
fn ambiguous_hex_prefix_among(
oids: &[ObjectId],
min_len: usize,
max_len: usize,
) -> Option<String> {
for i in 0..oids.len() {
for j in (i + 1)..oids.len() {
let a = oids[i].to_hex();
let b = oids[j].to_hex();
let cap = max_len.min(a.len()).min(b.len());
for len in (min_len..=cap).rev() {
if a[..len] == b[..len] {
return Some(a[..len].to_string());
}
}
}
}
None
}
fn seed_commits_until_ambiguous_prefix(
repo: &Repository,
tree: ObjectId,
oids: &mut Vec<ObjectId>,
min_len: usize,
max_commits: usize,
) -> String {
let mut counter = oids.len() as i32;
while oids.len() < max_commits && max_commits > 0 {
let body = format!(
"tree {tree}\nauthor T <t@e.com> {counter} +0000\ncommitter T <t@e.com> {counter} +0000\n\nc{counter}\n"
);
counter += 1;
let oid = repo
.odb()
.write_loose_materialize(ObjectKind::Commit, body.as_bytes())
.expect("commit");
oids.push(oid);
if let Some(prefix) = ambiguous_hex_prefix_among(oids, min_len, 12) {
return prefix;
}
}
panic!("could not find {min_len}+ hex prefix collision among {max_commits} commits");
}
fn exercise_resolve_base(repo: &Repository, spec: &str) {
for index_dwim in [false, true] {
for commit_only in [false, true] {
for use_disambiguate in [false, true] {
for implicit_tree in [false, true] {
for implicit_blob in [false, true] {
for remote_guess in [false, true] {
let _ = resolve_base(
repo,
spec,
index_dwim,
commit_only,
use_disambiguate,
None,
implicit_tree,
implicit_blob,
remote_guess,
);
}
}
}
}
}
}
}
#[test]
fn parser_helpers_and_config_fragments() {
assert_eq!(abbreviate_ref_name("refs/heads/main"), "main");
assert!(upstream_suffix_info("main@{upstream}").is_some());
assert!(spec_has_parent_shorthand_suffix("main^@"));
assert!(revision_spec_contains_ancestry_navigation("HEAD~3"));
let (base, peel) = parse_peel_suffix("v1^{tree}");
assert_eq!(base, "v1");
assert_eq!(peel, Some("tree"));
assert_eq!(
split_treeish_colon("HEAD:path/to"),
Some(("HEAD", "path/to"))
);
assert!(parse_index_colon_spec(":2:path").is_some());
assert!(split_double_dot_range("a..b").is_some());
assert!(split_triple_dot_range("a...b").is_some());
assert!(!split_double_dot_range("a...b").is_some());
let cfg = "[remote \"origin\"]\n\turl = /x\n\tpush = refs/heads/*:refs/heads/*\n";
assert!(!remote_push_refspecs(cfg, "origin").is_empty());
assert!(remote_has_push_refspec(cfg, "origin"));
assert_eq!(
match_refname_with_pattern("refs/heads/*", "refs/heads/main", "refs/remotes/origin/*"),
Some("refs/remotes/origin/main".into())
);
assert_eq!(
parse_config_value(cfg, "remote \"origin\"", "url").as_deref(),
Some("/x")
);
assert!(parse_branch_tracking(
"[branch \"main\"]\n\tremote = origin\n\tmerge = refs/heads/main\n",
"main"
)
.is_some());
let (root, steps) = parse_nav_steps("main~3^2");
assert_eq!(root, "main");
assert_eq!(steps.len(), 2);
assert!(find_parent_shorthand("main^!").is_some());
assert!(reflog_date_selector_timestamp_at("1.day.ago", 1_700_000_000).is_some());
assert!(next_reflog_at_open("main@{1}@{0}", 0).is_some());
}
#[test]
fn bare_repo_resolve_variants() -> crate::error::Result<()> {
let dir = tempdir().map_err(crate::error::Error::Io)?;
Repository::init(
dir.path(),
&InitOptions {
bare: true,
ref_storage: crate::RefStorageFormat::Files,
..InitOptions::default()
},
)?;
let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
let body = format!(
"tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nm\n"
);
let tip = repo
.odb()
.write_loose_materialize(ObjectKind::Commit, body.as_bytes())?;
fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
.map_err(crate::error::Error::Io)?;
for spec in ["main", "HEAD", "HEAD^0", "main^{commit}", "main~0"] {
let _ = resolve_revision_for_verify(&repo, spec)?;
let _ = resolve_revision_without_index_dwim(&repo, spec)?;
let _ = resolve_revision_for_range_end(&repo, spec)?;
let _ = resolve_revision_as_commit(&repo, spec)?;
}
let _ = peel_to_tree(&repo, tip)?;
let _ = try_parse_double_dot_log_range(&repo, "main..main")?;
Ok(())
}
#[test]
fn deep_worktree_chain_and_index_specs() -> crate::error::Result<()> {
let dir = tempdir().map_err(crate::error::Error::Io)?;
let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
let blob = repo
.odb()
.write_loose_materialize(ObjectKind::Blob, b"line\n")?;
fs::write(dir.path().join("f.txt"), b"line\n").map_err(crate::error::Error::Io)?;
let entry = entry_from_stat(&dir.path().join("f.txt"), b"f.txt", blob, MODE_REGULAR)?;
let mut index = Index::new();
index.entries_mut().push(entry);
index.write_to_path(&repo.git_dir().join("index"), false)?;
let tree = write_tree_from_index(&repo.odb(), &index, "")?;
let mut tip = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nroot\n"
)
.as_bytes(),
)?;
for i in 1..=120_i64 {
let body = format!(
"tree {tree}\nparent {tip}\nauthor T <t@e.com> {} +0000\ncommitter T <t@e.com> {} +0000\n\nc{i}\n",
100 + i,
100 + i
);
tip = repo
.odb()
.write_loose_materialize(ObjectKind::Commit, body.as_bytes())?;
}
fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
.map_err(crate::error::Error::Io)?;
for n in 0..120 {
for spec in [
format!("HEAD~{n}"),
format!("main~{n}"),
format!("HEAD~{n}^{{tree}}"),
format!("HEAD~{n}^{{commit}}"),
] {
exercise_resolve_base(&repo, &spec);
exercise_impl_matrix(&repo, &spec);
let _ = resolve_revision_for_verify(&repo, &spec);
let _ = resolve_revision_for_checkout_guess(&repo, &spec, true);
}
}
for spec in [":f.txt", ":0:f.txt", "HEAD:f.txt", "f.txt", ":/c80"] {
exercise_resolve_base(&repo, spec);
exercise_impl_matrix(&repo, spec);
}
Ok(())
}
#[test]
fn deep_chain_resolve_exercises_navigation() -> crate::error::Result<()> {
let dir = tempdir().map_err(crate::error::Error::Io)?;
Repository::init(
dir.path(),
&InitOptions {
bare: true,
ref_storage: crate::RefStorageFormat::Files,
..InitOptions::default()
},
)?;
let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
let mut tip = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nroot\n"
)
.as_bytes(),
)?;
for i in 1..=100_i64 {
let body = format!(
"tree {tree}\nparent {tip}\nauthor T <t@e.com> {} +0000\ncommitter T <t@e.com> {} +0000\n\nc{i}\n",
100 + i,
100 + i
);
tip = repo
.odb()
.write_loose_materialize(ObjectKind::Commit, body.as_bytes())?;
}
fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
.map_err(crate::error::Error::Io)?;
for n in 0..100 {
let spec = format!("HEAD~{n}");
let _ = resolve_revision_for_verify(&repo, &spec)?;
let _ = resolve_revision(&repo, &spec)?;
let _ = resolve_revision_for_checkout_guess(&repo, &spec, true)?;
}
Ok(())
}
#[test]
fn push_and_tracking_config_helpers() {
let cfg = "[remote \"origin\"]\n\turl = /x\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n\tpush = refs/heads/*:refs/heads/*\n";
assert!(push_refspec_mapped_tracking(cfg, "origin", "main").is_some());
assert!(map_dest_to_tracking(cfg, "origin", "refs/heads/main").is_some());
let norm = normalize_path_components(std::path::PathBuf::from("./a/../b"));
assert!(norm.to_string_lossy().contains('b'));
assert_eq!(
normalize_colon_path_for_bare_tree("x/y").expect("bare norm"),
"x/y"
);
}
#[test]
fn reflog_chain_and_at_step_parsers() {
assert!(try_parse_at_step_inner("0", 1_700_000_000).is_some());
assert!(try_parse_at_step_inner("now", 1_700_000_000).is_some());
assert!(split_reflog_at_chain("main@{1}@{0}", 1_700_000_000).is_some());
assert!(next_reflog_at_open("HEAD@{2}", 0).is_some());
let (root, steps) = parse_nav_steps("HEAD~2^2^1");
assert_eq!(root, "HEAD");
assert_eq!(steps.len(), 3);
}
fn exercise_impl_matrix(repo: &Repository, spec: &str) {
for index_dwim in [false, true] {
for commit_only in [false, true] {
for use_disambiguate in [false, true] {
for treeish_colon_lhs in [false, true] {
for implicit_tree_abbrev in [false, true] {
for implicit_blob_abbrev in [false, true] {
for remote_guess in [false, true] {
let _ = resolve_revision_impl(
repo,
spec,
index_dwim,
commit_only,
use_disambiguate,
treeish_colon_lhs,
implicit_tree_abbrev,
implicit_blob_abbrev,
remote_guess,
);
}
}
}
}
}
}
}
}
#[test]
fn resolve_revision_impl_flag_matrix() -> crate::error::Result<()> {
let dir = tempdir().map_err(crate::error::Error::Io)?;
Repository::init(
dir.path(),
&InitOptions {
bare: true,
ref_storage: crate::RefStorageFormat::Files,
..InitOptions::default()
},
)?;
let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
let mut tip = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nroot\n"
)
.as_bytes(),
)?;
for i in 1..=60_i64 {
let body = format!(
"tree {tree}\nparent {tip}\nauthor T <t@e.com> {} +0000\ncommitter T <t@e.com> {} +0000\n\nc{i}\n",
100 + i,
100 + i
);
tip = repo
.odb()
.write_loose_materialize(ObjectKind::Commit, body.as_bytes())?;
}
fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
.map_err(crate::error::Error::Io)?;
fs::write(
repo.git_dir().join("refs/tags/annotated"),
format!("{tip}\n"),
)
.map_err(crate::error::Error::Io)?;
let specs = [
"HEAD",
"main",
"main~1",
"main~10",
"main^",
"main^1",
"main^0",
"main^{commit}",
"main^{tree}",
"main^{/c50}",
":/c50",
":/",
"tags/annotated",
"refs/heads/main",
"refs/tags/annotated",
"HEAD~5",
"HEAD^!",
"HEAD^@",
"HEAD...main",
"main...HEAD",
"main..HEAD",
"@",
"HEAD@{0}",
"HEAD@{1}",
"HEAD@{1979-01-01}",
];
for spec in specs {
exercise_impl_matrix(&repo, spec);
}
for n in 0..60 {
let spec = format!("HEAD~{n}");
let _ = resolve_revision_for_verify(&repo, &spec);
for index_dwim in [false, true] {
for commit_only in [false, true] {
for remote_guess in [false, true] {
let _ = resolve_revision_impl(
&repo,
&spec,
index_dwim,
commit_only,
true,
false,
false,
false,
remote_guess,
);
}
}
}
}
Ok(())
}
#[test]
fn bare_merge_commit_and_tag_object_matrix() -> crate::error::Result<()> {
let dir = tempdir().map_err(crate::error::Error::Io)?;
Repository::init(
dir.path(),
&InitOptions {
bare: true,
ref_storage: crate::RefStorageFormat::Files,
..InitOptions::default()
},
)?;
let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
let base = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nbase\n"
)
.as_bytes(),
)?;
let side = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nparent {base}\nauthor T <t@e.com> 200 +0000\ncommitter T <t@e.com> 200 +0000\n\nside\n"
)
.as_bytes(),
)?;
let tip = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nparent {base}\nauthor T <t@e.com> 300 +0000\ncommitter T <t@e.com> 300 +0000\n\ntip\n"
)
.as_bytes(),
)?;
let merge = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nparent {tip}\nparent {side}\nauthor T <t@e.com> 400 +0000\ncommitter T <t@e.com> 400 +0000\n\nmerge\n"
)
.as_bytes(),
)?;
fs::write(repo.git_dir().join("refs/heads/main"), format!("{merge}\n"))
.map_err(crate::error::Error::Io)?;
fs::write(repo.git_dir().join("refs/heads/side"), format!("{side}\n"))
.map_err(crate::error::Error::Io)?;
let tag_body = format!(
"object {merge}\ntype commit\ntag v-merge\ntagger T <t@e.com> 500 +0000\n\nmsg\n"
);
let tag_oid = repo
.odb()
.write_loose_materialize(ObjectKind::Tag, tag_body.as_bytes())?;
fs::write(
repo.git_dir().join("refs/tags/v-merge"),
format!("{tag_oid}\n"),
)
.map_err(crate::error::Error::Io)?;
for spec in [
"main^1",
"main^2",
"main^3",
"main^",
"main^0",
"main^!",
"main^@",
"main~1",
"side",
"main...side",
"side...main",
"v-merge",
"v-merge^{commit}",
"v-merge^{}",
"tags/v-merge",
&format!("{tip}"),
&format!("{}", &tip.to_hex()[..7]),
] {
exercise_impl_matrix(&repo, spec);
let _ = resolve_revision_for_verify(&repo, spec);
let _ = resolve_revision_as_commit(&repo, spec);
}
Ok(())
}
#[test]
fn resolve_base_error_path_sweep() -> crate::error::Result<()> {
let dir = tempdir().map_err(crate::error::Error::Io)?;
Repository::init(
dir.path(),
&InitOptions {
bare: true,
ref_storage: RefStorageFormat::Files,
..InitOptions::default()
},
)?;
let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
let tip = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nx\n"
)
.as_bytes(),
)?;
fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
.map_err(crate::error::Error::Io)?;
const SPECS: &[&str] = &[
"missing-ref",
"HEAD~9999",
"main~9999",
"deadbeef",
"00000000",
"main^{tag}",
"main:missing/path",
":missing",
"HEAD^9",
"main..",
"...main",
"@{-99}",
"main@{upstream}@{upstream}",
"FETCH_HEAD",
"AUTO_MERGE",
"HEAD:../outside",
":1:missing",
":0:/etc/passwd",
":2:/etc/passwd",
"main...",
"...",
"tags/",
"refs/heads/",
"HEAD^{/}",
":/!!!",
"!!",
"main^999",
"HEAD^^^",
"notfound@{0}",
"bogus@{1}@{2}",
"HEAD@{upstream}@{push}",
"x..y",
"main..main..main",
"HEAD...",
"...HEAD",
"HEAD:HEAD:file",
"HEAD@{-0}",
"@{-0}",
"main@{999}",
"HEAD@",
"@{",
"HEAD@{",
"main@",
"HEAD@{}",
"HEAD@{x}",
"HEAD@{1}@{upstream}",
"upstream@{1}",
"HEAD^{object}",
"HEAD^{garbage}",
"HEAD^{commit}^{commit}",
"HEAD:.",
"HEAD:..",
":",
"::file",
":9:file",
"HEAD~",
"main~",
"^",
"^{}",
"HEAD^^{}",
"main^@^!",
":0:../../../../etc/passwd",
":2:../../../../etc/passwd",
":../../../../etc/passwd",
"HEAD:../../../../etc/passwd",
"remotes/origin/main",
"origin",
"FETCH_HEAD",
"MERGE_HEAD",
"AUTO_MERGE",
];
for spec in SPECS {
exercise_resolve_base(&repo, spec);
exercise_impl_matrix(&repo, spec);
}
Ok(())
}
#[test]
fn ambiguous_abbrev_and_disambiguate_config() -> crate::error::Result<()> {
let dir = tempdir().map_err(crate::error::Error::Io)?;
Repository::init(
dir.path(),
&InitOptions {
bare: true,
ref_storage: RefStorageFormat::Files,
..InitOptions::default()
},
)?;
let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
fs::write(
repo.git_dir().join("config"),
"[core]\n\tdisambiguate = commit\n",
)
.map_err(crate::error::Error::Io)?;
let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
let mut oids = Vec::new();
let prefix = seed_commits_until_ambiguous_prefix(&repo, tree, &mut oids, 4, 8_000);
let tip = *oids.last().expect("tip");
fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
.map_err(crate::error::Error::Io)?;
for commit_only in [false, true] {
for use_disambiguate in [false, true] {
let _ = resolve_revision_impl(
&repo,
&prefix,
false,
commit_only,
use_disambiguate,
false,
false,
false,
false,
);
let _ = resolve_revision_as_commit(&repo, &prefix);
let _ = ambiguous_object_hint_lines(&repo, &prefix, Some("commit"));
let _ = ambiguous_object_hint_lines(&repo, &prefix, Some("tree"));
let _ = ambiguous_object_hint_lines(&repo, &prefix, Some("blob"));
let _ = ambiguous_object_hint_lines(&repo, &prefix, Some("tag"));
let _ = ambiguous_object_hint_lines(&repo, &prefix, None);
}
}
Ok(())
}
#[test]
fn push_remote_ambiguous_and_graft_coverage() -> crate::error::Result<()> {
let dir = tempdir().map_err(crate::error::Error::Io)?;
let repo = crate::repo::Repository::init(dir.path(), &crate::repo::InitOptions::default())?;
let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
let tip = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nm\n"
)
.as_bytes(),
)?;
fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
.map_err(crate::error::Error::Io)?;
fs::create_dir_all(repo.git_dir().join("refs/remotes/origin"))
.map_err(crate::error::Error::Io)?;
fs::write(
repo.git_dir().join("refs/remotes/origin/main"),
format!("{tip}\n"),
)
.map_err(crate::error::Error::Io)?;
fs::write(
repo.git_dir().join("refs/remotes/origin/HEAD"),
b"ref: refs/remotes/origin/main\n",
)
.map_err(crate::error::Error::Io)?;
fs::create_dir_all(repo.git_dir().join("info")).map_err(crate::error::Error::Io)?;
let write_config = |body: &str| {
fs::write(repo.git_dir().join("config"), body).map_err(crate::error::Error::Io)
};
write_config(
"[remote \"origin\"]\n\turl = /x\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n[branch \"main\"]\n\tremote = origin\n\tmerge = refs/heads/main\n[push]\n\tdefault = nothing\n",
)?;
let _ = resolve_push_full_ref_for_branch(&repo, "main");
write_config(
"[remote \"origin\"]\n\turl = /x\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n[branch \"main\"]\n\tremote = origin\n\tmerge = refs/heads/main\n[push]\n\tdefault = simple\n",
)?;
let _ = resolve_push_full_ref_for_branch(&repo, "main");
write_config(
"[remote \"origin\"]\n\turl = /x\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n\tpush = refs/heads/main:refs/heads/main\n[branch \"main\"]\n\tremote = origin\n\tmerge = refs/heads/main\n",
)?;
let _ = resolve_push_full_ref_for_branch(&repo, "main");
write_config(
"[remote \"origin\"]\n\turl = /x\n[branch \"main\"]\n\tpushRemote = origin\n[push]\n\tdefault = upstream\n",
)?;
let _ = resolve_push_full_ref_for_branch(&repo, "main");
for s in [
"now",
"1.day.ago",
"2.weeks.ago",
"1979-01-01",
"not-a-date",
"@{1}",
] {
let _ = reflog_date_selector_timestamp_at(s, 1_700_000_000);
}
let graft_line = format!("{tip} {tip}\n");
fs::write(repo.git_dir().join("info/grafts"), graft_line)
.map_err(crate::error::Error::Io)?;
let _ = load_graft_parents(&repo.git_dir());
let prefix = &tip.to_hex()[..7];
let _ = ambiguous_object_hint_lines(&repo, prefix, None);
let _ = resolve_revision_as_commit_without_index_dwim(&repo, "main");
let _ = resolve_revision_as_commit_without_index_dwim(&repo, prefix);
let _ = symbolic_full_name(&repo, "main");
let _ = discover_optional(Some(dir.path()));
Ok(())
}
#[test]
fn tree_blob_peel_and_dwim_coverage() -> crate::error::Result<()> {
let dir = tempdir().map_err(crate::error::Error::Io)?;
let repo = crate::repo::Repository::init(dir.path(), &crate::repo::InitOptions::default())?;
let blob = repo
.odb()
.write_loose_materialize(ObjectKind::Blob, b"leaf\n")?;
let sub_tree = repo
.odb()
.write_loose_materialize(ObjectKind::Tree, b"100644 blob {blob}\tleaf.txt\n")?;
let root_tree = repo.odb().write_loose_materialize(
ObjectKind::Tree,
format!("040000 tree {sub_tree}\tsub\n").as_bytes(),
)?;
let c1 = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {root_tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\none\n"
)
.as_bytes(),
)?;
let c2 = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {root_tree}\nparent {c1}\nauthor T <t@e.com> 200 +0000\ncommitter T <t@e.com> 200 +0000\n\ntwo\n"
)
.as_bytes(),
)?;
let tag_body_a = format!(
"object {c2}\ntype commit\ntag ta\ntagger T <t@e.com> 300 +0000\n\nannotated\n"
);
let tag_a = repo
.odb()
.write_loose_materialize(ObjectKind::Tag, tag_body_a.as_bytes())?;
let tag_body_b = format!(
"object {c2}\ntype commit\ntag tb\ntagger T <t@e.com> 301 +0000\n\nannotated\n"
);
let tag_b = repo
.odb()
.write_loose_materialize(ObjectKind::Tag, tag_body_b.as_bytes())?;
fs::write(repo.git_dir().join("refs/heads/main"), format!("{c2}\n"))
.map_err(crate::error::Error::Io)?;
fs::write(repo.git_dir().join("refs/tags/t"), format!("{tag_a}\n"))
.map_err(crate::error::Error::Io)?;
let abbrev = &c2.to_hex()[..7];
fs::write(
repo.git_dir().join(format!("refs/heads/{abbrev}")),
format!("{c1}\n"),
)
.map_err(crate::error::Error::Io)?;
for spec in [
"HEAD:sub",
"HEAD:sub/leaf.txt",
"HEAD:",
"main:sub/leaf.txt",
"t",
"t^{commit}",
"tags/t",
abbrev,
] {
let _ = resolve_treeish_blob_at_path(&repo, spec);
let _ = resolve_revision_as_commit_without_index_dwim(&repo, spec);
exercise_resolve_base(&repo, spec);
exercise_impl_matrix(&repo, spec);
}
let _ = ambiguous_object_hint_lines(&repo, abbrev, Some("commit"));
let _ = disambiguate_hex_by_peel(&repo, abbrev, &[c2, tag_a], "commit");
let _ = disambiguate_hex_by_peel(&repo, "tags", &[tag_a, tag_b], "tag");
Ok(())
}
#[test]
fn resolve_base_outside_paths_and_ambiguous_hex() -> crate::error::Result<()> {
let dir = tempdir().map_err(crate::error::Error::Io)?;
let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
let mut oids = Vec::new();
let prefix = seed_commits_until_ambiguous_prefix(&repo, tree, &mut oids, 4, 8_000);
let tip = *oids.last().expect("tip");
fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
.map_err(crate::error::Error::Io)?;
fs::write(
repo.git_dir().join("config"),
"[core]\n\tdisambiguate = commit\n",
)
.map_err(crate::error::Error::Io)?;
let shared_prefix = Some(prefix);
const OUTSIDE: &[&str] = &[":0:/etc/passwd", ":2:/etc/passwd"];
for spec in OUTSIDE {
let err = resolve_base(&repo, spec, true, false, true, None, false, false, false)
.expect_err(spec);
assert!(
matches!(
err,
Error::RevParse(RevParseError::OutsideRepository { .. })
),
"{spec}: {err:?}"
);
for index_dwim in [false, true] {
for commit_only in [false, true] {
for use_disambiguate in [false, true] {
let _ = resolve_base(
&repo,
spec,
index_dwim,
commit_only,
use_disambiguate,
None,
false,
false,
false,
);
}
}
}
}
if let Some(prefix) = shared_prefix {
for commit_only in [false, true] {
for use_disambiguate in [false, true] {
let _ = resolve_base(
&repo,
&prefix,
false,
commit_only,
use_disambiguate,
Some("commit"),
false,
false,
false,
);
let _ = resolve_revision_as_commit(&repo, &prefix);
}
}
let mut pair: Option<(ObjectId, ObjectId)> = None;
for i in 0..oids.len() {
for j in (i + 1)..oids.len() {
let a = oids[i].to_hex();
let b = oids[j].to_hex();
if a.starts_with(prefix.as_str()) && b.starts_with(prefix.as_str()) {
pair = Some((oids[i], oids[j]));
break;
}
}
if pair.is_some() {
break;
}
}
if let Some((a, b)) = pair {
let _ = disambiguate_hex_by_peel(&repo, &prefix, &[a, b], "commit");
let _ = resolve_base(
&repo,
&prefix,
false,
true,
true,
Some("commit"),
false,
false,
false,
);
}
}
Ok(())
}
#[test]
fn commit_message_search_abbrev_and_loose_matches() -> crate::error::Result<()> {
let dir = tempdir().map_err(crate::error::Error::Io)?;
let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
let c1 = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nneedle alpha\n"
)
.as_bytes(),
)?;
let c2 = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nparent {c1}\nauthor T <t@e.com> 200 +0000\ncommitter T <t@e.com> 200 +0000\n\nbeta only\n"
)
.as_bytes(),
)?;
fs::write(repo.git_dir().join("refs/heads/main"), format!("{c2}\n"))
.map_err(crate::error::Error::Io)?;
fs::write(repo.git_dir().join("refs/heads/side"), format!("{c1}\n"))
.map_err(crate::error::Error::Io)?;
for spec in [
":/needle",
":/!beta",
":/!!needle",
":/n[e-e]dle",
":/missing",
] {
let _ = resolve_revision_for_verify(&repo, spec);
exercise_resolve_base(&repo, spec);
}
let missing = ObjectId::from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").expect("oid");
let _ = abbreviate_object_id(&repo, missing, 7);
let prefix = &c2.to_hex()[..7];
let _ = list_loose_abbrev_matches(&repo, prefix);
let _ = list_all_abbrev_matches(&repo, prefix);
let _ = list_all_abbrev_matches(&repo, "zzz");
let _ = to_relative_path(&dir.path().join("sub/leaf.txt"), &dir.path().join("sub"));
Ok(())
}
#[test]
fn internal_private_api_sweep() -> crate::error::Result<()> {
use std::path::{Component, Path};
assert!(parse_oneline_pattern("needle").is_some());
assert!(parse_oneline_pattern("!-skip").is_some());
assert!(parse_oneline_pattern("!!literal").is_some());
assert!(parse_oneline_pattern("!bad").is_none());
assert!(is_hex_prefix("abc1234"));
assert!(!is_hex_prefix("ghij"));
let _ = normalize_components(Path::new("/a/b/../c"));
let _ = path_is_within(Path::new("/a/b"), Path::new("/a"));
let dir = tempdir().map_err(crate::error::Error::Io)?;
Repository::init(
dir.path(),
&InitOptions {
bare: true,
ref_storage: RefStorageFormat::Files,
..InitOptions::default()
},
)?;
let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
let blob = repo
.odb()
.write_loose_materialize(ObjectKind::Blob, b"payload\n")?;
let mut tree_body = format!("100644 blob {blob}\tfile.txt\n");
let tree = repo
.odb()
.write_loose_materialize(ObjectKind::Tree, tree_body.as_bytes())?;
let c1 = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\none\n"
)
.as_bytes(),
)?;
let c2 = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nparent {c1}\nauthor T <t@e.com> 200 +0000\ncommitter T <t@e.com> 200 +0000\n\ntwo\n"
)
.as_bytes(),
)?;
let merge = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nparent {c2}\nparent {c1}\nauthor T <t@e.com> 300 +0000\ncommitter T <t@e.com> 300 +0000\n\nmerge\n"
)
.as_bytes(),
)?;
fs::write(repo.git_dir().join("refs/heads/main"), format!("{merge}\n"))
.map_err(crate::error::Error::Io)?;
let tag_data = format!(
"object {c1}\ntype commit\ntag ann\ntagger T <t@e.com> 400 +0000\n\nannotated\n"
);
let tag_oid = repo
.odb()
.write_loose_materialize(ObjectKind::Tag, tag_data.as_bytes())?;
fs::write(repo.git_dir().join("refs/tags/ann"), format!("{tag_oid}\n"))
.map_err(crate::error::Error::Io)?;
let _ = parse_tag_target(tag_data.as_bytes());
let _ = peel_annotated_tag_chain(&repo, tag_oid);
let _ = apply_peel(&repo, tag_oid, Some("commit"));
let _ = apply_peel(&repo, c2, Some("tree"));
let nav = NavParentCache::new(&repo);
let _ = apply_nav_step(&repo, &nav, merge, NavStep::ParentN(2));
let _ = apply_nav_step(&repo, &nav, merge, NavStep::AncestorN(2));
let _ = commit_reachable_closure(&repo, merge);
let _ = resolve_commit_message_search_from(&repo, merge, "merge");
let _ = resolve_commit_message_search_from(&repo, merge, "!-two");
let _ = find_abbrev_matches(&repo, &c2.to_hex()[..4]);
let _ = disambiguate_kind_rank(ObjectKind::Commit);
let _ = oid_satisfies_peel_filter(&repo, c2, "commit");
let _ = read_core_disambiguate(&repo);
let _ = resolve_tree_path(&repo, &tree, "file.txt");
let _ = resolve_tree_path_rev_parse(&repo, &tree, "file.txt");
let _ = walk_tree_to_blob_entry(&repo, &tree, "file.txt");
if let Some((a, b)) = split_treeish_spec("HEAD:file.txt") {
assert_eq!(a, "HEAD");
assert_eq!(b, "file.txt");
}
let _ = resolve_treeish_path_to_object(&repo, c2, "file.txt");
let _ = resolve_ref_dwim_for_rev_parse(&repo, "main");
let _ = resolve_ref_dwim_for_rev_parse(&repo, "ann");
let _ = remote_tracking_head_symbolic_target(&repo, "origin/main");
warn_if_branch_refname_collides_with_abbrev_hex(&repo, "main", c2);
warn_if_hex_ref_collides_with_objects(&repo, &c2.to_hex()[..7], c2);
let abbrev = &c2.to_hex()[..7];
let matches = find_abbrev_matches(&repo, abbrev)?;
let _ = disambiguate_hex_by_peel(&repo, abbrev, &matches, "commit");
let _ = describe_generation_count(&repo, merge, c1);
let _ = try_resolve_reflog_index(&repo, "main@{0}");
let _ = approxidate_at("1.day.ago", 1_700_000_000);
let _ = parse_reflog_entry_timestamp(&crate::reflog::ReflogEntry {
old_oid: c1,
new_oid: c2,
identity: "T <t@e.com> 100 +0000".into(),
message: "commit: msg".into(),
});
let _ = diagnose_tree_path_error(
&repo,
"HEAD",
"missing",
"missing",
Error::ObjectNotFound("missing".into()).into(),
);
let _ =
diagnose_index_path_error(&repo, "missing", 0, Error::ObjectNotFound("missing".into()));
for spec in [
"main",
"ann",
"ann^{commit}",
"merge^2",
"merge^1",
"merge~1",
"HEAD^!",
"HEAD^@",
":/merge",
":/!-two",
"tags/ann",
] {
exercise_resolve_base(&repo, spec);
}
Ok(())
}
#[test]
fn worktree_fetch_head_index_and_colon_paths() -> crate::error::Result<()> {
let dir = tempdir().map_err(crate::error::Error::Io)?;
let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
let blob = repo
.odb()
.write_loose_materialize(ObjectKind::Blob, b"indexed\n")?;
fs::write(dir.path().join("tracked.txt"), b"indexed\n").map_err(crate::error::Error::Io)?;
let entry = entry_from_stat(
&dir.path().join("tracked.txt"),
b"tracked.txt",
blob,
MODE_REGULAR,
)?;
let mut index = Index::new();
index.entries_mut().push(entry);
index.write_to_path(&repo.git_dir().join("index"), false)?;
let tree = write_tree_from_index(&repo.odb(), &index, "")?;
let tip = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\ninit\n"
)
.as_bytes(),
)?;
fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
.map_err(crate::error::Error::Io)?;
fs::write(
repo.git_dir().join("FETCH_HEAD"),
format!("{tip}\tnot-for-merge\n{tip}\trefs/heads/main\n"),
)
.map_err(crate::error::Error::Io)?;
for spec in [
"FETCH_HEAD",
":tracked.txt",
":0:tracked.txt",
"HEAD:tracked.txt",
"HEAD:",
"main:tracked.txt",
"tracked.txt",
"main",
"HEAD",
"@",
"HEAD^{tree}",
"HEAD^{commit}",
&tip.to_hex(),
&tip.to_hex()[..7],
] {
exercise_resolve_base(&repo, spec);
exercise_impl_matrix(&repo, spec);
}
let _ = head_tree_oid(&repo);
let _ = path_in_tree(&repo, tree, "tracked.txt");
let _ = path_in_index(&repo, "tracked.txt", 0);
Ok(())
}
#[test]
fn discovery_graft_and_abbrev_sweep() -> crate::error::Result<()> {
let dir = tempdir().map_err(crate::error::Error::Io)?;
let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
let cwd = dir.path();
let _ = discover_optional(Some(cwd))?;
let _ = is_inside_work_tree(&repo, cwd);
let _ = is_inside_git_dir(&repo, cwd);
let _ = show_prefix(&repo, cwd);
let _ = symbolic_full_name(&repo, "HEAD");
let _ = expand_at_minus_to_branch_name(&repo, "@{-1}").ok();
let _ = resolve_at_minus_to_oid(&repo, "HEAD").ok();
let _ = resolve_upstream_symbolic_name(&repo, "main@{upstream}").ok();
let _ = resolve_push_full_ref_for_branch(&repo, "main").ok();
let _ = load_graft_parents(&repo.git_dir());
let blob = repo
.odb()
.write_loose_materialize(ObjectKind::Blob, b"x\n")?;
let tree_body = format!("100644 f.txt\0").into_bytes();
let mut tree_bytes = tree_body;
tree_bytes.extend_from_slice(blob.as_bytes());
let tree = repo
.odb()
.write_loose_materialize(ObjectKind::Tree, &tree_bytes)?;
let tip = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nauthor T <t@e.com> 1 +0000\ncommitter T <t@e.com> 1 +0000\n\nm\n"
)
.as_bytes(),
)?;
fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
.map_err(crate::error::Error::Io)?;
let _ = list_all_abbrev_matches(&repo, &tip.to_hex()[..4])?;
let _ = list_loose_abbrev_matches(&repo, &tip.to_hex()[..4])?;
let _ = abbreviate_object_id(&repo, tip, 7)?;
let _ = resolve_treeish_blob_at_path(&repo, "HEAD:f.txt")?;
let _ = expand_rev_token_circ_bang(&repo, "main")?;
let _ = ambiguous_object_hint_lines(&repo, &tip.to_hex()[..7], None)?;
let _ = reflog_walk_refname(&repo, "HEAD@{0}").ok();
let _ = resolve_reflog_walk_log_ref(&repo, "HEAD").ok();
let _ = resolve_index_path_entry(&repo, ":f.txt").ok();
let _ = to_relative_path(&dir.path().join("f.txt"), dir.path());
Ok(())
}
#[test]
fn describe_prefilter_accepts_everything_the_pattern_matches() {
let re = Regex::new(r"(?i)^(.+)-(\d+)-g([0-9a-fA-F]+)$").unwrap();
for spec in [
"v1-1-gabc1234",
"V1-2-GABC",
"a-b-c-10-g0",
"v2.47.0-rc1-3-gdeadbeef",
"x-1-gab-2-gcd",
"tag-Ù£-gabc",
"HEAD",
"main",
"v1-gabc",
"-1-gabc",
"v1-1-g",
"v1-1-gxyz",
"v1-1-gabc-",
"feature/x-1-gabc\nfoo",
"a--gabc",
] {
if re.is_match(spec) {
assert!(may_be_describe_name(spec), "{spec:?}");
}
}
assert!(!may_be_describe_name("HEAD"));
assert!(!may_be_describe_name("refs/heads/main"));
assert!(!may_be_describe_name("v1-gabc"));
assert!(!may_be_describe_name("v1-1-gxyz"));
}
#[test]
fn describe_name_and_dwim_helpers() -> crate::error::Result<()> {
let dir = tempdir().map_err(crate::error::Error::Io)?;
Repository::init(
dir.path(),
&InitOptions {
bare: true,
ref_storage: crate::RefStorageFormat::Files,
..InitOptions::default()
},
)?;
let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
let c1 = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\none\n"
)
.as_bytes(),
)?;
let c2 = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nparent {c1}\nauthor T <t@e.com> 200 +0000\ncommitter T <t@e.com> 200 +0000\n\ntwo\n"
)
.as_bytes(),
)?;
fs::write(repo.git_dir().join("refs/heads/main"), format!("{c2}\n"))
.map_err(crate::error::Error::Io)?;
fs::write(repo.git_dir().join("refs/tags/v1"), format!("{c1}\n"))
.map_err(crate::error::Error::Io)?;
let abbrev = &c2.to_hex()[..7];
let describe_name = format!("v1-1-g{abbrev}");
assert_eq!(try_resolve_describe_name(&repo, &describe_name)?, Some(c2));
assert_eq!(
try_resolve_describe_name(&repo, &format!("V1-1-G{}", abbrev.to_uppercase()))?,
Some(c2)
);
assert_eq!(try_resolve_describe_name(&repo, "main")?, None);
assert_eq!(try_resolve_describe_name(&repo, "HEAD")?, None);
let _ = dwim_refname(&repo, "main");
let _ = reflog_display_name("refs/heads/main", "main");
Ok(())
}
fn conflict_index_entry(path: &str, stage: u8, oid: ObjectId, mode: u32) -> IndexEntry {
let path_bytes = path.as_bytes().to_vec();
IndexEntry {
ctime_sec: 0,
ctime_nsec: 0,
mtime_sec: 0,
mtime_nsec: 0,
dev: 0,
ino: 0,
mode,
uid: 0,
gid: 0,
size: 0,
oid,
flags: (path_bytes.len().min(0xFFF) as u16) | ((stage as u16) << 12),
flags_extended: None,
path: path_bytes,
base_index_pos: 0,
}
}
#[test]
fn unmerged_index_stages_resolve_and_diagnose() -> crate::error::Result<()> {
let dir = tempdir().map_err(crate::error::Error::Io)?;
let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
let base = repo
.odb()
.write_loose_materialize(ObjectKind::Blob, b"base\n")?;
let ours = repo
.odb()
.write_loose_materialize(ObjectKind::Blob, b"ours\n")?;
let theirs = repo
.odb()
.write_loose_materialize(ObjectKind::Blob, b"theirs\n")?;
let mut index = Index::new();
index
.entries_mut()
.push(conflict_index_entry("conflict.txt", 1, base, MODE_REGULAR));
index
.entries_mut()
.push(conflict_index_entry("conflict.txt", 2, ours, MODE_REGULAR));
index.entries_mut().push(conflict_index_entry(
"conflict.txt",
3,
theirs,
MODE_REGULAR,
));
let nested_blob = repo
.odb()
.write_loose_materialize(ObjectKind::Blob, b"nested\n")?;
fs::create_dir_all(dir.path().join("sub")).map_err(crate::error::Error::Io)?;
fs::write(dir.path().join("sub/leaf.txt"), b"nested\n").map_err(crate::error::Error::Io)?;
let nested_entry = entry_from_stat(
&dir.path().join("sub/leaf.txt"),
b"sub/leaf.txt",
nested_blob,
MODE_REGULAR,
)?;
index.entries_mut().push(nested_entry);
index.write_to_path(&repo.git_dir().join("index"), false)?;
assert_eq!(resolve_revision_for_verify(&repo, ":1:conflict.txt")?, base);
assert_eq!(resolve_revision_for_verify(&repo, ":2:conflict.txt")?, ours);
assert_eq!(
resolve_revision_for_verify(&repo, ":3:conflict.txt")?,
theirs
);
assert!(resolve_revision_for_verify(&repo, ":0:conflict.txt").is_err());
let _ = diagnose_index_path_error(
&repo,
"conflict.txt",
1,
Error::ObjectNotFound("conflict.txt".into()),
);
let _ =
diagnose_index_path_error(&repo, "no-such", 2, Error::ObjectNotFound("no-such".into()));
fs::write(dir.path().join("disk-only.txt"), b"x\n").map_err(crate::error::Error::Io)?;
let _ = diagnose_index_path_error(
&repo,
"disk-only.txt",
0,
Error::ObjectNotFound("disk-only.txt".into()),
);
let _ = resolve_revision_for_verify(&repo, ":0:sub/leaf.txt");
let _ = resolve_index_path_entry(&repo, ":sub/leaf.txt");
let _ = diagnose_tree_path_error(
&repo,
"HEAD",
"./sub/leaf.txt",
"sub/leaf.txt",
TreePathError::NotInTree,
);
Ok(())
}
#[test]
fn outside_repo_ambiguous_hex_and_shorthand_matrix() -> crate::error::Result<()> {
let dir = tempdir().map_err(crate::error::Error::Io)?;
let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
let base = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nbase\n"
)
.as_bytes(),
)?;
let side = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nparent {base}\nauthor T <t@e.com> 200 +0000\ncommitter T <t@e.com> 200 +0000\n\nside\n"
)
.as_bytes(),
)?;
let merge = repo.odb().write_loose_materialize(
ObjectKind::Commit,
format!(
"tree {tree}\nparent {base}\nparent {side}\nauthor T <t@e.com> 300 +0000\ncommitter T <t@e.com> 300 +0000\n\nmerge\n"
)
.as_bytes(),
)?;
fs::write(repo.git_dir().join("refs/heads/main"), format!("{merge}\n"))
.map_err(crate::error::Error::Io)?;
let outside = [
"HEAD:../../../../etc/passwd",
":0:../../../../etc/passwd",
":2:../../../../etc/passwd",
"HEAD:",
];
for spec in outside {
let _ = resolve_treeish_blob_at_path(&repo, spec);
let _ = resolve_revision_for_verify(&repo, spec);
let _ = resolve_index_path_entry(&repo, spec);
}
let bang = expand_rev_token_circ_bang(&repo, "main^!")?;
assert!(bang.len() >= 2);
let _ = expand_parent_shorthand_rev_parse_lines(&repo, "main^-2", false, None);
let _ = expand_parent_shorthand_rev_parse_lines(&repo, "main^-99", false, None);
let _ = resolve_reflog_walk_log_ref(&repo, "main");
let _ = resolve_reflog_walk_log_ref(&repo, "missing-branch");
let _ = resolve_reflog_walk_log_ref(&repo, "@{-1}");
fs::write(
repo.git_dir().join("config"),
"[core]\n\tdisambiguate = commit\n",
)
.map_err(crate::error::Error::Io)?;
let mut oids = Vec::new();
let prefix = seed_commits_until_ambiguous_prefix(&repo, tree, &mut oids, 4, 8_000);
let _ = resolve_revision(&repo, &prefix);
let _ = resolve_revision_as_commit(&repo, &prefix);
let _ = resolve_revision_for_verify(&repo, &prefix);
Ok(())
}
#[test]
#[should_panic(expected = "could not find")]
fn seed_commits_until_ambiguous_prefix_exhaustion() {
let dir = tempdir().expect("tempdir");
crate::repo::Repository::init(
dir.path(),
&InitOptions {
bare: true,
ref_storage: RefStorageFormat::Files,
..InitOptions::default()
},
)
.expect("init");
let repo = crate::repo::Repository::open(
dir.path(),
&crate::environment::RepositoryOptions::empty(),
)
.expect("open");
let tree = repo
.odb()
.write_loose_materialize(ObjectKind::Tree, b"")
.expect("tree");
let mut oids = Vec::new();
let _ = seed_commits_until_ambiguous_prefix(&repo, tree, &mut oids, 40, 0);
}
}
#[cfg(test)]
mod superproject_path_tests {
use super::superproject_work_tree_from_nested_git_modules;
use std::path::PathBuf;
#[test]
fn nested_modules_yields_superproject_work_tree() {
let git_dir = PathBuf::from("/tmp/super/.git/modules/dir/modules/sub");
assert_eq!(
superproject_work_tree_from_nested_git_modules(&git_dir),
Some(PathBuf::from("/tmp/super"))
);
}
#[test]
fn non_nested_returns_none() {
let git_dir = PathBuf::from("/tmp/repo/.git");
assert!(superproject_work_tree_from_nested_git_modules(&git_dir).is_none());
}
}