1use std::borrow::Cow;
9use std::ffi::OsStr;
10use std::fs;
11use std::path::{Component, Path, PathBuf};
12
13use regex::Regex;
14
15use std::collections::{HashMap, HashSet};
16
17use crate::config::ConfigSet;
18use crate::error::{Error, Result};
19use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectId, ObjectKind};
20use crate::pack;
21use crate::reflog::read_reflog;
22use crate::refs;
23use crate::repo::Repository;
24
25pub fn discover_optional(start: Option<&Path>) -> Result<Option<Repository>> {
36 match Repository::discover(start) {
37 Ok(repo) => Ok(Some(repo)),
38 Err(Error::NotARepository(msg)) => {
39 if msg.contains("invalid gitfile format")
43 || msg.contains("gitfile does not contain 'gitdir:' line")
44 || msg.contains("not a regular file")
45 {
46 return Err(Error::NotARepository(msg));
47 }
48
49 if let Some(start) = start {
50 let start = if start.is_absolute() {
51 start.to_path_buf()
52 } else if let Ok(cwd) = std::env::current_dir() {
53 cwd.join(start)
54 } else {
55 start.to_path_buf()
56 };
57 let dot_git = start.join(".git");
58 if dot_git.is_file() || dot_git.is_symlink() {
59 return Err(Error::NotARepository(msg));
60 }
61 }
62
63 Ok(None)
64 }
65 Err(err) => Err(err),
66 }
67}
68
69#[must_use]
71pub fn is_inside_work_tree(repo: &Repository, cwd: &Path) -> bool {
72 let Some(work_tree) = &repo.work_tree else {
73 return false;
74 };
75 path_is_within(cwd, work_tree)
76}
77
78#[must_use]
80pub fn is_inside_git_dir(repo: &Repository, cwd: &Path) -> bool {
81 path_is_within(cwd, &repo.git_dir)
82}
83
84#[must_use]
89pub fn show_prefix(repo: &Repository, cwd: &Path) -> String {
90 let Some(work_tree) = &repo.work_tree else {
91 return String::new();
92 };
93 if !path_is_within(cwd, work_tree) {
94 return String::new();
95 }
96 if cwd == work_tree {
97 return String::new();
98 }
99 let Ok(rel) = cwd.strip_prefix(work_tree) else {
100 return String::new();
101 };
102 let mut out = rel
103 .components()
104 .filter_map(component_to_text)
105 .collect::<Vec<_>>()
106 .join("/");
107 if !out.is_empty() {
108 out.push('/');
109 }
110 out
111}
112
113#[must_use]
119pub fn superproject_work_tree_from_nested_git_modules(git_dir: &Path) -> Option<PathBuf> {
120 let mut p = git_dir.to_path_buf();
121 while let Some(parent) = p.parent() {
122 if p.file_name().is_some_and(|n| n == "modules")
123 && parent.file_name().is_some_and(|n| n == ".git")
124 {
125 return parent.parent().map(PathBuf::from);
126 }
127 if parent == p {
128 break;
129 }
130 p = parent.to_path_buf();
131 }
132 None
133}
134
135#[must_use]
142pub fn symbolic_full_name(repo: &Repository, spec: &str) -> Option<String> {
143 if upstream_suffix_info(spec).is_some() {
145 return resolve_upstream_symbolic_name(repo, spec).ok();
146 }
147
148 if let Ok(Some(branch)) = expand_at_minus_to_branch_name(repo, spec) {
149 let ref_name = format!("refs/heads/{branch}");
150 if refs::resolve_ref(&repo.git_dir, &ref_name).is_ok() {
151 return Some(ref_name);
152 }
153 return None;
154 }
155
156 if spec == "HEAD" {
157 if let Ok(Some(target)) = refs::read_symbolic_ref(&repo.git_dir, "HEAD") {
158 return Some(target);
159 }
160 return None;
161 }
162 if spec.starts_with("refs/") {
164 if refs::resolve_ref(&repo.git_dir, spec).is_ok() {
165 return Some(spec.to_owned());
166 }
167 return None;
168 }
169 for prefix in &["refs/heads/", "refs/tags/", "refs/remotes/"] {
171 let candidate = format!("{prefix}{spec}");
172 if refs::resolve_ref(&repo.git_dir, &candidate).is_ok() {
173 return Some(candidate);
174 }
175 }
176 if let Some(full) = remote_tracking_head_symbolic_target(repo, spec) {
178 return Some(full);
179 }
180 None
181}
182
183fn remote_tracking_head_symbolic_target(repo: &Repository, name: &str) -> Option<String> {
185 if name.contains('/')
186 || matches!(
187 name,
188 "HEAD" | "FETCH_HEAD" | "MERGE_HEAD" | "CHERRY_PICK_HEAD" | "REVERT_HEAD"
189 )
190 {
191 return None;
192 }
193 let config = ConfigSet::load(Some(&repo.git_dir), true).ok()?;
194 let url_key = format!("remote.{name}.url");
195 config.get(&url_key)?;
196 let head_ref = format!("refs/remotes/{name}/HEAD");
197 let target = refs::read_symbolic_ref(&repo.git_dir, &head_ref).ok()??;
198 Some(target)
199}
200
201pub fn expand_at_minus_to_branch_name(repo: &Repository, spec: &str) -> Result<Option<String>> {
209 if !spec.starts_with("@{-") || !spec.ends_with('}') {
210 return Ok(None);
211 }
212 let inner = &spec[3..spec.len() - 1];
213 let n: usize = inner
214 .parse()
215 .map_err(|_| Error::InvalidRef(format!("invalid N in @{{-N}} for '{spec}'")))?;
216 if n < 1 {
217 return Ok(None);
218 }
219 resolve_at_minus_to_branch(repo, n).map(Some)
220}
221
222pub fn resolve_at_minus_to_oid(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
224 try_resolve_at_minus(repo, spec)
225}
226
227#[must_use]
231pub fn abbreviate_ref_name(full_name: &str) -> String {
232 for prefix in &["refs/heads/", "refs/tags/", "refs/remotes/"] {
233 if let Some(short) = full_name.strip_prefix(prefix) {
234 return short.to_owned();
235 }
236 }
237 if let Some(short) = full_name.strip_prefix("refs/") {
238 return short.to_owned();
239 }
240 full_name.to_owned()
241}
242
243#[must_use]
246pub fn upstream_suffix_info(spec: &str) -> Option<(&str, bool)> {
247 let lower = spec.to_ascii_lowercase();
248 if lower.ends_with("@{push}") {
249 let base = &spec[..spec.len() - 7];
250 return Some((base, true));
251 }
252 if lower.ends_with("@{upstream}") {
253 let base = &spec[..spec.len() - 11];
254 return Some((base, false));
255 }
256 if lower.ends_with("@{u}") {
257 let base = &spec[..spec.len() - 4];
258 return Some((base, false));
259 }
260 None
261}
262
263pub fn resolve_upstream_symbolic_name(repo: &Repository, spec: &str) -> Result<String> {
265 let Some((base, is_push)) = upstream_suffix_info(spec) else {
266 return Err(Error::InvalidRef(format!("not an upstream spec: {spec}")));
267 };
268 resolve_upstream_full_ref_name(repo, base, is_push)
269}
270
271fn resolve_upstream_full_ref_name(repo: &Repository, base: &str, is_push: bool) -> Result<String> {
272 if is_push {
273 return resolve_push_ref_name(repo, base);
274 }
275 let (branch_key, display_branch) = resolve_upstream_branch_context(repo, base)?;
276 let config_path = repo.git_dir.join("config");
277 let config_content = fs::read_to_string(&config_path).map_err(Error::Io)?;
278 let Some((remote, merge)) = parse_branch_tracking(&config_content, &branch_key) else {
279 return Err(Error::Message(format!(
280 "fatal: no upstream configured for branch '{display_branch}'"
281 )));
282 };
283 if remote == "." {
284 let m = merge.trim();
285 if m.starts_with("refs/") {
286 return Ok(m.to_owned());
287 }
288 return Ok(format!("refs/heads/{m}"));
289 }
290 let merge_branch = merge
291 .strip_prefix("refs/heads/")
292 .ok_or_else(|| Error::InvalidRef(format!("invalid merge ref: {merge}")))?;
293 let tracking = format!("refs/remotes/{remote}/{merge_branch}");
294 if refs::resolve_ref(&repo.git_dir, &tracking).is_err() {
295 return Err(Error::Message(format!(
296 "fatal: upstream branch '{merge}' not stored as a remote-tracking branch"
297 )));
298 }
299 Ok(tracking)
300}
301
302pub fn resolve_push_full_ref_for_branch(repo: &Repository, branch_short: &str) -> Result<String> {
307 let config_path = crate::refs::common_dir(&repo.git_dir)
308 .unwrap_or_else(|| repo.git_dir.clone())
309 .join("config");
310 let config_content = fs::read_to_string(&config_path).map_err(Error::Io)?;
311
312 let upstream_tracking =
313 parse_branch_tracking(&config_content, branch_short).and_then(|(remote, merge)| {
314 if remote == "." {
315 return None;
316 }
317 let mb = merge.strip_prefix("refs/heads/").unwrap_or(&merge);
318 let tr = format!("refs/remotes/{remote}/{mb}");
319 if refs::resolve_ref(&repo.git_dir, &tr).is_ok() {
320 Some(tr)
321 } else {
322 None
323 }
324 });
325
326 let push_remote = parse_config_value(&config_content, "remote", "pushRemote")
327 .or_else(|| parse_config_value(&config_content, "remote", "pushDefault"))
328 .or_else(|| {
329 let section = format!("[branch \"{}\"]", branch_short);
330 let mut in_section = false;
331 for line in config_content.lines() {
332 let trimmed = line.trim();
333 if trimmed.starts_with('[') {
334 in_section = trimmed == section;
335 continue;
336 }
337 if in_section {
338 if let Some(v) = trimmed
339 .strip_prefix("pushremote = ")
340 .or_else(|| trimmed.strip_prefix("pushRemote = "))
341 {
342 return Some(v.trim().to_owned());
343 }
344 }
345 }
346 None
347 })
348 .or_else(|| {
349 parse_branch_tracking(&config_content, branch_short)
350 .map(|(r, _)| r)
351 .filter(|r| r != ".")
352 });
353
354 let Some(push_remote_name) = push_remote else {
355 return upstream_tracking.ok_or_else(|| {
356 Error::Message("fatal: branch has no configured push remote".to_owned())
357 });
358 };
359
360 let push_default = parse_config_value(&config_content, "push", "default");
361 let push_default = push_default.as_deref().unwrap_or("simple");
362
363 if push_default == "nothing" {
364 return Err(Error::Message(
365 "fatal: push.default is nothing; no push destination".to_owned(),
366 ));
367 }
368
369 if let Some(mapped) =
370 push_refspec_mapped_tracking(&config_content, &push_remote_name, branch_short)
371 {
372 if refs::resolve_ref(&repo.git_dir, &mapped).is_ok() {
373 return Ok(mapped);
374 }
375 }
376
377 let current_tracking = format!("refs/remotes/{push_remote_name}/{branch_short}");
378
379 match push_default {
380 "upstream" => upstream_tracking.ok_or_else(|| {
381 Error::Message(format!(
382 "fatal: branch '{branch_short}' has no upstream for push.default upstream"
383 ))
384 }),
385 "simple" => {
386 if let Some(ref up) = upstream_tracking {
387 if up == ¤t_tracking
388 && refs::resolve_ref(&repo.git_dir, ¤t_tracking).is_ok()
389 {
390 return Ok(current_tracking);
391 }
392 }
393 Err(Error::Message(
394 "fatal: push.default simple: upstream and push ref differ".to_owned(),
395 ))
396 }
397 "current" | "matching" | _ => {
398 if refs::resolve_ref(&repo.git_dir, ¤t_tracking).is_ok() {
399 Ok(current_tracking)
400 } else if let Some(up) = upstream_tracking {
401 Ok(up)
402 } else {
403 Err(Error::Message(format!(
404 "fatal: no push tracking ref for branch '{branch_short}'"
405 )))
406 }
407 }
408 }
409}
410
411fn push_refspec_mapped_tracking(
412 config_content: &str,
413 remote_name: &str,
414 branch_short: &str,
415) -> Option<String> {
416 let section = format!("[remote \"{remote_name}\"]");
417 let mut in_section = false;
418 let src_want = format!("refs/heads/{branch_short}");
419 for line in config_content.lines() {
420 let trimmed = line.trim();
421 if trimmed.starts_with('[') {
422 in_section = trimmed == section;
423 continue;
424 }
425 if !in_section {
426 continue;
427 }
428 let Some(val) = trimmed
429 .strip_prefix("push = ")
430 .or_else(|| trimmed.strip_prefix("push="))
431 else {
432 continue;
433 };
434 let Some(spec) = val.split_whitespace().next() else {
435 continue;
436 };
437 let spec = spec.trim().strip_prefix('+').unwrap_or(spec);
438 let Some((left, right)) = spec.split_once(':') else {
439 continue;
440 };
441 let left = left.trim();
442 let right = right.trim();
443 if left != src_want {
444 continue;
445 }
446 let Some(dest_branch) = right.strip_prefix("refs/heads/") else {
447 continue;
448 };
449 return Some(format!("refs/remotes/{remote_name}/{dest_branch}"));
450 }
451 None
452}
453
454fn resolve_push_ref_name(repo: &Repository, base: &str) -> Result<String> {
455 let (branch_key, _display) = resolve_upstream_branch_context(repo, base)?;
456 resolve_push_full_ref_for_branch(repo, &branch_key)
457}
458
459fn resolve_upstream_branch_context(repo: &Repository, base: &str) -> Result<(String, String)> {
461 let base = if base == "HEAD" {
462 Cow::Borrowed("")
463 } else if base.starts_with("@{-") && base.ends_with('}') {
464 if let Ok(Some(b)) = expand_at_minus_to_branch_name(repo, base) {
465 Cow::Owned(b)
466 } else {
467 Cow::Borrowed(base)
468 }
469 } else {
470 Cow::Borrowed(base)
471 };
472 let base = base.as_ref();
473 let base = if base == "@" { "" } else { base };
474
475 if base.is_empty() {
476 let Some(head) = refs::read_head(&repo.git_dir)? else {
477 return Err(Error::Message(
478 "fatal: HEAD does not point to a branch".to_owned(),
479 ));
480 };
481 let Some(short) = head.strip_prefix("refs/heads/") else {
482 return Err(Error::Message(
483 "fatal: HEAD does not point to a branch".to_owned(),
484 ));
485 };
486 return Ok((short.to_owned(), short.to_owned()));
487 }
488 let head_branch = refs::read_head(&repo.git_dir)?.and_then(|h| {
489 h.strip_prefix("refs/heads/")
490 .map(std::borrow::ToOwned::to_owned)
491 });
492 if head_branch.as_deref() == Some(base) {
493 return Ok((base.to_owned(), base.to_owned()));
494 }
495 let refname = format!("refs/heads/{base}");
496 if refs::resolve_ref(&repo.git_dir, &refname).is_err() {
497 return Err(Error::Message(format!("fatal: no such branch: '{base}'")));
498 }
499 Ok((base.to_owned(), base.to_owned()))
500}
501
502fn parse_config_value(config: &str, section: &str, key: &str) -> Option<String> {
503 let section_header = format!("[{}]", section);
504 let key_lower = key.to_ascii_lowercase();
505 let mut in_section = false;
506 for line in config.lines() {
507 let trimmed = line.trim();
508 if trimmed.starts_with('[') {
509 in_section = trimmed.eq_ignore_ascii_case(§ion_header);
510 continue;
511 }
512 if in_section {
513 let lower = trimmed.to_ascii_lowercase();
514 if lower.starts_with(&key_lower) {
515 let rest = lower[key_lower.len()..].trim_start().to_string();
516 if rest.starts_with('=') {
517 if let Some(eq_pos) = trimmed.find('=') {
518 return Some(trimmed[eq_pos + 1..].trim().to_owned());
519 }
520 }
521 }
522 }
523 }
524 None
525}
526
527fn parse_branch_tracking(config: &str, branch: &str) -> Option<(String, String)> {
529 let mut remote = None;
530 let mut merge = None;
531 let mut in_section = false;
532 let target_section = format!("[branch \"{}\"]", branch);
533
534 for line in config.lines() {
535 let trimmed = line.trim();
536 if trimmed.starts_with('[') {
537 in_section = trimmed == target_section
538 || trimmed.starts_with(&format!("[branch \"{}\"", branch));
539 continue;
540 }
541 if !in_section {
542 continue;
543 }
544 if let Some(value) = trimmed.strip_prefix("remote = ") {
545 remote = Some(value.trim().to_owned());
546 } else if let Some(value) = trimmed.strip_prefix("merge = ") {
547 merge = Some(value.trim().to_owned());
548 }
549 if let Some(value) = trimmed.strip_prefix("remote=") {
551 remote = Some(value.trim().to_owned());
552 } else if let Some(value) = trimmed.strip_prefix("merge=") {
553 merge = Some(value.trim().to_owned());
554 }
555 }
556
557 match (remote, merge) {
558 (Some(r), Some(m)) => Some((r, m)),
559 _ => None,
560 }
561}
562
563#[must_use]
580pub(crate) fn load_graft_parents(git_dir: &Path) -> HashMap<ObjectId, Vec<ObjectId>> {
584 let graft_path = git_dir.join("info/grafts");
585 let mut grafts = HashMap::new();
586 let Ok(contents) = fs::read_to_string(&graft_path) else {
587 return grafts;
588 };
589 for raw_line in contents.lines() {
590 let line = raw_line.trim();
591 if line.is_empty() || line.starts_with('#') {
592 continue;
593 }
594 let mut fields = line.split_whitespace();
595 let Some(commit_hex) = fields.next() else {
596 continue;
597 };
598 let Ok(commit_oid) = commit_hex.parse::<ObjectId>() else {
599 continue;
600 };
601 let mut parents = Vec::new();
602 let mut valid = true;
603 for parent_hex in fields {
604 match parent_hex.parse::<ObjectId>() {
605 Ok(parent_oid) => parents.push(parent_oid),
606 Err(_) => {
607 valid = false;
608 break;
609 }
610 }
611 }
612 if valid {
613 grafts.insert(commit_oid, parents);
614 }
615 }
616 grafts
617}
618
619pub fn commit_parents_for_navigation(
621 repo: &Repository,
622 commit_oid: ObjectId,
623) -> Result<Vec<ObjectId>> {
624 let obj = repo.odb.read(&commit_oid)?;
625 if obj.kind != ObjectKind::Commit {
626 return Err(Error::InvalidRef(format!(
627 "invalid ref: {commit_oid} is not a commit"
628 )));
629 }
630 let commit = parse_commit(&obj.data)?;
631 let mut parents = commit.parents;
632 let grafts = load_graft_parents(&repo.git_dir);
633 if let Some(grafted) = grafts.get(&commit_oid) {
634 parents = grafted.clone();
635 }
636 Ok(parents)
637}
638
639#[derive(Debug, Clone, Copy)]
640enum ParentShorthandKind {
641 At,
643 Bang,
645 Minus { exclude_parent: usize },
647}
648
649#[must_use]
651pub fn spec_has_parent_shorthand_suffix(spec: &str) -> bool {
652 find_parent_shorthand(spec).is_some()
653}
654
655fn find_parent_shorthand(spec: &str) -> Option<(usize, ParentShorthandKind)> {
656 let mut best: Option<(usize, ParentShorthandKind, u8)> = None;
657 for (idx, _) in spec.match_indices('^') {
658 let Some(tail) = spec.get(idx + 1..) else {
659 continue;
660 };
661 if tail.starts_with('@') && idx + 2 == spec.len() {
662 best = Some((idx, ParentShorthandKind::At, 0));
663 break;
664 }
665 if tail.starts_with('!') && idx + 2 == spec.len() {
666 let cand = (idx, ParentShorthandKind::Bang, 1);
667 best = Some(match best {
668 Some(b) if b.2 < 1 => b,
669 _ => cand,
670 });
671 continue;
672 }
673 if let Some(after) = tail.strip_prefix('-') {
674 let (exclude_parent, valid) = if after.is_empty() {
675 (1usize, true)
676 } else if after.bytes().all(|b| b.is_ascii_digit()) && !after.is_empty() {
677 let n: usize = after.parse().unwrap_or(0);
678 (n, n >= 1)
679 } else {
680 (0, false)
681 };
682 if !valid {
683 continue;
684 }
685 let cand = (idx, ParentShorthandKind::Minus { exclude_parent }, 2);
686 best = Some(match best {
687 Some(b) if b.2 < 2 => b,
688 _ => cand,
689 });
690 }
691 }
692 best.map(|(i, k, _)| (i, k))
693}
694
695pub fn expand_parent_shorthand_rev_parse_lines(
703 repo: &Repository,
704 spec: &str,
705 symbolic: bool,
706 short_len: Option<usize>,
707) -> Result<Option<Vec<String>>> {
708 let Some((mark_idx, kind)) = find_parent_shorthand(spec) else {
709 return Ok(None);
710 };
711 let base_spec = &spec[..mark_idx];
712 let base_for_resolve = if base_spec.is_empty() {
713 "HEAD"
714 } else {
715 base_spec
716 };
717 let symbolic_base = if base_spec.is_empty() {
720 "HEAD"
721 } else {
722 base_spec
723 };
724 let tip_oid = resolve_revision_for_range_end(repo, base_for_resolve)?;
725 let commit_oid = peel_to_commit_for_merge_base(repo, tip_oid)?;
726 let parents = commit_parents_for_navigation(repo, commit_oid)?;
727
728 let mut out = Vec::new();
729 match kind {
730 ParentShorthandKind::At => {
731 if parents.is_empty() {
732 return Ok(Some(out));
733 }
734 for (i, p) in parents.iter().enumerate() {
735 let parent_n = i + 1;
736 if symbolic {
737 out.push(format!("{symbolic_base}^{parent_n}"));
738 } else if let Some(len) = short_len {
739 out.push(abbreviate_object_id(repo, *p, len)?);
740 } else {
741 out.push(p.to_string());
742 }
743 }
744 }
745 ParentShorthandKind::Bang => {
746 if parents.is_empty() {
747 if symbolic {
748 out.push(symbolic_base.to_string());
749 } else if let Some(len) = short_len {
750 out.push(abbreviate_object_id(repo, commit_oid, len)?);
751 } else {
752 out.push(commit_oid.to_string());
753 }
754 return Ok(Some(out));
755 }
756 if symbolic {
757 out.push(symbolic_base.to_string());
758 for (i, _) in parents.iter().enumerate() {
759 let parent_n = i + 1;
760 out.push(format!("^{symbolic_base}^{parent_n}"));
761 }
762 } else if let Some(len) = short_len {
763 out.push(abbreviate_object_id(repo, commit_oid, len)?);
764 for p in &parents {
765 out.push(format!("^{}", abbreviate_object_id(repo, *p, len)?));
766 }
767 } else {
768 out.push(commit_oid.to_string());
769 for p in &parents {
770 out.push(format!("^{p}"));
771 }
772 }
773 }
774 ParentShorthandKind::Minus { exclude_parent } => {
775 if exclude_parent > parents.len() {
776 return Ok(None);
777 }
778 let excluded_parent = parents[exclude_parent - 1];
779 if symbolic {
780 out.push(symbolic_base.to_string());
781 out.push(format!("^{symbolic_base}^{exclude_parent}"));
782 } else if let Some(len) = short_len {
783 out.push(abbreviate_object_id(repo, commit_oid, len)?);
784 out.push(format!(
785 "^{}",
786 abbreviate_object_id(repo, excluded_parent, len)?
787 ));
788 } else {
789 out.push(commit_oid.to_string());
790 out.push(format!("^{excluded_parent}"));
791 }
792 }
793 }
794 Ok(Some(out))
795}
796
797pub fn split_double_dot_range(spec: &str) -> Option<(&str, &str)> {
798 if spec == ".." {
799 return Some(("", ""));
800 }
801 let bytes = spec.as_bytes();
802 let mut search = 0usize;
803 while let Some(rel) = spec[search..].find("..") {
804 let idx = search + rel;
805 let touches_dot_before = idx > 0 && bytes[idx - 1] == b'.';
807 let touches_dot_after = idx + 2 < bytes.len() && bytes[idx + 2] == b'.';
808 if touches_dot_before || touches_dot_after {
809 search = idx + 1;
810 continue;
811 }
812 if idx + 2 < bytes.len() && (bytes[idx + 2] == b'/' || bytes[idx + 2] == b'\\') {
814 search = idx + 1;
815 continue;
816 }
817 let left = &spec[..idx];
818 let right = &spec[idx + 2..];
819 return Some((left, right));
820 }
821 None
822}
823
824#[must_use]
828pub fn split_triple_dot_range(spec: &str) -> Option<(&str, &str)> {
829 if spec == "..." {
830 return Some(("", ""));
831 }
832 let bytes = spec.as_bytes();
833 let mut search = 0usize;
834 while let Some(rel) = spec[search..].find("...") {
835 let idx = search + rel;
836 let four_before = idx >= 1 && bytes[idx - 1] == b'.';
837 let four_after = idx + 3 < bytes.len() && bytes[idx + 3] == b'.';
838 if four_before || four_after {
839 search = idx + 1;
840 continue;
841 }
842 let left = &spec[..idx];
843 let right = &spec[idx + 3..];
844 return Some((left, right));
845 }
846 None
847}
848
849pub fn resolve_revision_without_index_dwim(repo: &Repository, spec: &str) -> Result<ObjectId> {
852 resolve_revision_impl(repo, spec, false, false, true, false, false, false, true)
853}
854
855pub fn resolve_revision(repo: &Repository, spec: &str) -> Result<ObjectId> {
857 resolve_revision_impl(repo, spec, true, false, true, false, false, false, true)
858}
859
860pub fn resolve_revision_for_checkout_guess(
863 repo: &Repository,
864 spec: &str,
865 remote_branch_guess: bool,
866) -> Result<ObjectId> {
867 resolve_revision_impl(
868 repo,
869 spec,
870 true,
871 false,
872 true,
873 false,
874 false,
875 false,
876 remote_branch_guess,
877 )
878}
879
880pub fn resolve_revision_for_range_end(repo: &Repository, spec: &str) -> Result<ObjectId> {
883 resolve_revision_impl(repo, spec, true, true, true, false, false, false, true)
884}
885
886pub fn resolve_revision_for_range_end_without_index_dwim(
893 repo: &Repository,
894 spec: &str,
895) -> Result<ObjectId> {
896 resolve_revision_impl(repo, spec, false, true, true, false, false, false, true)
897}
898
899pub fn resolve_revision_for_verify(repo: &Repository, spec: &str) -> Result<ObjectId> {
904 resolve_revision_impl(repo, spec, false, true, true, false, false, false, true)
905}
906
907pub fn resolve_revision_for_commit_tree_tree(repo: &Repository, spec: &str) -> Result<ObjectId> {
909 resolve_revision_impl(repo, spec, true, false, true, false, true, false, true)
910}
911
912pub fn resolve_revision_for_patch_old_blob(repo: &Repository, spec: &str) -> Result<ObjectId> {
914 resolve_revision_impl(repo, spec, true, false, true, false, false, true, true)
915}
916
917pub fn try_parse_double_dot_log_range(
927 repo: &Repository,
928 spec: &str,
929) -> Result<Option<(ObjectId, ObjectId)>> {
930 let Some((left, right)) = split_double_dot_range(spec) else {
931 return Ok(None);
932 };
933 let left_tip = if left.is_empty() {
934 resolve_revision_for_range_end(repo, "HEAD")?
935 } else {
936 resolve_revision_for_range_end(repo, left)?
937 };
938 let right_tip = if right.is_empty() {
939 resolve_revision_for_range_end(repo, "HEAD")?
940 } else {
941 resolve_revision_for_range_end(repo, right)?
942 };
943 let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
944 let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
945 Ok(Some((left_c, right_c)))
946}
947
948fn try_parse_double_dot_log_range_without_index_dwim(
949 repo: &Repository,
950 spec: &str,
951) -> Result<Option<(ObjectId, ObjectId)>> {
952 let Some((left, right)) = split_double_dot_range(spec) else {
953 return Ok(None);
954 };
955 let left_tip = if left.is_empty() {
956 resolve_revision_for_range_end_without_index_dwim(repo, "HEAD")?
957 } else {
958 resolve_revision_for_range_end_without_index_dwim(repo, left)?
959 };
960 let right_tip = if right.is_empty() {
961 resolve_revision_for_range_end_without_index_dwim(repo, "HEAD")?
962 } else {
963 resolve_revision_for_range_end_without_index_dwim(repo, right)?
964 };
965 let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
966 let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
967 Ok(Some((left_c, right_c)))
968}
969
970#[must_use]
980pub fn revision_spec_contains_ancestry_navigation(spec: &str) -> bool {
981 let (_, steps) = parse_nav_steps(spec);
982 !steps.is_empty()
983}
984
985pub fn resolve_revision_as_commit(repo: &Repository, spec: &str) -> Result<ObjectId> {
986 if let Some((left, right)) = split_triple_dot_range(spec) {
987 let left_tip = if left.is_empty() {
988 resolve_revision_for_range_end(repo, "HEAD")?
989 } else {
990 resolve_revision_for_range_end(repo, left)?
991 };
992 let right_tip = if right.is_empty() {
993 resolve_revision_for_range_end(repo, "HEAD")?
994 } else {
995 resolve_revision_for_range_end(repo, right)?
996 };
997 let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
998 let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
999 let bases = crate::merge_base::merge_bases_first_vs_rest(repo, left_c, &[right_c])?;
1000 return bases
1001 .into_iter()
1002 .next()
1003 .ok_or_else(|| Error::ObjectNotFound(format!("no merge base for '{spec}'")));
1004 }
1005 if let Some((_excl, tip)) = try_parse_double_dot_log_range(repo, spec)? {
1006 return Ok(tip);
1007 }
1008 let oid = resolve_revision_for_range_end(repo, spec)?;
1009 peel_to_commit_for_merge_base(repo, oid)
1010}
1011
1012pub fn resolve_revision_as_commit_without_index_dwim(
1017 repo: &Repository,
1018 spec: &str,
1019) -> Result<ObjectId> {
1020 if let Some((left, right)) = split_triple_dot_range(spec) {
1021 let left_tip = if left.is_empty() {
1022 resolve_revision_for_range_end_without_index_dwim(repo, "HEAD")?
1023 } else {
1024 resolve_revision_for_range_end_without_index_dwim(repo, left)?
1025 };
1026 let right_tip = if right.is_empty() {
1027 resolve_revision_for_range_end_without_index_dwim(repo, "HEAD")?
1028 } else {
1029 resolve_revision_for_range_end_without_index_dwim(repo, right)?
1030 };
1031 let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
1032 let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
1033 let bases = crate::merge_base::merge_bases_first_vs_rest(repo, left_c, &[right_c])?;
1034 return bases
1035 .into_iter()
1036 .next()
1037 .ok_or_else(|| Error::ObjectNotFound(format!("no merge base for '{spec}'")));
1038 }
1039 if let Some((_excl, tip)) = try_parse_double_dot_log_range_without_index_dwim(repo, spec)? {
1040 return Ok(tip);
1041 }
1042 let oid = resolve_revision_for_range_end_without_index_dwim(repo, spec)?;
1043 peel_to_commit_for_merge_base(repo, oid)
1044}
1045
1046fn resolve_revision_impl(
1047 repo: &Repository,
1048 spec: &str,
1049 index_dwim: bool,
1050 commit_only_hex: bool,
1051 use_disambiguate_config: bool,
1052 treeish_colon_lhs: bool,
1053 implicit_tree_abbrev: bool,
1054 implicit_blob_abbrev: bool,
1055 remote_branch_name_guess: bool,
1056) -> Result<ObjectId> {
1057 if let Some(pattern) = spec.strip_prefix(":/") {
1060 if !pattern.is_empty() {
1061 return resolve_commit_message_search(repo, pattern);
1062 }
1063 }
1064
1065 if let Some(tag_path) = spec.strip_prefix("tags/") {
1067 if !tag_path.is_empty() {
1068 let tag_ref = format!("refs/tags/{tag_path}");
1069 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &tag_ref) {
1070 return Ok(oid);
1071 }
1072 }
1073 }
1074
1075 if spec == "AUTO_MERGE" {
1077 let raw = fs::read_to_string(repo.git_dir.join("AUTO_MERGE"))
1078 .map_err(|e| Error::Message(format!("failed to read AUTO_MERGE: {e}")))?;
1079 let line = raw.lines().next().unwrap_or("").trim();
1080 return line
1081 .parse::<ObjectId>()
1082 .map_err(|_| Error::InvalidRef("AUTO_MERGE: invalid object id".to_owned()));
1083 }
1084
1085 if spec.starts_with("refs/") && !spec.contains(':') {
1089 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, spec) {
1090 return Ok(oid);
1091 }
1092 }
1093
1094 if let Some(idx) = spec.find("...") {
1097 let left_raw = &spec[..idx];
1098 let right_raw = &spec[idx + 3..];
1099 if !left_raw.is_empty() || !right_raw.is_empty() {
1100 let left_oid = peel_to_commit_for_merge_base(
1101 repo,
1102 if left_raw.is_empty() {
1103 resolve_revision_impl(
1104 repo,
1105 "HEAD",
1106 index_dwim,
1107 commit_only_hex,
1108 use_disambiguate_config,
1109 false,
1110 false,
1111 false,
1112 remote_branch_name_guess,
1113 )?
1114 } else {
1115 resolve_revision_impl(
1116 repo,
1117 left_raw,
1118 index_dwim,
1119 commit_only_hex,
1120 use_disambiguate_config,
1121 false,
1122 false,
1123 false,
1124 remote_branch_name_guess,
1125 )?
1126 },
1127 )?;
1128 let right_oid = peel_to_commit_for_merge_base(
1129 repo,
1130 if right_raw.is_empty() {
1131 resolve_revision_impl(
1132 repo,
1133 "HEAD",
1134 index_dwim,
1135 commit_only_hex,
1136 use_disambiguate_config,
1137 false,
1138 false,
1139 false,
1140 remote_branch_name_guess,
1141 )?
1142 } else {
1143 resolve_revision_impl(
1144 repo,
1145 right_raw,
1146 index_dwim,
1147 commit_only_hex,
1148 use_disambiguate_config,
1149 false,
1150 false,
1151 false,
1152 remote_branch_name_guess,
1153 )?
1154 },
1155 )?;
1156 let bases = crate::merge_base::merge_bases_first_vs_rest(repo, left_oid, &[right_oid])?;
1157 return bases
1158 .into_iter()
1159 .next()
1160 .ok_or_else(|| Error::ObjectNotFound(format!("no merge base for '{spec}'")));
1161 }
1162 }
1163
1164 if let Some((before, after)) = split_treeish_colon(spec) {
1168 if !before.is_empty() && !spec.starts_with(":/") {
1169 let rev_oid = match resolve_revision_impl(
1171 repo,
1172 before,
1173 index_dwim,
1174 commit_only_hex,
1175 use_disambiguate_config,
1176 true,
1177 false,
1178 false,
1179 remote_branch_name_guess,
1180 ) {
1181 Ok(o) => o,
1182 Err(Error::ObjectNotFound(s)) if s == before => {
1183 return Err(Error::Message(format!(
1184 "fatal: invalid object name '{before}'."
1185 )));
1186 }
1187 Err(Error::Message(msg)) if msg.contains("ambiguous argument") => {
1188 return Err(Error::Message(format!(
1189 "fatal: invalid object name '{before}'."
1190 )));
1191 }
1192 Err(e) => return Err(e),
1193 };
1194 let tree_oid = peel_to_tree(repo, rev_oid)?;
1195 if after.is_empty() {
1196 return Ok(tree_oid);
1198 }
1199 let clean_path = match normalize_colon_path_for_tree(repo, after) {
1200 Ok(p) => p,
1201 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
1202 let wt = repo
1203 .work_tree
1204 .as_ref()
1205 .and_then(|p| p.canonicalize().ok())
1206 .map(|p| p.display().to_string())
1207 .unwrap_or_default();
1208 return Err(Error::Message(format!(
1209 "fatal: '{after}' is outside repository at '{wt}'"
1210 )));
1211 }
1212 Err(e) => return Err(e),
1213 };
1214 return resolve_tree_path_rev_parse(repo, &tree_oid, &clean_path)
1215 .map_err(|e| diagnose_tree_path_error(repo, before, after, &clean_path, e));
1216 }
1217 }
1218
1219 let (base_with_nav, peel) = parse_peel_suffix(spec);
1220 let (base, nav_steps) = parse_nav_steps(base_with_nav);
1221 let peel_for_hex = peel
1222 .or(((treeish_colon_lhs || implicit_tree_abbrev) && peel.is_none()).then_some("tree"))
1223 .or((implicit_blob_abbrev && peel.is_none()).then_some("blob"));
1224 let mut oid = resolve_base(
1225 repo,
1226 base,
1227 index_dwim,
1228 commit_only_hex,
1229 use_disambiguate_config,
1230 peel_for_hex,
1231 implicit_tree_abbrev,
1232 implicit_blob_abbrev,
1233 remote_branch_name_guess,
1234 )?;
1235 for step in nav_steps {
1236 oid = apply_nav_step(repo, oid, step).map_err(|e| {
1237 if matches!(e, Error::ObjectNotFound(_)) {
1238 Error::Message(format!(
1239 "fatal: ambiguous argument '{spec}': unknown revision or path not in the working tree.\n\
1240Use '--' to separate paths from revisions, like this:\n\
1241'git <command> [<revision>...] -- [<file>...]'"
1242 ))
1243 } else {
1244 e
1245 }
1246 })?;
1247 }
1248 apply_peel(repo, oid, peel)
1249}
1250
1251fn normalize_path_components(path: PathBuf) -> PathBuf {
1254 let mut out = PathBuf::new();
1255 for c in path.components() {
1256 match c {
1257 Component::Prefix(_) | Component::RootDir => out.push(c),
1258 Component::CurDir => {}
1259 Component::ParentDir => {
1260 let _ = out.pop();
1261 }
1262 Component::Normal(x) => out.push(x),
1263 }
1264 }
1265 out
1266}
1267
1268fn normalize_colon_path_for_bare_tree(raw_path: &str) -> Result<String> {
1273 let cwd_relative = raw_path.starts_with("./") || raw_path.starts_with("../") || raw_path == ".";
1274 if cwd_relative {
1275 return Err(Error::InvalidRef(
1276 "relative path syntax can't be used outside working tree".to_owned(),
1277 ));
1278 }
1279 let s = raw_path.trim_start_matches('/');
1280 let mut stack: Vec<&str> = Vec::new();
1281 for part in s.split('/') {
1282 if part.is_empty() || part == "." {
1283 continue;
1284 }
1285 if part == ".." {
1286 let _ = stack.pop();
1287 } else {
1288 stack.push(part);
1289 }
1290 }
1291 Ok(stack.join("/"))
1292}
1293
1294fn normalize_colon_path_for_tree(repo: &Repository, raw_path: &str) -> Result<String> {
1295 let Some(work_tree) = repo.work_tree.as_ref() else {
1296 return normalize_colon_path_for_bare_tree(raw_path);
1297 };
1298
1299 let cwd = std::env::current_dir().map_err(Error::Io)?;
1300 let wt_canon = work_tree.canonicalize().map_err(Error::Io)?;
1301
1302 let cwd_relative = raw_path.starts_with("./") || raw_path.starts_with("../") || raw_path == ".";
1303 if cwd_relative && !path_is_within(&cwd, work_tree) {
1304 return Err(Error::InvalidRef(
1305 "relative path syntax can't be used outside working tree".to_owned(),
1306 ));
1307 }
1308
1309 let full = if raw_path.starts_with('/') {
1311 PathBuf::from(raw_path)
1312 } else if cwd_relative {
1313 cwd.join(raw_path)
1314 } else {
1315 work_tree.join(raw_path)
1316 };
1317 let full = normalize_path_components(full);
1318
1319 if !path_is_within(&full, &wt_canon) {
1320 return Err(Error::InvalidRef("outside repository".to_owned()));
1321 }
1322 let rel = full
1323 .strip_prefix(&wt_canon)
1324 .map_err(|_| Error::InvalidRef("outside repository".to_owned()))?;
1325 let s = rel.to_string_lossy().replace('\\', "/");
1326 Ok(s.trim_end_matches('/').to_owned())
1327}
1328
1329pub fn peel_to_commit_for_merge_base(repo: &Repository, mut oid: ObjectId) -> Result<ObjectId> {
1331 oid = apply_peel(repo, oid, Some(""))?;
1332 let obj = repo.read_replaced(&oid)?;
1333 match obj.kind {
1334 ObjectKind::Commit => Ok(oid),
1335 ObjectKind::Tree => Err(Error::InvalidRef(format!(
1336 "object {oid} does not name a commit"
1337 ))),
1338 ObjectKind::Blob => Err(Error::InvalidRef(format!(
1339 "object {oid} does not name a commit"
1340 ))),
1341 ObjectKind::Tag => Err(Error::InvalidRef("unexpected tag after peel".to_owned())),
1342 }
1343}
1344
1345pub fn try_peel_to_commit_for_merge_base(
1348 repo: &Repository,
1349 oid: ObjectId,
1350) -> Result<Option<ObjectId>> {
1351 let oid = apply_peel(repo, oid, Some(""))?;
1352 let obj = repo.odb.read(&oid)?;
1353 match obj.kind {
1354 ObjectKind::Commit => Ok(Some(oid)),
1355 ObjectKind::Tree | ObjectKind::Blob => Ok(None),
1356 ObjectKind::Tag => Err(Error::InvalidRef("unexpected tag after peel".to_owned())),
1357 }
1358}
1359
1360pub fn peel_to_tree(repo: &Repository, oid: ObjectId) -> Result<ObjectId> {
1366 let obj = repo.read_replaced(&oid)?;
1367 match obj.kind {
1368 crate::objects::ObjectKind::Tree => Ok(oid),
1369 crate::objects::ObjectKind::Commit => {
1370 let commit = crate::objects::parse_commit(&obj.data)?;
1371 Ok(commit.tree)
1372 }
1373 crate::objects::ObjectKind::Tag => {
1374 let tag = crate::objects::parse_tag(&obj.data)?;
1375 peel_to_tree(repo, tag.object)
1376 }
1377 _ => Err(Error::ObjectNotFound(format!(
1378 "cannot peel {} to tree",
1379 oid
1380 ))),
1381 }
1382}
1383
1384fn resolve_tree_path(repo: &Repository, tree_oid: &ObjectId, path: &str) -> Result<ObjectId> {
1390 resolve_treeish_path_to_object(repo, *tree_oid, path)
1391}
1392
1393fn resolve_tree_path_rev_parse(
1395 repo: &Repository,
1396 tree_oid: &ObjectId,
1397 path: &str,
1398) -> Result<ObjectId> {
1399 let obj = repo.odb.read(tree_oid)?;
1400 let entries = crate::objects::parse_tree(&obj.data)?;
1401 let components: Vec<&str> = path.split('/').filter(|c| !c.is_empty()).collect();
1402 if components.is_empty() {
1403 return Err(Error::InvalidRef(format!(
1404 "path '{path}' does not name an object in tree {tree_oid}"
1405 )));
1406 }
1407
1408 let first = components[0];
1409 let rest: Vec<&str> = components[1..].to_vec();
1410 for entry in entries {
1411 let name = String::from_utf8_lossy(&entry.name);
1412 if name == first {
1413 if rest.is_empty() {
1414 if entry.mode == crate::index::MODE_GITLINK {
1415 return Err(Error::InvalidRef(format!(
1416 "'{path}' is a gitlink, not a blob"
1417 )));
1418 }
1419 return Ok(entry.oid);
1420 }
1421 if entry.mode != crate::index::MODE_TREE {
1422 return Err(Error::ObjectNotFound(path.to_owned()));
1423 }
1424 return resolve_tree_path_rev_parse(repo, &entry.oid, &rest.join("/"));
1425 }
1426 }
1427 Err(Error::ObjectNotFound(format!(
1428 "path '{path}' not found in tree {tree_oid}"
1429 )))
1430}
1431
1432#[derive(Debug, Clone)]
1436pub struct TreeishBlobAtPath {
1437 pub path: String,
1439 pub oid: ObjectId,
1441 pub mode: String,
1443}
1444
1445pub fn resolve_treeish_blob_at_path(repo: &Repository, spec: &str) -> Result<TreeishBlobAtPath> {
1450 let (before, after) = split_treeish_colon(spec)
1451 .filter(|(_, path)| !path.is_empty())
1452 .ok_or_else(|| Error::InvalidRef(format!("'{spec}' is not a treeish:path revision")))?;
1453
1454 let rev_oid =
1455 match resolve_revision_impl(repo, before, true, false, true, true, false, false, true) {
1456 Ok(o) => o,
1457 Err(Error::ObjectNotFound(s)) if s == before => {
1458 return Err(Error::Message(format!(
1459 "fatal: invalid object name '{before}'."
1460 )));
1461 }
1462 Err(Error::Message(msg)) if msg.contains("ambiguous argument") => {
1463 return Err(Error::Message(format!(
1464 "fatal: invalid object name '{before}'."
1465 )));
1466 }
1467 Err(e) => return Err(e),
1468 };
1469
1470 let tree_oid = peel_to_tree(repo, rev_oid)?;
1471 let clean_path = match normalize_colon_path_for_tree(repo, after) {
1472 Ok(p) => p,
1473 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
1474 let wt = repo
1475 .work_tree
1476 .as_ref()
1477 .and_then(|p| p.canonicalize().ok())
1478 .map(|p| p.display().to_string())
1479 .unwrap_or_default();
1480 return Err(Error::Message(format!(
1481 "fatal: '{after}' is outside repository at '{wt}'"
1482 )));
1483 }
1484 Err(e) => return Err(e),
1485 };
1486
1487 let (oid, mode_str) = walk_tree_to_blob_entry(repo, &tree_oid, &clean_path)
1488 .map_err(|e| diagnose_tree_path_error(repo, before, after, &clean_path, e))?;
1489 if mode_str == "160000" {
1490 return Err(Error::InvalidRef(format!(
1491 "'{clean_path}' is a gitlink, not a blob"
1492 )));
1493 }
1494 Ok(TreeishBlobAtPath {
1495 path: clean_path,
1496 oid,
1497 mode: mode_str,
1498 })
1499}
1500
1501fn walk_tree_to_blob_entry(
1505 repo: &Repository,
1506 tree_oid: &ObjectId,
1507 path: &str,
1508) -> Result<(ObjectId, String)> {
1509 let obj = repo.read_replaced(tree_oid)?;
1510 let entries = crate::objects::parse_tree(&obj.data)?;
1511 let components: Vec<&str> = path.split('/').filter(|c| !c.is_empty()).collect();
1512 if components.is_empty() {
1513 return Err(Error::InvalidRef(format!(
1514 "path '{path}' does not name a blob in tree {tree_oid}"
1515 )));
1516 }
1517
1518 let first = components[0];
1519 let rest: Vec<&str> = components[1..].to_vec();
1520 for entry in entries {
1521 let name = String::from_utf8_lossy(&entry.name);
1522 if name == first {
1523 if rest.is_empty() {
1524 if entry.mode == crate::index::MODE_TREE {
1525 return Err(Error::InvalidRef(format!("'{path}' is a tree, not a blob")));
1526 }
1527 if entry.mode == crate::index::MODE_GITLINK {
1528 return Err(Error::InvalidRef(format!(
1529 "'{path}' is a gitlink, not a blob"
1530 )));
1531 }
1532 return Ok((entry.oid, entry.mode_str()));
1533 }
1534 if entry.mode != crate::index::MODE_TREE {
1535 return Err(Error::ObjectNotFound(path.to_owned()));
1536 }
1537 return walk_tree_to_blob_entry(repo, &entry.oid, &rest.join("/"));
1538 }
1539 }
1540 Err(Error::ObjectNotFound(format!(
1541 "path '{path}' not found in tree {tree_oid}"
1542 )))
1543}
1544
1545#[derive(Debug, Clone, Copy)]
1547enum NavStep {
1548 ParentN(usize),
1550 AncestorN(usize),
1552}
1553
1554fn parse_nav_steps(spec: &str) -> (&str, Vec<NavStep>) {
1558 let mut steps = Vec::new();
1559 let mut remaining = spec;
1560
1561 loop {
1562 if let Some(tilde_pos) = remaining.rfind('~') {
1564 let after = &remaining[tilde_pos + 1..];
1565 if after.is_empty() {
1566 steps.push(NavStep::AncestorN(1));
1568 remaining = &remaining[..tilde_pos];
1569 continue;
1570 }
1571 if after.bytes().all(|b| b.is_ascii_digit()) {
1572 let n: usize = after.parse().unwrap_or(1);
1573 steps.push(NavStep::AncestorN(n));
1574 remaining = &remaining[..tilde_pos];
1575 continue;
1576 }
1577 }
1578
1579 if let Some(caret_pos) = remaining.rfind('^') {
1581 let after = &remaining[caret_pos + 1..];
1582 if after.is_empty() {
1583 steps.push(NavStep::ParentN(1));
1585 remaining = &remaining[..caret_pos];
1586 continue;
1587 }
1588 if after.bytes().all(|b| b.is_ascii_digit()) && !after.is_empty() {
1589 let n: usize = after.parse().unwrap_or(usize::MAX);
1590 steps.push(NavStep::ParentN(n));
1591 remaining = &remaining[..caret_pos];
1592 continue;
1593 }
1594 }
1595
1596 break;
1597 }
1598
1599 steps.reverse();
1600 (remaining, steps)
1601}
1602
1603fn peel_annotated_tag_chain(repo: &Repository, mut oid: ObjectId) -> Result<ObjectId> {
1605 loop {
1606 let obj = repo.read_replaced(&oid)?;
1607 if obj.kind != ObjectKind::Tag {
1608 return Ok(oid);
1609 }
1610 let tag = parse_tag(&obj.data)?;
1611 oid = tag.object;
1612 }
1613}
1614
1615fn apply_nav_step(repo: &Repository, oid: ObjectId, step: NavStep) -> Result<ObjectId> {
1617 match step {
1618 NavStep::ParentN(0) => Ok(oid),
1619 NavStep::ParentN(n) => {
1620 let oid = peel_annotated_tag_chain(repo, oid)?;
1621 let parents = commit_parents_for_navigation(repo, oid)?;
1622 parents
1623 .get(n - 1)
1624 .copied()
1625 .ok_or_else(|| Error::ObjectNotFound(format!("{oid}^{n}")))
1626 }
1627 NavStep::AncestorN(n) => {
1628 let mut current = peel_annotated_tag_chain(repo, oid)?;
1629 for _ in 0..n {
1630 current = apply_nav_step(repo, current, NavStep::ParentN(1))?;
1631 }
1632 Ok(current)
1633 }
1634 }
1635}
1636
1637pub fn abbreviate_object_id(repo: &Repository, oid: ObjectId, min_len: usize) -> Result<String> {
1646 let min_len = min_len.clamp(4, 40);
1647 let target = oid.to_hex();
1648
1649 if !repo.odb.exists(&oid) {
1651 return Ok(target[..min_len].to_owned());
1652 }
1653
1654 let all = collect_loose_object_ids(repo)?;
1655
1656 for len in min_len..=40 {
1657 let prefix = &target[..len];
1658 let matches = all
1659 .iter()
1660 .filter(|candidate| candidate.starts_with(prefix))
1661 .count();
1662 if matches <= 1 {
1663 return Ok(prefix.to_owned());
1664 }
1665 }
1666
1667 Ok(target)
1668}
1669
1670#[must_use]
1672pub fn to_relative_path(path: &Path, cwd: &Path) -> String {
1673 let path_components = normalize_components(path);
1674 let cwd_components = normalize_components(cwd);
1675
1676 let mut common = 0usize;
1677 let max_common = path_components.len().min(cwd_components.len());
1678 while common < max_common && path_components[common] == cwd_components[common] {
1679 common += 1;
1680 }
1681
1682 let mut parts = Vec::new();
1683 let up_count = cwd_components.len().saturating_sub(common);
1684 for _ in 0..up_count {
1685 parts.push("..".to_owned());
1686 }
1687 for item in path_components.iter().skip(common) {
1688 parts.push(item.clone());
1689 }
1690
1691 if parts.is_empty() {
1692 ".".to_owned()
1693 } else {
1694 parts.join("/")
1695 }
1696}
1697
1698fn object_storage_dirs_for_abbrev(repo: &Repository) -> Result<Vec<PathBuf>> {
1699 let mut dirs = Vec::new();
1700 let primary = repo.odb.objects_dir().to_path_buf();
1701 dirs.push(primary.clone());
1702 if let Ok(alts) = pack::read_alternates_recursive(&primary) {
1703 for alt in alts {
1704 if !dirs.iter().any(|d| d == &alt) {
1705 dirs.push(alt);
1706 }
1707 }
1708 }
1709 Ok(dirs)
1710}
1711
1712fn collect_pack_oids_with_prefix(objects_dir: &Path, prefix: &str) -> Result<Vec<ObjectId>> {
1713 let mut out = Vec::new();
1714 for idx in pack::read_local_pack_indexes_cached(objects_dir)? {
1715 for e in &idx.entries {
1716 if e.oid.len() != 20 {
1717 continue;
1718 }
1719 let hex = pack::oid_bytes_to_hex(&e.oid);
1720 if hex.starts_with(prefix) {
1721 if let Ok(oid) = crate::objects::ObjectId::from_bytes(&e.oid) {
1722 out.push(oid);
1723 }
1724 }
1725 }
1726 }
1727 Ok(out)
1728}
1729
1730fn disambiguate_kind_rank(kind: ObjectKind) -> u8 {
1731 match kind {
1732 ObjectKind::Tag => 0,
1733 ObjectKind::Commit => 1,
1734 ObjectKind::Tree => 2,
1735 ObjectKind::Blob => 3,
1736 }
1737}
1738
1739fn oid_satisfies_peel_filter(repo: &Repository, oid: ObjectId, peel_inner: &str) -> bool {
1740 apply_peel(repo, oid, Some(peel_inner)).is_ok()
1741}
1742
1743pub fn ambiguous_object_hint_lines(
1745 repo: &Repository,
1746 short_prefix: &str,
1747 peel_filter: Option<&str>,
1748) -> Result<Vec<String>> {
1749 let mut typed: Vec<(u8, String, &'static str)> = Vec::new();
1750 let mut bad_hex: Vec<String> = Vec::new();
1751 for oid in list_all_abbrev_matches(repo, short_prefix)? {
1752 let hex = oid.to_hex();
1753 match repo.read_replaced(&oid) {
1754 Ok(obj) => {
1755 let ok = peel_filter.is_none_or(|p| oid_satisfies_peel_filter(repo, oid, p));
1756 if ok {
1757 typed.push((disambiguate_kind_rank(obj.kind), hex, obj.kind.as_str()));
1758 }
1759 }
1760 Err(_) => bad_hex.push(hex),
1761 }
1762 }
1763 if typed.is_empty() && peel_filter.is_some() {
1764 return ambiguous_object_hint_lines(repo, short_prefix, None);
1765 }
1766 bad_hex.sort();
1767 typed.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
1768 let mut out = Vec::new();
1769 for h in bad_hex {
1770 out.push(format!("hint: {h} [bad object]"));
1771 }
1772 for (_, hex, kind) in typed {
1773 out.push(format!("hint: {hex} {kind}"));
1774 }
1775 Ok(out)
1776}
1777
1778fn read_core_disambiguate(repo: &Repository) -> Option<&'static str> {
1779 let config = ConfigSet::load(Some(&repo.git_dir), true).unwrap_or_else(|_| ConfigSet::new());
1780 let v = config.get("core.disambiguate")?;
1781 match v.to_ascii_lowercase().as_str() {
1782 "committish" | "commit" => Some("commit"),
1783 "treeish" | "tree" => Some("tree"),
1784 "blob" => Some("blob"),
1785 "tag" => Some("tag"),
1786 "none" => None,
1787 _ => None,
1788 }
1789}
1790
1791fn warn_if_branch_refname_collides_with_abbrev_hex(
1794 repo: &Repository,
1795 spec: &str,
1796 object_oid: ObjectId,
1797) {
1798 if spec.len() >= 40 {
1799 return;
1800 }
1801 let branch_ref = format!("refs/heads/{spec}");
1802 let Ok(ref_oid) = refs::resolve_ref(&repo.git_dir, &branch_ref) else {
1803 return;
1804 };
1805 if ref_oid != object_oid {
1806 eprintln!("warning: refname '{spec}' is ambiguous.");
1807 }
1808}
1809
1810fn warn_if_hex_ref_collides_with_objects(repo: &Repository, spec: &str, ref_oid: ObjectId) {
1813 if spec.len() >= 40 || !is_hex_prefix(spec) {
1814 return;
1815 }
1816 let Ok(matches) = find_abbrev_matches(repo, spec) else {
1817 return;
1818 };
1819 if matches.is_empty() {
1820 return;
1821 }
1822 if matches.len() > 1 || matches[0] != ref_oid {
1823 eprintln!("warning: refname '{spec}' is ambiguous.");
1824 }
1825}
1826
1827fn disambiguate_hex_by_peel(
1828 repo: &Repository,
1829 spec: &str,
1830 matches: &[ObjectId],
1831 peel: &str,
1832) -> Result<ObjectId> {
1833 let peel_some = Some(peel);
1834 let filtered: Vec<ObjectId> = matches
1835 .iter()
1836 .copied()
1837 .filter(|oid| apply_peel(repo, *oid, peel_some).is_ok())
1838 .collect();
1839 if filtered.len() == 1 {
1840 return Ok(filtered[0]);
1841 }
1842 if filtered.is_empty() {
1843 return Err(Error::InvalidRef(format!(
1844 "short object ID {spec} is ambiguous"
1845 )));
1846 }
1847 let mut peeled_targets: HashSet<ObjectId> = HashSet::new();
1848 for oid in &filtered {
1849 if let Ok(p) = apply_peel(repo, *oid, peel_some) {
1850 peeled_targets.insert(p);
1851 }
1852 }
1853 if peeled_targets.len() == 1 {
1854 let mut sorted = filtered;
1857 sorted.sort_by_key(|o| o.to_hex());
1858 return Ok(sorted[0]);
1859 }
1860 if peel == "commit" {
1863 let mut by_peeled: HashMap<ObjectId, Vec<ObjectId>> = HashMap::new();
1864 for oid in &filtered {
1865 if let Ok(c) = apply_peel(repo, *oid, Some("commit")) {
1866 by_peeled.entry(c).or_default().push(*oid);
1867 }
1868 }
1869 if by_peeled.len() == 1 {
1870 let mut reps: Vec<ObjectId> = by_peeled.into_values().next().unwrap_or_default();
1871 reps.sort_by_key(|o| o.to_hex());
1872 if let Some(oid) = reps.first().copied() {
1873 return Ok(oid);
1874 }
1875 }
1876 }
1877 Err(Error::InvalidRef(format!(
1878 "short object ID {spec} is ambiguous"
1879 )))
1880}
1881
1882fn commit_reachable_closure(repo: &Repository, start: ObjectId) -> Result<HashSet<ObjectId>> {
1883 use std::collections::VecDeque;
1884 let mut seen = HashSet::new();
1885 let mut q = VecDeque::from([start]);
1886 while let Some(oid) = q.pop_front() {
1887 if !seen.insert(oid) {
1888 continue;
1889 }
1890 let obj = match repo.read_replaced(&oid) {
1891 Ok(o) => o,
1892 Err(_) => continue,
1893 };
1894 if obj.kind != ObjectKind::Commit {
1895 continue;
1896 }
1897 let commit = match parse_commit(&obj.data) {
1898 Ok(c) => c,
1899 Err(_) => continue,
1900 };
1901 for p in &commit.parents {
1902 q.push_back(*p);
1903 }
1904 }
1905 Ok(seen)
1906}
1907
1908fn describe_generation_count(
1910 repo: &Repository,
1911 head: ObjectId,
1912 tag_commit: ObjectId,
1913) -> Result<usize> {
1914 let from_tag = commit_reachable_closure(repo, tag_commit)?;
1915 let from_head = commit_reachable_closure(repo, head)?;
1916 Ok(from_head.difference(&from_tag).count())
1917}
1918
1919fn try_resolve_describe_name(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
1920 let re = Regex::new(r"(?i)^(.+)-(\d+)-g([0-9a-fA-F]+)$")
1921 .map_err(|_| Error::Message("internal: describe regex".to_owned()))?;
1922 let Some(caps) = re.captures(spec) else {
1923 return Ok(None);
1924 };
1925 let tag_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
1926 let gen: usize = caps
1927 .get(2)
1928 .and_then(|m| m.as_str().parse().ok())
1929 .unwrap_or(0);
1930 let hex_abbrev = caps.get(3).map(|m| m.as_str()).unwrap_or("");
1931 if tag_name.is_empty() || hex_abbrev.is_empty() {
1932 return Ok(None);
1933 }
1934 let hex_lower = hex_abbrev.to_ascii_lowercase();
1935 let tag_oid = match refs::resolve_ref(&repo.git_dir, &format!("refs/tags/{tag_name}"))
1936 .or_else(|_| refs::resolve_ref(&repo.git_dir, tag_name))
1937 {
1938 Ok(o) => o,
1939 Err(_) => return Ok(None),
1940 };
1941 let tag_commit = peel_to_commit_for_merge_base(repo, tag_oid)?;
1942 let mut candidates: Vec<ObjectId> = find_abbrev_matches(repo, &hex_lower)?
1943 .into_iter()
1944 .filter(|oid| {
1945 repo.odb
1946 .read(oid)
1947 .map(|o| o.kind == ObjectKind::Commit)
1948 .unwrap_or(false)
1949 && describe_generation_count(repo, *oid, tag_commit).ok() == Some(gen)
1950 })
1951 .collect();
1952 candidates.sort_by_key(|o| o.to_hex());
1953 match candidates.len() {
1954 0 => Err(Error::ObjectNotFound(spec.to_owned())),
1955 1 => Ok(Some(candidates[0])),
1956 _ => Err(Error::InvalidRef(format!(
1957 "short object ID {hex_abbrev} is ambiguous"
1958 ))),
1959 }
1960}
1961
1962fn resolve_base(
1963 repo: &Repository,
1964 spec: &str,
1965 index_dwim: bool,
1966 commit_only_hex: bool,
1967 use_disambiguate_config: bool,
1968 peel_for_disambig: Option<&str>,
1969 implicit_tree_abbrev: bool,
1970 implicit_blob_abbrev: bool,
1971 remote_branch_name_guess: bool,
1972) -> Result<ObjectId> {
1973 if spec == "@" {
1975 return resolve_base(
1976 repo,
1977 "HEAD",
1978 index_dwim,
1979 commit_only_hex,
1980 use_disambiguate_config,
1981 peel_for_disambig,
1982 implicit_tree_abbrev,
1983 implicit_blob_abbrev,
1984 remote_branch_name_guess,
1985 );
1986 }
1987
1988 if spec == "FETCH_HEAD" {
1990 let path = repo.git_dir.join("FETCH_HEAD");
1991 let content = std::fs::read_to_string(&path)
1992 .map_err(|_| Error::ObjectNotFound("FETCH_HEAD".to_owned()))?;
1993 for line in content.lines() {
1994 let line = line.trim();
1995 if line.is_empty() {
1996 continue;
1997 }
1998 let mut parts = line.split('\t');
1999 let Some(oid_hex) = parts.next() else {
2000 continue;
2001 };
2002 let not_for_merge = parts.next().is_some_and(|v| v == "not-for-merge");
2003 if not_for_merge {
2004 continue;
2005 }
2006 if oid_hex.len() == 40 && oid_hex.bytes().all(|b| b.is_ascii_hexdigit()) {
2007 return oid_hex
2008 .parse::<ObjectId>()
2009 .map_err(|_| Error::InvalidRef("invalid FETCH_HEAD object id".to_owned()));
2010 }
2011 }
2012 return Err(Error::ObjectNotFound("FETCH_HEAD".to_owned()));
2013 }
2014
2015 if spec.starts_with("@{-") {
2017 if let Some(close) = spec[3..].find('}') {
2018 let n_str = &spec[3..3 + close];
2019 if let Ok(n) = n_str.parse::<usize>() {
2020 if n >= 1 {
2021 let suffix = &spec[3 + close + 1..];
2022 if suffix.is_empty() {
2023 if let Some(oid) = try_resolve_at_minus(repo, spec)? {
2024 return Ok(oid);
2025 }
2026 } else {
2027 let branch = resolve_at_minus_to_branch(repo, n)?;
2028 let new_spec = format!("{branch}{suffix}");
2029 return resolve_base(
2030 repo,
2031 &new_spec,
2032 index_dwim,
2033 commit_only_hex,
2034 use_disambiguate_config,
2035 peel_for_disambig,
2036 implicit_tree_abbrev,
2037 implicit_blob_abbrev,
2038 remote_branch_name_guess,
2039 );
2040 }
2041 }
2042 }
2043 }
2044 }
2045
2046 if upstream_suffix_info(spec).is_some() {
2048 let full_ref = resolve_upstream_symbolic_name(repo, spec)?;
2049 return refs::resolve_ref(&repo.git_dir, &full_ref)
2050 .map_err(|_| Error::ObjectNotFound(spec.to_owned()));
2051 }
2052
2053 if let Some(oid) = try_resolve_reflog_index(repo, spec)? {
2055 return Ok(oid);
2056 }
2057
2058 if let Some(pattern) = spec.strip_prefix(":/") {
2060 if !pattern.is_empty() {
2061 return resolve_commit_message_search(repo, pattern);
2062 }
2063 }
2064
2065 if let Some(rest) = spec.strip_prefix(':') {
2068 if !rest.is_empty() && !rest.starts_with('/') {
2069 if rest.len() >= 3 && rest.as_bytes()[1] == b':' {
2071 if let Some(stage_char) = rest.chars().next() {
2072 if let Some(stage) = stage_char.to_digit(10) {
2073 if stage <= 3 {
2074 let raw_path = &rest[2..];
2075 let path = match normalize_colon_path_for_tree(repo, raw_path) {
2076 Ok(p) => p,
2077 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
2078 let wt = repo
2079 .work_tree
2080 .as_ref()
2081 .and_then(|p| p.canonicalize().ok())
2082 .map(|p| p.display().to_string())
2083 .unwrap_or_default();
2084 return Err(Error::Message(format!(
2085 "fatal: '{raw_path}' is outside repository at '{wt}'"
2086 )));
2087 }
2088 Err(e) => return Err(e),
2089 };
2090 return resolve_index_path_at_stage(repo, &path, stage as u8).map_err(
2091 |e| diagnose_index_path_error(repo, &path, stage as u8, e),
2092 );
2093 }
2094 }
2095 }
2096 }
2097 let clean_rest = match normalize_colon_path_for_tree(repo, rest) {
2098 Ok(p) => p,
2099 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
2100 let wt = repo
2101 .work_tree
2102 .as_ref()
2103 .and_then(|p| p.canonicalize().ok())
2104 .map(|p| p.display().to_string())
2105 .unwrap_or_default();
2106 return Err(Error::Message(format!(
2107 "fatal: '{rest}' is outside repository at '{wt}'"
2108 )));
2109 }
2110 Err(e) => return Err(e),
2111 };
2112 return resolve_index_path(repo, &clean_rest)
2113 .map_err(|e| diagnose_index_path_error(repo, &clean_rest, 0, e));
2114 }
2115 }
2116
2117 if let Some((treeish, path)) = split_treeish_spec(spec) {
2118 let root_oid = resolve_revision_impl(
2119 repo,
2120 treeish,
2121 index_dwim,
2122 commit_only_hex,
2123 use_disambiguate_config,
2124 false,
2125 false,
2126 false,
2127 false,
2128 )?;
2129 return resolve_treeish_path_to_object(repo, root_oid, path);
2130 }
2131
2132 if let Ok(oid) = spec.parse::<ObjectId>() {
2133 let rn = format!("refs/heads/{spec}");
2136 if refs::resolve_ref(&repo.git_dir, &rn).is_ok() {
2137 eprintln!("warning: refname '{spec}' is ambiguous.");
2138 }
2139 return Ok(oid);
2140 }
2141
2142 match try_resolve_describe_name(repo, spec) {
2143 Ok(Some(oid)) => return Ok(oid),
2144 Err(e) => return Err(e),
2145 Ok(None) => {}
2146 }
2147
2148 if is_hex_prefix(spec) && spec.len() < 40 {
2151 let tag_ref = format!("refs/tags/{spec}");
2152 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &tag_ref) {
2153 warn_if_hex_ref_collides_with_objects(repo, spec, oid);
2154 return Ok(oid);
2155 }
2156 let branch_ref = format!("refs/heads/{spec}");
2157 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &branch_ref) {
2158 warn_if_hex_ref_collides_with_objects(repo, spec, oid);
2159 return Ok(oid);
2160 }
2161 }
2162
2163 if is_hex_prefix(spec) {
2164 let matches = find_abbrev_matches(repo, spec)?;
2165 if matches.is_empty() {
2166 if (4..40).contains(&spec.len()) {
2170 return Err(Error::ObjectNotFound(spec.to_owned()));
2171 }
2172 } else if matches.len() == 1 {
2173 let oid = matches[0];
2174 warn_if_branch_refname_collides_with_abbrev_hex(repo, spec, oid);
2175 return Ok(oid);
2176 } else if matches.len() > 1 {
2177 if commit_only_hex {
2178 let oid = disambiguate_hex_by_peel(repo, spec, &matches, "commit")?;
2179 warn_if_branch_refname_collides_with_abbrev_hex(repo, spec, oid);
2180 return Ok(oid);
2181 }
2182 if let Some(p) = peel_for_disambig {
2183 let oid = disambiguate_hex_by_peel(repo, spec, &matches, p)?;
2184 warn_if_branch_refname_collides_with_abbrev_hex(repo, spec, oid);
2185 return Ok(oid);
2186 }
2187 if use_disambiguate_config {
2188 if let Some(pref) = read_core_disambiguate(repo) {
2189 if let Ok(oid) = disambiguate_hex_by_peel(repo, spec, &matches, pref) {
2190 warn_if_branch_refname_collides_with_abbrev_hex(repo, spec, oid);
2191 return Ok(oid);
2192 }
2193 }
2194 }
2195 return Err(Error::InvalidRef(format!(
2196 "short object ID {} is ambiguous",
2197 spec
2198 )));
2199 }
2200 }
2201
2202 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, spec) {
2203 return Ok(oid);
2204 }
2205 if let Some(rest) = spec.strip_prefix("remotes/") {
2207 let full = format!("refs/remotes/{rest}");
2208 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &full) {
2209 return Ok(oid);
2210 }
2211 }
2212 if !spec.contains('/')
2216 && !spec.starts_with('.')
2217 && spec != "HEAD"
2218 && spec != "FETCH_HEAD"
2219 && spec != "MERGE_HEAD"
2220 && spec != "CHERRY_PICK_HEAD"
2221 && spec != "REVERT_HEAD"
2222 && spec != "REBASE_HEAD"
2223 && spec != "AUTO_MERGE"
2224 && spec != "stash"
2225 {
2226 let local_branch = format!("refs/heads/{spec}");
2227 if refs::resolve_ref(&repo.git_dir, &local_branch).is_err() {
2228 let remote_head = format!("refs/remotes/{spec}/HEAD");
2229 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &remote_head) {
2230 return Ok(oid);
2231 }
2232 }
2233 }
2234 if spec == "stash" {
2236 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, "refs/stash") {
2237 return Ok(oid);
2238 }
2239 }
2240 let head_ref = format!("refs/heads/{spec}");
2244 let tag_ref = format!("refs/tags/{spec}");
2245 let head_oid = refs::resolve_ref(&repo.git_dir, &head_ref).ok();
2246 let tag_oid = refs::resolve_ref(&repo.git_dir, &tag_ref).ok();
2247 match (head_oid, tag_oid) {
2248 (Some(h), Some(t)) if h != t => {
2249 eprintln!("warning: refname '{spec}' is ambiguous.");
2250 return Ok(h);
2251 }
2252 (Some(h), _) => return Ok(h),
2253 (None, Some(t)) => return Ok(t),
2254 (None, None) => {}
2255 }
2256
2257 if !spec.contains('/')
2261 && !spec.contains(':')
2262 && !spec.starts_with('.')
2263 && spec != "HEAD"
2264 && spec.len() <= 255
2265 {
2266 let mut ref_match: Option<ObjectId> = None;
2267 for prefix in ["refs/heads/", "refs/tags/", "refs/remotes/", "refs/notes/"] {
2268 let full = format!("{prefix}{spec}");
2269 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &full) {
2270 ref_match = Some(oid);
2271 break;
2272 }
2273 }
2274 if let Some(oid) = ref_match {
2275 return Ok(oid);
2276 }
2277 }
2278 for candidate in &[format!("refs/remotes/{spec}"), format!("refs/notes/{spec}")] {
2279 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, candidate) {
2280 return Ok(oid);
2281 }
2282 }
2283
2284 if let Some(head_ref) = remote_tracking_head_symbolic_target(repo, spec) {
2286 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &head_ref) {
2287 return Ok(oid);
2288 }
2289 }
2290
2291 if remote_branch_name_guess
2293 && !spec.contains('/')
2294 && spec != "HEAD"
2295 && spec != "FETCH_HEAD"
2296 && spec != "MERGE_HEAD"
2297 {
2298 const REMOTES: &str = "refs/remotes/";
2299 if let Ok(remote_refs) = refs::list_refs(&repo.git_dir, REMOTES) {
2300 let matches: Vec<ObjectId> = remote_refs
2301 .into_iter()
2302 .filter(|(r, _)| {
2303 r.strip_prefix(REMOTES)
2304 .is_some_and(|rest| rest == spec || rest.ends_with(&format!("/{spec}")))
2305 })
2306 .map(|(_, oid)| oid)
2307 .collect();
2308 if matches.len() == 1 {
2309 return Ok(matches[0]);
2310 }
2311 if matches.len() > 1 {
2312 return Err(Error::InvalidRef(format!(
2313 "ambiguous refname '{spec}': matches multiple remote-tracking branches"
2314 )));
2315 }
2316 }
2317 }
2318
2319 if !spec.contains(':') && !spec.starts_with('-') {
2321 if index_dwim {
2322 if let Ok(oid) = resolve_index_path(repo, spec) {
2323 return Ok(oid);
2324 }
2325 }
2326 return Err(Error::Message(format!(
2327 "fatal: ambiguous argument '{spec}': unknown revision or path not in the working tree.\n\
2328Use '--' to separate paths from revisions, like this:\n\
2329'git <command> [<revision>...] -- [<file>...]'"
2330 )));
2331 }
2332 Err(Error::ObjectNotFound(spec.to_owned()))
2333}
2334
2335fn resolve_at_minus_to_branch(repo: &Repository, n: usize) -> Result<String> {
2337 let entries = read_reflog(&repo.git_dir, "HEAD")?;
2338 let mut count = 0usize;
2339 for entry in entries.iter().rev() {
2340 let msg = &entry.message;
2341 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
2342 count += 1;
2343 if count == n {
2344 if let Some(to_pos) = rest.find(" to ") {
2345 return Ok(rest[..to_pos].to_string());
2346 }
2347 }
2348 }
2349 }
2350 Err(Error::InvalidRef(format!(
2351 "@{{-{n}}}: only {count} checkout(s) in reflog"
2352 )))
2353}
2354
2355fn try_resolve_at_minus(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
2358 if !spec.starts_with("@{-") || !spec.ends_with('}') {
2360 return Ok(None);
2361 }
2362 let inner = &spec[3..spec.len() - 1];
2363 let n: usize = match inner.parse() {
2364 Ok(n) if n >= 1 => n,
2365 _ => return Ok(None),
2366 };
2367 let entries = read_reflog(&repo.git_dir, "HEAD")?;
2369 let mut count = 0usize;
2370 for entry in entries.iter().rev() {
2372 let msg = &entry.message;
2373 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
2374 count += 1;
2375 if count == n {
2376 if let Some(to_pos) = rest.find(" to ") {
2377 let from_branch = &rest[..to_pos];
2378 let ref_name = format!("refs/heads/{from_branch}");
2379 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &ref_name) {
2380 return Ok(Some(oid));
2381 }
2382 if let Ok(oid) = from_branch.parse::<ObjectId>() {
2383 if repo.odb.exists(&oid) {
2384 return Ok(Some(oid));
2385 }
2386 }
2387 return Err(Error::InvalidRef(format!(
2388 "cannot resolve @{{-{n}}}: branch '{}' not found",
2389 from_branch
2390 )));
2391 }
2392 }
2393 }
2394 }
2395 Err(Error::InvalidRef(format!(
2396 "@{{-{n}}}: only {count} checkout(s) in reflog"
2397 )))
2398}
2399
2400#[derive(Debug, Clone)]
2401enum AtStep {
2402 Index(usize),
2403 Date(i64),
2404 Upstream,
2405 Push,
2406 Now,
2407}
2408
2409fn try_parse_at_step_inner(inner: &str) -> Option<AtStep> {
2410 if inner.eq_ignore_ascii_case("u") || inner.eq_ignore_ascii_case("upstream") {
2411 return Some(AtStep::Upstream);
2412 }
2413 if inner.eq_ignore_ascii_case("push") {
2414 return Some(AtStep::Push);
2415 }
2416 if inner.eq_ignore_ascii_case("now") {
2417 return Some(AtStep::Now);
2418 }
2419 if let Ok(n) = inner.parse::<usize>() {
2420 return Some(AtStep::Index(n));
2421 }
2422 approxidate(inner).map(AtStep::Date)
2423}
2424
2425fn next_reflog_at_open(spec: &str, mut from: usize) -> Option<usize> {
2426 let b = spec.as_bytes();
2427 while let Some(rel) = spec[from..].find("@{") {
2428 let i = from + rel;
2429 if b.get(i + 2) == Some(&b'-') {
2431 let after_open = i + 2;
2432 let close = spec[after_open..].find('}').map(|j| after_open + j)?;
2433 from = close + 1;
2434 continue;
2435 }
2436 return Some(i);
2437 }
2438 None
2439}
2440
2441fn split_reflog_at_chain(spec: &str) -> Option<(String, Vec<AtStep>)> {
2443 let at = next_reflog_at_open(spec, 0)?;
2444 let prefix = spec[..at].to_owned();
2445 let mut steps = Vec::new();
2446 let mut pos = at;
2447 while pos < spec.len() {
2448 let rest = &spec[pos..];
2449 if !rest.starts_with("@{") {
2450 return None;
2451 }
2452 if rest.as_bytes().get(2) == Some(&b'-') {
2453 return None;
2454 }
2455 let inner_start = pos + 2;
2456 let close = spec[inner_start..].find('}').map(|i| inner_start + i)?;
2457 let inner = &spec[inner_start..close];
2458 let step = try_parse_at_step_inner(inner)?;
2459 steps.push(step);
2460 pos = close + 1;
2461 }
2462 if steps.is_empty() {
2463 return None;
2464 }
2465 Some((prefix, steps))
2466}
2467
2468fn dwim_refname(repo: &Repository, raw: &str) -> String {
2469 if raw.is_empty() || raw == "HEAD" || raw.starts_with("refs/") {
2470 return raw.to_owned();
2471 }
2472 if raw == "stash" && refs::resolve_ref(&repo.git_dir, "refs/stash").is_ok() {
2474 return "refs/stash".to_owned();
2475 }
2476 let candidate = format!("refs/heads/{raw}");
2477 if refs::resolve_ref(&repo.git_dir, &candidate).is_ok() {
2478 candidate
2479 } else {
2480 raw.to_owned()
2481 }
2482}
2483
2484fn reflog_display_name(refname_raw: &str, refname: &str) -> String {
2485 if refname_raw.is_empty() {
2486 if let Some(b) = refname.strip_prefix("refs/heads/") {
2487 return b.to_owned();
2488 }
2489 return refname.to_owned();
2490 }
2491 refname_raw.to_owned()
2492}
2493
2494fn resolve_reflog_oid(
2495 repo: &Repository,
2496 refname: &str,
2497 refname_raw: &str,
2498 index_or_date: ReflogSelector,
2499) -> Result<ObjectId> {
2500 let entries = read_reflog(&repo.git_dir, refname)?;
2501 let display = reflog_display_name(refname_raw, refname);
2502 match index_or_date {
2503 ReflogSelector::Index(index) => {
2504 let len = entries.len();
2505 if index == 0 {
2506 if len == 0 {
2507 return refs::resolve_ref(&repo.git_dir, refname).map_err(|_| {
2508 Error::Message(format!("fatal: log for '{display}' is empty"))
2509 });
2510 }
2511 return Ok(entries[len - 1].new_oid);
2512 }
2513 if len == 0 {
2514 return Err(Error::Message(format!(
2515 "fatal: log for '{display}' is empty"
2516 )));
2517 }
2518 if index > len {
2519 return Err(Error::Message(format!(
2520 "fatal: log for '{display}' only has {len} entries"
2521 )));
2522 }
2523 if index == len {
2524 if len == 1 {
2525 return Ok(entries[0].old_oid);
2526 }
2527 return Err(Error::Message(format!(
2528 "fatal: log for '{display}' only has {len} entries"
2529 )));
2530 }
2531 Ok(entries[len - 1 - index].new_oid)
2532 }
2533 ReflogSelector::Date(target_ts) => {
2534 if entries.is_empty() {
2535 return Err(Error::Message(format!(
2536 "fatal: log for '{display}' is empty"
2537 )));
2538 }
2539 for entry in entries.iter().rev() {
2540 let ts = parse_reflog_entry_timestamp(entry);
2541 if let Some(t) = ts {
2542 if t <= target_ts {
2543 return Ok(entry.new_oid);
2544 }
2545 }
2546 }
2547 Ok(entries[0].new_oid)
2548 }
2549 }
2550}
2551
2552fn resolve_at_minus_token_to_branch(repo: &Repository, token: &str) -> Result<Option<String>> {
2553 if !token.starts_with("@{-") || !token.ends_with('}') {
2554 return Ok(None);
2555 }
2556 let inner = &token[3..token.len() - 1];
2557 let n: usize = inner
2558 .parse()
2559 .map_err(|_| Error::InvalidRef(format!("invalid N in @{{-N}} for '{token}'")))?;
2560 if n < 1 {
2561 return Ok(None);
2562 }
2563 Ok(Some(resolve_at_minus_to_branch(repo, n)?))
2564}
2565
2566pub fn reflog_walk_refname(repo: &Repository, spec: &str) -> Result<Option<String>> {
2570 let Some((prefix, steps)) = split_reflog_at_chain(spec) else {
2571 return Ok(None);
2572 };
2573
2574 let prefix_resolved = if let Some(b) = resolve_at_minus_token_to_branch(repo, &prefix)? {
2575 b
2576 } else {
2577 prefix.clone()
2578 };
2579
2580 let mut current_spec = if prefix_resolved.is_empty() {
2581 if let Ok(Some(b)) = refs::read_head(&repo.git_dir) {
2582 if let Some(short) = b.strip_prefix("refs/heads/") {
2583 short.to_owned()
2584 } else {
2585 "HEAD".to_owned()
2586 }
2587 } else {
2588 "HEAD".to_owned()
2589 }
2590 } else {
2591 prefix_resolved
2592 };
2593
2594 let last_reflog_peel = steps
2595 .iter()
2596 .rposition(|s| matches!(s, AtStep::Index(_) | AtStep::Date(_) | AtStep::Now));
2597
2598 let limit = last_reflog_peel.unwrap_or(steps.len());
2599 for step in steps.iter().take(limit) {
2600 match step {
2601 AtStep::Upstream => {
2602 let base = if current_spec == "@" {
2603 "HEAD"
2604 } else {
2605 current_spec.as_str()
2606 };
2607 let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{u}}"))?;
2608 current_spec = full;
2609 }
2610 AtStep::Push => {
2611 let base = if current_spec == "@" {
2612 "HEAD"
2613 } else {
2614 current_spec.as_str()
2615 };
2616 let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{push}}"))?;
2617 current_spec = full;
2618 }
2619 AtStep::Now | AtStep::Index(_) | AtStep::Date(_) => {}
2620 }
2621 }
2622
2623 Ok(Some(dwim_refname(repo, current_spec.as_str())))
2624}
2625
2626pub fn resolve_reflog_walk_log_ref(repo: &Repository, r: &str) -> Result<String> {
2631 if let Ok(Some(w)) = reflog_walk_refname(repo, r) {
2632 return Ok(w);
2633 }
2634 if r == "HEAD" || r.starts_with("refs/") {
2635 return Ok(r.to_string());
2636 }
2637 if r.starts_with("@{") {
2638 if let Some(n_str) = r.strip_prefix("@{").and_then(|s| s.strip_suffix('}')) {
2639 if let Some(stripped) = n_str.strip_prefix('-') {
2640 if stripped.parse::<usize>().is_ok() {
2641 if let Ok(branch) = refs::resolve_at_n_branch(&repo.git_dir, r) {
2642 return Ok(format!("refs/heads/{branch}"));
2643 }
2644 }
2645 }
2646 }
2647 return Ok(r.to_string());
2648 }
2649 let candidate = format!("refs/heads/{r}");
2650 if refs::resolve_ref(&repo.git_dir, &candidate).is_ok() {
2651 Ok(candidate)
2652 } else {
2653 Ok(r.to_string())
2654 }
2655}
2656
2657fn try_resolve_reflog_index(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
2659 let Some((prefix, steps)) = split_reflog_at_chain(spec) else {
2660 return Ok(None);
2661 };
2662
2663 let prefix_resolved = if let Some(b) = resolve_at_minus_token_to_branch(repo, &prefix)? {
2664 b
2665 } else {
2666 prefix.clone()
2667 };
2668
2669 let mut current_spec = if prefix_resolved.is_empty() {
2670 if let Ok(Some(b)) = refs::read_head(&repo.git_dir) {
2671 if let Some(short) = b.strip_prefix("refs/heads/") {
2672 short.to_owned()
2673 } else {
2674 "HEAD".to_owned()
2675 }
2676 } else {
2677 "HEAD".to_owned()
2678 }
2679 } else {
2680 prefix_resolved
2681 };
2682
2683 for (i, step) in steps.iter().enumerate() {
2684 match step {
2685 AtStep::Upstream => {
2686 let base = if current_spec == "@" {
2687 "HEAD"
2688 } else {
2689 current_spec.as_str()
2690 };
2691 let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{u}}"))?;
2692 current_spec = full;
2693 }
2694 AtStep::Push => {
2695 let base = if current_spec == "@" {
2696 "HEAD"
2697 } else {
2698 current_spec.as_str()
2699 };
2700 let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{push}}"))?;
2701 current_spec = full;
2702 }
2703 AtStep::Now => {
2704 let refname_raw = current_spec.as_str();
2705 let refname = dwim_refname(repo, refname_raw);
2706 let oid =
2707 resolve_reflog_oid(repo, &refname, refname_raw, ReflogSelector::Index(0))?;
2708 if i + 1 == steps.len() {
2709 return Ok(Some(oid));
2710 }
2711 current_spec = oid.to_hex();
2712 }
2713 AtStep::Index(n) => {
2714 let refname_raw = current_spec.as_str();
2715 let refname = dwim_refname(repo, refname_raw);
2716 let oid =
2717 resolve_reflog_oid(repo, &refname, refname_raw, ReflogSelector::Index(*n))?;
2718 if i + 1 == steps.len() {
2719 return Ok(Some(oid));
2720 }
2721 current_spec = oid.to_hex();
2722 }
2723 AtStep::Date(ts) => {
2724 let refname_raw = current_spec.as_str();
2725 let refname = dwim_refname(repo, refname_raw);
2726 let oid =
2727 resolve_reflog_oid(repo, &refname, refname_raw, ReflogSelector::Date(*ts))?;
2728 if i + 1 == steps.len() {
2729 return Ok(Some(oid));
2730 }
2731 current_spec = oid.to_hex();
2732 }
2733 }
2734 }
2735
2736 let refname_raw = current_spec.as_str();
2737 let refname = dwim_refname(repo, refname_raw);
2738 refs::resolve_ref(&repo.git_dir, &refname)
2739 .map(Some)
2740 .map_err(|_| Error::ObjectNotFound(spec.to_owned()))
2741}
2742
2743enum ReflogSelector {
2744 Index(usize),
2745 Date(i64),
2746}
2747
2748fn parse_reflog_entry_timestamp(entry: &crate::reflog::ReflogEntry) -> Option<i64> {
2750 let parts: Vec<&str> = entry.identity.rsplitn(3, ' ').collect();
2752 if parts.len() >= 2 {
2753 parts[1].parse::<i64>().ok()
2754 } else {
2755 None
2756 }
2757}
2758
2759#[must_use]
2763pub fn reflog_date_selector_timestamp(s: &str) -> Option<i64> {
2764 approxidate(s)
2765}
2766
2767fn approxidate(s: &str) -> Option<i64> {
2770 let now_ts = std::time::SystemTime::now()
2771 .duration_since(std::time::UNIX_EPOCH)
2772 .ok()
2773 .map(|d| d.as_secs() as i64)
2774 .unwrap_or(0);
2775 let lower = s.trim().to_ascii_lowercase();
2776 if lower == "now" {
2777 if let Ok(raw) =
2780 std::env::var("GIT_COMMITTER_DATE").or_else(|_| std::env::var("GIT_AUTHOR_DATE"))
2781 {
2782 let mut it = raw.split_whitespace();
2783 if let Some(ts) = it.next().and_then(|p| p.parse::<i64>().ok()) {
2784 return Some(ts);
2785 }
2786 }
2787 return Some(now_ts);
2788 }
2789 let relative = lower.replace('.', " ");
2792 let parts: Vec<&str> = relative.split_whitespace().collect();
2793 if parts.len() >= 2 {
2794 let (n_str, unit, is_ago) = if parts.len() >= 3 && parts[2] == "ago" {
2796 (parts[0], parts[1], true)
2797 } else if parts.len() == 2 {
2798 (parts[0], parts[1], false)
2799 } else {
2800 ("", "", false)
2801 };
2802 if !n_str.is_empty() {
2803 if let Ok(n) = n_str.parse::<i64>() {
2804 let secs: Option<i64> = match unit.trim_end_matches('s') {
2805 "second" => Some(n),
2806 "minute" => Some(n * 60),
2807 "hour" => Some(n * 3600),
2808 "day" => Some(n * 86400),
2809 "week" => Some(n * 604800),
2810 "month" => Some(n * 2592000),
2811 "year" => Some(n * 31536000),
2812 _ => None,
2813 };
2814 if let Some(s) = secs {
2815 return Some(if is_ago || true {
2816 now_ts - s
2817 } else {
2818 now_ts + s
2819 });
2820 }
2821 }
2822 }
2823 }
2824 let re_like = |input: &str| -> Option<i64> {
2826 for (i, _) in input.char_indices() {
2828 let rest = &input[i..];
2829 if rest.len() >= 10 {
2830 let bytes = rest.as_bytes();
2831 if bytes[4] == b'-'
2832 && bytes[7] == b'-'
2833 && bytes[0..4].iter().all(|b| b.is_ascii_digit())
2834 && bytes[5..7].iter().all(|b| b.is_ascii_digit())
2835 && bytes[8..10].iter().all(|b| b.is_ascii_digit())
2836 {
2837 let year: i32 = rest[0..4].parse().ok()?;
2838 let month: u8 = rest[5..7].parse().ok()?;
2839 let day: u8 = rest[8..10].parse().ok()?;
2840 let date = time::Date::from_calendar_date(
2841 year,
2842 time::Month::try_from(month).ok()?,
2843 day,
2844 )
2845 .ok()?;
2846 let dt = date.with_hms(0, 0, 0).ok()?;
2847 let odt = dt.assume_utc();
2848 return Some(odt.unix_timestamp());
2849 }
2850 }
2851 }
2852 None
2853 };
2854 re_like(s)
2855}
2856
2857fn head_tree_oid(repo: &Repository) -> Result<ObjectId> {
2858 let head_oid = refs::resolve_ref(&repo.git_dir, "HEAD")?;
2859 peel_to_tree(repo, head_oid)
2860}
2861
2862fn path_in_tree(repo: &Repository, tree_oid: ObjectId, path: &str) -> bool {
2863 resolve_tree_path(repo, &tree_oid, path).is_ok()
2864}
2865
2866fn path_in_index(repo: &Repository, path: &str, stage: u8) -> bool {
2867 resolve_index_path_at_stage(repo, path, stage).is_ok()
2868}
2869
2870fn diagnose_tree_path_error(
2871 repo: &Repository,
2872 rev_label: &str,
2873 raw_after_colon: &str,
2874 clean_path: &str,
2875 err: Error,
2876) -> Error {
2877 let Error::ObjectNotFound(msg) = err else {
2878 return err;
2879 };
2880 if !msg.contains("not found in tree") {
2881 return Error::ObjectNotFound(msg);
2882 }
2883 let rel_display: &str =
2884 if raw_after_colon.starts_with("./") || raw_after_colon.starts_with("../") {
2885 clean_path
2886 } else {
2887 raw_after_colon
2888 };
2889 if let Ok(head_tree) = head_tree_oid(repo) {
2890 if path_in_tree(repo, head_tree, clean_path) {
2891 return Error::Message(format!(
2892 "fatal: path '{rel_display}' exists on disk, but not in '{rev_label}'."
2893 ));
2894 }
2895 if let Ok(cwd) = std::env::current_dir() {
2896 let prefix = show_prefix(repo, &cwd);
2897 let pfx = prefix.trim_end_matches('/');
2898 if !pfx.is_empty() {
2899 let candidate = if clean_path.is_empty() {
2900 pfx.to_owned()
2901 } else {
2902 format!("{pfx}/{clean_path}")
2903 };
2904 if path_in_tree(repo, head_tree, &candidate) {
2905 return Error::Message(format!(
2906 "fatal: path '{candidate}' exists, but not '{rel_display}'\n\
2907hint: Did you mean '{rev_label}:{candidate}' aka '{rev_label}:./{rel_display}'?"
2908 ));
2909 }
2910 }
2911 }
2912 let on_disk = repo
2913 .work_tree
2914 .as_ref()
2915 .map(|wt| wt.join(clean_path))
2916 .is_some_and(|p| p.exists());
2917 let in_index = path_in_index(repo, clean_path, 0);
2918 if on_disk || in_index {
2919 return Error::Message(format!(
2920 "fatal: path '{rel_display}' exists on disk, but not in '{rev_label}'."
2921 ));
2922 }
2923 }
2924 Error::Message(format!(
2925 "fatal: path '{rel_display}' does not exist in '{rev_label}'"
2926 ))
2927}
2928
2929fn diagnose_index_path_error(repo: &Repository, path: &str, stage: u8, err: Error) -> Error {
2930 let Error::ObjectNotFound(_) = err else {
2931 return err;
2932 };
2933 let work_path = repo
2934 .work_tree
2935 .as_ref()
2936 .map(|wt| wt.join(path))
2937 .filter(|p| p.exists());
2938 let on_disk = work_path.is_some();
2939 let in_head = head_tree_oid(repo)
2940 .map(|t| path_in_tree(repo, t, path))
2941 .unwrap_or(false);
2942 let in_index = path_in_index(repo, path, 0);
2943 let at_stage = path_in_index(repo, path, stage);
2944
2945 if stage > 0 && !in_index {
2946 if let Ok(cwd) = std::env::current_dir() {
2947 let prefix = show_prefix(repo, &cwd);
2948 let pfx = prefix.trim_end_matches('/');
2949 if !pfx.is_empty() {
2950 let candidate = if path.is_empty() {
2951 pfx.to_owned()
2952 } else {
2953 format!("{pfx}/{path}")
2954 };
2955 if path_in_index(repo, &candidate, 0) && !path_in_index(repo, &candidate, stage) {
2956 return Error::Message(format!(
2957 "fatal: path '{candidate}' is in the index, but not '{path}'\n\
2958hint: Did you mean ':0:{candidate}' aka ':0:./{path}'?"
2959 ));
2960 }
2961 }
2962 }
2963 return Error::Message(format!(
2964 "fatal: path '{path}' does not exist (neither on disk nor in the index)"
2965 ));
2966 }
2967
2968 if stage > 0 && in_index && !at_stage {
2969 return Error::Message(format!(
2970 "fatal: path '{path}' is in the index, but not at stage {stage}\n\
2971hint: Did you mean ':0:{path}'?"
2972 ));
2973 }
2974
2975 if stage == 0 {
2976 if !on_disk && !in_index {
2977 if let Ok(cwd) = std::env::current_dir() {
2978 let prefix = show_prefix(repo, &cwd);
2979 let pfx = prefix.trim_end_matches('/');
2980 if !pfx.is_empty() {
2981 let candidate = if path.is_empty() {
2982 pfx.to_owned()
2983 } else {
2984 format!("{pfx}/{path}")
2985 };
2986 if path_in_index(repo, &candidate, 0) {
2987 return Error::Message(format!(
2988 "fatal: path '{candidate}' is in the index, but not '{path}'\n\
2989hint: Did you mean ':0:{candidate}' aka ':0:./{path}'?"
2990 ));
2991 }
2992 }
2993 }
2994 return Error::Message(format!(
2995 "fatal: path '{path}' does not exist (neither on disk nor in the index)"
2996 ));
2997 }
2998 if on_disk && !in_index && !in_head {
2999 return Error::Message(format!(
3000 "fatal: path '{path}' exists on disk, but not in the index"
3001 ));
3002 }
3003 }
3004 Error::Message(format!("fatal: path '{path}' does not exist in the index"))
3005}
3006
3007fn resolve_index_path(repo: &Repository, path: &str) -> Result<ObjectId> {
3009 resolve_index_path_at_stage(repo, path, 0)
3010}
3011
3012#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3014pub struct IndexColonSpec<'a> {
3015 pub stage: u8,
3017 pub raw_path: &'a str,
3019}
3020
3021#[must_use]
3025pub fn parse_index_colon_spec(spec: &str) -> Option<IndexColonSpec<'_>> {
3026 if !spec.starts_with(':') || spec.starts_with(":/") || spec.len() <= 1 {
3027 return None;
3028 }
3029 let rest = &spec[1..];
3030 if rest.is_empty() {
3031 return None;
3032 }
3033 if rest.len() >= 3 && rest.as_bytes()[1] == b':' {
3034 if let Some(stage_char) = rest.chars().next() {
3035 if let Some(stage) = stage_char.to_digit(10) {
3036 if stage <= 3 {
3037 return Some(IndexColonSpec {
3038 stage: stage as u8,
3039 raw_path: &rest[2..],
3040 });
3041 }
3042 }
3043 }
3044 }
3045 Some(IndexColonSpec {
3046 stage: 0,
3047 raw_path: rest,
3048 })
3049}
3050
3051#[derive(Debug, Clone, PartialEq, Eq)]
3053pub struct IndexPathEntry {
3054 pub path: String,
3056 pub oid: ObjectId,
3058 pub mode: u32,
3060}
3061
3062pub fn resolve_index_path_entry(repo: &Repository, spec: &str) -> Result<Option<IndexPathEntry>> {
3070 let Some(colon) = parse_index_colon_spec(spec) else {
3071 return Ok(None);
3072 };
3073 let path = match normalize_colon_path_for_tree(repo, colon.raw_path) {
3074 Ok(p) => p,
3075 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
3076 let wt = repo
3077 .work_tree
3078 .as_ref()
3079 .and_then(|p| p.canonicalize().ok())
3080 .map(|p| p.display().to_string())
3081 .unwrap_or_default();
3082 return Err(Error::Message(format!(
3083 "fatal: '{}' is outside repository at '{wt}'",
3084 colon.raw_path
3085 )));
3086 }
3087 Err(e) => return Err(e),
3088 };
3089 let index_path = if let Ok(raw) = std::env::var("GIT_INDEX_FILE") {
3090 let p = std::path::PathBuf::from(raw);
3091 if p.is_absolute() {
3092 p
3093 } else if let Ok(cwd) = std::env::current_dir() {
3094 cwd.join(p)
3095 } else {
3096 p
3097 }
3098 } else {
3099 repo.index_path()
3100 };
3101 use crate::index::Index;
3102 let index = Index::load_expand_sparse(&index_path, &repo.odb)
3103 .map_err(|_| Error::ObjectNotFound(format!(":{}:{}", colon.stage, path)))?;
3104 let entry = index
3105 .get(path.as_bytes(), colon.stage)
3106 .ok_or_else(|| Error::ObjectNotFound(format!(":{}:{}", colon.stage, path)))?;
3107 Ok(Some(IndexPathEntry {
3108 path,
3109 oid: entry.oid,
3110 mode: entry.mode,
3111 }))
3112}
3113
3114fn resolve_index_path_at_stage(repo: &Repository, path: &str, stage: u8) -> Result<ObjectId> {
3116 use crate::index::Index;
3117 let index_path = if let Ok(raw) = std::env::var("GIT_INDEX_FILE") {
3118 let p = std::path::PathBuf::from(raw);
3119 if p.is_absolute() {
3120 p
3121 } else if let Ok(cwd) = std::env::current_dir() {
3122 cwd.join(p)
3123 } else {
3124 p
3125 }
3126 } else {
3127 repo.index_path()
3128 };
3129 let index = Index::load_expand_sparse(&index_path, &repo.odb)
3130 .map_err(|_| Error::ObjectNotFound(format!(":{stage}:{path}")))?;
3131 match index.get(path.as_bytes(), stage) {
3132 Some(entry) => Ok(entry.oid),
3133 None => Err(Error::ObjectNotFound(format!(":{stage}:{path}"))),
3134 }
3135}
3136
3137pub fn split_treeish_colon(spec: &str) -> Option<(&str, &str)> {
3142 if spec.starts_with(':') {
3143 return None;
3144 }
3145 let bytes = spec.as_bytes();
3146 let mut i = 0usize;
3147 let mut peel_depth = 0usize;
3148 while i < bytes.len() {
3149 if i + 1 < bytes.len() && bytes[i] == b'^' && bytes[i + 1] == b'{' {
3150 peel_depth += 1;
3151 i += 2;
3152 continue;
3153 }
3154 if peel_depth > 0 {
3155 if bytes[i] == b'}' {
3156 peel_depth -= 1;
3157 }
3158 i += 1;
3159 continue;
3160 }
3161 if bytes[i] == b':' && i > 0 {
3162 let before = &spec[..i];
3163 let after = &spec[i + 1..];
3164 if !before.is_empty() {
3165 return Some((before, after)); }
3167 }
3168 i += 1;
3169 }
3170 None
3171}
3172
3173pub(crate) fn split_treeish_spec(spec: &str) -> Option<(&str, &str)> {
3174 split_treeish_colon(spec)
3175}
3176
3177pub(crate) fn resolve_treeish_path_to_object(
3181 repo: &Repository,
3182 treeish: ObjectId,
3183 path: &str,
3184) -> Result<ObjectId> {
3185 let object = repo.read_replaced(&treeish)?;
3186 let mut current_tree = match object.kind {
3187 ObjectKind::Commit => parse_commit(&object.data)?.tree,
3188 ObjectKind::Tree => treeish,
3189 _ => {
3190 return Err(Error::InvalidRef(format!(
3191 "object {treeish} does not name a tree"
3192 )))
3193 }
3194 };
3195
3196 let parts_vec: Vec<&str> = path.split('/').filter(|p| !p.is_empty()).collect();
3197 if parts_vec.is_empty() {
3198 return Ok(current_tree);
3199 }
3200 for (idx, part) in parts_vec.iter().enumerate() {
3201 let tree_object = repo.read_replaced(¤t_tree)?;
3202 if tree_object.kind != ObjectKind::Tree {
3203 return Err(Error::CorruptObject(format!(
3204 "object {current_tree} is not a tree"
3205 )));
3206 }
3207 let entries = parse_tree(&tree_object.data)?;
3208 let Some(entry) = entries.iter().find(|entry| entry.name == part.as_bytes()) else {
3209 return Err(Error::ObjectNotFound(path.to_owned()));
3210 };
3211 if idx + 1 == parts_vec.len() {
3212 return Ok(entry.oid);
3213 }
3214 if entry.mode != crate::index::MODE_TREE {
3215 return Err(Error::ObjectNotFound(path.to_owned()));
3216 }
3217 current_tree = entry.oid;
3218 }
3219
3220 Err(Error::ObjectNotFound(path.to_owned()))
3221}
3222
3223pub(crate) fn resolve_treeish_path(
3224 repo: &Repository,
3225 treeish: ObjectId,
3226 path: &str,
3227) -> Result<ObjectId> {
3228 resolve_treeish_path_to_object(repo, treeish, path)
3229}
3230
3231fn apply_peel(repo: &Repository, mut oid: ObjectId, peel: Option<&str>) -> Result<ObjectId> {
3232 match peel {
3233 None => Ok(oid),
3234 Some(search) if search.starts_with('/') => {
3235 let pattern = &search[1..];
3236 if pattern.is_empty() {
3237 return Err(Error::InvalidRef(
3238 "empty commit message search pattern".to_owned(),
3239 ));
3240 }
3241 resolve_commit_message_search_from(repo, oid, pattern)
3242 }
3243 Some("") => {
3244 while let Ok(obj) = repo.read_replaced(&oid) {
3245 if obj.kind != ObjectKind::Tag {
3246 break;
3247 }
3248 oid = parse_tag_target(&obj.data)?;
3249 }
3250 Ok(oid)
3251 }
3252 Some("commit") => {
3253 oid = apply_peel(repo, oid, Some(""))?;
3254 let obj = repo.read_replaced(&oid)?;
3255 if obj.kind == ObjectKind::Commit {
3256 Ok(oid)
3257 } else {
3258 Err(Error::InvalidRef("expected commit".to_owned()))
3259 }
3260 }
3261 Some("tree") => {
3262 oid = apply_peel(repo, oid, Some(""))?;
3264 let obj = repo.read_replaced(&oid)?;
3265 match obj.kind {
3266 ObjectKind::Tree => Ok(oid),
3267 ObjectKind::Commit => Ok(parse_commit(&obj.data)?.tree),
3268 _ => Err(Error::InvalidRef("expected tree or commit".to_owned())),
3269 }
3270 }
3271 Some("blob") => {
3272 let mut cur = oid;
3274 loop {
3275 let obj = repo.read_replaced(&cur)?;
3276 match obj.kind {
3277 ObjectKind::Blob => return Ok(cur),
3278 ObjectKind::Tag => {
3279 cur = parse_tag_target(&obj.data)?;
3280 }
3281 _ => return Err(Error::InvalidRef("expected blob".to_owned())),
3282 }
3283 }
3284 }
3285 Some("object") => Ok(oid),
3286 Some("tag") => {
3287 let obj = repo.read_replaced(&oid)?;
3289 if obj.kind == ObjectKind::Tag {
3290 Ok(oid)
3291 } else {
3292 Err(Error::InvalidRef("expected tag".to_owned()))
3293 }
3294 }
3295 Some(other) => Err(Error::InvalidRef(format!(
3296 "unsupported peel operator '{{{other}}}'"
3297 ))),
3298 }
3299}
3300
3301pub fn expand_rev_token_circ_bang(repo: &Repository, token: &str) -> Result<Vec<String>> {
3312 let Some(base) = token.strip_suffix("^!") else {
3313 return Ok(vec![token.to_owned()]);
3314 };
3315 if base.is_empty() {
3316 return Err(Error::Message(format!(
3317 "fatal: ambiguous argument '{token}': unknown revision or path not in the working tree.\n\
3318Use '--' to separate paths from revisions, like this:\n\
3319'git <command> [<revision>...] -- [<file>...]'"
3320 )));
3321 }
3322 let oid = resolve_revision_for_range_end(repo, base)?;
3323 let commit_oid = peel_to_commit_for_merge_base(repo, oid)?;
3324 let parents = commit_parents_for_navigation(repo, commit_oid)?;
3325 let mut out = vec![base.to_owned()];
3326 for p in parents {
3327 out.push(format!("^{}", p.to_hex()));
3328 }
3329 Ok(out)
3330}
3331
3332#[must_use]
3334pub fn parse_peel_suffix(spec: &str) -> (&str, Option<&str>) {
3335 if let Some(base) = spec.strip_suffix("^{}") {
3336 return (base, Some(""));
3337 }
3338 if let Some(start) = spec.rfind("^{") {
3339 if spec.ends_with('}') {
3340 let base = &spec[..start];
3341 let op = &spec[start + 2..spec.len() - 1];
3342 return (base, Some(op));
3343 }
3344 }
3345 if let Some(base) = spec.strip_suffix("^0") {
3347 if !base.ends_with('^') {
3350 return (base, Some("commit"));
3351 }
3352 }
3353 (spec, None)
3354}
3355
3356fn parse_tag_target(data: &[u8]) -> Result<ObjectId> {
3357 let text = std::str::from_utf8(data)
3358 .map_err(|_| Error::CorruptObject("invalid tag object".to_owned()))?;
3359 let Some(line) = text.lines().find(|line| line.starts_with("object ")) else {
3360 return Err(Error::CorruptObject("tag missing object header".to_owned()));
3361 };
3362 let oid_text = line.trim_start_matches("object ").trim();
3363 oid_text.parse::<ObjectId>()
3364}
3365
3366fn resolve_commit_message_search_from(
3369 repo: &Repository,
3370 start: ObjectId,
3371 pattern: &str,
3372) -> Result<ObjectId> {
3373 let regex = Regex::new(pattern).ok();
3375 let mut visited = std::collections::HashSet::new();
3376 let mut queue = std::collections::VecDeque::new();
3377 queue.push_back(start);
3378 visited.insert(start);
3379
3380 while let Some(oid) = queue.pop_front() {
3381 let obj = match repo.read_replaced(&oid) {
3382 Ok(o) => o,
3383 Err(_) => continue,
3384 };
3385 if obj.kind != ObjectKind::Commit {
3386 continue;
3387 }
3388 let commit = match parse_commit(&obj.data) {
3389 Ok(c) => c,
3390 Err(_) => continue,
3391 };
3392
3393 let is_match = if let Some(re) = ®ex {
3394 re.is_match(&commit.message)
3395 } else {
3396 commit.message.contains(pattern)
3397 };
3398 if is_match {
3399 return Ok(oid);
3400 }
3401
3402 for parent in &commit.parents {
3403 if visited.insert(*parent) {
3404 queue.push_back(*parent);
3405 }
3406 }
3407 }
3408
3409 Err(Error::ObjectNotFound(format!(":/{pattern}")))
3410}
3411
3412fn find_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
3413 if !is_hex_prefix(prefix) || !(4..=40).contains(&prefix.len()) {
3414 return Ok(Vec::new());
3415 }
3416 let mut seen = HashSet::new();
3417 let mut matches = Vec::new();
3418 for objects_dir in object_storage_dirs_for_abbrev(repo)? {
3419 for hex in collect_loose_object_ids_in_dir(&objects_dir)? {
3420 if hex.starts_with(prefix) {
3421 let oid = hex.parse::<ObjectId>()?;
3422 if seen.insert(oid) {
3423 matches.push(oid);
3424 }
3425 }
3426 }
3427 for oid in collect_pack_oids_with_prefix(&objects_dir, prefix)? {
3428 if seen.insert(oid) {
3429 matches.push(oid);
3430 }
3431 }
3432 }
3433 Ok(matches)
3434}
3435
3436fn collect_loose_object_ids(repo: &Repository) -> Result<Vec<String>> {
3437 collect_loose_object_ids_in_dir(repo.odb.objects_dir())
3438}
3439
3440fn collect_loose_object_ids_in_dir(objects_dir: &Path) -> Result<Vec<String>> {
3441 let mut ids = Vec::new();
3442 let read = match fs::read_dir(objects_dir) {
3443 Ok(read) => read,
3444 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(ids),
3445 Err(err) => return Err(Error::Io(err)),
3446 };
3447
3448 for dir_entry in read {
3449 let dir_entry = dir_entry?;
3450 let name = dir_entry.file_name();
3451 let Some(prefix) = name.to_str() else {
3452 continue;
3453 };
3454 if !is_two_hex(prefix) {
3455 continue;
3456 }
3457 if !dir_entry.file_type()?.is_dir() {
3458 continue;
3459 }
3460
3461 let files = fs::read_dir(dir_entry.path())?;
3462 for file_entry in files {
3463 let file_entry = file_entry?;
3464 if !file_entry.file_type()?.is_file() {
3465 continue;
3466 }
3467 let file_name = file_entry.file_name();
3468 let Some(suffix) = file_name.to_str() else {
3469 continue;
3470 };
3471 if suffix.len() == 38 && suffix.chars().all(|ch| ch.is_ascii_hexdigit()) {
3472 ids.push(format!("{prefix}{suffix}"));
3473 }
3474 }
3475 }
3476
3477 Ok(ids)
3478}
3479
3480fn is_two_hex(text: &str) -> bool {
3481 text.len() == 2 && text.chars().all(|ch| ch.is_ascii_hexdigit())
3482}
3483
3484fn is_hex_prefix(text: &str) -> bool {
3485 !text.is_empty() && text.chars().all(|ch| ch.is_ascii_hexdigit())
3486}
3487
3488fn path_is_within(path: &Path, container: &Path) -> bool {
3489 if path == container {
3490 return true;
3491 }
3492 path.starts_with(container)
3493}
3494
3495fn normalize_components(path: &Path) -> Vec<String> {
3496 path.components()
3497 .filter_map(|component| match component {
3498 Component::RootDir => Some(String::from("/")),
3499 Component::Normal(item) => Some(item.to_string_lossy().into_owned()),
3500 _ => None,
3501 })
3502 .collect()
3503}
3504
3505fn component_to_text(component: Component<'_>) -> Option<String> {
3506 match component {
3507 Component::Normal(item) => Some(os_to_string(item)),
3508 _ => None,
3509 }
3510}
3511
3512fn os_to_string(text: &OsStr) -> String {
3513 text.to_string_lossy().into_owned()
3514}
3515
3516fn resolve_commit_message_search(
3519 repo: &crate::repo::Repository,
3520 pattern: &str,
3521) -> Result<ObjectId> {
3522 let (negate, effective_pattern) = if pattern.starts_with('!') {
3524 if pattern.starts_with("!!") {
3525 (false, &pattern[1..]) } else {
3527 (true, &pattern[1..]) }
3529 } else {
3530 (false, pattern)
3531 };
3532 let regex = Regex::new(effective_pattern).ok();
3533 use crate::state::resolve_head;
3534 let head =
3535 resolve_head(&repo.git_dir).map_err(|_| Error::ObjectNotFound(format!(":/{pattern}")))?;
3536 let start_oid = match head.oid() {
3537 Some(oid) => *oid,
3538 None => return Err(Error::ObjectNotFound(format!(":/{pattern}"))),
3539 };
3540
3541 let mut visited = std::collections::HashSet::new();
3542 let mut queue = std::collections::VecDeque::new();
3543 queue.push_back(start_oid);
3544 visited.insert(start_oid);
3545
3546 while let Some(oid) = queue.pop_front() {
3547 let obj = match repo.read_replaced(&oid) {
3548 Ok(o) => o,
3549 Err(_) => continue,
3550 };
3551 if obj.kind != ObjectKind::Commit {
3553 continue;
3554 }
3555 let commit = match parse_commit(&obj.data) {
3556 Ok(c) => c,
3557 Err(_) => continue,
3558 };
3559
3560 let base_match = if let Some(re) = ®ex {
3562 re.is_match(&commit.message)
3563 } else {
3564 commit.message.contains(effective_pattern)
3565 };
3566 let is_match = if negate { !base_match } else { base_match };
3567 if is_match {
3568 return Ok(oid);
3569 }
3570
3571 for parent in &commit.parents {
3573 if visited.insert(*parent) {
3574 queue.push_back(*parent);
3575 }
3576 }
3577 }
3578
3579 Err(Error::ObjectNotFound(format!(":/{pattern}")))
3580}
3581
3582pub fn list_all_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
3584 find_abbrev_matches(repo, prefix)
3585}
3586
3587pub fn list_loose_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
3589 list_all_abbrev_matches(repo, prefix)
3590}
3591
3592#[cfg(test)]
3593mod superproject_path_tests {
3594 use super::superproject_work_tree_from_nested_git_modules;
3595 use std::path::PathBuf;
3596
3597 #[test]
3598 fn nested_modules_yields_superproject_work_tree() {
3599 let git_dir = PathBuf::from("/tmp/super/.git/modules/dir/modules/sub");
3600 assert_eq!(
3601 superproject_work_tree_from_nested_git_modules(&git_dir),
3602 Some(PathBuf::from("/tmp/super"))
3603 );
3604 }
3605
3606 #[test]
3607 fn non_nested_returns_none() {
3608 let git_dir = PathBuf::from("/tmp/repo/.git");
3609 assert!(superproject_work_tree_from_nested_git_modules(&git_dir).is_none());
3610 }
3611}