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