1use crate::environment::RepositoryOptions;
9use crate::git_path::CanonicalizeExt;
10
11use std::borrow::Cow;
12use std::ffi::OsStr;
13use std::fs;
14use std::path::{Component, Path, PathBuf};
15
16use regex::Regex;
17
18use std::cmp::Ordering;
19use std::collections::{BinaryHeap, HashMap, HashSet};
20
21use std::ops::ControlFlow;
22use std::sync::Arc;
23
24use crate::check_ref_format::{check_refname_format, RefNameOptions};
25use crate::config::ConfigSet;
26use crate::error::{Error, NotARepositoryReason, Result};
27use crate::objects::{parse_commit, parse_tag, parse_tree, CommitRef, ObjectId, ObjectKind};
28use crate::reflog::ReflogEntry;
29use crate::refs;
30use crate::refs::store::RawRef;
31use crate::repo::Repository;
32use crate::rev_parse_error::{AmbiguousObjectHint, RevParseError};
33
34pub fn discover_optional(start: Option<&Path>) -> Result<Option<Repository>> {
48 let err = match Repository::discover(start, &RepositoryOptions::empty()) {
49 Ok(repo) => return Ok(Some(repo)),
50 Err(err) => err,
51 };
52 let Error::NotARepository(reason) = &err else {
55 return Err(err);
56 };
57 let malformed_gitfile = matches!(
58 reason,
59 NotARepositoryReason::GitfileNotRegularFile { .. }
60 | NotARepositoryReason::GitfileMissingGitdir { .. }
61 );
62 let dot_git_at_start = start.is_some_and(|start| {
63 let dot_git = start.join(".git");
64 dot_git.is_file() || dot_git.is_symlink()
65 });
66 if malformed_gitfile || dot_git_at_start {
67 return Err(err);
68 }
69 Ok(None)
70}
71
72#[must_use]
74pub fn is_inside_work_tree(repo: &Repository, cwd: &Path) -> bool {
75 let Some(work_tree) = &repo.work_tree() else {
76 return false;
77 };
78 path_is_within(cwd, work_tree)
79}
80
81#[must_use]
83pub fn is_inside_git_dir(repo: &Repository, cwd: &Path) -> bool {
84 path_is_within(cwd, repo.git_dir())
85}
86
87#[must_use]
92pub fn show_prefix(repo: &Repository, cwd: &Path) -> String {
93 let Some(work_tree) = repo.work_tree() else {
94 return String::new();
95 };
96 if !path_is_within(cwd, work_tree) {
97 return String::new();
98 }
99 if cwd == work_tree {
100 return String::new();
101 }
102 let Ok(rel) = cwd.strip_prefix(work_tree) else {
103 return String::new();
104 };
105 let mut out = rel
106 .components()
107 .filter_map(component_to_text)
108 .collect::<Vec<_>>()
109 .join("/");
110 if !out.is_empty() {
111 out.push('/');
112 }
113 out
114}
115
116#[must_use]
122pub fn superproject_work_tree_from_nested_git_modules(git_dir: &Path) -> Option<PathBuf> {
123 let mut p = git_dir.to_path_buf();
124 while let Some(parent) = p.parent() {
125 if p.file_name().is_some_and(|n| n == "modules")
126 && parent.file_name().is_some_and(|n| n == ".git")
127 {
128 return parent.parent().map(PathBuf::from);
129 }
130 if parent == p {
131 break;
132 }
133 p = parent.to_path_buf();
134 }
135 None
136}
137
138#[must_use]
145pub fn symbolic_full_name(repo: &Repository, spec: &str) -> Option<String> {
146 if upstream_suffix_info(spec).is_some() {
148 return resolve_upstream_symbolic_name(repo, spec).ok();
149 }
150
151 if let Ok(Some(branch)) = expand_at_minus_to_branch_name(repo, spec) {
152 let ref_name = format!("refs/heads/{branch}");
153 if repo.resolve_ref_oid(&ref_name).is_ok() {
154 return Some(ref_name);
155 }
156 return None;
157 }
158
159 if spec == "HEAD" {
160 if let Ok(Some(target)) = refs::read_symbolic_ref_for_repository(repo, "HEAD") {
161 return Some(target);
162 }
163 return None;
164 }
165 if spec.starts_with("refs/") {
167 if repo.resolve_ref_oid(spec).is_ok() {
168 return Some(spec.to_owned());
169 }
170 return None;
171 }
172 for prefix in &["refs/heads/", "refs/tags/", "refs/remotes/"] {
174 let candidate = format!("{prefix}{spec}");
175 if repo.resolve_ref_oid(&candidate).is_ok() {
176 return Some(candidate);
177 }
178 }
179 if let Some(full) = remote_tracking_head_symbolic_target(repo, spec) {
181 return Some(full);
182 }
183 None
184}
185
186fn remote_tracking_head_symbolic_target(repo: &Repository, name: &str) -> Option<String> {
188 if name.contains('/')
189 || matches!(
190 name,
191 "HEAD" | "FETCH_HEAD" | "MERGE_HEAD" | "CHERRY_PICK_HEAD" | "REVERT_HEAD"
192 )
193 {
194 return None;
195 }
196 let config = repo.ensure_config_arc_fast().ok()?;
197 let url_key = format!("remote.{name}.url");
198 config.get(&url_key)?;
199 let head_ref = format!("refs/remotes/{name}/HEAD");
200 let target = refs::read_symbolic_ref_for_repository(repo, &head_ref).ok()??;
201 Some(target)
202}
203
204pub fn expand_at_minus_to_branch_name(repo: &Repository, spec: &str) -> Result<Option<String>> {
212 if !spec.starts_with("@{-") || !spec.ends_with('}') {
213 return Ok(None);
214 }
215 let inner = &spec[3..spec.len() - 1];
216 let n: usize = inner
217 .parse()
218 .map_err(|_| Error::InvalidRef(format!("invalid N in @{{-N}} for '{spec}'")))?;
219 if n < 1 {
220 return Ok(None);
221 }
222 resolve_at_minus_to_branch(repo, n).map(Some)
223}
224
225pub fn resolve_at_minus_to_oid(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
227 try_resolve_at_minus(repo, spec)
228}
229
230#[must_use]
234pub fn abbreviate_ref_name(full_name: &str) -> String {
235 for prefix in &["refs/heads/", "refs/tags/", "refs/remotes/"] {
236 if let Some(short) = full_name.strip_prefix(prefix) {
237 return short.to_owned();
238 }
239 }
240 if let Some(short) = full_name.strip_prefix("refs/") {
241 return short.to_owned();
242 }
243 full_name.to_owned()
244}
245
246#[must_use]
249pub fn upstream_suffix_info(spec: &str) -> Option<(&str, bool)> {
250 let lower = spec.to_ascii_lowercase();
251 if lower.ends_with("@{push}") {
252 let base = &spec[..spec.len() - 7];
253 return Some((base, true));
254 }
255 if lower.ends_with("@{upstream}") {
256 let base = &spec[..spec.len() - 11];
257 return Some((base, false));
258 }
259 if lower.ends_with("@{u}") {
260 let base = &spec[..spec.len() - 4];
261 return Some((base, false));
262 }
263 None
264}
265
266pub fn resolve_upstream_symbolic_name(repo: &Repository, spec: &str) -> Result<String> {
268 let Some((base, is_push)) = upstream_suffix_info(spec) else {
269 return Err(Error::InvalidRef(format!("not an upstream spec: {spec}")));
270 };
271 resolve_upstream_full_ref_name(repo, base, is_push)
272}
273
274fn resolve_upstream_full_ref_name(repo: &Repository, base: &str, is_push: bool) -> Result<String> {
275 if is_push {
276 return resolve_push_ref_name(repo, base);
277 }
278 let (branch_key, display_branch) = resolve_upstream_branch_context(repo, base)?;
279 let config_path = repo.common_git_dir().join("config");
280 let _ = repo.config()?;
281 let config_content = repo.caches().local_config_text(&config_path)?;
282 let Some((remote, merge)) = parse_branch_tracking(&config_content, &branch_key) else {
283 return Err(RevParseError::NoUpstream {
284 branch: display_branch,
285 }
286 .into());
287 };
288 if remote == "." {
289 let m = merge.trim();
290 if m.starts_with("refs/") {
291 return Ok(m.to_owned());
292 }
293 return Ok(format!("refs/heads/{m}"));
294 }
295 let merge_branch = merge
296 .strip_prefix("refs/heads/")
297 .ok_or_else(|| Error::InvalidRef(format!("invalid merge ref: {merge}")))?;
298 let tracking = format!("refs/remotes/{remote}/{merge_branch}");
299 if repo.resolve_ref_oid(&tracking).is_err() {
300 return Err(RevParseError::UpstreamNotTracked {
301 merge: merge.to_owned(),
302 }
303 .into());
304 }
305 Ok(tracking)
306}
307
308pub fn resolve_push_full_ref_for_branch(repo: &Repository, branch_short: &str) -> Result<String> {
313 let config_path = repo.common_git_dir().join("config");
314 let _ = repo.config()?;
315 let config_content = repo.caches().local_config_text(&config_path)?;
316
317 let upstream_tracking =
318 parse_branch_tracking(&config_content, branch_short).and_then(|(remote, merge)| {
319 if remote == "." {
320 return None;
321 }
322 let mb = merge.strip_prefix("refs/heads/").unwrap_or(&merge);
323 let tr = format!("refs/remotes/{remote}/{mb}");
324 if repo.resolve_ref_oid(&tr).is_ok() {
325 Some(tr)
326 } else {
327 None
328 }
329 });
330
331 let push_remote = parse_config_value(&config_content, "remote", "pushRemote")
332 .or_else(|| parse_config_value(&config_content, "remote", "pushDefault"))
333 .or_else(|| {
334 let section = format!("[branch \"{}\"]", branch_short);
335 let mut in_section = false;
336 for line in config_content.lines() {
337 let trimmed = line.trim();
338 if trimmed.starts_with('[') {
339 in_section = trimmed == section;
340 continue;
341 }
342 if in_section {
343 if let Some(v) = trimmed
344 .strip_prefix("pushremote = ")
345 .or_else(|| trimmed.strip_prefix("pushRemote = "))
346 {
347 return Some(v.trim().to_owned());
348 }
349 }
350 }
351 None
352 })
353 .or_else(|| {
354 parse_branch_tracking(&config_content, branch_short)
355 .map(|(r, _)| r)
356 .filter(|r| r != ".")
357 });
358
359 let Some(push_remote_name) = push_remote else {
360 return upstream_tracking.ok_or_else(|| RevParseError::NoPushRemote.into());
361 };
362
363 if remote_has_push_refspec(&config_content, &push_remote_name) {
368 return match push_refspec_mapped_tracking(&config_content, &push_remote_name, branch_short)
369 {
370 Some(mapped) => Ok(mapped),
371 None => Err(RevParseError::PushRefspecMissing {
372 remote: push_remote_name,
373 branch: branch_short.to_owned(),
374 }
375 .into()),
376 };
377 }
378
379 let push_default = parse_config_value(&config_content, "push", "default");
380 let push_default = push_default.as_deref().unwrap_or("simple");
381
382 if push_default == "nothing" {
383 return Err(RevParseError::PushDefaultNothing.into());
384 }
385
386 let current_tracking = format!("refs/remotes/{push_remote_name}/{branch_short}");
387
388 match push_default {
389 "upstream" => upstream_tracking.ok_or_else(|| {
390 RevParseError::NoUpstreamForPush {
391 branch: branch_short.to_owned(),
392 }
393 .into()
394 }),
395 "simple" => {
396 if let Some(ref up) = upstream_tracking {
397 if up == ¤t_tracking && repo.resolve_ref_oid(¤t_tracking).is_ok() {
398 return Ok(current_tracking);
399 }
400 }
401 Err(RevParseError::PushDefaultSimpleMismatch.into())
402 }
403 _ => {
404 if repo.resolve_ref_oid(¤t_tracking).is_ok() {
405 Ok(current_tracking)
406 } else if let Some(up) = upstream_tracking {
407 Ok(up)
408 } else {
409 Err(RevParseError::NoPushTrackingRef {
410 branch: branch_short.to_owned(),
411 }
412 .into())
413 }
414 }
415 }
416}
417
418fn remote_push_refspecs(config_content: &str, remote_name: &str) -> Vec<String> {
420 let section = format!("[remote \"{remote_name}\"]");
421 let mut in_section = false;
422 let mut out = Vec::new();
423 for line in config_content.lines() {
424 let trimmed = line.trim();
425 if trimmed.starts_with('[') {
426 in_section = trimmed == section;
427 continue;
428 }
429 if !in_section {
430 continue;
431 }
432 if let Some(val) = trimmed
433 .strip_prefix("push = ")
434 .or_else(|| trimmed.strip_prefix("push="))
435 {
436 if let Some(spec) = val.split_whitespace().next() {
437 out.push(spec.trim().to_owned());
438 }
439 }
440 }
441 out
442}
443
444fn remote_has_push_refspec(config_content: &str, remote_name: &str) -> bool {
446 !remote_push_refspecs(config_content, remote_name).is_empty()
447}
448
449fn match_refname_with_pattern(pattern: &str, refname: &str, replacement: &str) -> Option<String> {
453 match (pattern.find('*'), replacement.find('*')) {
454 (Some(pstar), Some(vstar)) => {
455 let kprefix = &pattern[..pstar];
456 let ksuffix = &pattern[pstar + 1..];
457 if !refname.starts_with(kprefix) || !refname.ends_with(ksuffix) {
458 return None;
459 }
460 if refname.len() < kprefix.len() + ksuffix.len() {
461 return None;
462 }
463 let middle = &refname[kprefix.len()..refname.len() - ksuffix.len()];
464 Some(format!(
465 "{}{}{}",
466 &replacement[..vstar],
467 middle,
468 &replacement[vstar + 1..]
469 ))
470 }
471 (None, None) => {
472 if pattern == refname {
473 Some(replacement.to_owned())
474 } else {
475 None
476 }
477 }
478 _ => None,
480 }
481}
482
483fn push_refspec_mapped_tracking(
488 config_content: &str,
489 remote_name: &str,
490 branch_short: &str,
491) -> Option<String> {
492 let src = format!("refs/heads/{branch_short}");
493
494 let mut dst = None;
496 for spec in remote_push_refspecs(config_content, remote_name) {
497 let spec = spec.strip_prefix('+').unwrap_or(&spec);
498 let Some((left, right)) = spec.split_once(':') else {
499 continue;
500 };
501 if let Some(mapped) = match_refname_with_pattern(left.trim(), &src, right.trim()) {
502 dst = Some(mapped);
503 break;
504 }
505 }
506 let dst = dst?;
507
508 map_dest_to_tracking(config_content, remote_name, &dst)
510}
511
512fn map_dest_to_tracking(config_content: &str, remote_name: &str, dst: &str) -> Option<String> {
516 let section = format!("[remote \"{remote_name}\"]");
517 let mut in_section = false;
518 for line in config_content.lines() {
519 let trimmed = line.trim();
520 if trimmed.starts_with('[') {
521 in_section = trimmed == section;
522 continue;
523 }
524 if !in_section {
525 continue;
526 }
527 let Some(val) = trimmed
528 .strip_prefix("fetch = ")
529 .or_else(|| trimmed.strip_prefix("fetch="))
530 else {
531 continue;
532 };
533 let Some(spec) = val.split_whitespace().next() else {
534 continue;
535 };
536 let spec = spec.trim().strip_prefix('+').unwrap_or(spec.trim());
537 let Some((left, right)) = spec.split_once(':') else {
538 continue;
539 };
540 if let Some(mapped) = match_refname_with_pattern(left.trim(), dst, right.trim()) {
541 return Some(mapped);
542 }
543 }
544 dst.strip_prefix("refs/heads/")
546 .map(|rest| format!("refs/remotes/{remote_name}/{rest}"))
547}
548
549fn resolve_push_ref_name(repo: &Repository, base: &str) -> Result<String> {
550 let (branch_key, _display) = resolve_upstream_branch_context(repo, base)?;
551 resolve_push_full_ref_for_branch(repo, &branch_key)
552}
553
554fn resolve_upstream_branch_context(repo: &Repository, base: &str) -> Result<(String, String)> {
556 let base = if base == "HEAD" {
557 Cow::Borrowed("")
558 } else if base.starts_with("@{-") && base.ends_with('}') {
559 if let Ok(Some(b)) = expand_at_minus_to_branch_name(repo, base) {
560 Cow::Owned(b)
561 } else {
562 Cow::Borrowed(base)
563 }
564 } else {
565 Cow::Borrowed(base)
566 };
567 let base = base.as_ref();
568 let base = if base == "@" { "" } else { base };
569
570 if base.is_empty() {
571 let Some(head) = refs::read_symbolic_ref_for_repository(repo, "HEAD")? else {
572 return Err(RevParseError::HeadNotBranch.into());
573 };
574 let Some(short) = head.strip_prefix("refs/heads/") else {
575 return Err(RevParseError::HeadNotBranch.into());
576 };
577 return Ok((short.to_owned(), short.to_owned()));
578 }
579 let head_branch = refs::read_symbolic_ref_for_repository(repo, "HEAD")?.and_then(|h| {
580 h.strip_prefix("refs/heads/")
581 .map(std::borrow::ToOwned::to_owned)
582 });
583 if head_branch.as_deref() == Some(base) {
584 return Ok((base.to_owned(), base.to_owned()));
585 }
586 let refname = format!("refs/heads/{base}");
587 if repo.resolve_ref_oid(&refname).is_err() {
588 return Err(RevParseError::NoSuchBranch {
589 branch: base.to_owned(),
590 }
591 .into());
592 }
593 Ok((base.to_owned(), base.to_owned()))
594}
595
596fn parse_config_value(config: &str, section: &str, key: &str) -> Option<String> {
597 let section_header = format!("[{}]", section);
598 let key_lower = key.to_ascii_lowercase();
599 let mut in_section = false;
600 for line in config.lines() {
601 let trimmed = line.trim();
602 if trimmed.starts_with('[') {
603 in_section = trimmed.eq_ignore_ascii_case(§ion_header);
604 continue;
605 }
606 if in_section {
607 let lower = trimmed.to_ascii_lowercase();
608 if lower.starts_with(&key_lower) {
609 let rest = lower[key_lower.len()..].trim_start().to_string();
610 if rest.starts_with('=') {
611 if let Some(eq_pos) = trimmed.find('=') {
612 return Some(trimmed[eq_pos + 1..].trim().to_owned());
613 }
614 }
615 }
616 }
617 }
618 None
619}
620
621fn parse_branch_tracking(config: &str, branch: &str) -> Option<(String, String)> {
623 let mut remote = None;
624 let mut merge = None;
625 let mut in_section = false;
626 let target_section = format!("[branch \"{}\"]", branch);
627
628 for line in config.lines() {
629 let trimmed = line.trim();
630 if trimmed.starts_with('[') {
631 in_section = trimmed == target_section
632 || trimmed.starts_with(&format!("[branch \"{}\"", branch));
633 continue;
634 }
635 if !in_section {
636 continue;
637 }
638 if let Some(value) = trimmed.strip_prefix("remote = ") {
639 remote = Some(value.trim().to_owned());
640 } else if let Some(value) = trimmed.strip_prefix("merge = ") {
641 merge = Some(value.trim().to_owned());
642 }
643 if let Some(value) = trimmed.strip_prefix("remote=") {
645 remote = Some(value.trim().to_owned());
646 } else if let Some(value) = trimmed.strip_prefix("merge=") {
647 merge = Some(value.trim().to_owned());
648 }
649 }
650
651 match (remote, merge) {
652 (Some(r), Some(m)) => Some((r, m)),
653 _ => None,
654 }
655}
656
657#[must_use]
674pub fn load_graft_parents(git_dir: &Path) -> HashMap<ObjectId, Vec<ObjectId>> {
678 let graft_path = crate::repo::common_git_dir_for_config(git_dir).join("info/grafts");
679 let mut grafts = HashMap::new();
680 let Ok(contents) = fs::read_to_string(&graft_path) else {
681 return grafts;
682 };
683 for raw_line in contents.lines() {
684 let line = raw_line.trim();
685 if line.is_empty() || line.starts_with('#') {
686 continue;
687 }
688 let mut fields = line.split_whitespace();
689 let Some(commit_hex) = fields.next() else {
690 continue;
691 };
692 let Ok(commit_oid) = commit_hex.parse::<ObjectId>() else {
693 continue;
694 };
695 let mut parents = Vec::new();
696 let mut valid = true;
697 for parent_hex in fields {
698 match parent_hex.parse::<ObjectId>() {
699 Ok(parent_oid) => parents.push(parent_oid),
700 Err(_) => {
701 valid = false;
702 break;
703 }
704 }
705 }
706 if valid {
707 grafts.insert(commit_oid, parents);
708 }
709 }
710 grafts
711}
712
713const NAV_LINKAGE_CACHE_LIMIT: usize = 1024;
716
717struct NavParentCache<'repo> {
722 repo: &'repo Repository,
723 grafts: HashMap<ObjectId, Vec<ObjectId>>,
724 graph: Option<crate::commit_graph_file::CommitGraphChain>,
725 share_linkage: std::cell::OnceCell<bool>,
726}
727
728enum NavigationMetadata {
729 Discard,
730 RetainForWalk,
731}
732
733impl<'repo> NavParentCache<'repo> {
734 fn new(repo: &'repo Repository, metadata: NavigationMetadata) -> Self {
735 let grafts = load_graft_parents(repo.git_dir());
736 let graph = repo.odb().commit_graph_chain().map(|arc| (*arc).clone());
737 Self {
738 repo,
739 grafts,
740 graph,
741 share_linkage: match metadata {
742 NavigationMetadata::Discard => std::cell::OnceCell::from(false),
743 NavigationMetadata::RetainForWalk => std::cell::OnceCell::new(),
744 },
745 }
746 }
747
748 fn parents_of(&self, commit_oid: ObjectId) -> Result<Vec<ObjectId>> {
749 if let Some(grafted) = self.grafts.get(&commit_oid) {
750 return Ok(grafted.clone());
751 }
752 if let Some(graph) = &self.graph {
753 if let Some((parents, _)) = graph.graph_commit(&commit_oid) {
754 return Ok(parents);
755 }
756 }
757 let share_linkage = *self.share_linkage.get_or_init(|| {
761 self.grafts.is_empty()
762 && crate::shallow::load_shallow_boundaries(self.repo.git_dir()).is_empty()
763 && !crate::promisor::repo_treats_promisor_packs(
764 self.repo.git_dir(),
765 &self.repo.config().unwrap_or_default(),
766 )
767 }) && self.repo.object_id_after_replace(&commit_oid) == commit_oid;
768 if share_linkage {
769 let cache = self
770 .repo
771 .caches()
772 .commit_linkage()
773 .lock()
774 .unwrap_or_else(|e| e.into_inner());
775 if let Some(parents) = cache.parents.get(&commit_oid) {
776 return Ok(parents.clone());
777 }
778 }
779 let object = self.repo.odb().read(&commit_oid)?;
780 if object.kind != ObjectKind::Commit {
781 return Err(Error::InvalidRef(format!(
782 "invalid ref: {commit_oid} is not a commit"
783 )));
784 }
785 let commit = crate::objects::CommitRef::parse(&object.data)?;
786 let parents = commit.parents().to_vec();
787 if share_linkage {
788 let mut cache = self
789 .repo
790 .caches()
791 .commit_linkage()
792 .lock()
793 .unwrap_or_else(|e| e.into_inner());
794 if cache.parents.len() < NAV_LINKAGE_CACHE_LIMIT {
795 cache.times.insert(commit_oid, commit.committer_timestamp());
796 cache.parents.insert(commit_oid, parents.clone());
797 }
798 }
799 Ok(parents)
800 }
801}
802
803pub fn commit_parents_for_navigation(
805 repo: &Repository,
806 commit_oid: ObjectId,
807) -> Result<Vec<ObjectId>> {
808 NavParentCache::new(repo, NavigationMetadata::Discard).parents_of(commit_oid)
809}
810
811#[derive(Debug, Clone, Copy)]
812enum ParentShorthandKind {
813 At,
815 Bang,
817 Minus { exclude_parent: usize },
819}
820
821#[must_use]
823pub fn spec_has_parent_shorthand_suffix(spec: &str) -> bool {
824 find_parent_shorthand(spec).is_some()
825}
826
827fn find_parent_shorthand(spec: &str) -> Option<(usize, ParentShorthandKind)> {
828 let mut best: Option<(usize, ParentShorthandKind, u8)> = None;
829 for (idx, _) in spec.match_indices('^') {
830 let Some(tail) = spec.get(idx + 1..) else {
831 continue;
832 };
833 if tail.starts_with('@') && idx + 2 == spec.len() {
834 best = Some((idx, ParentShorthandKind::At, 0));
835 break;
836 }
837 if tail.starts_with('!') && idx + 2 == spec.len() {
838 let cand = (idx, ParentShorthandKind::Bang, 1);
839 best = Some(match best {
840 Some(b) if b.2 < 1 => b,
841 _ => cand,
842 });
843 continue;
844 }
845 if let Some(after) = tail.strip_prefix('-') {
846 let (exclude_parent, valid) = if after.is_empty() {
847 (1usize, true)
848 } else if after.bytes().all(|b| b.is_ascii_digit()) && !after.is_empty() {
849 let n: usize = after.parse().unwrap_or(0);
850 (n, n >= 1)
851 } else {
852 (0, false)
853 };
854 if !valid {
855 continue;
856 }
857 let cand = (idx, ParentShorthandKind::Minus { exclude_parent }, 2);
858 best = Some(match best {
859 Some(b) if b.2 < 2 => b,
860 _ => cand,
861 });
862 }
863 }
864 best.map(|(i, k, _)| (i, k))
865}
866
867pub fn expand_parent_shorthand_rev_parse_lines(
875 repo: &Repository,
876 spec: &str,
877 symbolic: bool,
878 short_len: Option<usize>,
879) -> Result<Option<Vec<String>>> {
880 let Some((mark_idx, kind)) = find_parent_shorthand(spec) else {
881 return Ok(None);
882 };
883 let base_spec = &spec[..mark_idx];
884 let base_for_resolve = if base_spec.is_empty() {
885 "HEAD"
886 } else {
887 base_spec
888 };
889 let symbolic_base = if base_spec.is_empty() {
892 "HEAD"
893 } else {
894 base_spec
895 };
896 let tip_oid = resolve_revision_for_range_end(repo, base_for_resolve)?;
897 let commit_oid = peel_to_commit_for_merge_base(repo, tip_oid)?;
898 let parents = commit_parents_for_navigation(repo, commit_oid)?;
899
900 let mut out = Vec::new();
901 match kind {
902 ParentShorthandKind::At => {
903 if parents.is_empty() {
904 return Ok(Some(out));
905 }
906 for (i, p) in parents.iter().enumerate() {
907 let parent_n = i + 1;
908 if symbolic {
909 out.push(format!("{symbolic_base}^{parent_n}"));
910 } else if let Some(len) = short_len {
911 out.push(abbreviate_object_id(repo, *p, len)?);
912 } else {
913 out.push(p.to_string());
914 }
915 }
916 }
917 ParentShorthandKind::Bang => {
918 if parents.is_empty() {
919 if symbolic {
920 out.push(symbolic_base.to_string());
921 } else if let Some(len) = short_len {
922 out.push(abbreviate_object_id(repo, commit_oid, len)?);
923 } else {
924 out.push(commit_oid.to_string());
925 }
926 return Ok(Some(out));
927 }
928 if symbolic {
929 out.push(symbolic_base.to_string());
930 for (i, _) in parents.iter().enumerate() {
931 let parent_n = i + 1;
932 out.push(format!("^{symbolic_base}^{parent_n}"));
933 }
934 } else if let Some(len) = short_len {
935 out.push(abbreviate_object_id(repo, commit_oid, len)?);
936 for p in &parents {
937 out.push(format!("^{}", abbreviate_object_id(repo, *p, len)?));
938 }
939 } else {
940 out.push(commit_oid.to_string());
941 for p in &parents {
942 out.push(format!("^{p}"));
943 }
944 }
945 }
946 ParentShorthandKind::Minus { exclude_parent } => {
947 if exclude_parent > parents.len() {
948 return Ok(None);
949 }
950 let excluded_parent = parents[exclude_parent - 1];
951 if symbolic {
952 out.push(symbolic_base.to_string());
953 out.push(format!("^{symbolic_base}^{exclude_parent}"));
954 } else if let Some(len) = short_len {
955 out.push(abbreviate_object_id(repo, commit_oid, len)?);
956 out.push(format!(
957 "^{}",
958 abbreviate_object_id(repo, excluded_parent, len)?
959 ));
960 } else {
961 out.push(commit_oid.to_string());
962 out.push(format!("^{excluded_parent}"));
963 }
964 }
965 }
966 Ok(Some(out))
967}
968
969pub fn split_double_dot_range(spec: &str) -> Option<(&str, &str)> {
971 if spec == ".." {
972 return Some(("", ""));
973 }
974 let bytes = spec.as_bytes();
975 let mut search = 0usize;
976 while let Some(rel) = spec[search..].find("..") {
977 let idx = search + rel;
978 let touches_dot_before = idx > 0 && bytes[idx - 1] == b'.';
980 let touches_dot_after = idx + 2 < bytes.len() && bytes[idx + 2] == b'.';
981 if touches_dot_before || touches_dot_after {
982 search = idx + 1;
983 continue;
984 }
985 if idx + 2 < bytes.len() && (bytes[idx + 2] == b'/' || bytes[idx + 2] == b'\\') {
987 search = idx + 1;
988 continue;
989 }
990 let left = &spec[..idx];
991 let right = &spec[idx + 2..];
992 return Some((left, right));
993 }
994 None
995}
996
997#[must_use]
1001pub fn split_triple_dot_range(spec: &str) -> Option<(&str, &str)> {
1002 if spec == "..." {
1003 return Some(("", ""));
1004 }
1005 let bytes = spec.as_bytes();
1006 let mut search = 0usize;
1007 while let Some(rel) = spec[search..].find("...") {
1008 let idx = search + rel;
1009 let four_before = idx >= 1 && bytes[idx - 1] == b'.';
1010 let four_after = idx + 3 < bytes.len() && bytes[idx + 3] == b'.';
1011 if four_before || four_after {
1012 search = idx + 1;
1013 continue;
1014 }
1015 let left = &spec[..idx];
1016 let right = &spec[idx + 3..];
1017 return Some((left, right));
1018 }
1019 None
1020}
1021
1022fn selector_depth_after(bytes: &[u8], i: usize, depth: usize) -> usize {
1025 match bytes[i] {
1026 b'{' if depth > 0 || (i > 0 && matches!(bytes[i - 1], b'^' | b'@')) => depth + 1,
1027 b'}' => depth.saturating_sub(1),
1028 _ => depth,
1029 }
1030}
1031
1032#[must_use]
1042pub(crate) fn is_top_level_range(spec: &str) -> bool {
1043 let bytes = spec.as_bytes();
1044 let mut depth = 0usize;
1045 for (i, &b) in bytes.iter().enumerate() {
1046 depth = selector_depth_after(bytes, i, depth);
1047 match b {
1048 b':' if depth == 0 => return false,
1049 b'.' if depth == 0 && bytes.get(i + 1) == Some(&b'.') => return true,
1050 _ => {}
1051 }
1052 }
1053 false
1054}
1055
1056#[must_use]
1062pub(crate) fn split_top_level_triple_dot_range(spec: &str) -> Option<(&str, &str)> {
1063 let bytes = spec.as_bytes();
1064 let mut depth = 0usize;
1065 let mut i = 0;
1066 while i < bytes.len() {
1067 depth = selector_depth_after(bytes, i, depth);
1068 match bytes[i] {
1069 b':' if depth == 0 => return None,
1070 b'.' if depth == 0 => {
1071 let run = bytes[i..].iter().take_while(|&&b| b == b'.').count();
1072 if run == 3 {
1073 return Some((&spec[..i], &spec[i + 3..]));
1074 }
1075 i += run;
1076 continue;
1077 }
1078 _ => {}
1079 }
1080 i += 1;
1081 }
1082 None
1083}
1084
1085pub fn resolve_revision_without_index_dwim(repo: &Repository, spec: &str) -> Result<ObjectId> {
1088 resolve_revision_impl(repo, spec, false, false, true, false, false, false, false)
1089}
1090
1091pub fn resolve_revision(repo: &Repository, spec: &str) -> Result<ObjectId> {
1093 resolve_revision_impl(repo, spec, true, false, true, false, false, false, true)
1094}
1095
1096pub fn resolve_revision_for_checkout_guess(
1099 repo: &Repository,
1100 spec: &str,
1101 remote_branch_guess: bool,
1102) -> Result<ObjectId> {
1103 resolve_revision_impl(
1104 repo,
1105 spec,
1106 true,
1107 false,
1108 true,
1109 false,
1110 false,
1111 false,
1112 remote_branch_guess,
1113 )
1114}
1115
1116pub fn resolve_revision_for_range_end(repo: &Repository, spec: &str) -> Result<ObjectId> {
1119 resolve_revision_impl(repo, spec, true, true, true, false, false, false, true)
1120}
1121
1122pub fn resolve_revision_for_range_end_without_index_dwim(
1129 repo: &Repository,
1130 spec: &str,
1131) -> Result<ObjectId> {
1132 resolve_revision_impl(repo, spec, false, true, true, false, false, false, true)
1133}
1134
1135pub fn resolve_revision_for_verify(repo: &Repository, spec: &str) -> Result<ObjectId> {
1140 resolve_revision_impl(repo, spec, false, true, true, false, false, false, true)
1141}
1142
1143pub fn resolve_revision_for_commit_tree_tree(repo: &Repository, spec: &str) -> Result<ObjectId> {
1145 resolve_revision_impl(repo, spec, true, false, true, false, true, false, true)
1146}
1147
1148pub fn resolve_revision_for_patch_old_blob(repo: &Repository, spec: &str) -> Result<ObjectId> {
1150 resolve_revision_impl(repo, spec, true, false, true, false, false, true, true)
1151}
1152
1153pub fn try_parse_double_dot_log_range(
1163 repo: &Repository,
1164 spec: &str,
1165) -> Result<Option<(ObjectId, ObjectId)>> {
1166 let Some((left, right)) = split_double_dot_range(spec) else {
1167 return Ok(None);
1168 };
1169 let left_tip = if left.is_empty() {
1170 resolve_revision_for_range_end(repo, "HEAD")?
1171 } else {
1172 resolve_revision_for_range_end(repo, left)?
1173 };
1174 let right_tip = if right.is_empty() {
1175 resolve_revision_for_range_end(repo, "HEAD")?
1176 } else {
1177 resolve_revision_for_range_end(repo, right)?
1178 };
1179 let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
1180 let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
1181 Ok(Some((left_c, right_c)))
1182}
1183
1184fn try_parse_double_dot_log_range_without_index_dwim(
1185 repo: &Repository,
1186 spec: &str,
1187) -> Result<Option<(ObjectId, ObjectId)>> {
1188 let Some((left, right)) = split_double_dot_range(spec) else {
1189 return Ok(None);
1190 };
1191 let left_tip = if left.is_empty() {
1192 resolve_revision_for_range_end_without_index_dwim(repo, "HEAD")?
1193 } else {
1194 resolve_revision_for_range_end_without_index_dwim(repo, left)?
1195 };
1196 let right_tip = if right.is_empty() {
1197 resolve_revision_for_range_end_without_index_dwim(repo, "HEAD")?
1198 } else {
1199 resolve_revision_for_range_end_without_index_dwim(repo, right)?
1200 };
1201 let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
1202 let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
1203 Ok(Some((left_c, right_c)))
1204}
1205
1206#[must_use]
1216pub fn revision_spec_contains_ancestry_navigation(spec: &str) -> bool {
1217 let (_, steps) = parse_nav_steps(spec);
1218 !steps.is_empty()
1219}
1220
1221pub fn resolve_revision_as_commit(repo: &Repository, spec: &str) -> Result<ObjectId> {
1223 if let Some((left, right)) = split_triple_dot_range(spec) {
1224 let left_tip = if left.is_empty() {
1225 resolve_revision_for_range_end(repo, "HEAD")?
1226 } else {
1227 resolve_revision_for_range_end(repo, left)?
1228 };
1229 let right_tip = if right.is_empty() {
1230 resolve_revision_for_range_end(repo, "HEAD")?
1231 } else {
1232 resolve_revision_for_range_end(repo, right)?
1233 };
1234 let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
1235 let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
1236 let bases = crate::merge_base::merge_bases_first_vs_rest(repo, left_c, &[right_c])?;
1237 return bases
1238 .into_iter()
1239 .next()
1240 .ok_or_else(|| Error::ObjectNotFound(format!("no merge base for '{spec}'")));
1241 }
1242 if let Some((_excl, tip)) = try_parse_double_dot_log_range(repo, spec)? {
1243 return Ok(tip);
1244 }
1245 let oid = resolve_revision_for_range_end(repo, spec)?;
1246 peel_to_commit_for_merge_base(repo, oid)
1247}
1248
1249pub fn resolve_revision_as_commit_without_index_dwim(
1254 repo: &Repository,
1255 spec: &str,
1256) -> Result<ObjectId> {
1257 if let Some((left, right)) = split_triple_dot_range(spec) {
1258 let left_tip = if left.is_empty() {
1259 resolve_revision_for_range_end_without_index_dwim(repo, "HEAD")?
1260 } else {
1261 resolve_revision_for_range_end_without_index_dwim(repo, left)?
1262 };
1263 let right_tip = if right.is_empty() {
1264 resolve_revision_for_range_end_without_index_dwim(repo, "HEAD")?
1265 } else {
1266 resolve_revision_for_range_end_without_index_dwim(repo, right)?
1267 };
1268 let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
1269 let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
1270 let bases = crate::merge_base::merge_bases_first_vs_rest(repo, left_c, &[right_c])?;
1271 return bases
1272 .into_iter()
1273 .next()
1274 .ok_or_else(|| Error::ObjectNotFound(format!("no merge base for '{spec}'")));
1275 }
1276 if let Some((_excl, tip)) = try_parse_double_dot_log_range_without_index_dwim(repo, spec)? {
1277 return Ok(tip);
1278 }
1279 let oid = resolve_revision_for_range_end_without_index_dwim(repo, spec)?;
1280 peel_to_commit_for_merge_base(repo, oid)
1281}
1282
1283fn resolve_ref_dwim_for_rev_parse(repo: &Repository, spec: &str) -> (usize, Option<ObjectId>) {
1284 const RULES: &[&str] = &[
1285 "{0}",
1286 "refs/{0}",
1287 "refs/tags/{0}",
1288 "refs/heads/{0}",
1289 "refs/remotes/{0}",
1290 "refs/remotes/{0}/HEAD",
1291 ];
1292
1293 let mut count = 0usize;
1294 let mut first = None;
1295 let refname_opts = RefNameOptions::default();
1296 for rule in RULES {
1297 let candidate = rule.replace("{0}", spec);
1298 let resolved = match repo.refs().read_raw(&candidate) {
1300 Ok(None) => continue,
1301 Ok(Some(refs::store::RawRef::Direct(oid))) => Ok(oid),
1302 Ok(Some(refs::store::RawRef::Symbolic(target))) => {
1303 if check_refname_format(&target, &refname_opts).is_err()
1304 || repo.resolve_ref_oid(&target).is_err()
1305 {
1306 if candidate != "HEAD" {
1311 repo.diagnostics()
1312 .warn(crate::diagnostics::Warning::DanglingSymref {
1313 name: candidate.clone(),
1314 });
1315 }
1316 continue;
1317 }
1318 repo.resolve_ref_oid(&candidate)
1319 }
1320 Err(_) => repo.resolve_ref_oid(&candidate),
1321 };
1322 if let Ok(oid) = resolved {
1323 count += 1;
1324 if first.is_none() {
1325 first = Some(oid);
1326 }
1327 }
1328 }
1329 (count, first)
1330}
1331
1332#[expect(clippy::too_many_arguments)]
1333fn resolve_revision_impl(
1334 repo: &Repository,
1335 spec: &str,
1336 index_dwim: bool,
1337 commit_only_hex: bool,
1338 use_disambiguate_config: bool,
1339 treeish_colon_lhs: bool,
1340 implicit_tree_abbrev: bool,
1341 implicit_blob_abbrev: bool,
1342 remote_branch_name_guess: bool,
1343) -> Result<ObjectId> {
1344 resolve_revision_with_navigation(
1345 repo,
1346 spec,
1347 index_dwim,
1348 commit_only_hex,
1349 use_disambiguate_config,
1350 treeish_colon_lhs,
1351 implicit_tree_abbrev,
1352 implicit_blob_abbrev,
1353 remote_branch_name_guess,
1354 NavigationMetadata::Discard,
1355 )
1356}
1357
1358pub(crate) fn resolve_walk_range_end(repo: &Repository, spec: &str) -> Result<ObjectId> {
1360 resolve_revision_with_navigation(
1361 repo,
1362 spec,
1363 true,
1364 true,
1365 true,
1366 false,
1367 false,
1368 false,
1369 true,
1370 NavigationMetadata::RetainForWalk,
1371 )
1372}
1373
1374#[expect(clippy::too_many_arguments)]
1375fn resolve_revision_with_navigation(
1376 repo: &Repository,
1377 spec: &str,
1378 index_dwim: bool,
1379 commit_only_hex: bool,
1380 use_disambiguate_config: bool,
1381 treeish_colon_lhs: bool,
1382 implicit_tree_abbrev: bool,
1383 implicit_blob_abbrev: bool,
1384 remote_branch_name_guess: bool,
1385 metadata: NavigationMetadata,
1386) -> Result<ObjectId> {
1387 if let Some(pattern) = spec.strip_prefix(":/") {
1390 if pattern.is_empty() {
1391 let head = crate::state::resolve_head_for_repository(repo)
1393 .map_err(|_| Error::ObjectNotFound(":/".to_owned()))?;
1394 return head
1395 .oid()
1396 .copied()
1397 .ok_or_else(|| Error::ObjectNotFound(":/".to_owned()));
1398 }
1399 return resolve_commit_message_search(repo, pattern);
1400 }
1401
1402 if let Some(index_spec) = parse_index_colon_spec(spec) {
1403 let path = normalize_colon_path_for_tree(repo, index_spec.raw_path)?;
1404 return resolve_index_path_at_stage(repo, &path, index_spec.stage)
1405 .map_err(|e| diagnose_index_path_error(repo, &path, index_spec.stage, e));
1406 }
1407
1408 if let Some(tag_path) = spec.strip_prefix("tags/") {
1410 if !tag_path.is_empty() {
1411 let tag_ref = format!("refs/tags/{tag_path}");
1412 if let Ok(oid) = repo.resolve_ref_oid(&tag_ref) {
1413 return Ok(oid);
1414 }
1415 }
1416 }
1417
1418 if spec == "AUTO_MERGE" {
1420 let raw = fs::read_to_string(repo.git_dir().join("AUTO_MERGE")).map_err(|e| {
1421 Error::from(RevParseError::AutoMergeRead {
1422 detail: e.to_string(),
1423 })
1424 })?;
1425 let line = raw.lines().next().unwrap_or("").trim();
1426 return line
1427 .parse::<ObjectId>()
1428 .map_err(|_| Error::InvalidRef("AUTO_MERGE: invalid object id".to_owned()));
1429 }
1430
1431 if spec.starts_with("refs/") && !spec.contains(':') {
1435 if let Ok(oid) = repo.resolve_ref_oid(spec) {
1436 return Ok(oid);
1437 }
1438 }
1439
1440 if let Some((left_raw, right_raw)) = split_top_level_triple_dot_range(spec) {
1443 if !left_raw.is_empty() || !right_raw.is_empty() {
1444 let left_oid = peel_to_commit_for_merge_base(
1445 repo,
1446 if left_raw.is_empty() {
1447 resolve_revision_impl(
1448 repo,
1449 "HEAD",
1450 index_dwim,
1451 commit_only_hex,
1452 use_disambiguate_config,
1453 false,
1454 false,
1455 false,
1456 remote_branch_name_guess,
1457 )?
1458 } else {
1459 resolve_revision_impl(
1460 repo,
1461 left_raw,
1462 index_dwim,
1463 commit_only_hex,
1464 use_disambiguate_config,
1465 false,
1466 false,
1467 false,
1468 remote_branch_name_guess,
1469 )?
1470 },
1471 )?;
1472 let right_oid = peel_to_commit_for_merge_base(
1473 repo,
1474 if right_raw.is_empty() {
1475 resolve_revision_impl(
1476 repo,
1477 "HEAD",
1478 index_dwim,
1479 commit_only_hex,
1480 use_disambiguate_config,
1481 false,
1482 false,
1483 false,
1484 remote_branch_name_guess,
1485 )?
1486 } else {
1487 resolve_revision_impl(
1488 repo,
1489 right_raw,
1490 index_dwim,
1491 commit_only_hex,
1492 use_disambiguate_config,
1493 false,
1494 false,
1495 false,
1496 remote_branch_name_guess,
1497 )?
1498 },
1499 )?;
1500 let bases = crate::merge_base::merge_bases_first_vs_rest(repo, left_oid, &[right_oid])?;
1501 return bases
1502 .into_iter()
1503 .next()
1504 .ok_or_else(|| Error::ObjectNotFound(format!("no merge base for '{spec}'")));
1505 }
1506 }
1507
1508 if let Some((before, after)) = split_treeish_colon(spec) {
1512 if !before.is_empty() && !spec.starts_with(":/") {
1513 let rev_oid = match resolve_revision_impl(
1515 repo,
1516 before,
1517 index_dwim,
1518 commit_only_hex,
1519 use_disambiguate_config,
1520 true,
1521 false,
1522 false,
1523 remote_branch_name_guess,
1524 ) {
1525 Ok(o) => o,
1526 Err(Error::ObjectNotFound(s)) if s == before => {
1527 return Err(RevParseError::InvalidObjectName {
1528 name: before.to_owned(),
1529 }
1530 .into());
1531 }
1532 Err(Error::RevParse(RevParseError::AmbiguousArgument { .. })) => {
1533 return Err(RevParseError::InvalidObjectName {
1534 name: before.to_owned(),
1535 }
1536 .into());
1537 }
1538 Err(e) => return Err(e),
1539 };
1540 let tree_oid = peel_to_tree(repo, rev_oid)?;
1541 if after.is_empty() {
1542 return Ok(tree_oid);
1544 }
1545 let clean_path = match normalize_colon_path_for_tree(repo, after) {
1546 Ok(p) => p,
1547 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
1548 let wt = repo
1549 .work_tree()
1550 .and_then(|p| p.canonicalize_simplified().ok())
1551 .map(|p| p.display().to_string())
1552 .unwrap_or_default();
1553 return Err(RevParseError::OutsideRepository {
1554 path: after.to_owned(),
1555 work_tree: wt,
1556 }
1557 .into());
1558 }
1559 Err(e) => return Err(e),
1560 };
1561 return resolve_tree_path_rev_parse(repo, &tree_oid, &clean_path)
1562 .map_err(|e| diagnose_tree_path_error(repo, before, after, &clean_path, e));
1563 }
1564 }
1565
1566 let (base_with_nav, peel) = parse_peel_suffix(spec);
1567 let (base, nav_steps) = parse_nav_steps(base_with_nav);
1568 let peel_for_hex = peel
1569 .or(((treeish_colon_lhs || implicit_tree_abbrev) && peel.is_none()).then_some("tree"))
1570 .or((implicit_blob_abbrev && peel.is_none()).then_some("blob"));
1571 let mut oid = resolve_base(
1572 repo,
1573 base,
1574 index_dwim,
1575 commit_only_hex,
1576 use_disambiguate_config,
1577 peel_for_hex,
1578 implicit_tree_abbrev,
1579 implicit_blob_abbrev,
1580 remote_branch_name_guess,
1581 )?;
1582 let nav_cache = NavParentCache::new(repo, metadata);
1583 for step in nav_steps {
1584 oid = apply_nav_step(repo, &nav_cache, oid, step).map_err(|e| {
1585 if matches!(e, Error::ObjectNotFound(_)) {
1586 RevParseError::RevisionOutOfRange {
1587 spec: spec.to_owned(),
1588 }
1589 .into()
1590 } else {
1591 e
1592 }
1593 })?;
1594 }
1595 apply_peel(repo, oid, peel)
1596}
1597
1598fn normalize_path_components(path: PathBuf) -> PathBuf {
1601 let mut out = PathBuf::new();
1602 for c in path.components() {
1603 match c {
1604 Component::Prefix(_) | Component::RootDir => out.push(c),
1605 Component::CurDir => {}
1606 Component::ParentDir => {
1607 let _ = out.pop();
1608 }
1609 Component::Normal(x) => out.push(x),
1610 }
1611 }
1612 out
1613}
1614
1615fn normalize_colon_path_for_bare_tree(raw_path: &str) -> Result<String> {
1620 let cwd_relative = raw_path.starts_with("./") || raw_path.starts_with("../") || raw_path == ".";
1621 if cwd_relative {
1622 return Err(Error::InvalidRef(
1623 "relative path syntax can't be used outside working tree".to_owned(),
1624 ));
1625 }
1626 let s = raw_path.trim_start_matches('/');
1627 let mut stack: Vec<&str> = Vec::new();
1628 for part in s.split('/') {
1629 if part.is_empty() || part == "." {
1630 continue;
1631 }
1632 if part == ".." {
1633 let _ = stack.pop();
1634 } else {
1635 stack.push(part);
1636 }
1637 }
1638 Ok(stack.join("/"))
1639}
1640
1641fn normalize_colon_path_for_tree(repo: &Repository, raw_path: &str) -> Result<String> {
1642 let Some(work_tree) = repo.work_tree() else {
1643 return normalize_colon_path_for_bare_tree(raw_path);
1644 };
1645
1646 let cwd = repo.environment().cwd.clone();
1647 let wt_canon = work_tree.canonicalize_simplified().map_err(Error::Io)?;
1648
1649 let cwd_relative = raw_path.starts_with("./") || raw_path.starts_with("../") || raw_path == ".";
1650 if cwd_relative && !path_is_within(&cwd, work_tree) {
1651 return Err(Error::InvalidRef(
1652 "relative path syntax can't be used outside working tree".to_owned(),
1653 ));
1654 }
1655
1656 let full = if raw_path.starts_with('/') {
1658 PathBuf::from(raw_path)
1659 } else if cwd_relative {
1660 cwd.join(raw_path)
1661 } else {
1662 work_tree.join(raw_path)
1663 };
1664 let full = normalize_path_components(full);
1665
1666 if !path_is_within(&full, &wt_canon) {
1667 return Err(Error::InvalidRef("outside repository".to_owned()));
1668 }
1669 let rel = full
1670 .strip_prefix(&wt_canon)
1671 .map_err(|_| Error::InvalidRef("outside repository".to_owned()))?;
1672 let s = rel.to_string_lossy().replace('\\', "/");
1673 Ok(s.trim_end_matches('/').to_owned())
1674}
1675
1676fn is_unreplaced_commit(repo: &Repository, oid: ObjectId) -> bool {
1681 if repo.object_id_after_replace(&oid) != oid {
1682 return false;
1683 }
1684 let in_commit_graph = repo
1685 .odb()
1686 .commit_graph_chain()
1687 .is_some_and(|graph| graph.find_commit(&oid).is_some());
1688 in_commit_graph
1689 || repo
1690 .odb()
1691 .read_info(&oid)
1692 .is_ok_and(|info| info.kind == ObjectKind::Commit)
1693}
1694
1695pub fn peel_to_commit_for_merge_base(repo: &Repository, oid: ObjectId) -> Result<ObjectId> {
1697 peel_tags_to_commit(repo, oid)?
1698 .map_err(|other| Error::InvalidRef(format!("object {other} does not name a commit")))
1699}
1700
1701pub(crate) fn peel_tags_to_commit(
1706 repo: &Repository,
1707 mut oid: ObjectId,
1708) -> Result<std::result::Result<ObjectId, ObjectId>> {
1709 loop {
1711 match repo.read_info_replaced(&oid)?.kind {
1712 ObjectKind::Commit => return Ok(Ok(oid)),
1713 ObjectKind::Tag => oid = parse_tag_target(&repo.read_replaced(&oid)?.data)?,
1714 ObjectKind::Tree | ObjectKind::Blob => return Ok(Err(oid)),
1715 }
1716 }
1717}
1718
1719pub fn try_peel_to_commit_for_merge_base(
1722 repo: &Repository,
1723 oid: ObjectId,
1724) -> Result<Option<ObjectId>> {
1725 if is_unreplaced_commit(repo, oid) {
1726 return Ok(Some(oid));
1727 }
1728 let oid = apply_peel(repo, oid, Some(""))?;
1729 let obj = repo.odb().read(&oid)?;
1730 match obj.kind {
1731 ObjectKind::Commit => Ok(Some(oid)),
1732 ObjectKind::Tree | ObjectKind::Blob => Ok(None),
1733 ObjectKind::Tag => Err(Error::InvalidRef("unexpected tag after peel".to_owned())),
1734 }
1735}
1736
1737pub fn peel_to_tree(repo: &Repository, oid: ObjectId) -> Result<ObjectId> {
1743 let obj = repo.read_replaced(&oid)?;
1744 match obj.kind {
1745 crate::objects::ObjectKind::Tree => Ok(oid),
1746 crate::objects::ObjectKind::Commit => {
1747 let commit = crate::objects::parse_commit(&obj.data)?;
1748 Ok(commit.tree)
1749 }
1750 crate::objects::ObjectKind::Tag => {
1751 let tag = crate::objects::parse_tag(&obj.data)?;
1752 peel_to_tree(repo, tag.object)
1753 }
1754 _ => Err(Error::ObjectNotFound(format!(
1755 "cannot peel {} to tree",
1756 oid
1757 ))),
1758 }
1759}
1760
1761fn resolve_tree_path(repo: &Repository, tree_oid: &ObjectId, path: &str) -> Result<ObjectId> {
1767 resolve_treeish_path_to_object(repo, *tree_oid, path)
1768}
1769
1770#[derive(Debug)]
1772enum TreePathError {
1773 NotInTree,
1775 Other(Error),
1777}
1778
1779impl From<Error> for TreePathError {
1780 fn from(err: Error) -> Self {
1781 Self::Other(err)
1782 }
1783}
1784
1785fn resolve_tree_path_rev_parse(
1787 repo: &Repository,
1788 tree_oid: &ObjectId,
1789 path: &str,
1790) -> std::result::Result<ObjectId, TreePathError> {
1791 let obj = repo.odb().read(tree_oid)?;
1792 let entries = crate::objects::parse_tree(&obj.data)?;
1793 let components: Vec<&str> = path.split('/').filter(|c| !c.is_empty()).collect();
1794 if components.is_empty() {
1795 return Err(Error::InvalidRef(format!(
1796 "path '{path}' does not name an object in tree {tree_oid}"
1797 ))
1798 .into());
1799 }
1800
1801 let first = components[0];
1802 let rest: Vec<&str> = components[1..].to_vec();
1803 for entry in entries {
1804 let name = String::from_utf8_lossy(&entry.name);
1805 if name == first {
1806 if rest.is_empty() {
1807 return Ok(entry.oid);
1813 }
1814 if entry.mode != crate::index::MODE_TREE {
1815 return Err(Error::ObjectNotFound(path.to_owned()).into());
1816 }
1817 return resolve_tree_path_rev_parse(repo, &entry.oid, &rest.join("/"));
1818 }
1819 }
1820 Err(TreePathError::NotInTree)
1821}
1822
1823#[derive(Debug, Clone)]
1827pub struct TreeishBlobAtPath {
1828 pub path: String,
1830 pub oid: ObjectId,
1832 pub mode: String,
1834}
1835
1836pub fn resolve_treeish_blob_at_path(repo: &Repository, spec: &str) -> Result<TreeishBlobAtPath> {
1841 let (before, after) = split_treeish_colon(spec)
1842 .ok_or_else(|| Error::InvalidRef(format!("'{spec}' is not a treeish:path revision")))?;
1843
1844 let rev_oid =
1845 match resolve_revision_impl(repo, before, true, false, true, true, false, false, true) {
1846 Ok(o) => o,
1847 Err(Error::ObjectNotFound(s)) if s == before => {
1848 return Err(RevParseError::InvalidObjectName {
1849 name: before.to_owned(),
1850 }
1851 .into());
1852 }
1853 Err(Error::RevParse(RevParseError::AmbiguousArgument { .. })) => {
1854 return Err(RevParseError::InvalidObjectName {
1855 name: before.to_owned(),
1856 }
1857 .into());
1858 }
1859 Err(e) => return Err(e),
1860 };
1861
1862 let tree_oid = peel_to_tree(repo, rev_oid)?;
1863
1864 if after.is_empty() {
1866 return Ok(TreeishBlobAtPath {
1867 path: String::new(),
1868 oid: tree_oid,
1869 mode: "040000".to_string(),
1870 });
1871 }
1872
1873 let clean_path = match normalize_colon_path_for_tree(repo, after) {
1874 Ok(p) => p,
1875 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
1876 let wt = repo
1877 .work_tree()
1878 .and_then(|p| p.canonicalize_simplified().ok())
1879 .map(|p| p.display().to_string())
1880 .unwrap_or_default();
1881 return Err(RevParseError::OutsideRepository {
1882 path: after.to_owned(),
1883 work_tree: wt,
1884 }
1885 .into());
1886 }
1887 Err(e) => return Err(e),
1888 };
1889
1890 let (oid, mode_str) = walk_tree_to_blob_entry(repo, &tree_oid, &clean_path)
1891 .map_err(|e| diagnose_tree_path_error(repo, before, after, &clean_path, e))?;
1892 Ok(TreeishBlobAtPath {
1893 path: clean_path,
1894 oid,
1895 mode: mode_str,
1896 })
1897}
1898
1899fn walk_tree_to_blob_entry(
1903 repo: &Repository,
1904 tree_oid: &ObjectId,
1905 path: &str,
1906) -> std::result::Result<(ObjectId, String), TreePathError> {
1907 let obj = repo.read_replaced(tree_oid)?;
1908 let entries = crate::objects::parse_tree(&obj.data)?;
1909 let components: Vec<&str> = path.split('/').filter(|c| !c.is_empty()).collect();
1910 if components.is_empty() {
1911 return Err(Error::InvalidRef(format!(
1912 "path '{path}' does not name a blob in tree {tree_oid}"
1913 ))
1914 .into());
1915 }
1916
1917 let first = components[0];
1918 let rest: Vec<&str> = components[1..].to_vec();
1919 for entry in entries {
1920 let name = String::from_utf8_lossy(&entry.name);
1921 if name == first {
1922 if rest.is_empty() {
1923 if entry.mode == crate::index::MODE_TREE {
1924 return Err(Error::InvalidRef(format!("'{path}' is a tree, not a blob")).into());
1925 }
1926 return Ok((entry.oid, entry.mode_str()));
1927 }
1928 if entry.mode != crate::index::MODE_TREE {
1929 return Err(Error::ObjectNotFound(path.to_owned()).into());
1930 }
1931 return walk_tree_to_blob_entry(repo, &entry.oid, &rest.join("/"));
1932 }
1933 }
1934 Err(TreePathError::NotInTree)
1935}
1936
1937#[derive(Debug, Clone, Copy)]
1939enum NavStep {
1940 ParentN(usize),
1942 AncestorN(usize),
1944}
1945
1946fn parse_nav_steps(spec: &str) -> (&str, Vec<NavStep>) {
1950 let mut steps = Vec::new();
1951 let mut remaining = spec;
1952
1953 loop {
1954 if let Some(tilde_pos) = remaining.rfind('~') {
1956 let after = &remaining[tilde_pos + 1..];
1957 if after.is_empty() {
1958 steps.push(NavStep::AncestorN(1));
1960 remaining = &remaining[..tilde_pos];
1961 continue;
1962 }
1963 if after.bytes().all(|b| b.is_ascii_digit()) {
1964 let n: usize = after.parse().unwrap_or(1);
1965 steps.push(NavStep::AncestorN(n));
1966 remaining = &remaining[..tilde_pos];
1967 continue;
1968 }
1969 }
1970
1971 if let Some(caret_pos) = remaining.rfind('^') {
1973 let after = &remaining[caret_pos + 1..];
1974 if after.is_empty() {
1975 steps.push(NavStep::ParentN(1));
1977 remaining = &remaining[..caret_pos];
1978 continue;
1979 }
1980 if after.bytes().all(|b| b.is_ascii_digit()) && !after.is_empty() {
1981 let n: usize = after.parse().unwrap_or(usize::MAX);
1982 steps.push(NavStep::ParentN(n));
1983 remaining = &remaining[..caret_pos];
1984 continue;
1985 }
1986 }
1987
1988 break;
1989 }
1990
1991 steps.reverse();
1992 (remaining, steps)
1993}
1994
1995fn peel_annotated_tag_chain(repo: &Repository, mut oid: ObjectId) -> Result<ObjectId> {
1997 loop {
1998 let obj = repo.read_replaced(&oid)?;
1999 if obj.kind != ObjectKind::Tag {
2000 return Ok(oid);
2001 }
2002 let tag = parse_tag(&obj.data)?;
2003 oid = tag.object;
2004 }
2005}
2006
2007fn apply_nav_step(
2009 repo: &Repository,
2010 nav: &NavParentCache<'_>,
2011 oid: ObjectId,
2012 step: NavStep,
2013) -> Result<ObjectId> {
2014 match step {
2015 NavStep::ParentN(0) => Ok(oid),
2016 NavStep::ParentN(n) => {
2017 let oid = peel_annotated_tag_chain(repo, oid)?;
2018 let parents = nav.parents_of(oid)?;
2019 parents
2020 .get(n - 1)
2021 .copied()
2022 .ok_or_else(|| Error::ObjectNotFound(format!("{oid}^{n}")))
2023 }
2024 NavStep::AncestorN(n) => {
2025 let mut current = peel_annotated_tag_chain(repo, oid)?;
2026 for _ in 0..n {
2027 let parents = nav.parents_of(current)?;
2028 current = parents
2029 .first()
2030 .copied()
2031 .ok_or_else(|| Error::ObjectNotFound(format!("{current}~1")))?;
2032 }
2033 Ok(current)
2034 }
2035 }
2036}
2037
2038pub fn abbreviate_object_id(repo: &Repository, oid: ObjectId, min_len: usize) -> Result<String> {
2051 abbreviate_object_id_with_prefix_cache(repo, oid, min_len, &mut HashMap::new())
2052}
2053
2054pub(crate) fn abbreviate_object_id_with_prefix_cache(
2058 repo: &Repository,
2059 oid: ObjectId,
2060 min_len: usize,
2061 prefix_ambiguous: &mut HashMap<String, bool>,
2062) -> Result<String> {
2063 let hex_len = oid.algo().hex_len();
2064 let min_len = min_len.clamp(4, hex_len);
2065 let mut target = oid.to_hex();
2066
2067 if !repo.odb().contains(&oid)? {
2068 target.truncate(min_len);
2069 return Ok(target);
2070 }
2071
2072 crate::oid_abbrev::abbreviate_object_id_known(
2073 repo,
2074 oid,
2075 min_len,
2076 prefix_ambiguous,
2077 &mut [None; 256],
2078 &mut None,
2079 )
2080}
2081
2082#[must_use]
2084pub fn to_relative_path(path: &Path, cwd: &Path) -> String {
2085 let path_components = normalize_components(path);
2086 let cwd_components = normalize_components(cwd);
2087
2088 let mut common = 0usize;
2089 let max_common = path_components.len().min(cwd_components.len());
2090 while common < max_common && path_components[common] == cwd_components[common] {
2091 common += 1;
2092 }
2093
2094 let mut parts = Vec::new();
2095 let up_count = cwd_components.len().saturating_sub(common);
2096 for _ in 0..up_count {
2097 parts.push("..".to_owned());
2098 }
2099 for item in path_components.iter().skip(common) {
2100 parts.push(item.clone());
2101 }
2102
2103 if parts.is_empty() {
2104 ".".to_owned()
2105 } else {
2106 parts.join("/")
2107 }
2108}
2109
2110fn disambiguate_kind_rank(kind: ObjectKind) -> u8 {
2111 match kind {
2112 ObjectKind::Tag => 0,
2113 ObjectKind::Commit => 1,
2114 ObjectKind::Tree => 2,
2115 ObjectKind::Blob => 3,
2116 }
2117}
2118
2119fn oid_satisfies_peel_filter(repo: &Repository, oid: ObjectId, peel_inner: &str) -> bool {
2120 apply_peel(repo, oid, Some(peel_inner)).is_ok()
2121}
2122
2123pub fn ambiguous_object_hint_lines(
2125 repo: &Repository,
2126 short_prefix: &str,
2127 peel_filter: Option<&str>,
2128) -> Result<Vec<AmbiguousObjectHint>> {
2129 let mut typed: Vec<(u8, String, &'static str)> = Vec::new();
2130 let mut bad_hex: Vec<String> = Vec::new();
2131 for oid in list_all_abbrev_matches(repo, short_prefix)? {
2132 let hex = oid.to_hex();
2133 match repo.read_replaced(&oid) {
2134 Ok(obj) => {
2135 let ok = peel_filter.is_none_or(|p| oid_satisfies_peel_filter(repo, oid, p));
2136 if ok {
2137 typed.push((disambiguate_kind_rank(obj.kind), hex, obj.kind.as_str()));
2138 }
2139 }
2140 Err(_) => bad_hex.push(hex),
2141 }
2142 }
2143 if typed.is_empty() && peel_filter.is_some() {
2144 return ambiguous_object_hint_lines(repo, short_prefix, None);
2145 }
2146 bad_hex.sort();
2147 typed.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
2148 let mut out = Vec::new();
2149 for h in bad_hex {
2150 out.push(AmbiguousObjectHint {
2151 oid_hex: h,
2152 kind: None,
2153 });
2154 }
2155 for (_, hex, kind) in typed {
2156 out.push(AmbiguousObjectHint {
2157 oid_hex: hex,
2158 kind: Some(kind),
2159 });
2160 }
2161 Ok(out)
2162}
2163
2164fn read_core_disambiguate(repo: &Repository) -> Option<&'static str> {
2165 let config = repo
2166 .ensure_config_arc_fast()
2167 .unwrap_or_else(|_| Arc::new(ConfigSet::new()));
2168 let v = config.get("core.disambiguate")?;
2169 match v.to_ascii_lowercase().as_str() {
2170 "committish" | "commit" => Some("commit"),
2171 "treeish" | "tree" => Some("tree"),
2172 "blob" => Some("blob"),
2173 "tag" => Some("tag"),
2174 "none" => None,
2175 _ => None,
2176 }
2177}
2178
2179fn warn_if_branch_refname_collides_with_abbrev_hex(
2182 repo: &Repository,
2183 spec: &str,
2184 object_oid: ObjectId,
2185) {
2186 if spec.len() >= repo.odb().hash_algo().hex_len() {
2187 return;
2188 }
2189 let branch_ref = format!("refs/heads/{spec}");
2190 let Ok(ref_oid) = repo.resolve_ref_oid(&branch_ref) else {
2191 return;
2192 };
2193 if ref_oid != object_oid {
2194 repo.warn(crate::diagnostics::Warning::AmbiguousRefname {
2195 spec: spec.to_owned(),
2196 });
2197 }
2198}
2199
2200fn warn_if_hex_ref_collides_with_objects(repo: &Repository, spec: &str, ref_oid: ObjectId) {
2203 if spec.len() >= repo.odb().hash_algo().hex_len() || !is_hex_prefix(spec) {
2204 return;
2205 }
2206 let Ok(matches) = find_abbrev_matches(repo, spec) else {
2207 return;
2208 };
2209 if matches.is_empty() {
2210 return;
2211 }
2212 if matches.len() > 1 || matches[0] != ref_oid {
2213 repo.warn(crate::diagnostics::Warning::AmbiguousRefname {
2214 spec: spec.to_owned(),
2215 });
2216 }
2217}
2218
2219fn disambiguate_hex_by_peel(
2220 repo: &Repository,
2221 spec: &str,
2222 matches: &[ObjectId],
2223 peel: &str,
2224) -> Result<ObjectId> {
2225 let peel_some = Some(peel);
2226 let filtered: Vec<ObjectId> = matches
2227 .iter()
2228 .copied()
2229 .filter(|oid| apply_peel(repo, *oid, peel_some).is_ok())
2230 .collect();
2231 if filtered.len() == 1 {
2232 return Ok(filtered[0]);
2233 }
2234 if filtered.is_empty() {
2235 return Err(RevParseError::AmbiguousShortObject {
2236 spec: spec.to_owned(),
2237 }
2238 .into());
2239 }
2240 let mut peeled_targets: HashSet<ObjectId> = HashSet::new();
2241 for oid in &filtered {
2242 if let Ok(p) = apply_peel(repo, *oid, peel_some) {
2243 peeled_targets.insert(p);
2244 }
2245 }
2246 if peeled_targets.len() == 1 {
2247 let mut sorted = filtered;
2250 sorted.sort_by_key(|o| o.to_hex());
2251 return Ok(sorted[0]);
2252 }
2253 if peel == "commit" {
2256 let mut by_peeled: HashMap<ObjectId, Vec<ObjectId>> = HashMap::new();
2257 for oid in &filtered {
2258 if let Ok(c) = apply_peel(repo, *oid, Some("commit")) {
2259 by_peeled.entry(c).or_default().push(*oid);
2260 }
2261 }
2262 if by_peeled.len() == 1 {
2263 let mut reps: Vec<ObjectId> = by_peeled.into_values().next().unwrap_or_default();
2264 reps.sort_by_key(|o| o.to_hex());
2265 if let Some(oid) = reps.first().copied() {
2266 return Ok(oid);
2267 }
2268 }
2269 }
2270 Err(RevParseError::AmbiguousShortObject {
2271 spec: spec.to_owned(),
2272 }
2273 .into())
2274}
2275
2276fn commit_reachable_closure(repo: &Repository, start: ObjectId) -> Result<HashSet<ObjectId>> {
2277 use std::collections::VecDeque;
2278 let mut seen = HashSet::new();
2279 let mut q = VecDeque::from([start]);
2280 while let Some(oid) = q.pop_front() {
2281 if !seen.insert(oid) {
2282 continue;
2283 }
2284 let obj = match repo.read_replaced(&oid) {
2285 Ok(o) => o,
2286 Err(_) => continue,
2287 };
2288 if obj.kind != ObjectKind::Commit {
2289 continue;
2290 }
2291 let commit = match parse_commit(&obj.data) {
2292 Ok(c) => c,
2293 Err(_) => continue,
2294 };
2295 for p in &commit.parents {
2296 q.push_back(*p);
2297 }
2298 }
2299 Ok(seen)
2300}
2301
2302fn describe_generation_count(
2304 repo: &Repository,
2305 head: ObjectId,
2306 tag_commit: ObjectId,
2307) -> Result<usize> {
2308 let from_tag = commit_reachable_closure(repo, tag_commit)?;
2309 let from_head = commit_reachable_closure(repo, head)?;
2310 Ok(from_head.difference(&from_tag).count())
2311}
2312
2313fn may_be_describe_name(spec: &str) -> bool {
2317 let Some((head, tail)) = spec.rsplit_once('-') else {
2318 return false;
2319 };
2320 let Some(hex) = tail.strip_prefix(['g', 'G']) else {
2321 return false;
2322 };
2323 !hex.is_empty() && hex.bytes().all(|b| b.is_ascii_hexdigit()) && head.contains('-')
2324}
2325
2326fn try_resolve_describe_name(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
2327 if !may_be_describe_name(spec) {
2328 return Ok(None);
2329 }
2330 let re = Regex::new(r"(?i)^(.+)-(\d+)-g([0-9a-fA-F]+)$")
2331 .map_err(|_| Error::from(RevParseError::InternalDescribeRegex))?;
2332 let Some(caps) = re.captures(spec) else {
2333 return Ok(None);
2334 };
2335 let tag_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
2336 let gen: usize = caps
2337 .get(2)
2338 .and_then(|m| m.as_str().parse().ok())
2339 .unwrap_or(0);
2340 let hex_abbrev = caps.get(3).map(|m| m.as_str()).unwrap_or("");
2341 if tag_name.is_empty() || hex_abbrev.is_empty() {
2342 return Ok(None);
2343 }
2344 let hex_lower = hex_abbrev.to_ascii_lowercase();
2345 let mut commit_candidates: Vec<ObjectId> = find_abbrev_matches(repo, &hex_lower)?
2346 .into_iter()
2347 .filter(|oid| {
2348 repo.odb()
2349 .read(oid)
2350 .is_ok_and(|o| o.kind == ObjectKind::Commit)
2351 })
2352 .collect();
2353 commit_candidates.sort_by_key(|o| o.to_hex());
2354 commit_candidates.dedup();
2355
2356 if let Ok(tag_oid) = repo
2357 .resolve_ref_oid(&format!("refs/tags/{tag_name}"))
2358 .or_else(|_| repo.resolve_ref_oid(tag_name))
2359 {
2360 let tag_commit = peel_to_commit_for_merge_base(repo, tag_oid)?;
2361 let mut strict_candidates = commit_candidates
2362 .iter()
2363 .copied()
2364 .filter(|oid| describe_generation_count(repo, *oid, tag_commit).ok() == Some(gen))
2365 .collect::<Vec<_>>();
2366 strict_candidates.sort_by_key(|o| o.to_hex());
2367 if strict_candidates.len() == 1 {
2368 return Ok(Some(strict_candidates[0]));
2369 }
2370 if strict_candidates.len() > 1 {
2371 return Err(RevParseError::AmbiguousShortObject {
2372 spec: hex_abbrev.to_owned(),
2373 }
2374 .into());
2375 }
2376 }
2377
2378 match commit_candidates.len() {
2379 0 => Err(Error::ObjectNotFound(spec.to_owned())),
2380 1 => Ok(Some(commit_candidates[0])),
2381 _ => Err(RevParseError::AmbiguousShortObject {
2382 spec: hex_abbrev.to_owned(),
2383 }
2384 .into()),
2385 }
2386}
2387
2388#[expect(clippy::too_many_arguments, clippy::only_used_in_recursion)]
2389fn resolve_base(
2390 repo: &Repository,
2391 spec: &str,
2392 index_dwim: bool,
2393 commit_only_hex: bool,
2394 use_disambiguate_config: bool,
2395 peel_for_disambig: Option<&str>,
2396 implicit_tree_abbrev: bool,
2397 implicit_blob_abbrev: bool,
2398 remote_branch_name_guess: bool,
2399) -> Result<ObjectId> {
2400 if spec == "@" {
2402 return resolve_base(
2403 repo,
2404 "HEAD",
2405 index_dwim,
2406 commit_only_hex,
2407 use_disambiguate_config,
2408 peel_for_disambig,
2409 implicit_tree_abbrev,
2410 implicit_blob_abbrev,
2411 remote_branch_name_guess,
2412 );
2413 }
2414
2415 if spec == "HEAD" {
2416 return match crate::state::resolve_head_for_repository(repo) {
2417 Ok(crate::state::HeadState::Branch { oid: None, .. }) => Err(Error::UnbornHead),
2418 Ok(crate::state::HeadState::Branch { oid: Some(oid), .. }) => Ok(oid),
2419 Ok(crate::state::HeadState::Detached { oid }) => Ok(oid),
2420 Ok(crate::state::HeadState::Invalid) | Err(_) => {
2421 Err(Error::ObjectNotFound(spec.to_owned()))
2422 }
2423 };
2424 }
2425
2426 if spec == "FETCH_HEAD" {
2429 let path = repo.git_dir().join("FETCH_HEAD");
2430 let content = std::fs::read_to_string(&path)
2431 .map_err(|_| Error::ObjectNotFound("FETCH_HEAD".to_owned()))?;
2432 let mut first_oid = None;
2433 for line in content.lines() {
2434 let line = line.trim();
2435 if line.is_empty() {
2436 continue;
2437 }
2438 let mut parts = line.split('\t');
2439 let Some(oid_hex) = parts.next() else {
2440 continue;
2441 };
2442 if ObjectId::is_full_hex(oid_hex) {
2443 let oid = oid_hex
2444 .parse::<ObjectId>()
2445 .map_err(|_| Error::InvalidRef("invalid FETCH_HEAD object id".to_owned()))?;
2446 first_oid.get_or_insert(oid);
2447 let not_for_merge = parts.next().is_some_and(|v| v == "not-for-merge");
2448 if !not_for_merge {
2449 return Ok(oid);
2450 }
2451 }
2452 }
2453 return first_oid.ok_or_else(|| Error::ObjectNotFound("FETCH_HEAD".to_owned()));
2454 }
2455
2456 if let Some(after_open) = spec.strip_prefix("@{-") {
2458 if let Some(close) = after_open.find('}') {
2459 let n_str = &after_open[..close];
2460 if let Ok(n) = n_str.parse::<usize>() {
2461 if n >= 1 {
2462 let suffix = &after_open[close + 1..];
2463 if suffix.is_empty() {
2464 if let Some(oid) = try_resolve_at_minus(repo, spec)? {
2465 return Ok(oid);
2466 }
2467 } else {
2468 let branch = resolve_at_minus_to_branch(repo, n)?;
2469 let new_spec = format!("{branch}{suffix}");
2470 return resolve_base(
2471 repo,
2472 &new_spec,
2473 index_dwim,
2474 commit_only_hex,
2475 use_disambiguate_config,
2476 peel_for_disambig,
2477 implicit_tree_abbrev,
2478 implicit_blob_abbrev,
2479 remote_branch_name_guess,
2480 );
2481 }
2482 }
2483 }
2484 }
2485 }
2486
2487 if upstream_suffix_info(spec).is_some() {
2489 let full_ref = resolve_upstream_symbolic_name(repo, spec)?;
2490 return repo
2491 .resolve_ref_oid(&full_ref)
2492 .map_err(|_| Error::ObjectNotFound(spec.to_owned()));
2493 }
2494
2495 if let Some(oid) = try_resolve_reflog_index(repo, spec)? {
2497 return Ok(oid);
2498 }
2499
2500 if let Some(pattern) = spec.strip_prefix(":/") {
2502 if !pattern.is_empty() {
2503 return resolve_commit_message_search(repo, pattern);
2504 }
2505 }
2506
2507 if let Some(rest) = spec.strip_prefix(':') {
2510 if !rest.is_empty() && !rest.starts_with('/') {
2511 if rest.len() >= 3 && rest.as_bytes()[1] == b':' {
2513 if let Some(stage_char) = rest.chars().next() {
2514 if let Some(stage) = stage_char.to_digit(10) {
2515 if stage <= 3 {
2516 let raw_path = &rest[2..];
2517 let path = match normalize_colon_path_for_tree(repo, raw_path) {
2518 Ok(p) => p,
2519 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
2520 let wt = repo
2521 .work_tree()
2522 .and_then(|p| p.canonicalize_simplified().ok())
2523 .map(|p| p.display().to_string())
2524 .unwrap_or_default();
2525 return Err(RevParseError::OutsideRepository {
2526 path: raw_path.to_owned(),
2527 work_tree: wt,
2528 }
2529 .into());
2530 }
2531 Err(e) => return Err(e),
2532 };
2533 return resolve_index_path_at_stage(repo, &path, stage as u8).map_err(
2534 |e| diagnose_index_path_error(repo, &path, stage as u8, e),
2535 );
2536 }
2537 }
2538 }
2539 }
2540 let clean_rest = match normalize_colon_path_for_tree(repo, rest) {
2541 Ok(p) => p,
2542 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
2543 let wt = repo
2544 .work_tree()
2545 .and_then(|p| p.canonicalize_simplified().ok())
2546 .map(|p| p.display().to_string())
2547 .unwrap_or_default();
2548 return Err(RevParseError::OutsideRepository {
2549 path: rest.to_owned(),
2550 work_tree: wt,
2551 }
2552 .into());
2553 }
2554 Err(e) => return Err(e),
2555 };
2556 return resolve_index_path(repo, &clean_rest)
2557 .map_err(|e| diagnose_index_path_error(repo, &clean_rest, 0, e));
2558 }
2559 }
2560
2561 if let Some((treeish, path)) = split_treeish_spec(spec) {
2562 let root_oid = resolve_revision_impl(
2563 repo,
2564 treeish,
2565 index_dwim,
2566 commit_only_hex,
2567 use_disambiguate_config,
2568 false,
2569 false,
2570 false,
2571 false,
2572 )?;
2573 return resolve_treeish_path_to_object(repo, root_oid, path);
2574 }
2575
2576 if let Ok(oid) = spec.parse::<ObjectId>() {
2577 let rn = format!("refs/heads/{spec}");
2580 if repo.resolve_ref_oid(&rn).is_ok() {
2581 repo.warn(crate::diagnostics::Warning::AmbiguousRefname {
2582 spec: spec.to_owned(),
2583 });
2584 }
2585 return Ok(oid);
2586 }
2587
2588 match try_resolve_describe_name(repo, spec) {
2589 Ok(Some(oid)) => return Ok(oid),
2590 Err(e) => return Err(e),
2591 Ok(None) => {}
2592 }
2593
2594 if is_hex_prefix(spec) && spec.len() < repo.odb().hash_algo().hex_len() {
2597 let tag_ref = format!("refs/tags/{spec}");
2598 if let Ok(oid) = repo.resolve_ref_oid(&tag_ref) {
2599 warn_if_hex_ref_collides_with_objects(repo, spec, oid);
2600 return Ok(oid);
2601 }
2602 let branch_ref = format!("refs/heads/{spec}");
2603 if let Ok(oid) = repo.resolve_ref_oid(&branch_ref) {
2604 warn_if_hex_ref_collides_with_objects(repo, spec, oid);
2605 return Ok(oid);
2606 }
2607 }
2608
2609 if is_hex_prefix(spec) {
2610 let matches = find_abbrev_matches(repo, spec)?;
2611 if matches.is_empty() {
2612 if (4..repo.odb().hash_algo().hex_len()).contains(&spec.len()) {
2616 return Err(Error::ObjectNotFound(spec.to_owned()));
2617 }
2618 } else if matches.len() == 1 {
2619 let oid = matches[0];
2620 warn_if_branch_refname_collides_with_abbrev_hex(repo, spec, oid);
2621 return Ok(oid);
2622 } else if matches.len() > 1 {
2623 if let Some(p) = peel_for_disambig {
2624 let oid = disambiguate_hex_by_peel(repo, spec, &matches, p)?;
2625 warn_if_branch_refname_collides_with_abbrev_hex(repo, spec, oid);
2626 return Ok(oid);
2627 }
2628 if commit_only_hex {
2629 let oid = disambiguate_hex_by_peel(repo, spec, &matches, "commit")?;
2630 warn_if_branch_refname_collides_with_abbrev_hex(repo, spec, oid);
2631 return Ok(oid);
2632 }
2633 if use_disambiguate_config {
2634 if let Some(pref) = read_core_disambiguate(repo) {
2635 if let Ok(oid) = disambiguate_hex_by_peel(repo, spec, &matches, pref) {
2636 warn_if_branch_refname_collides_with_abbrev_hex(repo, spec, oid);
2637 return Ok(oid);
2638 }
2639 }
2640 }
2641 return Err(RevParseError::AmbiguousShortObject {
2642 spec: spec.to_owned(),
2643 }
2644 .into());
2645 }
2646 }
2647
2648 let (dwim_count, dwim_oid) = resolve_ref_dwim_for_rev_parse(repo, spec);
2649 if dwim_count > 1 {
2650 repo.warn(crate::diagnostics::Warning::AmbiguousRefname {
2651 spec: spec.to_owned(),
2652 });
2653 }
2654 if let Some(oid) = dwim_oid {
2655 return Ok(oid);
2656 }
2657 if let Some(rest) = spec.strip_prefix("remotes/") {
2659 let full = format!("refs/remotes/{rest}");
2660 if let Ok(oid) = repo.resolve_ref_oid(&full) {
2661 return Ok(oid);
2662 }
2663 }
2664 if !spec.contains('/')
2668 && !spec.starts_with('.')
2669 && spec != "HEAD"
2670 && spec != "FETCH_HEAD"
2671 && spec != "MERGE_HEAD"
2672 && spec != "CHERRY_PICK_HEAD"
2673 && spec != "REVERT_HEAD"
2674 && spec != "REBASE_HEAD"
2675 && spec != "AUTO_MERGE"
2676 && spec != "stash"
2677 {
2678 let local_branch = format!("refs/heads/{spec}");
2679 if repo.resolve_ref_oid(&local_branch).is_err() {
2680 let remote_head = format!("refs/remotes/{spec}/HEAD");
2681 if let Ok(oid) = repo.resolve_ref_oid(&remote_head) {
2682 return Ok(oid);
2683 }
2684 }
2685 }
2686 if spec == "stash" {
2688 if let Ok(oid) = repo.resolve_ref_oid("refs/stash") {
2689 return Ok(oid);
2690 }
2691 }
2692 let head_ref = format!("refs/heads/{spec}");
2696 let tag_ref = format!("refs/tags/{spec}");
2697 let head_oid = repo.resolve_ref_oid(&head_ref).ok();
2698 let tag_oid = repo.resolve_ref_oid(&tag_ref).ok();
2699 match (head_oid, tag_oid) {
2700 (Some(h), Some(t)) if h != t => {
2701 repo.warn(crate::diagnostics::Warning::AmbiguousRefname {
2702 spec: spec.to_owned(),
2703 });
2704 return Ok(h);
2705 }
2706 (Some(h), _) => return Ok(h),
2707 (None, Some(t)) => return Ok(t),
2708 (None, None) => {}
2709 }
2710
2711 if !spec.contains('/')
2715 && !spec.contains(':')
2716 && !spec.starts_with('.')
2717 && spec != "HEAD"
2718 && spec.len() <= 255
2719 {
2720 let mut ref_match: Option<ObjectId> = None;
2721 for prefix in ["refs/heads/", "refs/tags/", "refs/remotes/", "refs/notes/"] {
2722 let full = format!("{prefix}{spec}");
2723 if let Ok(oid) = repo.resolve_ref_oid(&full) {
2724 ref_match = Some(oid);
2725 break;
2726 }
2727 }
2728 if let Some(oid) = ref_match {
2729 return Ok(oid);
2730 }
2731 }
2732 for candidate in &[format!("refs/remotes/{spec}"), format!("refs/notes/{spec}")] {
2733 if let Ok(oid) = repo.resolve_ref_oid(candidate) {
2734 return Ok(oid);
2735 }
2736 }
2737
2738 if let Some(head_ref) = remote_tracking_head_symbolic_target(repo, spec) {
2740 if let Ok(oid) = repo.resolve_ref_oid(&head_ref) {
2741 return Ok(oid);
2742 }
2743 }
2744
2745 if remote_branch_name_guess
2747 && !spec.contains('/')
2748 && spec != "HEAD"
2749 && spec != "FETCH_HEAD"
2750 && spec != "MERGE_HEAD"
2751 {
2752 const REMOTES: &str = "refs/remotes/";
2753 if let Ok(remote_refs) = refs::list_refs_in(repo, REMOTES) {
2754 let matches: Vec<ObjectId> = remote_refs
2755 .into_iter()
2756 .filter(|(r, _)| {
2757 r.strip_prefix(REMOTES)
2758 .is_some_and(|rest| rest == spec || rest.ends_with(&format!("/{spec}")))
2759 })
2760 .map(|(_, oid)| oid)
2761 .collect();
2762 if matches.len() == 1 {
2763 return Ok(matches[0]);
2764 }
2765 if matches.len() > 1 {
2766 return Err(Error::InvalidRef(format!(
2767 "ambiguous refname '{spec}': matches multiple remote-tracking branches"
2768 )));
2769 }
2770 }
2771 }
2772
2773 if crate::change_id::is_change_id_spec_token(spec) {
2774 return crate::change_id::resolve_commit_by_change_id_prefix(repo, spec);
2775 }
2776
2777 if !spec.contains(':') && !spec.starts_with('-') {
2779 if index_dwim {
2780 if resolve_index_path(repo, spec).is_ok() {
2781 if repo.work_tree().is_some() {
2782 return Err(RevParseError::AmbiguousArgument {
2783 spec: spec.to_owned(),
2784 }
2785 .into());
2786 }
2787 return resolve_index_path(repo, spec);
2788 }
2789 return Err(RevParseError::AmbiguousArgument {
2790 spec: spec.to_owned(),
2791 }
2792 .into());
2793 }
2794 return Err(RevParseError::InvalidObjectName {
2795 name: spec.to_owned(),
2796 }
2797 .into());
2798 }
2799 Err(Error::ObjectNotFound(spec.to_owned()))
2800}
2801
2802fn read_reflog_for_repo(repo: &Repository, refname: &str) -> Result<Vec<ReflogEntry>> {
2803 let mut entries = Vec::new();
2804 repo.refs()
2805 .for_each_reflog_entry(refname, false, &mut |entry| {
2806 entries.push(entry.clone());
2807 ControlFlow::Continue(())
2808 })?;
2809 Ok(entries)
2810}
2811
2812fn resolve_at_minus_to_branch(repo: &Repository, n: usize) -> Result<String> {
2814 let entries = read_reflog_for_repo(repo, "HEAD")?;
2815 let mut count = 0usize;
2816 for entry in entries.iter().rev() {
2817 let msg = &entry.message;
2818 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
2819 count += 1;
2820 if count == n {
2821 if let Some(to_pos) = rest.find(" to ") {
2822 return Ok(rest[..to_pos].to_string());
2823 }
2824 }
2825 }
2826 }
2827 Err(Error::InvalidRef(format!(
2828 "@{{-{n}}}: only {count} checkout(s) in reflog"
2829 )))
2830}
2831
2832fn try_resolve_at_minus(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
2835 if !spec.starts_with("@{-") || !spec.ends_with('}') {
2837 return Ok(None);
2838 }
2839 let inner = &spec[3..spec.len() - 1];
2840 let n: usize = match inner.parse() {
2841 Ok(n) if n >= 1 => n,
2842 _ => return Ok(None),
2843 };
2844 let entries = read_reflog_for_repo(repo, "HEAD")?;
2846 let mut count = 0usize;
2847 for entry in entries.iter().rev() {
2849 let msg = &entry.message;
2850 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
2851 count += 1;
2852 if count == n {
2853 if let Some(to_pos) = rest.find(" to ") {
2854 let from_branch = &rest[..to_pos];
2855 let ref_name = format!("refs/heads/{from_branch}");
2856 if let Ok(oid) = repo.resolve_ref_oid(&ref_name) {
2857 return Ok(Some(oid));
2858 }
2859 if let Ok(oid) = from_branch.parse::<ObjectId>() {
2860 if repo.odb().contains(&oid)? {
2861 return Ok(Some(oid));
2862 }
2863 }
2864 if is_hex_prefix(from_branch) {
2865 if let Ok(oid) = resolve_revision_for_range_end(repo, from_branch)
2866 .and_then(|oid| peel_to_commit_for_merge_base(repo, oid))
2867 {
2868 return Ok(Some(oid));
2869 }
2870 }
2871 return Err(Error::InvalidRef(format!(
2872 "cannot resolve @{{-{n}}}: branch '{}' not found",
2873 from_branch
2874 )));
2875 }
2876 }
2877 }
2878 }
2879 Err(Error::InvalidRef(format!(
2880 "@{{-{n}}}: only {count} checkout(s) in reflog"
2881 )))
2882}
2883
2884#[derive(Debug, Clone)]
2885enum AtStep {
2886 Index(usize),
2887 Date(i64),
2888 Upstream,
2889 Push,
2890 Now,
2891}
2892
2893fn try_parse_at_step_inner(
2894 inner: &str,
2895 now_sec: i64,
2896) -> std::result::Result<AtStep, RevParseError> {
2897 if inner.eq_ignore_ascii_case("u") || inner.eq_ignore_ascii_case("upstream") {
2898 return Ok(AtStep::Upstream);
2899 }
2900 if inner.eq_ignore_ascii_case("push") {
2901 return Ok(AtStep::Push);
2902 }
2903 if inner.eq_ignore_ascii_case("now") {
2904 return Ok(AtStep::Now);
2905 }
2906 if let Ok(n) = inner.parse::<usize>() {
2907 return Ok(AtStep::Index(n));
2908 }
2909 approxidate_at(inner, now_sec).map_or_else(
2910 || {
2911 Err(RevParseError::ReflogDateUnparseable {
2912 date: inner.to_owned(),
2913 })
2914 },
2915 |ts| Ok(AtStep::Date(ts)),
2916 )
2917}
2918
2919fn next_reflog_at_open(spec: &str, mut from: usize) -> Option<usize> {
2920 let b = spec.as_bytes();
2921 while let Some(rel) = spec[from..].find("@{") {
2922 let i = from + rel;
2923 if b.get(i + 2) == Some(&b'-') {
2925 let after_open = i + 2;
2926 let close = spec[after_open..].find('}').map(|j| after_open + j)?;
2927 from = close + 1;
2928 continue;
2929 }
2930 return Some(i);
2931 }
2932 None
2933}
2934
2935fn split_reflog_at_chain(spec: &str, now_sec: i64) -> Result<Option<(String, Vec<AtStep>)>> {
2937 let Some(at) = next_reflog_at_open(spec, 0) else {
2938 return Ok(None);
2939 };
2940 let prefix = spec[..at].to_owned();
2941 let mut steps = Vec::new();
2942 let mut pos = at;
2943 while pos < spec.len() {
2944 let rest = &spec[pos..];
2945 if !rest.starts_with("@{") {
2946 return Ok(None);
2947 }
2948 if rest.as_bytes().get(2) == Some(&b'-') {
2949 return Ok(None);
2950 }
2951 let inner_start = pos + 2;
2952 let Some(close_rel) = spec[inner_start..].find('}') else {
2953 return Ok(None);
2954 };
2955 let close = inner_start + close_rel;
2956 let inner = &spec[inner_start..close];
2957 let step = try_parse_at_step_inner(inner, now_sec).map_err(Error::from)?;
2958 steps.push(step);
2959 pos = close + 1;
2960 }
2961 if steps.is_empty() {
2962 return Ok(None);
2963 }
2964 Ok(Some((prefix, steps)))
2965}
2966
2967fn dwim_refname(repo: &Repository, raw: &str) -> String {
2968 if raw.is_empty() || raw == "HEAD" || raw.starts_with("refs/") {
2969 return raw.to_owned();
2970 }
2971 if raw == "stash" && repo.resolve_ref_oid("refs/stash").is_ok() {
2973 return "refs/stash".to_owned();
2974 }
2975 let candidate = format!("refs/heads/{raw}");
2976 if repo.resolve_ref_oid(&candidate).is_ok() {
2977 candidate
2978 } else {
2979 raw.to_owned()
2980 }
2981}
2982
2983fn reflog_display_name(refname_raw: &str, refname: &str) -> String {
2984 if refname_raw.is_empty() {
2985 if let Some(b) = refname.strip_prefix("refs/heads/") {
2986 return b.to_owned();
2987 }
2988 return refname.to_owned();
2989 }
2990 refname_raw.to_owned()
2991}
2992
2993fn resolve_reflog_oid(
2994 repo: &Repository,
2995 refname: &str,
2996 refname_raw: &str,
2997 index_or_date: ReflogSelector,
2998) -> Result<ObjectId> {
2999 let mut entries = read_reflog_for_repo(repo, refname)?;
3000 if refname == "HEAD" {
3001 if let ReflogSelector::Index(index) = index_or_date {
3002 if index >= entries.len() {
3003 if let Ok(Some(RawRef::Symbolic(branch_ref))) = repo.refs().read_raw("HEAD") {
3004 if let Ok(branch_entries) = read_reflog_for_repo(repo, &branch_ref) {
3005 if index < branch_entries.len() {
3006 entries = branch_entries;
3007 }
3008 }
3009 }
3010 }
3011 }
3012 }
3013 let display = reflog_display_name(refname_raw, refname);
3014 match index_or_date {
3015 ReflogSelector::Index(index) => {
3016 let len = entries.len();
3017 if index == 0 {
3018 if len == 0 {
3019 if !repo.refs().reflog_exists(refname).unwrap_or(false) {
3025 return Err(RevParseError::ReflogEmpty {
3026 ref_display: display.clone(),
3027 }
3028 .into());
3029 }
3030 return repo.resolve_ref_oid(refname).map_err(|_| {
3031 RevParseError::ReflogEmpty {
3032 ref_display: display.clone(),
3033 }
3034 .into()
3035 });
3036 }
3037 return Ok(entries[len - 1].new_oid);
3038 }
3039 if len == 0 {
3040 return Err(RevParseError::ReflogEmpty {
3041 ref_display: display.clone(),
3042 }
3043 .into());
3044 }
3045 if index > len {
3046 return Err(RevParseError::ReflogInsufficientEntries {
3047 ref_display: display.clone(),
3048 available: len,
3049 }
3050 .into());
3051 }
3052 let oid = entries[len - index].old_oid;
3053 if oid.is_zero() {
3054 return Err(RevParseError::ReflogInsufficientEntries {
3055 ref_display: display.clone(),
3056 available: len,
3057 }
3058 .into());
3059 }
3060 Ok(oid)
3061 }
3062 ReflogSelector::Date(target_ts) => {
3063 if entries.is_empty() {
3064 return Err(RevParseError::ReflogEmpty {
3065 ref_display: display.clone(),
3066 }
3067 .into());
3068 }
3069 for entry in entries.iter().rev() {
3070 let ts = parse_reflog_entry_timestamp(entry);
3071 if let Some(t) = ts {
3072 if t <= target_ts {
3073 return Ok(entry.new_oid);
3074 }
3075 }
3076 }
3077 let oldest = &entries[0];
3078 let oid = if oldest.old_oid.is_zero() {
3079 oldest.new_oid
3080 } else {
3081 oldest.old_oid
3082 };
3083 repo.warn(crate::diagnostics::Warning::ReflogDateBeforeOldestEntry {
3084 ref_display: display.clone(),
3085 cutoff_sec: target_ts,
3086 });
3087 Ok(oid)
3088 }
3089 }
3090}
3091
3092fn resolve_at_minus_token_to_branch(repo: &Repository, token: &str) -> Result<Option<String>> {
3093 if !token.starts_with("@{-") || !token.ends_with('}') {
3094 return Ok(None);
3095 }
3096 let inner = &token[3..token.len() - 1];
3097 let n: usize = inner
3098 .parse()
3099 .map_err(|_| Error::InvalidRef(format!("invalid N in @{{-N}} for '{token}'")))?;
3100 if n < 1 {
3101 return Ok(None);
3102 }
3103 Ok(Some(resolve_at_minus_to_branch(repo, n)?))
3104}
3105
3106pub fn reflog_walk_refname(repo: &Repository, spec: &str) -> Result<Option<String>> {
3110 let now_sec = repo.wall_clock_epoch();
3111 let Some((prefix, steps)) = split_reflog_at_chain(spec, now_sec)? else {
3112 return Ok(None);
3113 };
3114
3115 let prefix_resolved = if let Some(b) = resolve_at_minus_token_to_branch(repo, &prefix)? {
3116 b
3117 } else {
3118 prefix.clone()
3119 };
3120
3121 let mut current_spec = if prefix_resolved.is_empty() {
3122 if let Ok(Some(b)) = refs::read_symbolic_ref_for_repository(repo, "HEAD") {
3123 if let Some(short) = b.strip_prefix("refs/heads/") {
3124 short.to_owned()
3125 } else {
3126 "HEAD".to_owned()
3127 }
3128 } else {
3129 "HEAD".to_owned()
3130 }
3131 } else {
3132 prefix_resolved
3133 };
3134
3135 let last_reflog_peel = steps
3136 .iter()
3137 .rposition(|s| matches!(s, AtStep::Index(_) | AtStep::Date(_) | AtStep::Now));
3138
3139 let limit = last_reflog_peel.unwrap_or(steps.len());
3140 for step in steps.iter().take(limit) {
3141 match step {
3142 AtStep::Upstream => {
3143 let base = if current_spec == "@" {
3144 "HEAD"
3145 } else {
3146 current_spec.as_str()
3147 };
3148 let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{u}}"))?;
3149 current_spec = full;
3150 }
3151 AtStep::Push => {
3152 let base = if current_spec == "@" {
3153 "HEAD"
3154 } else {
3155 current_spec.as_str()
3156 };
3157 let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{push}}"))?;
3158 current_spec = full;
3159 }
3160 AtStep::Now | AtStep::Index(_) | AtStep::Date(_) => {}
3161 }
3162 }
3163
3164 Ok(Some(dwim_refname(repo, current_spec.as_str())))
3165}
3166
3167pub fn resolve_reflog_walk_log_ref(repo: &Repository, r: &str) -> Result<String> {
3172 if let Ok(Some(w)) = reflog_walk_refname(repo, r) {
3173 return Ok(w);
3174 }
3175 if r == "HEAD" || r.starts_with("refs/") {
3176 return Ok(r.to_string());
3177 }
3178 if r.starts_with("@{") {
3179 if let Ok(Some(branch)) = expand_at_minus_to_branch_name(repo, r) {
3180 return Ok(format!("refs/heads/{branch}"));
3181 }
3182 return Ok(r.to_string());
3183 }
3184 let candidate = format!("refs/heads/{r}");
3185 if repo.resolve_ref_oid(&candidate).is_ok() {
3186 Ok(candidate)
3187 } else {
3188 Ok(r.to_string())
3189 }
3190}
3191
3192fn try_resolve_reflog_index(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
3194 let now_sec = repo.wall_clock_epoch();
3195 let Some((prefix, steps)) = split_reflog_at_chain(spec, now_sec)? else {
3196 return Ok(None);
3197 };
3198
3199 let prefix_resolved = if let Some(b) = resolve_at_minus_token_to_branch(repo, &prefix)? {
3200 b
3201 } else {
3202 prefix.clone()
3203 };
3204
3205 let mut current_spec = if prefix_resolved.is_empty() {
3206 if let Ok(Some(b)) = refs::read_symbolic_ref_for_repository(repo, "HEAD") {
3207 if let Some(short) = b.strip_prefix("refs/heads/") {
3208 short.to_owned()
3209 } else {
3210 "HEAD".to_owned()
3211 }
3212 } else {
3213 "HEAD".to_owned()
3214 }
3215 } else {
3216 prefix_resolved
3217 };
3218
3219 for (i, step) in steps.iter().enumerate() {
3220 match step {
3221 AtStep::Upstream => {
3222 let base = if current_spec == "@" {
3223 "HEAD"
3224 } else {
3225 current_spec.as_str()
3226 };
3227 let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{u}}"))?;
3228 current_spec = full;
3229 }
3230 AtStep::Push => {
3231 let base = if current_spec == "@" {
3232 "HEAD"
3233 } else {
3234 current_spec.as_str()
3235 };
3236 let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{push}}"))?;
3237 current_spec = full;
3238 }
3239 AtStep::Now => {
3240 let refname_raw = current_spec.as_str();
3241 let refname = dwim_refname(repo, refname_raw);
3242 let oid =
3243 resolve_reflog_oid(repo, &refname, refname_raw, ReflogSelector::Index(0))?;
3244 if i + 1 == steps.len() {
3245 return Ok(Some(oid));
3246 }
3247 current_spec = oid.to_hex();
3248 }
3249 AtStep::Index(n) => {
3250 let refname_raw = current_spec.as_str();
3251 let refname = dwim_refname(repo, refname_raw);
3252 let oid =
3253 resolve_reflog_oid(repo, &refname, refname_raw, ReflogSelector::Index(*n))?;
3254 if i + 1 == steps.len() {
3255 return Ok(Some(oid));
3256 }
3257 current_spec = oid.to_hex();
3258 }
3259 AtStep::Date(ts) => {
3260 let refname_raw = current_spec.as_str();
3261 let refname = dwim_refname(repo, refname_raw);
3262 let oid =
3263 resolve_reflog_oid(repo, &refname, refname_raw, ReflogSelector::Date(*ts))?;
3264 if i + 1 == steps.len() {
3265 return Ok(Some(oid));
3266 }
3267 current_spec = oid.to_hex();
3268 }
3269 }
3270 }
3271
3272 let refname_raw = current_spec.as_str();
3273 let refname = dwim_refname(repo, refname_raw);
3274 repo.resolve_ref_oid(&refname)
3275 .map(Some)
3276 .map_err(|_| Error::ObjectNotFound(spec.to_owned()))
3277}
3278
3279enum ReflogSelector {
3280 Index(usize),
3281 Date(i64),
3282}
3283
3284fn parse_reflog_entry_timestamp(entry: &crate::reflog::ReflogEntry) -> Option<i64> {
3286 let parts: Vec<&str> = entry.identity.rsplitn(3, ' ').collect();
3288 if parts.len() >= 2 {
3289 parts[1].parse::<i64>().ok()
3290 } else {
3291 None
3292 }
3293}
3294
3295#[must_use]
3299pub fn reflog_date_selector_timestamp(s: &str) -> Option<i64> {
3300 reflog_date_selector_timestamp_at(s, crate::git_date::tm::process_wall_clock_sec())
3301}
3302
3303pub fn reflog_date_selector_timestamp_at(s: &str, now_sec: i64) -> Option<i64> {
3305 approxidate_at(s, now_sec)
3306}
3307
3308fn approxidate_at(s: &str, now_ts: i64) -> Option<i64> {
3310 let trimmed = s.trim();
3311 if trimmed.eq_ignore_ascii_case("now") {
3312 if let Some(raw) = crate::environment::Environment::empty()
3315 .var("GIT_COMMITTER_DATE")
3316 .or_else(|| crate::environment::Environment::empty().var("GIT_AUTHOR_DATE"))
3317 {
3318 let mut it = raw.split_whitespace();
3319 if let Some(ts) = it.next().and_then(|p| p.parse::<i64>().ok()) {
3320 return Some(ts);
3321 }
3322 }
3323 return Some(now_ts);
3324 }
3325 crate::git_date::parse_date_cutoff(trimmed, now_ts).ok()
3326}
3327
3328fn head_tree_oid(repo: &Repository) -> Result<ObjectId> {
3329 let head_oid = repo.resolve_ref_oid("HEAD")?;
3330 peel_to_tree(repo, head_oid)
3331}
3332
3333fn path_in_tree(repo: &Repository, tree_oid: ObjectId, path: &str) -> bool {
3334 resolve_tree_path(repo, &tree_oid, path).is_ok()
3335}
3336
3337fn path_in_index(repo: &Repository, path: &str, stage: u8) -> bool {
3338 resolve_index_path_at_stage(repo, path, stage).is_ok()
3339}
3340
3341fn diagnose_tree_path_error(
3342 repo: &Repository,
3343 rev_label: &str,
3344 raw_after_colon: &str,
3345 clean_path: &str,
3346 err: TreePathError,
3347) -> Error {
3348 if let TreePathError::Other(err) = err {
3349 return err;
3350 }
3351 let rel_display: &str =
3352 if raw_after_colon.starts_with("./") || raw_after_colon.starts_with("../") {
3353 clean_path
3354 } else {
3355 raw_after_colon
3356 };
3357 if let Ok(head_tree) = head_tree_oid(repo) {
3358 if path_in_tree(repo, head_tree, clean_path) {
3359 return RevParseError::PathOnDiskNotInRevision {
3360 path: rel_display.to_owned(),
3361 revision: rev_label.to_owned(),
3362 }
3363 .into();
3364 }
3365 {
3366 let cwd = repo.environment().cwd.clone();
3367 let prefix = show_prefix(repo, &cwd);
3368 let pfx = prefix.trim_end_matches('/');
3369 if !pfx.is_empty() {
3370 let candidate = if clean_path.is_empty() {
3371 pfx.to_owned()
3372 } else {
3373 format!("{pfx}/{clean_path}")
3374 };
3375 if path_in_tree(repo, head_tree, &candidate) {
3376 let alternate_spec = Some(format!("{rev_label}:{candidate}"));
3377 return RevParseError::PathPrefixExists {
3378 candidate,
3379 path: rel_display.to_owned(),
3380 alternate_spec,
3381 }
3382 .into();
3383 }
3384 }
3385 }
3386 let on_disk = repo
3387 .work_tree()
3388 .map(|wt| wt.join(clean_path))
3389 .is_some_and(|p| p.exists());
3390 let in_index = path_in_index(repo, clean_path, 0);
3391 if on_disk || in_index {
3392 return RevParseError::PathOnDiskNotInRevision {
3393 path: rel_display.to_owned(),
3394 revision: rev_label.to_owned(),
3395 }
3396 .into();
3397 }
3398 }
3399 RevParseError::PathNotInRevision {
3400 path: rel_display.to_owned(),
3401 revision: rev_label.to_owned(),
3402 }
3403 .into()
3404}
3405
3406fn diagnose_index_path_error(repo: &Repository, path: &str, stage: u8, err: Error) -> Error {
3407 let Error::ObjectNotFound(_) = err else {
3408 return err;
3409 };
3410 let work_path = repo
3411 .work_tree()
3412 .map(|wt| wt.join(path))
3413 .filter(|p| p.exists());
3414 let on_disk = work_path.is_some();
3415 let in_head = head_tree_oid(repo)
3416 .map(|t| path_in_tree(repo, t, path))
3417 .unwrap_or(false);
3418 let in_index = path_in_index(repo, path, 0);
3419 let at_stage = path_in_index(repo, path, stage);
3420
3421 if stage > 0 && !in_index {
3422 {
3423 let cwd = repo.environment().cwd.clone();
3424 let prefix = show_prefix(repo, &cwd);
3425 let pfx = prefix.trim_end_matches('/');
3426 if !pfx.is_empty() {
3427 let candidate = if path.is_empty() {
3428 pfx.to_owned()
3429 } else {
3430 format!("{pfx}/{path}")
3431 };
3432 if path_in_index(repo, &candidate, 0) && !path_in_index(repo, &candidate, stage) {
3433 let alternate_spec = Some(format!(":0:{candidate}"));
3434 return RevParseError::PathIndexPrefixMismatch {
3435 candidate,
3436 path: path.to_owned(),
3437 alternate_spec,
3438 }
3439 .into();
3440 }
3441 }
3442 }
3443 return RevParseError::PathMissingFromWorktreeAndIndex {
3444 path: path.to_owned(),
3445 }
3446 .into();
3447 }
3448
3449 if stage > 0 && in_index && !at_stage {
3450 return RevParseError::PathWrongIndexStage {
3451 path: path.to_owned(),
3452 stage,
3453 alternate_spec: Some(format!(":0:{path}")),
3454 }
3455 .into();
3456 }
3457
3458 if stage == 0 {
3459 if !on_disk && !in_index {
3460 {
3461 let cwd = repo.environment().cwd.clone();
3462 let prefix = show_prefix(repo, &cwd);
3463 let pfx = prefix.trim_end_matches('/');
3464 if !pfx.is_empty() {
3465 let candidate = if path.is_empty() {
3466 pfx.to_owned()
3467 } else {
3468 format!("{pfx}/{path}")
3469 };
3470 if path_in_index(repo, &candidate, 0) {
3471 let alternate_spec = Some(format!(":0:{candidate}"));
3472 return RevParseError::PathIndexPrefixMismatch {
3473 candidate,
3474 path: path.to_owned(),
3475 alternate_spec,
3476 }
3477 .into();
3478 }
3479 }
3480 }
3481 return RevParseError::PathMissingFromWorktreeAndIndex {
3482 path: path.to_owned(),
3483 }
3484 .into();
3485 }
3486 if on_disk && !in_index && !in_head {
3487 return RevParseError::PathOnDiskNotInIndex {
3488 path: path.to_owned(),
3489 }
3490 .into();
3491 }
3492 }
3493 RevParseError::PathNotInIndex {
3494 path: path.to_owned(),
3495 }
3496 .into()
3497}
3498
3499fn resolve_index_path(repo: &Repository, path: &str) -> Result<ObjectId> {
3501 resolve_index_path_at_stage(repo, path, 0)
3502}
3503
3504#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3506pub struct IndexColonSpec<'a> {
3507 pub stage: u8,
3509 pub raw_path: &'a str,
3511}
3512
3513#[must_use]
3517pub fn parse_index_colon_spec(spec: &str) -> Option<IndexColonSpec<'_>> {
3518 if !spec.starts_with(':') || spec.starts_with(":/") || spec.len() <= 1 {
3519 return None;
3520 }
3521 let rest = &spec[1..];
3522 if rest.is_empty() {
3523 return None;
3524 }
3525 if rest.len() >= 3 && rest.as_bytes()[1] == b':' {
3526 if let Some(stage_char) = rest.chars().next() {
3527 if let Some(stage) = stage_char.to_digit(10) {
3528 if stage <= 3 {
3529 return Some(IndexColonSpec {
3530 stage: stage as u8,
3531 raw_path: &rest[2..],
3532 });
3533 }
3534 }
3535 }
3536 }
3537 Some(IndexColonSpec {
3538 stage: 0,
3539 raw_path: rest,
3540 })
3541}
3542
3543#[derive(Debug, Clone, PartialEq, Eq)]
3545pub struct IndexPathEntry {
3546 pub path: String,
3548 pub oid: ObjectId,
3550 pub mode: u32,
3552}
3553
3554pub fn resolve_index_path_entry(repo: &Repository, spec: &str) -> Result<Option<IndexPathEntry>> {
3562 let Some(colon) = parse_index_colon_spec(spec) else {
3563 return Ok(None);
3564 };
3565 let path = match normalize_colon_path_for_tree(repo, colon.raw_path) {
3566 Ok(p) => p,
3567 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
3568 let wt = repo
3569 .work_tree()
3570 .and_then(|p| p.canonicalize_simplified().ok())
3571 .map(|p| p.display().to_string())
3572 .unwrap_or_default();
3573 return Err(RevParseError::OutsideRepository {
3574 path: colon.raw_path.to_owned(),
3575 work_tree: wt,
3576 }
3577 .into());
3578 }
3579 Err(e) => return Err(e),
3580 };
3581 let index_path = if let Some(raw) = repo.environment().var("GIT_INDEX_FILE") {
3582 let p = std::path::PathBuf::from(raw);
3583 if p.is_absolute() {
3584 p
3585 } else {
3586 let cwd = repo.environment().cwd.clone();
3587 cwd.join(p)
3588 }
3589 } else {
3590 repo.index_path()
3591 };
3592 use crate::index::Index;
3593 let config = repo.config()?;
3594 let index = Index::load_expand_sparse_with_config(&index_path, repo.odb(), &config)
3595 .map_err(|_| Error::ObjectNotFound(format!(":{}:{}", colon.stage, path)))?;
3596 let entry = index
3597 .get(path.as_bytes(), colon.stage)
3598 .ok_or_else(|| Error::ObjectNotFound(format!(":{}:{}", colon.stage, path)))?;
3599 Ok(Some(IndexPathEntry {
3600 path,
3601 oid: entry.oid,
3602 mode: entry.mode,
3603 }))
3604}
3605
3606fn resolve_index_path_at_stage(repo: &Repository, path: &str, stage: u8) -> Result<ObjectId> {
3608 use crate::index::Index;
3609 let index_path = if let Some(raw) = repo.environment().var("GIT_INDEX_FILE") {
3610 let p = std::path::PathBuf::from(raw);
3611 if p.is_absolute() {
3612 p
3613 } else {
3614 let cwd = repo.environment().cwd.clone();
3615 cwd.join(p)
3616 }
3617 } else {
3618 repo.index_path()
3619 };
3620 let config = repo.config()?;
3621 let index = Index::load_expand_sparse_with_config(&index_path, repo.odb(), &config)
3622 .map_err(|_| Error::ObjectNotFound(format!(":{stage}:{path}")))?;
3623 let lookup_path = if repo.pathspec_precompose_enabled() {
3624 crate::unicode_normalization::precompose_utf8_path(path).into_owned()
3625 } else {
3626 path.to_owned()
3627 };
3628 match index.get(lookup_path.as_bytes(), stage) {
3629 Some(entry) => Ok(entry.oid),
3630 None => Err(Error::ObjectNotFound(format!(":{stage}:{path}"))),
3631 }
3632}
3633
3634pub fn split_treeish_colon(spec: &str) -> Option<(&str, &str)> {
3639 if spec.starts_with(':') {
3640 return None;
3641 }
3642 let bytes = spec.as_bytes();
3643 let mut i = 0usize;
3644 let mut peel_depth = 0usize;
3645 while i < bytes.len() {
3646 if i + 1 < bytes.len() && bytes[i] == b'^' && bytes[i + 1] == b'{' {
3647 peel_depth += 1;
3648 i += 2;
3649 continue;
3650 }
3651 if peel_depth > 0 {
3652 if bytes[i] == b'}' {
3653 peel_depth -= 1;
3654 }
3655 i += 1;
3656 continue;
3657 }
3658 if bytes[i] == b':' && i > 0 {
3659 let before = &spec[..i];
3660 let after = &spec[i + 1..];
3661 if !before.is_empty() {
3662 return Some((before, after)); }
3664 }
3665 i += 1;
3666 }
3667 None
3668}
3669
3670pub(crate) fn split_treeish_spec(spec: &str) -> Option<(&str, &str)> {
3671 split_treeish_colon(spec)
3672}
3673
3674pub(crate) fn resolve_treeish_path_to_object(
3678 repo: &Repository,
3679 treeish: ObjectId,
3680 path: &str,
3681) -> Result<ObjectId> {
3682 let object = repo.read_replaced(&treeish)?;
3683 let mut current_tree = match object.kind {
3684 ObjectKind::Commit => parse_commit(&object.data)?.tree,
3685 ObjectKind::Tree => treeish,
3686 _ => {
3687 return Err(Error::InvalidRef(format!(
3688 "object {treeish} does not name a tree"
3689 )))
3690 }
3691 };
3692
3693 let parts_vec: Vec<&str> = path.split('/').filter(|p| !p.is_empty()).collect();
3694 if parts_vec.is_empty() {
3695 return Ok(current_tree);
3696 }
3697 for (idx, part) in parts_vec.iter().enumerate() {
3698 let tree_object = repo.read_replaced(¤t_tree)?;
3699 if tree_object.kind != ObjectKind::Tree {
3700 return Err(Error::CorruptObject(format!(
3701 "object {current_tree} is not a tree"
3702 )));
3703 }
3704 let entries = parse_tree(&tree_object.data)?;
3705 let Some(entry) = entries.iter().find(|entry| entry.name == part.as_bytes()) else {
3706 return Err(Error::ObjectNotFound(path.to_owned()));
3707 };
3708 if idx + 1 == parts_vec.len() {
3709 return Ok(entry.oid);
3710 }
3711 if entry.mode != crate::index::MODE_TREE {
3712 return Err(Error::ObjectNotFound(path.to_owned()));
3713 }
3714 current_tree = entry.oid;
3715 }
3716
3717 Err(Error::ObjectNotFound(path.to_owned()))
3718}
3719
3720pub(crate) fn resolve_treeish_path(
3721 repo: &Repository,
3722 treeish: ObjectId,
3723 path: &str,
3724) -> Result<ObjectId> {
3725 resolve_treeish_path_to_object(repo, treeish, path)
3726}
3727
3728fn apply_peel(repo: &Repository, mut oid: ObjectId, peel: Option<&str>) -> Result<ObjectId> {
3729 match peel {
3730 None => Ok(oid),
3731 Some(search) if search.starts_with('/') => {
3732 let pattern = &search[1..];
3733 if pattern.is_empty() {
3734 return Err(Error::InvalidRef(
3735 "empty commit message search pattern".to_owned(),
3736 ));
3737 }
3738 resolve_commit_message_search_from(repo, oid, pattern)
3739 }
3740 Some("") => {
3741 while let Ok(obj) = repo.read_replaced(&oid) {
3742 if obj.kind != ObjectKind::Tag {
3743 break;
3744 }
3745 oid = parse_tag_target(&obj.data)?;
3746 }
3747 Ok(oid)
3748 }
3749 Some("commit") => {
3750 oid = apply_peel(repo, oid, Some(""))?;
3751 let obj = repo.read_replaced(&oid)?;
3752 if obj.kind == ObjectKind::Commit {
3753 Ok(oid)
3754 } else {
3755 Err(Error::InvalidRef("expected commit".to_owned()))
3756 }
3757 }
3758 Some("tree") => {
3759 oid = apply_peel(repo, oid, Some(""))?;
3761 let obj = repo.read_replaced(&oid)?;
3762 match obj.kind {
3763 ObjectKind::Tree => Ok(oid),
3764 ObjectKind::Commit => Ok(parse_commit(&obj.data)?.tree),
3765 _ => Err(Error::InvalidRef("expected tree or commit".to_owned())),
3766 }
3767 }
3768 Some("blob") => {
3769 let mut cur = oid;
3771 loop {
3772 let obj = repo.read_replaced(&cur)?;
3773 match obj.kind {
3774 ObjectKind::Blob => return Ok(cur),
3775 ObjectKind::Tag => {
3776 cur = parse_tag_target(&obj.data)?;
3777 }
3778 _ => return Err(Error::InvalidRef("expected blob".to_owned())),
3779 }
3780 }
3781 }
3782 Some("object") => Ok(oid),
3783 Some("tag") => {
3784 let obj = repo.read_replaced(&oid)?;
3786 if obj.kind == ObjectKind::Tag {
3787 Ok(oid)
3788 } else {
3789 Err(Error::InvalidRef("expected tag".to_owned()))
3790 }
3791 }
3792 Some(other) => Err(Error::InvalidRef(format!(
3793 "unsupported peel operator '{{{other}}}'"
3794 ))),
3795 }
3796}
3797
3798pub fn expand_rev_token_circ_bang(repo: &Repository, token: &str) -> Result<Vec<String>> {
3809 let Some(base) = token.strip_suffix("^!") else {
3810 return Ok(vec![token.to_owned()]);
3811 };
3812 if base.is_empty() {
3813 return Err(RevParseError::AmbiguousArgument {
3814 spec: token.to_owned(),
3815 }
3816 .into());
3817 }
3818 let oid = resolve_revision_for_range_end(repo, base)?;
3819 let commit_oid = peel_to_commit_for_merge_base(repo, oid)?;
3820 let parents = commit_parents_for_navigation(repo, commit_oid)?;
3821 let mut out = vec![base.to_owned()];
3822 for p in parents {
3823 out.push(format!("^{}", p.to_hex()));
3824 }
3825 Ok(out)
3826}
3827
3828#[must_use]
3830pub fn parse_peel_suffix(spec: &str) -> (&str, Option<&str>) {
3831 if let Some(base) = spec.strip_suffix("^{}") {
3832 return (base, Some(""));
3833 }
3834 if let Some(start) = spec.rfind("^{") {
3835 if spec.ends_with('}') {
3836 let base = &spec[..start];
3837 let op = &spec[start + 2..spec.len() - 1];
3838 return (base, Some(op));
3839 }
3840 }
3841 if let Some(base) = spec.strip_suffix("^0") {
3843 if !base.ends_with('^') {
3846 return (base, Some("commit"));
3847 }
3848 }
3849 (spec, None)
3850}
3851
3852fn parse_tag_target(data: &[u8]) -> Result<ObjectId> {
3853 let text = std::str::from_utf8(data)
3854 .map_err(|_| Error::CorruptObject("invalid tag object".to_owned()))?;
3855 let Some(line) = text.lines().find(|line| line.starts_with("object ")) else {
3856 return Err(Error::CorruptObject("tag missing object header".to_owned()));
3857 };
3858 let oid_text = line.trim_start_matches("object ").trim();
3859 oid_text.parse::<ObjectId>()
3860}
3861
3862fn parse_oneline_pattern(pattern: &str) -> Option<(bool, &str)> {
3874 let Some(after_bang) = pattern.strip_prefix('!') else {
3875 return Some((false, pattern));
3876 };
3877 if let Some(neg) = after_bang.strip_prefix('-') {
3878 return Some((true, neg));
3879 }
3880 if after_bang.starts_with('!') {
3881 return Some((false, after_bang));
3883 }
3884 None
3886}
3887
3888#[derive(Eq, PartialEq)]
3889struct CommitMessageHeapEntry {
3890 committer_time: i64,
3891 oid: ObjectId,
3892}
3893
3894impl Ord for CommitMessageHeapEntry {
3895 fn cmp(&self, other: &Self) -> Ordering {
3896 self.committer_time
3897 .cmp(&other.committer_time)
3898 .then_with(|| self.oid.cmp(&other.oid))
3899 }
3900}
3901
3902impl PartialOrd for CommitMessageHeapEntry {
3903 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
3904 Some(self.cmp(other))
3905 }
3906}
3907
3908struct MessageSearchCommitMeta {
3910 parents: smallvec::SmallVec<[ObjectId; 2]>,
3911 committer_time: i64,
3912 message: Option<String>,
3913}
3914
3915struct MessageSearchWalker<'repo> {
3916 repo: &'repo Repository,
3917 graph: Option<Arc<crate::commit_graph_file::CommitGraphChain>>,
3918 cache: HashMap<ObjectId, MessageSearchCommitMeta>,
3919 visited: HashSet<ObjectId>,
3920 heap: BinaryHeap<CommitMessageHeapEntry>,
3921}
3922
3923impl<'repo> MessageSearchWalker<'repo> {
3924 fn new(repo: &'repo Repository) -> Self {
3925 Self {
3926 repo,
3927 graph: repo.odb().commit_graph_chain(),
3928 cache: HashMap::new(),
3929 visited: HashSet::new(),
3930 heap: BinaryHeap::new(),
3931 }
3932 }
3933
3934 fn enqueue_peeled_tip(&mut self, tip: ObjectId) {
3935 let commit_oid = match peel_tags_to_commit(self.repo, tip) {
3936 Ok(Ok(oid)) => oid,
3937 Ok(Err(_)) | Err(_) => return,
3938 };
3939 self.try_enqueue(commit_oid);
3940 }
3941
3942 fn try_enqueue(&mut self, oid: ObjectId) {
3943 if !self.visited.insert(oid) {
3944 return;
3945 }
3946 let Some(committer_time) = self.ensure_linkage(oid) else {
3947 self.visited.remove(&oid);
3948 return;
3949 };
3950 self.heap.push(CommitMessageHeapEntry {
3951 committer_time,
3952 oid,
3953 });
3954 }
3955
3956 fn ensure_linkage(&mut self, oid: ObjectId) -> Option<i64> {
3958 if let Some(meta) = self.cache.get(&oid) {
3959 return Some(meta.committer_time);
3960 }
3961 if let Some(ref graph) = self.graph {
3962 if let Some((parents, committer_time)) = graph.graph_commit(&oid) {
3963 self.cache.insert(
3964 oid,
3965 MessageSearchCommitMeta {
3966 parents: parents.into(),
3967 committer_time,
3968 message: None,
3969 },
3970 );
3971 return Some(committer_time);
3972 }
3973 }
3974 let obj = self.repo.read_replaced(&oid).ok()?;
3975 if obj.kind != ObjectKind::Commit {
3976 return None;
3977 }
3978 let commit = CommitRef::parse(&obj.data).ok()?;
3979 let committer_time = commit.committer_timestamp();
3980 self.cache.insert(
3981 oid,
3982 MessageSearchCommitMeta {
3983 parents: commit.parents().into(),
3984 committer_time,
3985 message: Some(commit.message().into_owned()),
3986 },
3987 );
3988 Some(committer_time)
3989 }
3990
3991 fn ensure_message(&mut self, oid: ObjectId) -> Option<String> {
3992 if let Some(message) = self.cache.get(&oid)?.message.clone() {
3993 return Some(message);
3994 }
3995 let obj = self.repo.read_replaced(&oid).ok()?;
3996 if obj.kind != ObjectKind::Commit {
3997 return None;
3998 }
3999 let commit = CommitRef::parse(&obj.data).ok()?;
4000 let message = commit.message().into_owned();
4001 self.cache.get_mut(&oid)?.message = Some(message.clone());
4002 Some(message)
4003 }
4004
4005 fn run(
4006 mut self,
4007 seeds: &[ObjectId],
4008 pattern: &str,
4009 negate: bool,
4010 effective_pattern: &str,
4011 regex: Option<&Regex>,
4012 ) -> Result<ObjectId> {
4013 for &seed in seeds {
4014 self.enqueue_peeled_tip(seed);
4015 }
4016
4017 while let Some(entry) = self.heap.pop() {
4018 let Some(message) = self.ensure_message(entry.oid) else {
4019 continue;
4020 };
4021 let base_match = if let Some(re) = regex {
4022 re.is_match(&message)
4023 } else {
4024 message.contains(effective_pattern)
4025 };
4026 if negate ^ base_match {
4027 return Ok(entry.oid);
4028 }
4029 let parents = self
4030 .cache
4031 .get(&entry.oid)
4032 .map(|meta| meta.parents.clone())
4033 .unwrap_or_default();
4034 for parent in parents {
4035 self.try_enqueue(parent);
4036 }
4037 }
4038
4039 Err(Error::ObjectNotFound(format!(":/{pattern}")))
4040 }
4041}
4042
4043fn resolve_commit_message_search_seeds(
4044 repo: &Repository,
4045 seeds: &[ObjectId],
4046 pattern: &str,
4047) -> Result<ObjectId> {
4048 let Some((negate, effective_pattern)) = parse_oneline_pattern(pattern) else {
4049 return Err(Error::ObjectNotFound(format!(":/{pattern}")));
4050 };
4051 let regex = Regex::new(effective_pattern).ok();
4052 MessageSearchWalker::new(repo).run(seeds, pattern, negate, effective_pattern, regex.as_ref())
4053}
4054
4055fn resolve_commit_message_search_from(
4056 repo: &Repository,
4057 start: ObjectId,
4058 pattern: &str,
4059) -> Result<ObjectId> {
4060 resolve_commit_message_search_seeds(repo, &[start], pattern)
4061}
4062
4063fn find_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
4064 let max_len = repo.odb().hash_algo().hex_len();
4065 if !is_hex_prefix(prefix) || !(4..=max_len).contains(&prefix.len()) {
4066 return Ok(Vec::new());
4067 }
4068 let mut matches = Vec::new();
4069 repo.odb().lookup_prefix(prefix, 0, &mut matches)?;
4070 Ok(matches)
4071}
4072
4073fn is_hex_prefix(text: &str) -> bool {
4074 !text.is_empty() && text.chars().all(|ch| ch.is_ascii_hexdigit())
4075}
4076
4077fn path_is_within(path: &Path, container: &Path) -> bool {
4078 if path == container {
4079 return true;
4080 }
4081 path.starts_with(container)
4082}
4083
4084fn normalize_components(path: &Path) -> Vec<String> {
4085 path.components()
4086 .filter_map(|component| match component {
4087 Component::RootDir => Some(String::from("/")),
4088 Component::Normal(item) => Some(item.to_string_lossy().into_owned()),
4089 _ => None,
4090 })
4091 .collect()
4092}
4093
4094fn component_to_text(component: Component<'_>) -> Option<String> {
4095 match component {
4096 Component::Normal(item) => Some(os_to_string(item)),
4097 _ => None,
4098 }
4099}
4100
4101fn os_to_string(text: &OsStr) -> String {
4102 text.to_string_lossy().into_owned()
4103}
4104
4105fn resolve_commit_message_search(
4108 repo: &crate::repo::Repository,
4109 pattern: &str,
4110) -> Result<ObjectId> {
4111 let mut seeds = Vec::new();
4112 let mut seen = HashSet::new();
4113 if let Ok(head) = crate::state::resolve_head_for_repository(repo) {
4114 if let Some(oid) = head.oid() {
4115 if seen.insert(*oid) {
4116 seeds.push(*oid);
4117 }
4118 }
4119 }
4120 if let Ok(refs) = crate::refs::list_refs_in(repo, "refs/") {
4121 for (_name, oid) in refs {
4122 if seen.insert(oid) {
4123 seeds.push(oid);
4124 }
4125 }
4126 }
4127 resolve_commit_message_search_seeds(repo, &seeds, pattern)
4128}
4129
4130pub fn list_all_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
4132 find_abbrev_matches(repo, prefix)
4133}
4134
4135pub fn list_loose_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
4137 list_all_abbrev_matches(repo, prefix)
4138}
4139
4140#[cfg(test)]
4141mod line_coverage_tests {
4142 use super::*;
4143 use crate::index::{entry_from_stat, Index, IndexEntry, MODE_REGULAR};
4144 use crate::objects::ObjectId;
4145 use crate::objects::ObjectKind;
4146
4147 use crate::ref_storage::RefStorageFormat;
4148 use crate::repo::{init_repository, InitOptions, Repository};
4149 use crate::write_tree::write_tree_from_index;
4150 use std::fs;
4151 use tempfile::tempdir;
4152
4153 fn ambiguous_hex_prefix_among(
4154 oids: &[ObjectId],
4155 min_len: usize,
4156 max_len: usize,
4157 ) -> Option<String> {
4158 for i in 0..oids.len() {
4159 for j in (i + 1)..oids.len() {
4160 let a = oids[i].to_hex();
4161 let b = oids[j].to_hex();
4162 let cap = max_len.min(a.len()).min(b.len());
4163 for len in (min_len..=cap).rev() {
4164 if a[..len] == b[..len] {
4165 return Some(a[..len].to_string());
4166 }
4167 }
4168 }
4169 }
4170 None
4171 }
4172
4173 fn seed_commits_until_ambiguous_prefix(
4174 repo: &Repository,
4175 tree: ObjectId,
4176 oids: &mut Vec<ObjectId>,
4177 min_len: usize,
4178 max_commits: usize,
4179 ) -> String {
4180 let mut counter = oids.len() as i32;
4181 while oids.len() < max_commits && max_commits > 0 {
4182 let body = format!(
4183 "tree {tree}\nauthor T <t@e.com> {counter} +0000\ncommitter T <t@e.com> {counter} +0000\n\nc{counter}\n"
4184 );
4185 counter += 1;
4186 let oid = repo
4187 .odb()
4188 .write_loose_materialize(ObjectKind::Commit, body.as_bytes())
4189 .expect("commit");
4190 oids.push(oid);
4191 if let Some(prefix) = ambiguous_hex_prefix_among(oids, min_len, 12) {
4192 return prefix;
4193 }
4194 }
4195 panic!("could not find {min_len}+ hex prefix collision among {max_commits} commits");
4196 }
4197
4198 fn exercise_resolve_base(repo: &Repository, spec: &str) {
4199 for index_dwim in [false, true] {
4200 for commit_only in [false, true] {
4201 for use_disambiguate in [false, true] {
4202 for implicit_tree in [false, true] {
4203 for implicit_blob in [false, true] {
4204 for remote_guess in [false, true] {
4205 let _ = resolve_base(
4206 repo,
4207 spec,
4208 index_dwim,
4209 commit_only,
4210 use_disambiguate,
4211 None,
4212 implicit_tree,
4213 implicit_blob,
4214 remote_guess,
4215 );
4216 }
4217 }
4218 }
4219 }
4220 }
4221 }
4222 }
4223
4224 #[test]
4225 fn parser_helpers_and_config_fragments() {
4226 assert_eq!(abbreviate_ref_name("refs/heads/main"), "main");
4227 assert!(upstream_suffix_info("main@{upstream}").is_some());
4228 assert!(spec_has_parent_shorthand_suffix("main^@"));
4229 assert!(revision_spec_contains_ancestry_navigation("HEAD~3"));
4230 let (base, peel) = parse_peel_suffix("v1^{tree}");
4231 assert_eq!(base, "v1");
4232 assert_eq!(peel, Some("tree"));
4233 assert_eq!(
4234 split_treeish_colon("HEAD:path/to"),
4235 Some(("HEAD", "path/to"))
4236 );
4237 assert!(parse_index_colon_spec(":2:path").is_some());
4238 assert!(split_double_dot_range("a..b").is_some());
4239 assert!(split_triple_dot_range("a...b").is_some());
4240 assert!(!split_double_dot_range("a...b").is_some());
4241
4242 let cfg = "[remote \"origin\"]\n\turl = /x\n\tpush = refs/heads/*:refs/heads/*\n";
4243 assert!(!remote_push_refspecs(cfg, "origin").is_empty());
4244 assert!(remote_has_push_refspec(cfg, "origin"));
4245 assert_eq!(
4246 match_refname_with_pattern("refs/heads/*", "refs/heads/main", "refs/remotes/origin/*"),
4247 Some("refs/remotes/origin/main".into())
4248 );
4249 assert_eq!(
4250 parse_config_value(cfg, "remote \"origin\"", "url").as_deref(),
4251 Some("/x")
4252 );
4253 assert!(parse_branch_tracking(
4254 "[branch \"main\"]\n\tremote = origin\n\tmerge = refs/heads/main\n",
4255 "main"
4256 )
4257 .is_some());
4258
4259 let (root, steps) = parse_nav_steps("main~3^2");
4260 assert_eq!(root, "main");
4261 assert_eq!(steps.len(), 2);
4262 assert!(find_parent_shorthand("main^!").is_some());
4263 assert!(reflog_date_selector_timestamp_at("1.day.ago", 1_700_000_000).is_some());
4264 assert!(reflog_date_selector_timestamp_at("yesterday", 1_700_000_000).is_some());
4265 assert!(reflog_date_selector_timestamp_at("last week", 1_700_000_000).is_some());
4266 assert!(reflog_date_selector_timestamp_at("noon yesterday", 1_700_000_000).is_some());
4267 assert!(next_reflog_at_open("main@{1}@{0}", 0).is_some());
4268 }
4269
4270 #[test]
4271 fn top_level_triple_dot_skips_selectors_and_paths() {
4272 assert_eq!(
4273 split_top_level_triple_dot_range("main...side"),
4274 Some(("main", "side"))
4275 );
4276 assert_eq!(
4277 split_top_level_triple_dot_range("main..."),
4278 Some(("main", ""))
4279 );
4280 assert_eq!(
4281 split_top_level_triple_dot_range("...side"),
4282 Some(("", "side"))
4283 );
4284 assert_eq!(split_top_level_triple_dot_range("..."), Some(("", "")));
4285 assert_eq!(
4286 split_top_level_triple_dot_range("main^{/a...b}...side"),
4287 Some(("main^{/a...b}", "side"))
4288 );
4289 for (spec, left, right) in [
4290 ("foo{bar...main", "foo{bar", "main"),
4291 ("main...foo{bar", "main", "foo{bar"),
4292 ("foo}bar...main", "foo}bar", "main"),
4293 ("a{b}c...main", "a{b}c", "main"),
4294 ("foo{bar@{0}...main", "foo{bar@{0}", "main"),
4295 ("main^{/a{2}...b}...side", "main^{/a{2}...b}", "side"),
4296 ] {
4297 assert_eq!(
4298 split_top_level_triple_dot_range(spec),
4299 Some((left, right)),
4300 "{spec}"
4301 );
4302 }
4303 for single in [
4304 "foo{bar",
4305 "foo{bar^{/fix...up}",
4306 "main^{/a{2}...b}",
4307 ":/fix...up",
4308 "main^{/fix...up}",
4309 "HEAD:dir/a...b",
4310 "main@{1...}",
4311 "a....b",
4312 "a..b",
4313 "main",
4314 ] {
4315 assert_eq!(split_top_level_triple_dot_range(single), None, "{single}");
4316 }
4317 }
4318
4319 #[test]
4320 fn bare_repo_resolve_variants() -> crate::error::Result<()> {
4321 let dir = tempdir().map_err(crate::error::Error::Io)?;
4322 Repository::init(
4323 dir.path(),
4324 &InitOptions {
4325 bare: true,
4326
4327 ref_storage: crate::RefStorageFormat::Files,
4328 ..InitOptions::default()
4329 },
4330 )?;
4331 let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
4332 let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
4333 let body = format!(
4334 "tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nm\n"
4335 );
4336 let tip = repo
4337 .odb()
4338 .write_loose_materialize(ObjectKind::Commit, body.as_bytes())?;
4339 fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
4340 .map_err(crate::error::Error::Io)?;
4341
4342 for spec in ["main", "HEAD", "HEAD^0", "main^{commit}", "main~0"] {
4343 let _ = resolve_revision_for_verify(&repo, spec)?;
4344 let _ = resolve_revision_without_index_dwim(&repo, spec)?;
4345 let _ = resolve_revision_for_range_end(&repo, spec)?;
4346 let _ = resolve_revision_as_commit(&repo, spec)?;
4347 }
4348 let _ = peel_to_tree(&repo, tip)?;
4349 let _ = try_parse_double_dot_log_range(&repo, "main..main")?;
4350 Ok(())
4351 }
4352
4353 #[test]
4354 fn peel_to_commit_for_merge_base_follows_tag_chains_only_to_commits() -> crate::error::Result<()>
4355 {
4356 let dir = tempdir().map_err(crate::error::Error::Io)?;
4357 Repository::init(
4358 dir.path(),
4359 &InitOptions {
4360 bare: true,
4361 ref_storage: crate::RefStorageFormat::Files,
4362 ..InitOptions::default()
4363 },
4364 )?;
4365 let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
4366 let odb = repo.odb();
4367 let tree = odb.write_loose_materialize(ObjectKind::Tree, b"")?;
4368 let blob = odb.write_loose_materialize(ObjectKind::Blob, b"data\n")?;
4369 let commit = odb.write_loose_materialize(
4370 ObjectKind::Commit,
4371 format!(
4372 "tree {tree}\nauthor T <t@e.com> 1 +0000\ncommitter T <t@e.com> 1 +0000\n\nm\n"
4373 )
4374 .as_bytes(),
4375 )?;
4376 let tag = |target: ObjectId, kind: &str, name: &str| {
4377 odb.write_loose_materialize(
4378 ObjectKind::Tag,
4379 format!(
4380 "object {target}\ntype {kind}\ntag {name}\ntagger T <t@e.com> 1 +0000\n\nt\n"
4381 )
4382 .as_bytes(),
4383 )
4384 };
4385 let inner = tag(commit, "commit", "inner")?;
4386 let outer = tag(inner, "tag", "outer")?;
4387 let blob_tag = tag(blob, "blob", "blobtag")?;
4388
4389 assert_eq!(peel_to_commit_for_merge_base(&repo, commit)?, commit);
4390 assert_eq!(peel_to_commit_for_merge_base(&repo, inner)?, commit);
4391 assert_eq!(peel_to_commit_for_merge_base(&repo, outer)?, commit);
4392 for not_commit in [tree, blob, blob_tag] {
4393 assert!(matches!(
4394 peel_to_commit_for_merge_base(&repo, not_commit),
4395 Err(crate::error::Error::InvalidRef(_))
4396 ));
4397 }
4398 let absent = ObjectId::from_hex("1111111111111111111111111111111111111111")?;
4399 assert!(matches!(
4400 peel_to_commit_for_merge_base(&repo, absent),
4401 Err(crate::error::Error::ObjectNotFound(_))
4402 ));
4403 Ok(())
4404 }
4405
4406 #[test]
4407 fn deep_worktree_chain_and_index_specs() -> crate::error::Result<()> {
4408 let dir = tempdir().map_err(crate::error::Error::Io)?;
4409 let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
4410 let blob = repo
4411 .odb()
4412 .write_loose_materialize(ObjectKind::Blob, b"line\n")?;
4413 fs::write(dir.path().join("f.txt"), b"line\n").map_err(crate::error::Error::Io)?;
4414 let entry = entry_from_stat(&dir.path().join("f.txt"), b"f.txt", blob, MODE_REGULAR)?;
4415 let mut index = Index::new();
4416 index.entries_mut().push(entry);
4417 index.write_to_path(&repo.git_dir().join("index"), false)?;
4418 let tree = write_tree_from_index(&repo.odb(), &index, "")?;
4419 let mut tip = repo.odb().write_loose_materialize(
4420 ObjectKind::Commit,
4421 format!(
4422 "tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nroot\n"
4423 )
4424 .as_bytes(),
4425 )?;
4426 for i in 1..=120_i64 {
4427 let body = format!(
4428 "tree {tree}\nparent {tip}\nauthor T <t@e.com> {} +0000\ncommitter T <t@e.com> {} +0000\n\nc{i}\n",
4429 100 + i,
4430 100 + i
4431 );
4432 tip = repo
4433 .odb()
4434 .write_loose_materialize(ObjectKind::Commit, body.as_bytes())?;
4435 }
4436 fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
4437 .map_err(crate::error::Error::Io)?;
4438 for n in 0..120 {
4439 for spec in [
4440 format!("HEAD~{n}"),
4441 format!("main~{n}"),
4442 format!("HEAD~{n}^{{tree}}"),
4443 format!("HEAD~{n}^{{commit}}"),
4444 ] {
4445 exercise_resolve_base(&repo, &spec);
4446 exercise_impl_matrix(&repo, &spec);
4447 let _ = resolve_revision_for_verify(&repo, &spec);
4448 let _ = resolve_revision_for_checkout_guess(&repo, &spec, true);
4449 }
4450 }
4451 for spec in [":f.txt", ":0:f.txt", "HEAD:f.txt", "f.txt", ":/c80"] {
4452 exercise_resolve_base(&repo, spec);
4453 exercise_impl_matrix(&repo, spec);
4454 }
4455 Ok(())
4456 }
4457
4458 #[test]
4459 fn deep_chain_resolve_exercises_navigation() -> crate::error::Result<()> {
4460 let dir = tempdir().map_err(crate::error::Error::Io)?;
4461 Repository::init(
4462 dir.path(),
4463 &InitOptions {
4464 bare: true,
4465
4466 ref_storage: crate::RefStorageFormat::Files,
4467 ..InitOptions::default()
4468 },
4469 )?;
4470 let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
4471 let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
4472 let mut tip = repo.odb().write_loose_materialize(
4473 ObjectKind::Commit,
4474 format!(
4475 "tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nroot\n"
4476 )
4477 .as_bytes(),
4478 )?;
4479 for i in 1..=100_i64 {
4480 let body = format!(
4481 "tree {tree}\nparent {tip}\nauthor T <t@e.com> {} +0000\ncommitter T <t@e.com> {} +0000\n\nc{i}\n",
4482 100 + i,
4483 100 + i
4484 );
4485 tip = repo
4486 .odb()
4487 .write_loose_materialize(ObjectKind::Commit, body.as_bytes())?;
4488 }
4489 fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
4490 .map_err(crate::error::Error::Io)?;
4491 for n in 0..100 {
4492 let spec = format!("HEAD~{n}");
4493 let _ = resolve_revision_for_verify(&repo, &spec)?;
4494 let _ = resolve_revision(&repo, &spec)?;
4495 let _ = resolve_revision_for_checkout_guess(&repo, &spec, true)?;
4496 }
4497 Ok(())
4498 }
4499
4500 #[test]
4501 fn push_and_tracking_config_helpers() {
4502 let cfg = "[remote \"origin\"]\n\turl = /x\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n\tpush = refs/heads/*:refs/heads/*\n";
4503 assert!(push_refspec_mapped_tracking(cfg, "origin", "main").is_some());
4504 assert!(map_dest_to_tracking(cfg, "origin", "refs/heads/main").is_some());
4505 let norm = normalize_path_components(std::path::PathBuf::from("./a/../b"));
4506 assert!(norm.to_string_lossy().contains('b'));
4507 assert_eq!(
4508 normalize_colon_path_for_bare_tree("x/y").expect("bare norm"),
4509 "x/y"
4510 );
4511 }
4512
4513 #[test]
4514 fn reflog_chain_and_at_step_parsers() {
4515 assert!(matches!(
4516 try_parse_at_step_inner("0", 1_700_000_000),
4517 Ok(AtStep::Index(0))
4518 ));
4519 assert!(matches!(
4520 try_parse_at_step_inner("now", 1_700_000_000),
4521 Ok(AtStep::Now)
4522 ));
4523 assert!(matches!(
4524 try_parse_at_step_inner("yesterday", 1_700_000_000),
4525 Ok(AtStep::Date(_))
4526 ));
4527 assert!(split_reflog_at_chain("main@{1}@{0}", 1_700_000_000)
4528 .unwrap()
4529 .is_some());
4530 assert!(next_reflog_at_open("HEAD@{2}", 0).is_some());
4531 let (root, steps) = parse_nav_steps("HEAD~2^2^1");
4532 assert_eq!(root, "HEAD");
4533 assert_eq!(steps.len(), 3);
4534 }
4535
4536 fn exercise_impl_matrix(repo: &Repository, spec: &str) {
4537 for index_dwim in [false, true] {
4538 for commit_only in [false, true] {
4539 for use_disambiguate in [false, true] {
4540 for treeish_colon_lhs in [false, true] {
4541 for implicit_tree_abbrev in [false, true] {
4542 for implicit_blob_abbrev in [false, true] {
4543 for remote_guess in [false, true] {
4544 let _ = resolve_revision_impl(
4545 repo,
4546 spec,
4547 index_dwim,
4548 commit_only,
4549 use_disambiguate,
4550 treeish_colon_lhs,
4551 implicit_tree_abbrev,
4552 implicit_blob_abbrev,
4553 remote_guess,
4554 );
4555 }
4556 }
4557 }
4558 }
4559 }
4560 }
4561 }
4562 }
4563
4564 #[test]
4565 fn top_level_range_ignores_dots_in_selectors_and_paths() {
4566 for range in [
4567 "a..b",
4568 "a...b",
4569 "..b",
4570 "a..",
4571 "...main",
4572 "main^{/x}..side",
4573 "main@{1}...side",
4574 "foo{bar...main",
4575 "foo{bar..main",
4576 "main...foo{bar",
4577 "foo}bar...main",
4578 "a{b}c..main",
4579 "foo{bar@{1}...main",
4580 "main^{/a{2}}..side",
4581 ] {
4582 assert!(is_top_level_range(range), "{range}");
4583 }
4584 for single in [
4585 "main",
4586 "v1.2.3",
4587 "keep~1",
4588 "main^{/release..candidate}",
4589 "main^{/release...candidate}",
4590 "main@{1.week.ago}",
4591 ":/fix..up",
4592 "HEAD:../file",
4593 "HEAD:a..b",
4594 "foo{bar",
4595 "foo{bar~1",
4596 "foo{bar^{/release..candidate}",
4597 "main^{/a{2}..b}",
4598 "@{1.week.ago}",
4599 ] {
4600 assert!(!is_top_level_range(single), "{single}");
4601 }
4602 }
4603
4604 #[test]
4605 fn resolve_revision_impl_flag_matrix() -> crate::error::Result<()> {
4606 let dir = tempdir().map_err(crate::error::Error::Io)?;
4607 Repository::init(
4608 dir.path(),
4609 &InitOptions {
4610 bare: true,
4611
4612 ref_storage: crate::RefStorageFormat::Files,
4613 ..InitOptions::default()
4614 },
4615 )?;
4616 let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
4617 let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
4618 let mut tip = repo.odb().write_loose_materialize(
4619 ObjectKind::Commit,
4620 format!(
4621 "tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nroot\n"
4622 )
4623 .as_bytes(),
4624 )?;
4625 for i in 1..=60_i64 {
4626 let body = format!(
4627 "tree {tree}\nparent {tip}\nauthor T <t@e.com> {} +0000\ncommitter T <t@e.com> {} +0000\n\nc{i}\n",
4628 100 + i,
4629 100 + i
4630 );
4631 tip = repo
4632 .odb()
4633 .write_loose_materialize(ObjectKind::Commit, body.as_bytes())?;
4634 }
4635 fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
4636 .map_err(crate::error::Error::Io)?;
4637 fs::write(
4638 repo.git_dir().join("refs/tags/annotated"),
4639 format!("{tip}\n"),
4640 )
4641 .map_err(crate::error::Error::Io)?;
4642 let specs = [
4643 "HEAD",
4644 "main",
4645 "main~1",
4646 "main~10",
4647 "main^",
4648 "main^1",
4649 "main^0",
4650 "main^{commit}",
4651 "main^{tree}",
4652 "main^{/c50}",
4653 ":/c50",
4654 ":/",
4655 "tags/annotated",
4656 "refs/heads/main",
4657 "refs/tags/annotated",
4658 "HEAD~5",
4659 "HEAD^!",
4660 "HEAD^@",
4661 "HEAD...main",
4662 "main...HEAD",
4663 "main..HEAD",
4664 "@",
4665 "HEAD@{0}",
4666 "HEAD@{1}",
4667 "HEAD@{1979-01-01}",
4668 ];
4669 for spec in specs {
4670 exercise_impl_matrix(&repo, spec);
4671 }
4672 for n in 0..60 {
4673 let spec = format!("HEAD~{n}");
4674 let _ = resolve_revision_for_verify(&repo, &spec);
4675 for index_dwim in [false, true] {
4676 for commit_only in [false, true] {
4677 for remote_guess in [false, true] {
4678 let _ = resolve_revision_impl(
4679 &repo,
4680 &spec,
4681 index_dwim,
4682 commit_only,
4683 true,
4684 false,
4685 false,
4686 false,
4687 remote_guess,
4688 );
4689 }
4690 }
4691 }
4692 }
4693 Ok(())
4694 }
4695
4696 #[test]
4697 fn bare_merge_commit_and_tag_object_matrix() -> crate::error::Result<()> {
4698 let dir = tempdir().map_err(crate::error::Error::Io)?;
4699 Repository::init(
4700 dir.path(),
4701 &InitOptions {
4702 bare: true,
4703
4704 ref_storage: crate::RefStorageFormat::Files,
4705 ..InitOptions::default()
4706 },
4707 )?;
4708 let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
4709 let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
4710 let base = repo.odb().write_loose_materialize(
4711 ObjectKind::Commit,
4712 format!(
4713 "tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nbase\n"
4714 )
4715 .as_bytes(),
4716 )?;
4717 let side = repo.odb().write_loose_materialize(
4718 ObjectKind::Commit,
4719 format!(
4720 "tree {tree}\nparent {base}\nauthor T <t@e.com> 200 +0000\ncommitter T <t@e.com> 200 +0000\n\nside\n"
4721 )
4722 .as_bytes(),
4723 )?;
4724 let tip = repo.odb().write_loose_materialize(
4725 ObjectKind::Commit,
4726 format!(
4727 "tree {tree}\nparent {base}\nauthor T <t@e.com> 300 +0000\ncommitter T <t@e.com> 300 +0000\n\ntip\n"
4728 )
4729 .as_bytes(),
4730 )?;
4731 let merge = repo.odb().write_loose_materialize(
4732 ObjectKind::Commit,
4733 format!(
4734 "tree {tree}\nparent {tip}\nparent {side}\nauthor T <t@e.com> 400 +0000\ncommitter T <t@e.com> 400 +0000\n\nmerge\n"
4735 )
4736 .as_bytes(),
4737 )?;
4738 fs::write(repo.git_dir().join("refs/heads/main"), format!("{merge}\n"))
4739 .map_err(crate::error::Error::Io)?;
4740 fs::write(repo.git_dir().join("refs/heads/side"), format!("{side}\n"))
4741 .map_err(crate::error::Error::Io)?;
4742 let tag_body = format!(
4743 "object {merge}\ntype commit\ntag v-merge\ntagger T <t@e.com> 500 +0000\n\nmsg\n"
4744 );
4745 let tag_oid = repo
4746 .odb()
4747 .write_loose_materialize(ObjectKind::Tag, tag_body.as_bytes())?;
4748 fs::write(
4749 repo.git_dir().join("refs/tags/v-merge"),
4750 format!("{tag_oid}\n"),
4751 )
4752 .map_err(crate::error::Error::Io)?;
4753 for spec in [
4754 "main^1",
4755 "main^2",
4756 "main^3",
4757 "main^",
4758 "main^0",
4759 "main^!",
4760 "main^@",
4761 "main~1",
4762 "side",
4763 "main...side",
4764 "side...main",
4765 "v-merge",
4766 "v-merge^{commit}",
4767 "v-merge^{}",
4768 "tags/v-merge",
4769 &format!("{tip}"),
4770 &format!("{}", &tip.to_hex()[..7]),
4771 ] {
4772 exercise_impl_matrix(&repo, spec);
4773 let _ = resolve_revision_for_verify(&repo, spec);
4774 let _ = resolve_revision_as_commit(&repo, spec);
4775 }
4776 Ok(())
4777 }
4778
4779 #[test]
4780 fn resolve_base_error_path_sweep() -> crate::error::Result<()> {
4781 let dir = tempdir().map_err(crate::error::Error::Io)?;
4782 Repository::init(
4783 dir.path(),
4784 &InitOptions {
4785 bare: true,
4786
4787 ref_storage: RefStorageFormat::Files,
4788 ..InitOptions::default()
4789 },
4790 )?;
4791 let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
4792 let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
4793 let tip = repo.odb().write_loose_materialize(
4794 ObjectKind::Commit,
4795 format!(
4796 "tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nx\n"
4797 )
4798 .as_bytes(),
4799 )?;
4800 fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
4801 .map_err(crate::error::Error::Io)?;
4802 const SPECS: &[&str] = &[
4803 "missing-ref",
4804 "HEAD~9999",
4805 "main~9999",
4806 "deadbeef",
4807 "00000000",
4808 "main^{tag}",
4809 "main:missing/path",
4810 ":missing",
4811 "HEAD^9",
4812 "main..",
4813 "...main",
4814 "@{-99}",
4815 "main@{upstream}@{upstream}",
4816 "FETCH_HEAD",
4817 "AUTO_MERGE",
4818 "HEAD:../outside",
4819 ":1:missing",
4820 ":0:/etc/passwd",
4821 ":2:/etc/passwd",
4822 "main...",
4823 "...",
4824 "tags/",
4825 "refs/heads/",
4826 "HEAD^{/}",
4827 ":/!!!",
4828 "!!",
4829 "main^999",
4830 "HEAD^^^",
4831 "notfound@{0}",
4832 "bogus@{1}@{2}",
4833 "HEAD@{upstream}@{push}",
4834 "x..y",
4835 "main..main..main",
4836 "HEAD...",
4837 "...HEAD",
4838 "HEAD:HEAD:file",
4839 "HEAD@{-0}",
4840 "@{-0}",
4841 "main@{999}",
4842 "HEAD@",
4843 "@{",
4844 "HEAD@{",
4845 "main@",
4846 "HEAD@{}",
4847 "HEAD@{x}",
4848 "HEAD@{1}@{upstream}",
4849 "upstream@{1}",
4850 "HEAD^{object}",
4851 "HEAD^{garbage}",
4852 "HEAD^{commit}^{commit}",
4853 "HEAD:.",
4854 "HEAD:..",
4855 ":",
4856 "::file",
4857 ":9:file",
4858 "HEAD~",
4859 "main~",
4860 "^",
4861 "^{}",
4862 "HEAD^^{}",
4863 "main^@^!",
4864 ":0:../../../../etc/passwd",
4865 ":2:../../../../etc/passwd",
4866 ":../../../../etc/passwd",
4867 "HEAD:../../../../etc/passwd",
4868 "remotes/origin/main",
4869 "origin",
4870 "FETCH_HEAD",
4871 "MERGE_HEAD",
4872 "AUTO_MERGE",
4873 ];
4874 for spec in SPECS {
4875 exercise_resolve_base(&repo, spec);
4876 exercise_impl_matrix(&repo, spec);
4877 }
4878 Ok(())
4879 }
4880
4881 #[test]
4882 fn ambiguous_abbrev_and_disambiguate_config() -> crate::error::Result<()> {
4883 let dir = tempdir().map_err(crate::error::Error::Io)?;
4884 Repository::init(
4885 dir.path(),
4886 &InitOptions {
4887 bare: true,
4888
4889 ref_storage: RefStorageFormat::Files,
4890 ..InitOptions::default()
4891 },
4892 )?;
4893 let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
4894 fs::write(
4895 repo.git_dir().join("config"),
4896 "[core]\n\tdisambiguate = commit\n",
4897 )
4898 .map_err(crate::error::Error::Io)?;
4899 let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
4900 let mut oids = Vec::new();
4901 let prefix = seed_commits_until_ambiguous_prefix(&repo, tree, &mut oids, 4, 8_000);
4902 let tip = *oids.last().expect("tip");
4903 fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
4904 .map_err(crate::error::Error::Io)?;
4905 for commit_only in [false, true] {
4906 for use_disambiguate in [false, true] {
4907 let _ = resolve_revision_impl(
4908 &repo,
4909 &prefix,
4910 false,
4911 commit_only,
4912 use_disambiguate,
4913 false,
4914 false,
4915 false,
4916 false,
4917 );
4918 let _ = resolve_revision_as_commit(&repo, &prefix);
4919 let _ = ambiguous_object_hint_lines(&repo, &prefix, Some("commit"));
4920 let _ = ambiguous_object_hint_lines(&repo, &prefix, Some("tree"));
4921 let _ = ambiguous_object_hint_lines(&repo, &prefix, Some("blob"));
4922 let _ = ambiguous_object_hint_lines(&repo, &prefix, Some("tag"));
4923 let _ = ambiguous_object_hint_lines(&repo, &prefix, None);
4924 }
4925 }
4926 Ok(())
4927 }
4928
4929 #[test]
4930 fn unborn_head_resolves_as_unborn_not_object_not_found() -> crate::error::Result<()> {
4931 let dir = tempdir().map_err(crate::error::Error::Io)?;
4932 let repo = crate::repo::Repository::init(dir.path(), &crate::repo::InitOptions::default())?;
4933 let err = resolve_revision_for_range_end(&repo, "HEAD").expect_err("HEAD");
4934 assert!(matches!(err, Error::UnbornHead), "{err:?}");
4935 Ok(())
4936 }
4937
4938 #[test]
4939 fn push_remote_ambiguous_and_graft_coverage() -> crate::error::Result<()> {
4940 let dir = tempdir().map_err(crate::error::Error::Io)?;
4941 let repo = crate::repo::Repository::init(dir.path(), &crate::repo::InitOptions::default())?;
4942 let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
4943 let tip = repo.odb().write_loose_materialize(
4944 ObjectKind::Commit,
4945 format!(
4946 "tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nm\n"
4947 )
4948 .as_bytes(),
4949 )?;
4950 fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
4951 .map_err(crate::error::Error::Io)?;
4952 fs::create_dir_all(repo.git_dir().join("refs/remotes/origin"))
4953 .map_err(crate::error::Error::Io)?;
4954 fs::write(
4955 repo.git_dir().join("refs/remotes/origin/main"),
4956 format!("{tip}\n"),
4957 )
4958 .map_err(crate::error::Error::Io)?;
4959 fs::write(
4960 repo.git_dir().join("refs/remotes/origin/HEAD"),
4961 b"ref: refs/remotes/origin/main\n",
4962 )
4963 .map_err(crate::error::Error::Io)?;
4964 fs::create_dir_all(repo.git_dir().join("info")).map_err(crate::error::Error::Io)?;
4965
4966 let write_config = |body: &str| {
4967 fs::write(repo.git_dir().join("config"), body).map_err(crate::error::Error::Io)
4968 };
4969 write_config(
4970 "[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",
4971 )?;
4972 let _ = resolve_push_full_ref_for_branch(&repo, "main");
4973 write_config(
4974 "[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",
4975 )?;
4976 let _ = resolve_push_full_ref_for_branch(&repo, "main");
4977 write_config(
4978 "[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",
4979 )?;
4980 let _ = resolve_push_full_ref_for_branch(&repo, "main");
4981 write_config(
4982 "[remote \"origin\"]\n\turl = /x\n[branch \"main\"]\n\tpushRemote = origin\n[push]\n\tdefault = upstream\n",
4983 )?;
4984 let _ = resolve_push_full_ref_for_branch(&repo, "main");
4985
4986 for s in [
4987 "now",
4988 "1.day.ago",
4989 "2.weeks.ago",
4990 "1979-01-01",
4991 "not-a-date",
4992 "@{1}",
4993 ] {
4994 let _ = reflog_date_selector_timestamp_at(s, 1_700_000_000);
4995 }
4996
4997 let graft_line = format!("{tip} {tip}\n");
4998 fs::write(repo.git_dir().join("info/grafts"), graft_line)
4999 .map_err(crate::error::Error::Io)?;
5000 let _ = load_graft_parents(&repo.git_dir());
5001
5002 let prefix = &tip.to_hex()[..7];
5003 let _ = ambiguous_object_hint_lines(&repo, prefix, None);
5004 let _ = resolve_revision_as_commit_without_index_dwim(&repo, "main");
5005 let _ = resolve_revision_as_commit_without_index_dwim(&repo, prefix);
5006 let _ = symbolic_full_name(&repo, "main");
5007 let _ = discover_optional(Some(dir.path()));
5008 Ok(())
5009 }
5010
5011 #[test]
5012 fn tree_blob_peel_and_dwim_coverage() -> crate::error::Result<()> {
5013 let dir = tempdir().map_err(crate::error::Error::Io)?;
5014 let repo = crate::repo::Repository::init(dir.path(), &crate::repo::InitOptions::default())?;
5015 let sub_tree = repo
5016 .odb()
5017 .write_loose_materialize(ObjectKind::Tree, b"100644 blob {blob}\tleaf.txt\n")?;
5018 let root_tree = repo.odb().write_loose_materialize(
5019 ObjectKind::Tree,
5020 format!("040000 tree {sub_tree}\tsub\n").as_bytes(),
5021 )?;
5022 let c1 = repo.odb().write_loose_materialize(
5023 ObjectKind::Commit,
5024 format!(
5025 "tree {root_tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\none\n"
5026 )
5027 .as_bytes(),
5028 )?;
5029 let c2 = repo.odb().write_loose_materialize(
5030 ObjectKind::Commit,
5031 format!(
5032 "tree {root_tree}\nparent {c1}\nauthor T <t@e.com> 200 +0000\ncommitter T <t@e.com> 200 +0000\n\ntwo\n"
5033 )
5034 .as_bytes(),
5035 )?;
5036 let tag_body_a = format!(
5037 "object {c2}\ntype commit\ntag ta\ntagger T <t@e.com> 300 +0000\n\nannotated\n"
5038 );
5039 let tag_a = repo
5040 .odb()
5041 .write_loose_materialize(ObjectKind::Tag, tag_body_a.as_bytes())?;
5042 let tag_body_b = format!(
5043 "object {c2}\ntype commit\ntag tb\ntagger T <t@e.com> 301 +0000\n\nannotated\n"
5044 );
5045 let tag_b = repo
5046 .odb()
5047 .write_loose_materialize(ObjectKind::Tag, tag_body_b.as_bytes())?;
5048 fs::write(repo.git_dir().join("refs/heads/main"), format!("{c2}\n"))
5049 .map_err(crate::error::Error::Io)?;
5050 fs::write(repo.git_dir().join("refs/tags/t"), format!("{tag_a}\n"))
5051 .map_err(crate::error::Error::Io)?;
5052 let abbrev = &c2.to_hex()[..7];
5053 fs::write(
5054 repo.git_dir().join(format!("refs/heads/{abbrev}")),
5055 format!("{c1}\n"),
5056 )
5057 .map_err(crate::error::Error::Io)?;
5058
5059 for spec in [
5060 "HEAD:sub",
5061 "HEAD:sub/leaf.txt",
5062 "HEAD:",
5063 "main:sub/leaf.txt",
5064 "t",
5065 "t^{commit}",
5066 "tags/t",
5067 abbrev,
5068 ] {
5069 let _ = resolve_treeish_blob_at_path(&repo, spec);
5070 let _ = resolve_revision_as_commit_without_index_dwim(&repo, spec);
5071 exercise_resolve_base(&repo, spec);
5072 exercise_impl_matrix(&repo, spec);
5073 }
5074 let _ = ambiguous_object_hint_lines(&repo, abbrev, Some("commit"));
5075 let _ = disambiguate_hex_by_peel(&repo, abbrev, &[c2, tag_a], "commit");
5076 let _ = disambiguate_hex_by_peel(&repo, "tags", &[tag_a, tag_b], "tag");
5077 Ok(())
5078 }
5079
5080 #[test]
5081 fn resolve_base_outside_paths_and_ambiguous_hex() -> crate::error::Result<()> {
5082 let dir = tempdir().map_err(crate::error::Error::Io)?;
5083 let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
5084 let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
5085 let mut oids = Vec::new();
5086 let prefix = seed_commits_until_ambiguous_prefix(&repo, tree, &mut oids, 4, 8_000);
5087 let tip = *oids.last().expect("tip");
5088 fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
5089 .map_err(crate::error::Error::Io)?;
5090 fs::write(
5091 repo.git_dir().join("config"),
5092 "[core]\n\tdisambiguate = commit\n",
5093 )
5094 .map_err(crate::error::Error::Io)?;
5095 let shared_prefix = Some(prefix);
5096
5097 const OUTSIDE: &[&str] = &[":0:/etc/passwd", ":2:/etc/passwd"];
5098 for spec in OUTSIDE {
5099 let err = resolve_base(&repo, spec, true, false, true, None, false, false, false)
5100 .expect_err(spec);
5101 assert!(
5102 matches!(
5103 err,
5104 Error::RevParse(RevParseError::OutsideRepository { .. })
5105 ),
5106 "{spec}: {err:?}"
5107 );
5108 for index_dwim in [false, true] {
5109 for commit_only in [false, true] {
5110 for use_disambiguate in [false, true] {
5111 let _ = resolve_base(
5112 &repo,
5113 spec,
5114 index_dwim,
5115 commit_only,
5116 use_disambiguate,
5117 None,
5118 false,
5119 false,
5120 false,
5121 );
5122 }
5123 }
5124 }
5125 }
5126
5127 if let Some(prefix) = shared_prefix {
5128 for commit_only in [false, true] {
5129 for use_disambiguate in [false, true] {
5130 let _ = resolve_base(
5131 &repo,
5132 &prefix,
5133 false,
5134 commit_only,
5135 use_disambiguate,
5136 Some("commit"),
5137 false,
5138 false,
5139 false,
5140 );
5141 let _ = resolve_revision_as_commit(&repo, &prefix);
5142 }
5143 }
5144 let mut pair: Option<(ObjectId, ObjectId)> = None;
5145 for i in 0..oids.len() {
5146 for j in (i + 1)..oids.len() {
5147 let a = oids[i].to_hex();
5148 let b = oids[j].to_hex();
5149 if a.starts_with(prefix.as_str()) && b.starts_with(prefix.as_str()) {
5150 pair = Some((oids[i], oids[j]));
5151 break;
5152 }
5153 }
5154 if pair.is_some() {
5155 break;
5156 }
5157 }
5158 if let Some((a, b)) = pair {
5159 let _ = disambiguate_hex_by_peel(&repo, &prefix, &[a, b], "commit");
5160 let _ = resolve_base(
5161 &repo,
5162 &prefix,
5163 false,
5164 true,
5165 true,
5166 Some("commit"),
5167 false,
5168 false,
5169 false,
5170 );
5171 }
5172 }
5173 Ok(())
5174 }
5175
5176 #[test]
5177 fn commit_message_search_abbrev_and_loose_matches() -> crate::error::Result<()> {
5178 let dir = tempdir().map_err(crate::error::Error::Io)?;
5179 let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
5180 let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
5181 let c1 = repo.odb().write_loose_materialize(
5182 ObjectKind::Commit,
5183 format!(
5184 "tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nneedle alpha\n"
5185 )
5186 .as_bytes(),
5187 )?;
5188 let c2 = repo.odb().write_loose_materialize(
5189 ObjectKind::Commit,
5190 format!(
5191 "tree {tree}\nparent {c1}\nauthor T <t@e.com> 200 +0000\ncommitter T <t@e.com> 200 +0000\n\nbeta only\n"
5192 )
5193 .as_bytes(),
5194 )?;
5195 fs::write(repo.git_dir().join("refs/heads/main"), format!("{c2}\n"))
5196 .map_err(crate::error::Error::Io)?;
5197 fs::write(repo.git_dir().join("refs/heads/side"), format!("{c1}\n"))
5198 .map_err(crate::error::Error::Io)?;
5199
5200 for spec in [
5201 ":/needle",
5202 ":/!beta",
5203 ":/!!needle",
5204 ":/n[e-e]dle",
5205 ":/missing",
5206 ] {
5207 let _ = resolve_revision_for_verify(&repo, spec);
5208 exercise_resolve_base(&repo, spec);
5209 }
5210
5211 let missing = ObjectId::from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").expect("oid");
5212 let _ = abbreviate_object_id(&repo, missing, 7);
5213 let prefix = &c2.to_hex()[..7];
5214 let _ = list_loose_abbrev_matches(&repo, prefix);
5215 let _ = list_all_abbrev_matches(&repo, prefix);
5216 let _ = list_all_abbrev_matches(&repo, "zzz");
5217 let _ = to_relative_path(&dir.path().join("sub/leaf.txt"), &dir.path().join("sub"));
5218 Ok(())
5219 }
5220
5221 #[test]
5222 fn parent_navigation_shares_only_equivalent_linkage() -> crate::error::Result<()> {
5223 let dir = tempdir()?;
5224 let repo = Repository::init(dir.path(), &InitOptions::default())?;
5225 let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
5226 let root = repo.odb().write_loose_materialize(
5227 ObjectKind::Commit,
5228 format!(
5229 "tree {tree}\nauthor Test <t@example.com> 1 +0000\n\
5230 committer Test <t@example.com> 1 +0000\n\nroot\n"
5231 )
5232 .as_bytes(),
5233 )?;
5234 let child = repo.odb().write_loose_materialize(
5235 ObjectKind::Commit,
5236 format!(
5237 "tree {tree}\nparent {root}\nauthor Test <t@example.com> 2 +0000\n\
5238 committer Test <t@example.com> 2 +0000\n\n{}\n",
5239 "long message".repeat(10_000)
5240 )
5241 .as_bytes(),
5242 )?;
5243 let blob = repo
5244 .odb()
5245 .write_loose_materialize(ObjectKind::Blob, b"not a commit")?;
5246 let malformed = repo
5247 .odb()
5248 .write_loose_materialize(ObjectKind::Commit, b"tree invalid\n")?;
5249 let spec = format!("{child}~1");
5250 assert_eq!(resolve_revision(&repo, &spec)?, root);
5251 assert!(
5252 repo.caches()
5253 .commit_linkage()
5254 .lock()
5255 .unwrap()
5256 .parents
5257 .is_empty(),
5258 "standalone resolution must not retain ancestry metadata"
5259 );
5260 assert_eq!(resolve_walk_range_end(&repo, &spec)?, root);
5261 let nav = NavParentCache::new(&repo, NavigationMetadata::RetainForWalk);
5262 assert_eq!(nav.parents_of(child)?, vec![root]);
5263 assert!(matches!(nav.parents_of(blob), Err(Error::InvalidRef(_))));
5264 assert_eq!(nav.parents_of(root)?, Vec::<ObjectId>::new());
5265 assert!(matches!(
5266 nav.parents_of(malformed),
5267 Err(Error::CorruptObject(_))
5268 ));
5269 assert_eq!(nav.parents_of(child)?, vec![root]);
5270 assert_eq!(
5271 apply_nav_step(&repo, &nav, child, NavStep::AncestorN(1))?,
5272 root
5273 );
5274 use crate::merge_base::intersect_walk_test_hooks::{
5275 commit_parent_lookups, reset_commit_parent_lookups,
5276 };
5277 use crate::merge_base::{
5278 count_commits_reachable_excluding_ancestors_of, ReachableCountFilter,
5279 };
5280 reset_commit_parent_lookups();
5281 assert_eq!(
5282 count_commits_reachable_excluding_ancestors_of(
5283 &repo,
5284 &[child],
5285 &[root],
5286 ReachableCountFilter::default()
5287 )?,
5288 1
5289 );
5290 assert_eq!(
5291 commit_parent_lookups(),
5292 0,
5293 "the range walk must reuse navigation metadata"
5294 );
5295
5296 fs::write(repo.git_dir().join("shallow"), format!("{child}\n"))?;
5299 let shallow_repo = Repository::open(&repo.git_dir(), &RepositoryOptions::empty())?;
5300 assert_eq!(
5301 NavParentCache::new(&shallow_repo, NavigationMetadata::RetainForWalk)
5302 .parents_of(child)?,
5303 vec![root]
5304 );
5305 assert_eq!(
5306 count_commits_reachable_excluding_ancestors_of(
5307 &shallow_repo,
5308 &[child],
5309 &[],
5310 ReachableCountFilter::default()
5311 )?,
5312 1
5313 );
5314 fs::remove_file(repo.git_dir().join("shallow"))?;
5315 fs::create_dir_all(repo.git_dir().join("refs/replace"))?;
5316 fs::write(
5317 repo.git_dir().join("refs/replace").join(child.to_hex()),
5318 format!("{root}\n"),
5319 )?;
5320 let replaced_repo = Repository::open(&repo.git_dir(), &RepositoryOptions::empty())?;
5321 assert_eq!(
5322 NavParentCache::new(&replaced_repo, NavigationMetadata::RetainForWalk)
5323 .parents_of(child)?,
5324 vec![root]
5325 );
5326 assert_eq!(
5327 count_commits_reachable_excluding_ancestors_of(
5328 &replaced_repo,
5329 &[child],
5330 &[],
5331 ReachableCountFilter::default()
5332 )?,
5333 1
5334 );
5335 Ok(())
5336 }
5337
5338 #[test]
5339 fn colon_message_search_oneline_bang_patterns() -> crate::error::Result<()> {
5340 let dir = tempdir().map_err(crate::error::Error::Io)?;
5341 Repository::init(
5342 dir.path(),
5343 &InitOptions {
5344 bare: true,
5345 ref_storage: RefStorageFormat::Files,
5346 ..InitOptions::default()
5347 },
5348 )?;
5349 let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
5350 let tree = repo
5351 .odb()
5352 .write_loose_materialize(ObjectKind::Tree, b"100644 blob deadbeef\tf\n")?;
5353 let with_feature = repo.odb().write_loose_materialize(
5354 ObjectKind::Commit,
5355 format!(
5356 "tree {tree}\nauthor T <t@e.com> 200 +0000\ncommitter T <t@e.com> 200 +0000\n\nfeature msg\n"
5357 )
5358 .as_bytes(),
5359 )?;
5360 let plain = repo.odb().write_loose_materialize(
5361 ObjectKind::Commit,
5362 format!(
5363 "tree {tree}\nparent {with_feature}\nauthor T <t@e.com> 300 +0000\ncommitter T <t@e.com> 300 +0000\n\nother\n"
5364 )
5365 .as_bytes(),
5366 )?;
5367 fs::write(repo.git_dir().join("refs/heads/main"), format!("{plain}\n"))
5368 .map_err(crate::error::Error::Io)?;
5369 assert_eq!(
5370 resolve_commit_message_search_from(&repo, plain, "!-feature")?,
5371 plain
5372 );
5373 assert!(resolve_commit_message_search_from(&repo, plain, "!foo").is_err());
5374 Ok(())
5375 }
5376
5377 #[test]
5378 fn internal_private_api_sweep() -> crate::error::Result<()> {
5379 use std::path::Path;
5380 assert!(parse_oneline_pattern("needle").is_some());
5381 assert!(parse_oneline_pattern("!-skip").is_some());
5382 assert!(parse_oneline_pattern("!!literal").is_some());
5383 assert!(parse_oneline_pattern("!bad").is_none());
5384 assert!(is_hex_prefix("abc1234"));
5385 assert!(!is_hex_prefix("ghij"));
5386 let _ = normalize_components(Path::new("/a/b/../c"));
5387 let _ = path_is_within(Path::new("/a/b"), Path::new("/a"));
5388 let dir = tempdir().map_err(crate::error::Error::Io)?;
5389 Repository::init(
5390 dir.path(),
5391 &InitOptions {
5392 bare: true,
5393
5394 ref_storage: RefStorageFormat::Files,
5395 ..InitOptions::default()
5396 },
5397 )?;
5398 let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
5399 let blob = repo
5400 .odb()
5401 .write_loose_materialize(ObjectKind::Blob, b"payload\n")?;
5402 let tree_body = format!("100644 blob {blob}\tfile.txt\n");
5403 let tree = repo
5404 .odb()
5405 .write_loose_materialize(ObjectKind::Tree, tree_body.as_bytes())?;
5406 let c1 = repo.odb().write_loose_materialize(
5407 ObjectKind::Commit,
5408 format!(
5409 "tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\none\n"
5410 )
5411 .as_bytes(),
5412 )?;
5413 let c2 = repo.odb().write_loose_materialize(
5414 ObjectKind::Commit,
5415 format!(
5416 "tree {tree}\nparent {c1}\nauthor T <t@e.com> 200 +0000\ncommitter T <t@e.com> 200 +0000\n\ntwo\n"
5417 )
5418 .as_bytes(),
5419 )?;
5420 let merge = repo.odb().write_loose_materialize(
5421 ObjectKind::Commit,
5422 format!(
5423 "tree {tree}\nparent {c2}\nparent {c1}\nauthor T <t@e.com> 300 +0000\ncommitter T <t@e.com> 300 +0000\n\nmerge\n"
5424 )
5425 .as_bytes(),
5426 )?;
5427 fs::write(repo.git_dir().join("refs/heads/main"), format!("{merge}\n"))
5428 .map_err(crate::error::Error::Io)?;
5429 let tag_data = format!(
5430 "object {c1}\ntype commit\ntag ann\ntagger T <t@e.com> 400 +0000\n\nannotated\n"
5431 );
5432 let tag_oid = repo
5433 .odb()
5434 .write_loose_materialize(ObjectKind::Tag, tag_data.as_bytes())?;
5435 fs::write(repo.git_dir().join("refs/tags/ann"), format!("{tag_oid}\n"))
5436 .map_err(crate::error::Error::Io)?;
5437 let _ = parse_tag_target(tag_data.as_bytes());
5438 let _ = peel_annotated_tag_chain(&repo, tag_oid);
5439 let _ = apply_peel(&repo, tag_oid, Some("commit"));
5440 let _ = apply_peel(&repo, c2, Some("tree"));
5441 let nav = NavParentCache::new(&repo, NavigationMetadata::RetainForWalk);
5442 let _ = apply_nav_step(&repo, &nav, merge, NavStep::ParentN(2));
5443 let _ = apply_nav_step(&repo, &nav, merge, NavStep::AncestorN(2));
5444 let _ = commit_reachable_closure(&repo, merge);
5445 let _ = resolve_commit_message_search_from(&repo, merge, "merge");
5446 let _ = resolve_commit_message_search_from(&repo, merge, "!-two");
5447 let _ = find_abbrev_matches(&repo, &c2.to_hex()[..4]);
5448 let _ = disambiguate_kind_rank(ObjectKind::Commit);
5449 let _ = oid_satisfies_peel_filter(&repo, c2, "commit");
5450 let _ = read_core_disambiguate(&repo);
5451 let _ = resolve_tree_path(&repo, &tree, "file.txt");
5452 let _ = resolve_tree_path_rev_parse(&repo, &tree, "file.txt");
5453 let _ = walk_tree_to_blob_entry(&repo, &tree, "file.txt");
5454 if let Some((a, b)) = split_treeish_spec("HEAD:file.txt") {
5455 assert_eq!(a, "HEAD");
5456 assert_eq!(b, "file.txt");
5457 }
5458 let _ = resolve_treeish_path_to_object(&repo, c2, "file.txt");
5459 let _ = resolve_ref_dwim_for_rev_parse(&repo, "main");
5460 let _ = resolve_ref_dwim_for_rev_parse(&repo, "ann");
5461 let _ = remote_tracking_head_symbolic_target(&repo, "origin/main");
5462 warn_if_branch_refname_collides_with_abbrev_hex(&repo, "main", c2);
5463 warn_if_hex_ref_collides_with_objects(&repo, &c2.to_hex()[..7], c2);
5464 let abbrev = &c2.to_hex()[..7];
5465 let matches = find_abbrev_matches(&repo, abbrev)?;
5466 let _ = disambiguate_hex_by_peel(&repo, abbrev, &matches, "commit");
5467 let _ = describe_generation_count(&repo, merge, c1);
5468 let _ = try_resolve_reflog_index(&repo, "main@{0}");
5469 let _ = approxidate_at("1.day.ago", 1_700_000_000);
5470 let _ = parse_reflog_entry_timestamp(&crate::reflog::ReflogEntry {
5471 old_oid: c1,
5472 new_oid: c2,
5473 identity: "T <t@e.com> 100 +0000".into(),
5474 message: "commit: msg".into(),
5475 });
5476 let _ = diagnose_tree_path_error(
5477 &repo,
5478 "HEAD",
5479 "missing",
5480 "missing",
5481 Error::ObjectNotFound("missing".into()).into(),
5482 );
5483 let _ =
5484 diagnose_index_path_error(&repo, "missing", 0, Error::ObjectNotFound("missing".into()));
5485 for spec in [
5486 "main",
5487 "ann",
5488 "ann^{commit}",
5489 "merge^2",
5490 "merge^1",
5491 "merge~1",
5492 "HEAD^!",
5493 "HEAD^@",
5494 ":/merge",
5495 ":/!-two",
5496 "tags/ann",
5497 ] {
5498 exercise_resolve_base(&repo, spec);
5499 }
5500 Ok(())
5501 }
5502
5503 #[test]
5504 fn worktree_fetch_head_index_and_colon_paths() -> crate::error::Result<()> {
5505 let dir = tempdir().map_err(crate::error::Error::Io)?;
5506 let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
5507 let blob = repo
5508 .odb()
5509 .write_loose_materialize(ObjectKind::Blob, b"indexed\n")?;
5510 fs::write(dir.path().join("tracked.txt"), b"indexed\n").map_err(crate::error::Error::Io)?;
5511 let entry = entry_from_stat(
5512 &dir.path().join("tracked.txt"),
5513 b"tracked.txt",
5514 blob,
5515 MODE_REGULAR,
5516 )?;
5517 let mut index = Index::new();
5518 index.entries_mut().push(entry);
5519 index.write_to_path(&repo.git_dir().join("index"), false)?;
5520 let tree = write_tree_from_index(&repo.odb(), &index, "")?;
5521 let tip = repo.odb().write_loose_materialize(
5522 ObjectKind::Commit,
5523 format!(
5524 "tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\ninit\n"
5525 )
5526 .as_bytes(),
5527 )?;
5528 fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
5529 .map_err(crate::error::Error::Io)?;
5530 fs::write(
5531 repo.git_dir().join("FETCH_HEAD"),
5532 format!("{tip}\tnot-for-merge\n{tip}\trefs/heads/main\n"),
5533 )
5534 .map_err(crate::error::Error::Io)?;
5535 for spec in [
5536 "FETCH_HEAD",
5537 ":tracked.txt",
5538 ":0:tracked.txt",
5539 "HEAD:tracked.txt",
5540 "HEAD:",
5541 "main:tracked.txt",
5542 "tracked.txt",
5543 "main",
5544 "HEAD",
5545 "@",
5546 "HEAD^{tree}",
5547 "HEAD^{commit}",
5548 &tip.to_hex(),
5549 &tip.to_hex()[..7],
5550 ] {
5551 exercise_resolve_base(&repo, spec);
5552 exercise_impl_matrix(&repo, spec);
5553 }
5554 let _ = head_tree_oid(&repo);
5555 let _ = path_in_tree(&repo, tree, "tracked.txt");
5556 let _ = path_in_index(&repo, "tracked.txt", 0);
5557 Ok(())
5558 }
5559
5560 #[test]
5561 fn discovery_graft_and_abbrev_sweep() -> crate::error::Result<()> {
5562 let dir = tempdir().map_err(crate::error::Error::Io)?;
5563 let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
5564 let cwd = dir.path();
5565 let _ = discover_optional(Some(cwd))?;
5566 let _ = is_inside_work_tree(&repo, cwd);
5567 let _ = is_inside_git_dir(&repo, cwd);
5568 let _ = show_prefix(&repo, cwd);
5569 let _ = symbolic_full_name(&repo, "HEAD");
5570 let _ = expand_at_minus_to_branch_name(&repo, "@{-1}").ok();
5571 let _ = resolve_at_minus_to_oid(&repo, "HEAD").ok();
5572 let _ = resolve_upstream_symbolic_name(&repo, "main@{upstream}").ok();
5573 let _ = resolve_push_full_ref_for_branch(&repo, "main").ok();
5574 let _ = load_graft_parents(&repo.git_dir());
5575 let blob = repo
5576 .odb()
5577 .write_loose_materialize(ObjectKind::Blob, b"x\n")?;
5578 let tree_body = format!("100644 f.txt\0").into_bytes();
5579 let mut tree_bytes = tree_body;
5580 tree_bytes.extend_from_slice(blob.as_bytes());
5581 let tree = repo
5582 .odb()
5583 .write_loose_materialize(ObjectKind::Tree, &tree_bytes)?;
5584 let tip = repo.odb().write_loose_materialize(
5585 ObjectKind::Commit,
5586 format!(
5587 "tree {tree}\nauthor T <t@e.com> 1 +0000\ncommitter T <t@e.com> 1 +0000\n\nm\n"
5588 )
5589 .as_bytes(),
5590 )?;
5591 fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
5592 .map_err(crate::error::Error::Io)?;
5593 let _ = list_all_abbrev_matches(&repo, &tip.to_hex()[..4])?;
5594 let _ = list_loose_abbrev_matches(&repo, &tip.to_hex()[..4])?;
5595 let _ = abbreviate_object_id(&repo, tip, 7)?;
5596 let _ = resolve_treeish_blob_at_path(&repo, "HEAD:f.txt")?;
5597 let _ = expand_rev_token_circ_bang(&repo, "main")?;
5598 let _ = ambiguous_object_hint_lines(&repo, &tip.to_hex()[..7], None)?;
5599 let _ = reflog_walk_refname(&repo, "HEAD@{0}").ok();
5600 let _ = resolve_reflog_walk_log_ref(&repo, "HEAD").ok();
5601 let _ = resolve_index_path_entry(&repo, ":f.txt").ok();
5602 let _ = to_relative_path(&dir.path().join("f.txt"), dir.path());
5603 Ok(())
5604 }
5605
5606 #[test]
5607 fn describe_prefilter_accepts_everything_the_pattern_matches() {
5608 let re = Regex::new(r"(?i)^(.+)-(\d+)-g([0-9a-fA-F]+)$").unwrap();
5609 for spec in [
5610 "v1-1-gabc1234",
5611 "V1-2-GABC",
5612 "a-b-c-10-g0",
5613 "v2.47.0-rc1-3-gdeadbeef",
5614 "x-1-gab-2-gcd",
5615 "tag-٣-gabc",
5616 "HEAD",
5617 "main",
5618 "v1-gabc",
5619 "-1-gabc",
5620 "v1-1-g",
5621 "v1-1-gxyz",
5622 "v1-1-gabc-",
5623 "feature/x-1-gabc\nfoo",
5624 "a--gabc",
5625 ] {
5626 if re.is_match(spec) {
5627 assert!(may_be_describe_name(spec), "{spec:?}");
5628 }
5629 }
5630 assert!(!may_be_describe_name("HEAD"));
5631 assert!(!may_be_describe_name("refs/heads/main"));
5632 assert!(!may_be_describe_name("v1-gabc"));
5633 assert!(!may_be_describe_name("v1-1-gxyz"));
5634 }
5635
5636 #[test]
5637 fn describe_name_and_dwim_helpers() -> crate::error::Result<()> {
5638 let dir = tempdir().map_err(crate::error::Error::Io)?;
5639 Repository::init(
5640 dir.path(),
5641 &InitOptions {
5642 bare: true,
5643
5644 ref_storage: crate::RefStorageFormat::Files,
5645 ..InitOptions::default()
5646 },
5647 )?;
5648 let repo = crate::repo::Repository::open(dir.path(), &RepositoryOptions::empty())?;
5649 let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
5650 let c1 = repo.odb().write_loose_materialize(
5651 ObjectKind::Commit,
5652 format!(
5653 "tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\none\n"
5654 )
5655 .as_bytes(),
5656 )?;
5657 let c2 = repo.odb().write_loose_materialize(
5658 ObjectKind::Commit,
5659 format!(
5660 "tree {tree}\nparent {c1}\nauthor T <t@e.com> 200 +0000\ncommitter T <t@e.com> 200 +0000\n\ntwo\n"
5661 )
5662 .as_bytes(),
5663 )?;
5664 fs::write(repo.git_dir().join("refs/heads/main"), format!("{c2}\n"))
5665 .map_err(crate::error::Error::Io)?;
5666 fs::write(repo.git_dir().join("refs/tags/v1"), format!("{c1}\n"))
5667 .map_err(crate::error::Error::Io)?;
5668 let abbrev = &c2.to_hex()[..7];
5669 let describe_name = format!("v1-1-g{abbrev}");
5670 assert_eq!(try_resolve_describe_name(&repo, &describe_name)?, Some(c2));
5671 assert_eq!(
5672 try_resolve_describe_name(&repo, &format!("V1-1-G{}", abbrev.to_uppercase()))?,
5673 Some(c2)
5674 );
5675 assert_eq!(try_resolve_describe_name(&repo, "main")?, None);
5676 assert_eq!(try_resolve_describe_name(&repo, "HEAD")?, None);
5677 let _ = dwim_refname(&repo, "main");
5678 let _ = reflog_display_name("refs/heads/main", "main");
5679 Ok(())
5680 }
5681
5682 fn conflict_index_entry(path: &str, stage: u8, oid: ObjectId, mode: u32) -> IndexEntry {
5683 let path_bytes = path.as_bytes().to_vec();
5684 IndexEntry {
5685 ctime_sec: 0,
5686 ctime_nsec: 0,
5687 mtime_sec: 0,
5688 mtime_nsec: 0,
5689 dev: 0,
5690 ino: 0,
5691 mode,
5692 uid: 0,
5693 gid: 0,
5694 size: 0,
5695 oid,
5696 flags: (path_bytes.len().min(0xFFF) as u16) | ((stage as u16) << 12),
5697 flags_extended: None,
5698 path: path_bytes,
5699 base_index_pos: 0,
5700 }
5701 }
5702
5703 #[test]
5704 fn unmerged_index_stages_resolve_and_diagnose() -> crate::error::Result<()> {
5705 let dir = tempdir().map_err(crate::error::Error::Io)?;
5706 let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
5707 let base = repo
5708 .odb()
5709 .write_loose_materialize(ObjectKind::Blob, b"base\n")?;
5710 let ours = repo
5711 .odb()
5712 .write_loose_materialize(ObjectKind::Blob, b"ours\n")?;
5713 let theirs = repo
5714 .odb()
5715 .write_loose_materialize(ObjectKind::Blob, b"theirs\n")?;
5716 let mut index = Index::new();
5717 index
5718 .entries_mut()
5719 .push(conflict_index_entry("conflict.txt", 1, base, MODE_REGULAR));
5720 index
5721 .entries_mut()
5722 .push(conflict_index_entry("conflict.txt", 2, ours, MODE_REGULAR));
5723 index.entries_mut().push(conflict_index_entry(
5724 "conflict.txt",
5725 3,
5726 theirs,
5727 MODE_REGULAR,
5728 ));
5729 let nested_blob = repo
5730 .odb()
5731 .write_loose_materialize(ObjectKind::Blob, b"nested\n")?;
5732 fs::create_dir_all(dir.path().join("sub")).map_err(crate::error::Error::Io)?;
5733 fs::write(dir.path().join("sub/leaf.txt"), b"nested\n").map_err(crate::error::Error::Io)?;
5734 let nested_entry = entry_from_stat(
5735 &dir.path().join("sub/leaf.txt"),
5736 b"sub/leaf.txt",
5737 nested_blob,
5738 MODE_REGULAR,
5739 )?;
5740 index.entries_mut().push(nested_entry);
5741 index.write_to_path(&repo.git_dir().join("index"), false)?;
5742
5743 assert_eq!(resolve_revision_for_verify(&repo, ":1:conflict.txt")?, base);
5744 assert_eq!(resolve_revision_for_verify(&repo, ":2:conflict.txt")?, ours);
5745 assert_eq!(
5746 resolve_revision_for_verify(&repo, ":3:conflict.txt")?,
5747 theirs
5748 );
5749 assert!(resolve_revision_for_verify(&repo, ":0:conflict.txt").is_err());
5750
5751 let _ = diagnose_index_path_error(
5752 &repo,
5753 "conflict.txt",
5754 1,
5755 Error::ObjectNotFound("conflict.txt".into()),
5756 );
5757 let _ =
5758 diagnose_index_path_error(&repo, "no-such", 2, Error::ObjectNotFound("no-such".into()));
5759 fs::write(dir.path().join("disk-only.txt"), b"x\n").map_err(crate::error::Error::Io)?;
5760 let _ = diagnose_index_path_error(
5761 &repo,
5762 "disk-only.txt",
5763 0,
5764 Error::ObjectNotFound("disk-only.txt".into()),
5765 );
5766
5767 let _ = resolve_revision_for_verify(&repo, ":0:sub/leaf.txt");
5768 let _ = resolve_index_path_entry(&repo, ":sub/leaf.txt");
5769 let _ = diagnose_tree_path_error(
5770 &repo,
5771 "HEAD",
5772 "./sub/leaf.txt",
5773 "sub/leaf.txt",
5774 TreePathError::NotInTree,
5775 );
5776 Ok(())
5777 }
5778
5779 #[test]
5780 fn outside_repo_ambiguous_hex_and_shorthand_matrix() -> crate::error::Result<()> {
5781 let dir = tempdir().map_err(crate::error::Error::Io)?;
5782 let repo = init_repository(dir.path(), false, "main", None, RefStorageFormat::Files)?;
5783 let tree = repo.odb().write_loose_materialize(ObjectKind::Tree, b"")?;
5784 let base = repo.odb().write_loose_materialize(
5785 ObjectKind::Commit,
5786 format!(
5787 "tree {tree}\nauthor T <t@e.com> 100 +0000\ncommitter T <t@e.com> 100 +0000\n\nbase\n"
5788 )
5789 .as_bytes(),
5790 )?;
5791 let side = repo.odb().write_loose_materialize(
5792 ObjectKind::Commit,
5793 format!(
5794 "tree {tree}\nparent {base}\nauthor T <t@e.com> 200 +0000\ncommitter T <t@e.com> 200 +0000\n\nside\n"
5795 )
5796 .as_bytes(),
5797 )?;
5798 let merge = repo.odb().write_loose_materialize(
5799 ObjectKind::Commit,
5800 format!(
5801 "tree {tree}\nparent {base}\nparent {side}\nauthor T <t@e.com> 300 +0000\ncommitter T <t@e.com> 300 +0000\n\nmerge\n"
5802 )
5803 .as_bytes(),
5804 )?;
5805 fs::write(repo.git_dir().join("refs/heads/main"), format!("{merge}\n"))
5806 .map_err(crate::error::Error::Io)?;
5807
5808 let outside = [
5809 "HEAD:../../../../etc/passwd",
5810 ":0:../../../../etc/passwd",
5811 ":2:../../../../etc/passwd",
5812 "HEAD:",
5813 ];
5814 for spec in outside {
5815 let _ = resolve_treeish_blob_at_path(&repo, spec);
5816 let _ = resolve_revision_for_verify(&repo, spec);
5817 let _ = resolve_index_path_entry(&repo, spec);
5818 }
5819
5820 let bang = expand_rev_token_circ_bang(&repo, "main^!")?;
5821 assert!(bang.len() >= 2);
5822 let _ = expand_parent_shorthand_rev_parse_lines(&repo, "main^-2", false, None);
5823 let _ = expand_parent_shorthand_rev_parse_lines(&repo, "main^-99", false, None);
5824 let _ = resolve_reflog_walk_log_ref(&repo, "main");
5825 let _ = resolve_reflog_walk_log_ref(&repo, "missing-branch");
5826 let _ = resolve_reflog_walk_log_ref(&repo, "@{-1}");
5827
5828 fs::write(
5829 repo.git_dir().join("config"),
5830 "[core]\n\tdisambiguate = commit\n",
5831 )
5832 .map_err(crate::error::Error::Io)?;
5833 let mut oids = Vec::new();
5834 let prefix = seed_commits_until_ambiguous_prefix(&repo, tree, &mut oids, 4, 8_000);
5835 let _ = resolve_revision(&repo, &prefix);
5836 let _ = resolve_revision_as_commit(&repo, &prefix);
5837 let _ = resolve_revision_for_verify(&repo, &prefix);
5838 Ok(())
5839 }
5840
5841 #[test]
5842 #[should_panic(expected = "could not find")]
5843 fn seed_commits_until_ambiguous_prefix_exhaustion() {
5844 let dir = tempdir().expect("tempdir");
5845 crate::repo::Repository::init(
5846 dir.path(),
5847 &InitOptions {
5848 bare: true,
5849
5850 ref_storage: RefStorageFormat::Files,
5851 ..InitOptions::default()
5852 },
5853 )
5854 .expect("init");
5855 let repo = crate::repo::Repository::open(
5856 dir.path(),
5857 &crate::environment::RepositoryOptions::empty(),
5858 )
5859 .expect("open");
5860 let tree = repo
5861 .odb()
5862 .write_loose_materialize(ObjectKind::Tree, b"")
5863 .expect("tree");
5864 let mut oids = Vec::new();
5865 let _ = seed_commits_until_ambiguous_prefix(&repo, tree, &mut oids, 40, 0);
5866 }
5867}
5868
5869#[cfg(test)]
5870mod superproject_path_tests {
5871 use super::superproject_work_tree_from_nested_git_modules;
5872 use std::path::PathBuf;
5873
5874 #[test]
5875 fn nested_modules_yields_superproject_work_tree() {
5876 let git_dir = PathBuf::from("/tmp/super/.git/modules/dir/modules/sub");
5877 assert_eq!(
5878 superproject_work_tree_from_nested_git_modules(&git_dir),
5879 Some(PathBuf::from("/tmp/super"))
5880 );
5881 }
5882
5883 #[test]
5884 fn non_nested_returns_none() {
5885 let git_dir = PathBuf::from("/tmp/repo/.git");
5886 assert!(superproject_work_tree_from_nested_git_modules(&git_dir).is_none());
5887 }
5888}