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 if remote_has_push_refspec(&config_content, &push_remote_name) {
366 return match push_refspec_mapped_tracking(&config_content, &push_remote_name, branch_short)
367 {
368 Some(mapped) => Ok(mapped),
369 None => Err(Error::Message(format!(
370 "fatal: push refspecs for '{push_remote_name}' do not include '{branch_short}'"
371 ))),
372 };
373 }
374
375 let push_default = parse_config_value(&config_content, "push", "default");
376 let push_default = push_default.as_deref().unwrap_or("simple");
377
378 if push_default == "nothing" {
379 return Err(Error::Message(
380 "fatal: push.default is nothing; no push destination".to_owned(),
381 ));
382 }
383
384 let current_tracking = format!("refs/remotes/{push_remote_name}/{branch_short}");
385
386 match push_default {
387 "upstream" => upstream_tracking.ok_or_else(|| {
388 Error::Message(format!(
389 "fatal: branch '{branch_short}' has no upstream for push.default upstream"
390 ))
391 }),
392 "simple" => {
393 if let Some(ref up) = upstream_tracking {
394 if up == ¤t_tracking
395 && refs::resolve_ref(&repo.git_dir, ¤t_tracking).is_ok()
396 {
397 return Ok(current_tracking);
398 }
399 }
400 Err(Error::Message(
401 "fatal: push.default simple: upstream and push ref differ".to_owned(),
402 ))
403 }
404 "current" | "matching" | _ => {
405 if refs::resolve_ref(&repo.git_dir, ¤t_tracking).is_ok() {
406 Ok(current_tracking)
407 } else if let Some(up) = upstream_tracking {
408 Ok(up)
409 } else {
410 Err(Error::Message(format!(
411 "fatal: no push tracking ref for branch '{branch_short}'"
412 )))
413 }
414 }
415 }
416}
417
418fn remote_push_refspecs(config_content: &str, remote_name: &str) -> Vec<String> {
420 let section = format!("[remote \"{remote_name}\"]");
421 let mut in_section = false;
422 let mut out = Vec::new();
423 for line in config_content.lines() {
424 let trimmed = line.trim();
425 if trimmed.starts_with('[') {
426 in_section = trimmed == section;
427 continue;
428 }
429 if !in_section {
430 continue;
431 }
432 if let Some(val) = trimmed
433 .strip_prefix("push = ")
434 .or_else(|| trimmed.strip_prefix("push="))
435 {
436 if let Some(spec) = val.split_whitespace().next() {
437 out.push(spec.trim().to_owned());
438 }
439 }
440 }
441 out
442}
443
444fn remote_has_push_refspec(config_content: &str, remote_name: &str) -> bool {
446 !remote_push_refspecs(config_content, remote_name).is_empty()
447}
448
449fn match_refname_with_pattern(pattern: &str, refname: &str, replacement: &str) -> Option<String> {
453 match (pattern.find('*'), replacement.find('*')) {
454 (Some(pstar), Some(vstar)) => {
455 let kprefix = &pattern[..pstar];
456 let ksuffix = &pattern[pstar + 1..];
457 if !refname.starts_with(kprefix) || !refname.ends_with(ksuffix) {
458 return None;
459 }
460 if refname.len() < kprefix.len() + ksuffix.len() {
461 return None;
462 }
463 let middle = &refname[kprefix.len()..refname.len() - ksuffix.len()];
464 Some(format!(
465 "{}{}{}",
466 &replacement[..vstar],
467 middle,
468 &replacement[vstar + 1..]
469 ))
470 }
471 (None, None) => {
472 if pattern == refname {
473 Some(replacement.to_owned())
474 } else {
475 None
476 }
477 }
478 _ => None,
480 }
481}
482
483fn push_refspec_mapped_tracking(
488 config_content: &str,
489 remote_name: &str,
490 branch_short: &str,
491) -> Option<String> {
492 let src = format!("refs/heads/{branch_short}");
493
494 let mut dst = None;
496 for spec in remote_push_refspecs(config_content, remote_name) {
497 let spec = spec.strip_prefix('+').unwrap_or(&spec);
498 let Some((left, right)) = spec.split_once(':') else {
499 continue;
500 };
501 if let Some(mapped) = match_refname_with_pattern(left.trim(), &src, right.trim()) {
502 dst = Some(mapped);
503 break;
504 }
505 }
506 let dst = dst?;
507
508 map_dest_to_tracking(config_content, remote_name, &dst)
510}
511
512fn map_dest_to_tracking(config_content: &str, remote_name: &str, dst: &str) -> Option<String> {
516 let section = format!("[remote \"{remote_name}\"]");
517 let mut in_section = false;
518 for line in config_content.lines() {
519 let trimmed = line.trim();
520 if trimmed.starts_with('[') {
521 in_section = trimmed == section;
522 continue;
523 }
524 if !in_section {
525 continue;
526 }
527 let Some(val) = trimmed
528 .strip_prefix("fetch = ")
529 .or_else(|| trimmed.strip_prefix("fetch="))
530 else {
531 continue;
532 };
533 let Some(spec) = val.split_whitespace().next() else {
534 continue;
535 };
536 let spec = spec.trim().strip_prefix('+').unwrap_or(spec.trim());
537 let Some((left, right)) = spec.split_once(':') else {
538 continue;
539 };
540 if let Some(mapped) = match_refname_with_pattern(left.trim(), dst, right.trim()) {
541 return Some(mapped);
542 }
543 }
544 dst.strip_prefix("refs/heads/")
546 .map(|rest| format!("refs/remotes/{remote_name}/{rest}"))
547}
548
549fn resolve_push_ref_name(repo: &Repository, base: &str) -> Result<String> {
550 let (branch_key, _display) = resolve_upstream_branch_context(repo, base)?;
551 resolve_push_full_ref_for_branch(repo, &branch_key)
552}
553
554fn resolve_upstream_branch_context(repo: &Repository, base: &str) -> Result<(String, String)> {
556 let base = if base == "HEAD" {
557 Cow::Borrowed("")
558 } else if base.starts_with("@{-") && base.ends_with('}') {
559 if let Ok(Some(b)) = expand_at_minus_to_branch_name(repo, base) {
560 Cow::Owned(b)
561 } else {
562 Cow::Borrowed(base)
563 }
564 } else {
565 Cow::Borrowed(base)
566 };
567 let base = base.as_ref();
568 let base = if base == "@" { "" } else { base };
569
570 if base.is_empty() {
571 let Some(head) = refs::read_head(&repo.git_dir)? else {
572 return Err(Error::Message(
573 "fatal: HEAD does not point to a branch".to_owned(),
574 ));
575 };
576 let Some(short) = head.strip_prefix("refs/heads/") else {
577 return Err(Error::Message(
578 "fatal: HEAD does not point to a branch".to_owned(),
579 ));
580 };
581 return Ok((short.to_owned(), short.to_owned()));
582 }
583 let head_branch = refs::read_head(&repo.git_dir)?.and_then(|h| {
584 h.strip_prefix("refs/heads/")
585 .map(std::borrow::ToOwned::to_owned)
586 });
587 if head_branch.as_deref() == Some(base) {
588 return Ok((base.to_owned(), base.to_owned()));
589 }
590 let refname = format!("refs/heads/{base}");
591 if refs::resolve_ref(&repo.git_dir, &refname).is_err() {
592 return Err(Error::Message(format!("fatal: no such branch: '{base}'")));
593 }
594 Ok((base.to_owned(), base.to_owned()))
595}
596
597fn parse_config_value(config: &str, section: &str, key: &str) -> Option<String> {
598 let section_header = format!("[{}]", section);
599 let key_lower = key.to_ascii_lowercase();
600 let mut in_section = false;
601 for line in config.lines() {
602 let trimmed = line.trim();
603 if trimmed.starts_with('[') {
604 in_section = trimmed.eq_ignore_ascii_case(§ion_header);
605 continue;
606 }
607 if in_section {
608 let lower = trimmed.to_ascii_lowercase();
609 if lower.starts_with(&key_lower) {
610 let rest = lower[key_lower.len()..].trim_start().to_string();
611 if rest.starts_with('=') {
612 if let Some(eq_pos) = trimmed.find('=') {
613 return Some(trimmed[eq_pos + 1..].trim().to_owned());
614 }
615 }
616 }
617 }
618 }
619 None
620}
621
622fn parse_branch_tracking(config: &str, branch: &str) -> Option<(String, String)> {
624 let mut remote = None;
625 let mut merge = None;
626 let mut in_section = false;
627 let target_section = format!("[branch \"{}\"]", branch);
628
629 for line in config.lines() {
630 let trimmed = line.trim();
631 if trimmed.starts_with('[') {
632 in_section = trimmed == target_section
633 || trimmed.starts_with(&format!("[branch \"{}\"", branch));
634 continue;
635 }
636 if !in_section {
637 continue;
638 }
639 if let Some(value) = trimmed.strip_prefix("remote = ") {
640 remote = Some(value.trim().to_owned());
641 } else if let Some(value) = trimmed.strip_prefix("merge = ") {
642 merge = Some(value.trim().to_owned());
643 }
644 if let Some(value) = trimmed.strip_prefix("remote=") {
646 remote = Some(value.trim().to_owned());
647 } else if let Some(value) = trimmed.strip_prefix("merge=") {
648 merge = Some(value.trim().to_owned());
649 }
650 }
651
652 match (remote, merge) {
653 (Some(r), Some(m)) => Some((r, m)),
654 _ => None,
655 }
656}
657
658#[must_use]
675pub fn load_graft_parents(git_dir: &Path) -> HashMap<ObjectId, Vec<ObjectId>> {
679 let graft_path = crate::repo::common_git_dir_for_config(git_dir).join("info/grafts");
680 let mut grafts = HashMap::new();
681 let Ok(contents) = fs::read_to_string(&graft_path) else {
682 return grafts;
683 };
684 for raw_line in contents.lines() {
685 let line = raw_line.trim();
686 if line.is_empty() || line.starts_with('#') {
687 continue;
688 }
689 let mut fields = line.split_whitespace();
690 let Some(commit_hex) = fields.next() else {
691 continue;
692 };
693 let Ok(commit_oid) = commit_hex.parse::<ObjectId>() else {
694 continue;
695 };
696 let mut parents = Vec::new();
697 let mut valid = true;
698 for parent_hex in fields {
699 match parent_hex.parse::<ObjectId>() {
700 Ok(parent_oid) => parents.push(parent_oid),
701 Err(_) => {
702 valid = false;
703 break;
704 }
705 }
706 }
707 if valid {
708 grafts.insert(commit_oid, parents);
709 }
710 }
711 grafts
712}
713
714pub fn commit_parents_for_navigation(
716 repo: &Repository,
717 commit_oid: ObjectId,
718) -> Result<Vec<ObjectId>> {
719 let obj = repo.odb.read(&commit_oid)?;
720 if obj.kind != ObjectKind::Commit {
721 return Err(Error::InvalidRef(format!(
722 "invalid ref: {commit_oid} is not a commit"
723 )));
724 }
725 let commit = parse_commit(&obj.data)?;
726 let mut parents = commit.parents;
727 let grafts = load_graft_parents(&repo.git_dir);
728 if let Some(grafted) = grafts.get(&commit_oid) {
729 parents = grafted.clone();
730 }
731 Ok(parents)
732}
733
734#[derive(Debug, Clone, Copy)]
735enum ParentShorthandKind {
736 At,
738 Bang,
740 Minus { exclude_parent: usize },
742}
743
744#[must_use]
746pub fn spec_has_parent_shorthand_suffix(spec: &str) -> bool {
747 find_parent_shorthand(spec).is_some()
748}
749
750fn find_parent_shorthand(spec: &str) -> Option<(usize, ParentShorthandKind)> {
751 let mut best: Option<(usize, ParentShorthandKind, u8)> = None;
752 for (idx, _) in spec.match_indices('^') {
753 let Some(tail) = spec.get(idx + 1..) else {
754 continue;
755 };
756 if tail.starts_with('@') && idx + 2 == spec.len() {
757 best = Some((idx, ParentShorthandKind::At, 0));
758 break;
759 }
760 if tail.starts_with('!') && idx + 2 == spec.len() {
761 let cand = (idx, ParentShorthandKind::Bang, 1);
762 best = Some(match best {
763 Some(b) if b.2 < 1 => b,
764 _ => cand,
765 });
766 continue;
767 }
768 if let Some(after) = tail.strip_prefix('-') {
769 let (exclude_parent, valid) = if after.is_empty() {
770 (1usize, true)
771 } else if after.bytes().all(|b| b.is_ascii_digit()) && !after.is_empty() {
772 let n: usize = after.parse().unwrap_or(0);
773 (n, n >= 1)
774 } else {
775 (0, false)
776 };
777 if !valid {
778 continue;
779 }
780 let cand = (idx, ParentShorthandKind::Minus { exclude_parent }, 2);
781 best = Some(match best {
782 Some(b) if b.2 < 2 => b,
783 _ => cand,
784 });
785 }
786 }
787 best.map(|(i, k, _)| (i, k))
788}
789
790pub fn expand_parent_shorthand_rev_parse_lines(
798 repo: &Repository,
799 spec: &str,
800 symbolic: bool,
801 short_len: Option<usize>,
802) -> Result<Option<Vec<String>>> {
803 let Some((mark_idx, kind)) = find_parent_shorthand(spec) else {
804 return Ok(None);
805 };
806 let base_spec = &spec[..mark_idx];
807 let base_for_resolve = if base_spec.is_empty() {
808 "HEAD"
809 } else {
810 base_spec
811 };
812 let symbolic_base = if base_spec.is_empty() {
815 "HEAD"
816 } else {
817 base_spec
818 };
819 let tip_oid = resolve_revision_for_range_end(repo, base_for_resolve)?;
820 let commit_oid = peel_to_commit_for_merge_base(repo, tip_oid)?;
821 let parents = commit_parents_for_navigation(repo, commit_oid)?;
822
823 let mut out = Vec::new();
824 match kind {
825 ParentShorthandKind::At => {
826 if parents.is_empty() {
827 return Ok(Some(out));
828 }
829 for (i, p) in parents.iter().enumerate() {
830 let parent_n = i + 1;
831 if symbolic {
832 out.push(format!("{symbolic_base}^{parent_n}"));
833 } else if let Some(len) = short_len {
834 out.push(abbreviate_object_id(repo, *p, len)?);
835 } else {
836 out.push(p.to_string());
837 }
838 }
839 }
840 ParentShorthandKind::Bang => {
841 if parents.is_empty() {
842 if symbolic {
843 out.push(symbolic_base.to_string());
844 } else if let Some(len) = short_len {
845 out.push(abbreviate_object_id(repo, commit_oid, len)?);
846 } else {
847 out.push(commit_oid.to_string());
848 }
849 return Ok(Some(out));
850 }
851 if symbolic {
852 out.push(symbolic_base.to_string());
853 for (i, _) in parents.iter().enumerate() {
854 let parent_n = i + 1;
855 out.push(format!("^{symbolic_base}^{parent_n}"));
856 }
857 } else if let Some(len) = short_len {
858 out.push(abbreviate_object_id(repo, commit_oid, len)?);
859 for p in &parents {
860 out.push(format!("^{}", abbreviate_object_id(repo, *p, len)?));
861 }
862 } else {
863 out.push(commit_oid.to_string());
864 for p in &parents {
865 out.push(format!("^{p}"));
866 }
867 }
868 }
869 ParentShorthandKind::Minus { exclude_parent } => {
870 if exclude_parent > parents.len() {
871 return Ok(None);
872 }
873 let excluded_parent = parents[exclude_parent - 1];
874 if symbolic {
875 out.push(symbolic_base.to_string());
876 out.push(format!("^{symbolic_base}^{exclude_parent}"));
877 } else if let Some(len) = short_len {
878 out.push(abbreviate_object_id(repo, commit_oid, len)?);
879 out.push(format!(
880 "^{}",
881 abbreviate_object_id(repo, excluded_parent, len)?
882 ));
883 } else {
884 out.push(commit_oid.to_string());
885 out.push(format!("^{excluded_parent}"));
886 }
887 }
888 }
889 Ok(Some(out))
890}
891
892pub fn split_double_dot_range(spec: &str) -> Option<(&str, &str)> {
893 if spec == ".." {
894 return Some(("", ""));
895 }
896 let bytes = spec.as_bytes();
897 let mut search = 0usize;
898 while let Some(rel) = spec[search..].find("..") {
899 let idx = search + rel;
900 let touches_dot_before = idx > 0 && bytes[idx - 1] == b'.';
902 let touches_dot_after = idx + 2 < bytes.len() && bytes[idx + 2] == b'.';
903 if touches_dot_before || touches_dot_after {
904 search = idx + 1;
905 continue;
906 }
907 if idx + 2 < bytes.len() && (bytes[idx + 2] == b'/' || bytes[idx + 2] == b'\\') {
909 search = idx + 1;
910 continue;
911 }
912 let left = &spec[..idx];
913 let right = &spec[idx + 2..];
914 return Some((left, right));
915 }
916 None
917}
918
919#[must_use]
923pub fn split_triple_dot_range(spec: &str) -> Option<(&str, &str)> {
924 if spec == "..." {
925 return Some(("", ""));
926 }
927 let bytes = spec.as_bytes();
928 let mut search = 0usize;
929 while let Some(rel) = spec[search..].find("...") {
930 let idx = search + rel;
931 let four_before = idx >= 1 && bytes[idx - 1] == b'.';
932 let four_after = idx + 3 < bytes.len() && bytes[idx + 3] == b'.';
933 if four_before || four_after {
934 search = idx + 1;
935 continue;
936 }
937 let left = &spec[..idx];
938 let right = &spec[idx + 3..];
939 return Some((left, right));
940 }
941 None
942}
943
944pub fn resolve_revision_without_index_dwim(repo: &Repository, spec: &str) -> Result<ObjectId> {
947 resolve_revision_impl(repo, spec, false, false, true, false, false, false, false)
948}
949
950pub fn resolve_revision(repo: &Repository, spec: &str) -> Result<ObjectId> {
952 resolve_revision_impl(repo, spec, true, false, true, false, false, false, true)
953}
954
955pub fn resolve_revision_for_checkout_guess(
958 repo: &Repository,
959 spec: &str,
960 remote_branch_guess: bool,
961) -> Result<ObjectId> {
962 resolve_revision_impl(
963 repo,
964 spec,
965 true,
966 false,
967 true,
968 false,
969 false,
970 false,
971 remote_branch_guess,
972 )
973}
974
975pub fn resolve_revision_for_range_end(repo: &Repository, spec: &str) -> Result<ObjectId> {
978 resolve_revision_impl(repo, spec, true, true, true, false, false, false, true)
979}
980
981pub fn resolve_revision_for_range_end_without_index_dwim(
988 repo: &Repository,
989 spec: &str,
990) -> Result<ObjectId> {
991 resolve_revision_impl(repo, spec, false, true, true, false, false, false, true)
992}
993
994pub fn resolve_revision_for_verify(repo: &Repository, spec: &str) -> Result<ObjectId> {
999 resolve_revision_impl(repo, spec, false, true, true, false, false, false, true)
1000}
1001
1002pub fn resolve_revision_for_commit_tree_tree(repo: &Repository, spec: &str) -> Result<ObjectId> {
1004 resolve_revision_impl(repo, spec, true, false, true, false, true, false, true)
1005}
1006
1007pub fn resolve_revision_for_patch_old_blob(repo: &Repository, spec: &str) -> Result<ObjectId> {
1009 resolve_revision_impl(repo, spec, true, false, true, false, false, true, true)
1010}
1011
1012pub fn try_parse_double_dot_log_range(
1022 repo: &Repository,
1023 spec: &str,
1024) -> Result<Option<(ObjectId, ObjectId)>> {
1025 let Some((left, right)) = split_double_dot_range(spec) else {
1026 return Ok(None);
1027 };
1028 let left_tip = if left.is_empty() {
1029 resolve_revision_for_range_end(repo, "HEAD")?
1030 } else {
1031 resolve_revision_for_range_end(repo, left)?
1032 };
1033 let right_tip = if right.is_empty() {
1034 resolve_revision_for_range_end(repo, "HEAD")?
1035 } else {
1036 resolve_revision_for_range_end(repo, right)?
1037 };
1038 let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
1039 let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
1040 Ok(Some((left_c, right_c)))
1041}
1042
1043fn try_parse_double_dot_log_range_without_index_dwim(
1044 repo: &Repository,
1045 spec: &str,
1046) -> Result<Option<(ObjectId, ObjectId)>> {
1047 let Some((left, right)) = split_double_dot_range(spec) else {
1048 return Ok(None);
1049 };
1050 let left_tip = if left.is_empty() {
1051 resolve_revision_for_range_end_without_index_dwim(repo, "HEAD")?
1052 } else {
1053 resolve_revision_for_range_end_without_index_dwim(repo, left)?
1054 };
1055 let right_tip = if right.is_empty() {
1056 resolve_revision_for_range_end_without_index_dwim(repo, "HEAD")?
1057 } else {
1058 resolve_revision_for_range_end_without_index_dwim(repo, right)?
1059 };
1060 let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
1061 let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
1062 Ok(Some((left_c, right_c)))
1063}
1064
1065#[must_use]
1075pub fn revision_spec_contains_ancestry_navigation(spec: &str) -> bool {
1076 let (_, steps) = parse_nav_steps(spec);
1077 !steps.is_empty()
1078}
1079
1080pub fn resolve_revision_as_commit(repo: &Repository, spec: &str) -> Result<ObjectId> {
1081 if let Some((left, right)) = split_triple_dot_range(spec) {
1082 let left_tip = if left.is_empty() {
1083 resolve_revision_for_range_end(repo, "HEAD")?
1084 } else {
1085 resolve_revision_for_range_end(repo, left)?
1086 };
1087 let right_tip = if right.is_empty() {
1088 resolve_revision_for_range_end(repo, "HEAD")?
1089 } else {
1090 resolve_revision_for_range_end(repo, right)?
1091 };
1092 let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
1093 let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
1094 let bases = crate::merge_base::merge_bases_first_vs_rest(repo, left_c, &[right_c])?;
1095 return bases
1096 .into_iter()
1097 .next()
1098 .ok_or_else(|| Error::ObjectNotFound(format!("no merge base for '{spec}'")));
1099 }
1100 if let Some((_excl, tip)) = try_parse_double_dot_log_range(repo, spec)? {
1101 return Ok(tip);
1102 }
1103 let oid = resolve_revision_for_range_end(repo, spec)?;
1104 peel_to_commit_for_merge_base(repo, oid)
1105}
1106
1107pub fn resolve_revision_as_commit_without_index_dwim(
1112 repo: &Repository,
1113 spec: &str,
1114) -> Result<ObjectId> {
1115 if let Some((left, right)) = split_triple_dot_range(spec) {
1116 let left_tip = if left.is_empty() {
1117 resolve_revision_for_range_end_without_index_dwim(repo, "HEAD")?
1118 } else {
1119 resolve_revision_for_range_end_without_index_dwim(repo, left)?
1120 };
1121 let right_tip = if right.is_empty() {
1122 resolve_revision_for_range_end_without_index_dwim(repo, "HEAD")?
1123 } else {
1124 resolve_revision_for_range_end_without_index_dwim(repo, right)?
1125 };
1126 let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
1127 let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
1128 let bases = crate::merge_base::merge_bases_first_vs_rest(repo, left_c, &[right_c])?;
1129 return bases
1130 .into_iter()
1131 .next()
1132 .ok_or_else(|| Error::ObjectNotFound(format!("no merge base for '{spec}'")));
1133 }
1134 if let Some((_excl, tip)) = try_parse_double_dot_log_range_without_index_dwim(repo, spec)? {
1135 return Ok(tip);
1136 }
1137 let oid = resolve_revision_for_range_end_without_index_dwim(repo, spec)?;
1138 peel_to_commit_for_merge_base(repo, oid)
1139}
1140
1141fn resolve_ref_dwim_for_rev_parse(repo: &Repository, spec: &str) -> (usize, Option<ObjectId>) {
1142 const RULES: &[&str] = &[
1143 "{0}",
1144 "refs/{0}",
1145 "refs/tags/{0}",
1146 "refs/heads/{0}",
1147 "refs/remotes/{0}",
1148 "refs/remotes/{0}/HEAD",
1149 ];
1150
1151 let mut count = 0usize;
1152 let mut first = None;
1153 let refname_opts = RefNameOptions::default();
1154 for rule in RULES {
1155 let candidate = rule.replace("{0}", spec);
1156 if let Ok(Some(target)) = refs::read_symbolic_ref(&repo.git_dir, &candidate) {
1157 if check_refname_format(&target, &refname_opts).is_err()
1158 || refs::resolve_ref(&repo.git_dir, &target).is_err()
1159 {
1160 if candidate != "HEAD" {
1165 eprintln!("warning: ignoring dangling symref {candidate}");
1166 }
1167 continue;
1168 }
1169 }
1170 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &candidate) {
1171 count += 1;
1172 if first.is_none() {
1173 first = Some(oid);
1174 }
1175 }
1176 }
1177 (count, first)
1178}
1179
1180fn resolve_revision_impl(
1181 repo: &Repository,
1182 spec: &str,
1183 index_dwim: bool,
1184 commit_only_hex: bool,
1185 use_disambiguate_config: bool,
1186 treeish_colon_lhs: bool,
1187 implicit_tree_abbrev: bool,
1188 implicit_blob_abbrev: bool,
1189 remote_branch_name_guess: bool,
1190) -> Result<ObjectId> {
1191 if let Some(pattern) = spec.strip_prefix(":/") {
1194 if pattern.is_empty() {
1195 let head = crate::state::resolve_head(&repo.git_dir)
1197 .map_err(|_| Error::ObjectNotFound(":/".to_owned()))?;
1198 return head
1199 .oid()
1200 .copied()
1201 .ok_or_else(|| Error::ObjectNotFound(":/".to_owned()));
1202 }
1203 return resolve_commit_message_search(repo, pattern);
1204 }
1205
1206 if let Some(index_spec) = parse_index_colon_spec(spec) {
1207 let path = normalize_colon_path_for_tree(repo, index_spec.raw_path)?;
1208 return resolve_index_path_at_stage(repo, &path, index_spec.stage)
1209 .map_err(|e| diagnose_index_path_error(repo, &path, index_spec.stage, e));
1210 }
1211
1212 if let Some(tag_path) = spec.strip_prefix("tags/") {
1214 if !tag_path.is_empty() {
1215 let tag_ref = format!("refs/tags/{tag_path}");
1216 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &tag_ref) {
1217 return Ok(oid);
1218 }
1219 }
1220 }
1221
1222 if spec == "AUTO_MERGE" {
1224 let raw = fs::read_to_string(repo.git_dir.join("AUTO_MERGE"))
1225 .map_err(|e| Error::Message(format!("failed to read AUTO_MERGE: {e}")))?;
1226 let line = raw.lines().next().unwrap_or("").trim();
1227 return line
1228 .parse::<ObjectId>()
1229 .map_err(|_| Error::InvalidRef("AUTO_MERGE: invalid object id".to_owned()));
1230 }
1231
1232 if spec.starts_with("refs/") && !spec.contains(':') {
1236 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, spec) {
1237 return Ok(oid);
1238 }
1239 }
1240
1241 if let Some(idx) = spec.find("...") {
1244 let left_raw = &spec[..idx];
1245 let right_raw = &spec[idx + 3..];
1246 if !left_raw.is_empty() || !right_raw.is_empty() {
1247 let left_oid = peel_to_commit_for_merge_base(
1248 repo,
1249 if left_raw.is_empty() {
1250 resolve_revision_impl(
1251 repo,
1252 "HEAD",
1253 index_dwim,
1254 commit_only_hex,
1255 use_disambiguate_config,
1256 false,
1257 false,
1258 false,
1259 remote_branch_name_guess,
1260 )?
1261 } else {
1262 resolve_revision_impl(
1263 repo,
1264 left_raw,
1265 index_dwim,
1266 commit_only_hex,
1267 use_disambiguate_config,
1268 false,
1269 false,
1270 false,
1271 remote_branch_name_guess,
1272 )?
1273 },
1274 )?;
1275 let right_oid = peel_to_commit_for_merge_base(
1276 repo,
1277 if right_raw.is_empty() {
1278 resolve_revision_impl(
1279 repo,
1280 "HEAD",
1281 index_dwim,
1282 commit_only_hex,
1283 use_disambiguate_config,
1284 false,
1285 false,
1286 false,
1287 remote_branch_name_guess,
1288 )?
1289 } else {
1290 resolve_revision_impl(
1291 repo,
1292 right_raw,
1293 index_dwim,
1294 commit_only_hex,
1295 use_disambiguate_config,
1296 false,
1297 false,
1298 false,
1299 remote_branch_name_guess,
1300 )?
1301 },
1302 )?;
1303 let bases = crate::merge_base::merge_bases_first_vs_rest(repo, left_oid, &[right_oid])?;
1304 return bases
1305 .into_iter()
1306 .next()
1307 .ok_or_else(|| Error::ObjectNotFound(format!("no merge base for '{spec}'")));
1308 }
1309 }
1310
1311 if let Some((before, after)) = split_treeish_colon(spec) {
1315 if !before.is_empty() && !spec.starts_with(":/") {
1316 let rev_oid = match resolve_revision_impl(
1318 repo,
1319 before,
1320 index_dwim,
1321 commit_only_hex,
1322 use_disambiguate_config,
1323 true,
1324 false,
1325 false,
1326 remote_branch_name_guess,
1327 ) {
1328 Ok(o) => o,
1329 Err(Error::ObjectNotFound(s)) if s == before => {
1330 return Err(Error::Message(format!(
1331 "fatal: invalid object name '{before}'."
1332 )));
1333 }
1334 Err(Error::Message(msg)) if msg.contains("ambiguous argument") => {
1335 return Err(Error::Message(format!(
1336 "fatal: invalid object name '{before}'."
1337 )));
1338 }
1339 Err(e) => return Err(e),
1340 };
1341 let tree_oid = peel_to_tree(repo, rev_oid)?;
1342 if after.is_empty() {
1343 return Ok(tree_oid);
1345 }
1346 let clean_path = match normalize_colon_path_for_tree(repo, after) {
1347 Ok(p) => p,
1348 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
1349 let wt = repo
1350 .work_tree
1351 .as_ref()
1352 .and_then(|p| p.canonicalize().ok())
1353 .map(|p| p.display().to_string())
1354 .unwrap_or_default();
1355 return Err(Error::Message(format!(
1356 "fatal: '{after}' is outside repository at '{wt}'"
1357 )));
1358 }
1359 Err(e) => return Err(e),
1360 };
1361 return resolve_tree_path_rev_parse(repo, &tree_oid, &clean_path)
1362 .map_err(|e| diagnose_tree_path_error(repo, before, after, &clean_path, e));
1363 }
1364 }
1365
1366 let (base_with_nav, peel) = parse_peel_suffix(spec);
1367 let (base, nav_steps) = parse_nav_steps(base_with_nav);
1368 let peel_for_hex = peel
1369 .or(((treeish_colon_lhs || implicit_tree_abbrev) && peel.is_none()).then_some("tree"))
1370 .or((implicit_blob_abbrev && peel.is_none()).then_some("blob"));
1371 let mut oid = resolve_base(
1372 repo,
1373 base,
1374 index_dwim,
1375 commit_only_hex,
1376 use_disambiguate_config,
1377 peel_for_hex,
1378 implicit_tree_abbrev,
1379 implicit_blob_abbrev,
1380 remote_branch_name_guess,
1381 )?;
1382 for step in nav_steps {
1383 oid = apply_nav_step(repo, oid, step).map_err(|e| {
1384 if matches!(e, Error::ObjectNotFound(_)) {
1385 Error::Message(format!(
1386 "fatal: ambiguous argument '{spec}': unknown revision or path not in the working tree.\n\
1387Use '--' to separate paths from revisions, like this:\n\
1388'git <command> [<revision>...] -- [<file>...]'"
1389 ))
1390 } else {
1391 e
1392 }
1393 })?;
1394 }
1395 apply_peel(repo, oid, peel)
1396}
1397
1398fn normalize_path_components(path: PathBuf) -> PathBuf {
1401 let mut out = PathBuf::new();
1402 for c in path.components() {
1403 match c {
1404 Component::Prefix(_) | Component::RootDir => out.push(c),
1405 Component::CurDir => {}
1406 Component::ParentDir => {
1407 let _ = out.pop();
1408 }
1409 Component::Normal(x) => out.push(x),
1410 }
1411 }
1412 out
1413}
1414
1415fn normalize_colon_path_for_bare_tree(raw_path: &str) -> Result<String> {
1420 let cwd_relative = raw_path.starts_with("./") || raw_path.starts_with("../") || raw_path == ".";
1421 if cwd_relative {
1422 return Err(Error::InvalidRef(
1423 "relative path syntax can't be used outside working tree".to_owned(),
1424 ));
1425 }
1426 let s = raw_path.trim_start_matches('/');
1427 let mut stack: Vec<&str> = Vec::new();
1428 for part in s.split('/') {
1429 if part.is_empty() || part == "." {
1430 continue;
1431 }
1432 if part == ".." {
1433 let _ = stack.pop();
1434 } else {
1435 stack.push(part);
1436 }
1437 }
1438 Ok(stack.join("/"))
1439}
1440
1441fn normalize_colon_path_for_tree(repo: &Repository, raw_path: &str) -> Result<String> {
1442 let Some(work_tree) = repo.work_tree.as_ref() else {
1443 return normalize_colon_path_for_bare_tree(raw_path);
1444 };
1445
1446 let cwd = std::env::current_dir().map_err(Error::Io)?;
1447 let wt_canon = work_tree.canonicalize().map_err(Error::Io)?;
1448
1449 let cwd_relative = raw_path.starts_with("./") || raw_path.starts_with("../") || raw_path == ".";
1450 if cwd_relative && !path_is_within(&cwd, work_tree) {
1451 return Err(Error::InvalidRef(
1452 "relative path syntax can't be used outside working tree".to_owned(),
1453 ));
1454 }
1455
1456 let full = if raw_path.starts_with('/') {
1458 PathBuf::from(raw_path)
1459 } else if cwd_relative {
1460 cwd.join(raw_path)
1461 } else {
1462 work_tree.join(raw_path)
1463 };
1464 let full = normalize_path_components(full);
1465
1466 if !path_is_within(&full, &wt_canon) {
1467 return Err(Error::InvalidRef("outside repository".to_owned()));
1468 }
1469 let rel = full
1470 .strip_prefix(&wt_canon)
1471 .map_err(|_| Error::InvalidRef("outside repository".to_owned()))?;
1472 let s = rel.to_string_lossy().replace('\\', "/");
1473 Ok(s.trim_end_matches('/').to_owned())
1474}
1475
1476pub fn peel_to_commit_for_merge_base(repo: &Repository, mut oid: ObjectId) -> Result<ObjectId> {
1478 oid = apply_peel(repo, oid, Some(""))?;
1479 let obj = repo.read_replaced(&oid)?;
1480 match obj.kind {
1481 ObjectKind::Commit => Ok(oid),
1482 ObjectKind::Tree => Err(Error::InvalidRef(format!(
1483 "object {oid} does not name a commit"
1484 ))),
1485 ObjectKind::Blob => Err(Error::InvalidRef(format!(
1486 "object {oid} does not name a commit"
1487 ))),
1488 ObjectKind::Tag => Err(Error::InvalidRef("unexpected tag after peel".to_owned())),
1489 }
1490}
1491
1492pub fn try_peel_to_commit_for_merge_base(
1495 repo: &Repository,
1496 oid: ObjectId,
1497) -> Result<Option<ObjectId>> {
1498 let oid = apply_peel(repo, oid, Some(""))?;
1499 let obj = repo.odb.read(&oid)?;
1500 match obj.kind {
1501 ObjectKind::Commit => Ok(Some(oid)),
1502 ObjectKind::Tree | ObjectKind::Blob => Ok(None),
1503 ObjectKind::Tag => Err(Error::InvalidRef("unexpected tag after peel".to_owned())),
1504 }
1505}
1506
1507pub fn peel_to_tree(repo: &Repository, oid: ObjectId) -> Result<ObjectId> {
1513 let obj = repo.read_replaced(&oid)?;
1514 match obj.kind {
1515 crate::objects::ObjectKind::Tree => Ok(oid),
1516 crate::objects::ObjectKind::Commit => {
1517 let commit = crate::objects::parse_commit(&obj.data)?;
1518 Ok(commit.tree)
1519 }
1520 crate::objects::ObjectKind::Tag => {
1521 let tag = crate::objects::parse_tag(&obj.data)?;
1522 peel_to_tree(repo, tag.object)
1523 }
1524 _ => Err(Error::ObjectNotFound(format!(
1525 "cannot peel {} to tree",
1526 oid
1527 ))),
1528 }
1529}
1530
1531fn resolve_tree_path(repo: &Repository, tree_oid: &ObjectId, path: &str) -> Result<ObjectId> {
1537 resolve_treeish_path_to_object(repo, *tree_oid, path)
1538}
1539
1540fn resolve_tree_path_rev_parse(
1542 repo: &Repository,
1543 tree_oid: &ObjectId,
1544 path: &str,
1545) -> Result<ObjectId> {
1546 let obj = repo.odb.read(tree_oid)?;
1547 let entries = crate::objects::parse_tree(&obj.data)?;
1548 let components: Vec<&str> = path.split('/').filter(|c| !c.is_empty()).collect();
1549 if components.is_empty() {
1550 return Err(Error::InvalidRef(format!(
1551 "path '{path}' does not name an object in tree {tree_oid}"
1552 )));
1553 }
1554
1555 let first = components[0];
1556 let rest: Vec<&str> = components[1..].to_vec();
1557 for entry in entries {
1558 let name = String::from_utf8_lossy(&entry.name);
1559 if name == first {
1560 if rest.is_empty() {
1561 return Ok(entry.oid);
1567 }
1568 if entry.mode != crate::index::MODE_TREE {
1569 return Err(Error::ObjectNotFound(path.to_owned()));
1570 }
1571 return resolve_tree_path_rev_parse(repo, &entry.oid, &rest.join("/"));
1572 }
1573 }
1574 Err(Error::ObjectNotFound(format!(
1575 "path '{path}' not found in tree {tree_oid}"
1576 )))
1577}
1578
1579#[derive(Debug, Clone)]
1583pub struct TreeishBlobAtPath {
1584 pub path: String,
1586 pub oid: ObjectId,
1588 pub mode: String,
1590}
1591
1592pub fn resolve_treeish_blob_at_path(repo: &Repository, spec: &str) -> Result<TreeishBlobAtPath> {
1597 let (before, after) = split_treeish_colon(spec)
1598 .ok_or_else(|| Error::InvalidRef(format!("'{spec}' is not a treeish:path revision")))?;
1599
1600 let rev_oid =
1601 match resolve_revision_impl(repo, before, true, false, true, true, false, false, true) {
1602 Ok(o) => o,
1603 Err(Error::ObjectNotFound(s)) if s == before => {
1604 return Err(Error::Message(format!(
1605 "fatal: invalid object name '{before}'."
1606 )));
1607 }
1608 Err(Error::Message(msg)) if msg.contains("ambiguous argument") => {
1609 return Err(Error::Message(format!(
1610 "fatal: invalid object name '{before}'."
1611 )));
1612 }
1613 Err(e) => return Err(e),
1614 };
1615
1616 let tree_oid = peel_to_tree(repo, rev_oid)?;
1617
1618 if after.is_empty() {
1620 return Ok(TreeishBlobAtPath {
1621 path: String::new(),
1622 oid: tree_oid,
1623 mode: "040000".to_string(),
1624 });
1625 }
1626
1627 let clean_path = match normalize_colon_path_for_tree(repo, after) {
1628 Ok(p) => p,
1629 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
1630 let wt = repo
1631 .work_tree
1632 .as_ref()
1633 .and_then(|p| p.canonicalize().ok())
1634 .map(|p| p.display().to_string())
1635 .unwrap_or_default();
1636 return Err(Error::Message(format!(
1637 "fatal: '{after}' is outside repository at '{wt}'"
1638 )));
1639 }
1640 Err(e) => return Err(e),
1641 };
1642
1643 let (oid, mode_str) = walk_tree_to_blob_entry(repo, &tree_oid, &clean_path)
1644 .map_err(|e| diagnose_tree_path_error(repo, before, after, &clean_path, e))?;
1645 Ok(TreeishBlobAtPath {
1646 path: clean_path,
1647 oid,
1648 mode: mode_str,
1649 })
1650}
1651
1652fn walk_tree_to_blob_entry(
1656 repo: &Repository,
1657 tree_oid: &ObjectId,
1658 path: &str,
1659) -> Result<(ObjectId, String)> {
1660 let obj = repo.read_replaced(tree_oid)?;
1661 let entries = crate::objects::parse_tree(&obj.data)?;
1662 let components: Vec<&str> = path.split('/').filter(|c| !c.is_empty()).collect();
1663 if components.is_empty() {
1664 return Err(Error::InvalidRef(format!(
1665 "path '{path}' does not name a blob in tree {tree_oid}"
1666 )));
1667 }
1668
1669 let first = components[0];
1670 let rest: Vec<&str> = components[1..].to_vec();
1671 for entry in entries {
1672 let name = String::from_utf8_lossy(&entry.name);
1673 if name == first {
1674 if rest.is_empty() {
1675 if entry.mode == crate::index::MODE_TREE {
1676 return Err(Error::InvalidRef(format!("'{path}' is a tree, not a blob")));
1677 }
1678 return Ok((entry.oid, entry.mode_str()));
1679 }
1680 if entry.mode != crate::index::MODE_TREE {
1681 return Err(Error::ObjectNotFound(path.to_owned()));
1682 }
1683 return walk_tree_to_blob_entry(repo, &entry.oid, &rest.join("/"));
1684 }
1685 }
1686 Err(Error::ObjectNotFound(format!(
1687 "path '{path}' not found in tree {tree_oid}"
1688 )))
1689}
1690
1691#[derive(Debug, Clone, Copy)]
1693enum NavStep {
1694 ParentN(usize),
1696 AncestorN(usize),
1698}
1699
1700fn parse_nav_steps(spec: &str) -> (&str, Vec<NavStep>) {
1704 let mut steps = Vec::new();
1705 let mut remaining = spec;
1706
1707 loop {
1708 if let Some(tilde_pos) = remaining.rfind('~') {
1710 let after = &remaining[tilde_pos + 1..];
1711 if after.is_empty() {
1712 steps.push(NavStep::AncestorN(1));
1714 remaining = &remaining[..tilde_pos];
1715 continue;
1716 }
1717 if after.bytes().all(|b| b.is_ascii_digit()) {
1718 let n: usize = after.parse().unwrap_or(1);
1719 steps.push(NavStep::AncestorN(n));
1720 remaining = &remaining[..tilde_pos];
1721 continue;
1722 }
1723 }
1724
1725 if let Some(caret_pos) = remaining.rfind('^') {
1727 let after = &remaining[caret_pos + 1..];
1728 if after.is_empty() {
1729 steps.push(NavStep::ParentN(1));
1731 remaining = &remaining[..caret_pos];
1732 continue;
1733 }
1734 if after.bytes().all(|b| b.is_ascii_digit()) && !after.is_empty() {
1735 let n: usize = after.parse().unwrap_or(usize::MAX);
1736 steps.push(NavStep::ParentN(n));
1737 remaining = &remaining[..caret_pos];
1738 continue;
1739 }
1740 }
1741
1742 break;
1743 }
1744
1745 steps.reverse();
1746 (remaining, steps)
1747}
1748
1749fn peel_annotated_tag_chain(repo: &Repository, mut oid: ObjectId) -> Result<ObjectId> {
1751 loop {
1752 let obj = repo.read_replaced(&oid)?;
1753 if obj.kind != ObjectKind::Tag {
1754 return Ok(oid);
1755 }
1756 let tag = parse_tag(&obj.data)?;
1757 oid = tag.object;
1758 }
1759}
1760
1761fn apply_nav_step(repo: &Repository, oid: ObjectId, step: NavStep) -> Result<ObjectId> {
1763 match step {
1764 NavStep::ParentN(0) => Ok(oid),
1765 NavStep::ParentN(n) => {
1766 let oid = peel_annotated_tag_chain(repo, oid)?;
1767 let parents = commit_parents_for_navigation(repo, oid)?;
1768 parents
1769 .get(n - 1)
1770 .copied()
1771 .ok_or_else(|| Error::ObjectNotFound(format!("{oid}^{n}")))
1772 }
1773 NavStep::AncestorN(n) => {
1774 let mut current = peel_annotated_tag_chain(repo, oid)?;
1775 for _ in 0..n {
1776 current = apply_nav_step(repo, current, NavStep::ParentN(1))?;
1777 }
1778 Ok(current)
1779 }
1780 }
1781}
1782
1783pub fn abbreviate_object_id(repo: &Repository, oid: ObjectId, min_len: usize) -> Result<String> {
1792 let min_len = min_len.clamp(4, 40);
1793 let target = oid.to_hex();
1794
1795 if !repo.odb.exists(&oid) {
1797 return Ok(target[..min_len].to_owned());
1798 }
1799
1800 let all = collect_loose_object_ids(repo)?;
1801
1802 for len in min_len..=40 {
1803 let prefix = &target[..len];
1804 let matches = all
1805 .iter()
1806 .filter(|candidate| candidate.starts_with(prefix))
1807 .count();
1808 if matches <= 1 {
1809 return Ok(prefix.to_owned());
1810 }
1811 }
1812
1813 Ok(target)
1814}
1815
1816#[must_use]
1818pub fn to_relative_path(path: &Path, cwd: &Path) -> String {
1819 let path_components = normalize_components(path);
1820 let cwd_components = normalize_components(cwd);
1821
1822 let mut common = 0usize;
1823 let max_common = path_components.len().min(cwd_components.len());
1824 while common < max_common && path_components[common] == cwd_components[common] {
1825 common += 1;
1826 }
1827
1828 let mut parts = Vec::new();
1829 let up_count = cwd_components.len().saturating_sub(common);
1830 for _ in 0..up_count {
1831 parts.push("..".to_owned());
1832 }
1833 for item in path_components.iter().skip(common) {
1834 parts.push(item.clone());
1835 }
1836
1837 if parts.is_empty() {
1838 ".".to_owned()
1839 } else {
1840 parts.join("/")
1841 }
1842}
1843
1844fn object_storage_dirs_for_abbrev(repo: &Repository) -> Result<Vec<PathBuf>> {
1845 let mut dirs = Vec::new();
1846 let primary = repo.odb.objects_dir().to_path_buf();
1847 dirs.push(primary.clone());
1848 if let Ok(alts) = pack::read_alternates_recursive(&primary) {
1849 for alt in alts {
1850 if !dirs.iter().any(|d| d == &alt) {
1851 dirs.push(alt);
1852 }
1853 }
1854 }
1855 Ok(dirs)
1856}
1857
1858fn collect_pack_oids_with_prefix(objects_dir: &Path, prefix: &str) -> Result<Vec<ObjectId>> {
1859 let mut out = Vec::new();
1860 for idx in pack::read_local_pack_indexes_cached(objects_dir)? {
1861 for e in &idx.entries {
1862 if e.oid.len() != 20 && e.oid.len() != 32 {
1863 continue;
1864 }
1865 let hex = pack::oid_bytes_to_hex(&e.oid);
1866 if hex.starts_with(prefix) {
1867 if let Ok(oid) = crate::objects::ObjectId::from_bytes(&e.oid) {
1868 out.push(oid);
1869 }
1870 }
1871 }
1872 }
1873 Ok(out)
1874}
1875
1876fn disambiguate_kind_rank(kind: ObjectKind) -> u8 {
1877 match kind {
1878 ObjectKind::Tag => 0,
1879 ObjectKind::Commit => 1,
1880 ObjectKind::Tree => 2,
1881 ObjectKind::Blob => 3,
1882 }
1883}
1884
1885fn oid_satisfies_peel_filter(repo: &Repository, oid: ObjectId, peel_inner: &str) -> bool {
1886 apply_peel(repo, oid, Some(peel_inner)).is_ok()
1887}
1888
1889pub fn ambiguous_object_hint_lines(
1891 repo: &Repository,
1892 short_prefix: &str,
1893 peel_filter: Option<&str>,
1894) -> Result<Vec<String>> {
1895 let mut typed: Vec<(u8, String, &'static str)> = Vec::new();
1896 let mut bad_hex: Vec<String> = Vec::new();
1897 for oid in list_all_abbrev_matches(repo, short_prefix)? {
1898 let hex = oid.to_hex();
1899 match repo.read_replaced(&oid) {
1900 Ok(obj) => {
1901 let ok = peel_filter.is_none_or(|p| oid_satisfies_peel_filter(repo, oid, p));
1902 if ok {
1903 typed.push((disambiguate_kind_rank(obj.kind), hex, obj.kind.as_str()));
1904 }
1905 }
1906 Err(_) => bad_hex.push(hex),
1907 }
1908 }
1909 if typed.is_empty() && peel_filter.is_some() {
1910 return ambiguous_object_hint_lines(repo, short_prefix, None);
1911 }
1912 bad_hex.sort();
1913 typed.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
1914 let mut out = Vec::new();
1915 for h in bad_hex {
1916 out.push(format!("hint: {h} [bad object]"));
1917 }
1918 for (_, hex, kind) in typed {
1919 out.push(format!("hint: {hex} {kind}"));
1920 }
1921 Ok(out)
1922}
1923
1924fn read_core_disambiguate(repo: &Repository) -> Option<&'static str> {
1925 let config = ConfigSet::load(Some(&repo.git_dir), true).unwrap_or_else(|_| ConfigSet::new());
1926 let v = config.get("core.disambiguate")?;
1927 match v.to_ascii_lowercase().as_str() {
1928 "committish" | "commit" => Some("commit"),
1929 "treeish" | "tree" => Some("tree"),
1930 "blob" => Some("blob"),
1931 "tag" => Some("tag"),
1932 "none" => None,
1933 _ => None,
1934 }
1935}
1936
1937fn warn_if_branch_refname_collides_with_abbrev_hex(
1940 repo: &Repository,
1941 spec: &str,
1942 object_oid: ObjectId,
1943) {
1944 if spec.len() >= repo.odb.hash_algo().hex_len() {
1945 return;
1946 }
1947 let branch_ref = format!("refs/heads/{spec}");
1948 let Ok(ref_oid) = refs::resolve_ref(&repo.git_dir, &branch_ref) else {
1949 return;
1950 };
1951 if ref_oid != object_oid {
1952 eprintln!("warning: refname '{spec}' is ambiguous.");
1953 }
1954}
1955
1956fn warn_if_hex_ref_collides_with_objects(repo: &Repository, spec: &str, ref_oid: ObjectId) {
1959 if spec.len() >= repo.odb.hash_algo().hex_len() || !is_hex_prefix(spec) {
1960 return;
1961 }
1962 let Ok(matches) = find_abbrev_matches(repo, spec) else {
1963 return;
1964 };
1965 if matches.is_empty() {
1966 return;
1967 }
1968 if matches.len() > 1 || matches[0] != ref_oid {
1969 eprintln!("warning: refname '{spec}' is ambiguous.");
1970 }
1971}
1972
1973fn disambiguate_hex_by_peel(
1974 repo: &Repository,
1975 spec: &str,
1976 matches: &[ObjectId],
1977 peel: &str,
1978) -> Result<ObjectId> {
1979 let peel_some = Some(peel);
1980 let filtered: Vec<ObjectId> = matches
1981 .iter()
1982 .copied()
1983 .filter(|oid| apply_peel(repo, *oid, peel_some).is_ok())
1984 .collect();
1985 if filtered.len() == 1 {
1986 return Ok(filtered[0]);
1987 }
1988 if filtered.is_empty() {
1989 return Err(Error::InvalidRef(format!(
1990 "short object ID {spec} is ambiguous"
1991 )));
1992 }
1993 let mut peeled_targets: HashSet<ObjectId> = HashSet::new();
1994 for oid in &filtered {
1995 if let Ok(p) = apply_peel(repo, *oid, peel_some) {
1996 peeled_targets.insert(p);
1997 }
1998 }
1999 if peeled_targets.len() == 1 {
2000 let mut sorted = filtered;
2003 sorted.sort_by_key(|o| o.to_hex());
2004 return Ok(sorted[0]);
2005 }
2006 if peel == "commit" {
2009 let mut by_peeled: HashMap<ObjectId, Vec<ObjectId>> = HashMap::new();
2010 for oid in &filtered {
2011 if let Ok(c) = apply_peel(repo, *oid, Some("commit")) {
2012 by_peeled.entry(c).or_default().push(*oid);
2013 }
2014 }
2015 if by_peeled.len() == 1 {
2016 let mut reps: Vec<ObjectId> = by_peeled.into_values().next().unwrap_or_default();
2017 reps.sort_by_key(|o| o.to_hex());
2018 if let Some(oid) = reps.first().copied() {
2019 return Ok(oid);
2020 }
2021 }
2022 }
2023 Err(Error::InvalidRef(format!(
2024 "short object ID {spec} is ambiguous"
2025 )))
2026}
2027
2028fn commit_reachable_closure(repo: &Repository, start: ObjectId) -> Result<HashSet<ObjectId>> {
2029 use std::collections::VecDeque;
2030 let mut seen = HashSet::new();
2031 let mut q = VecDeque::from([start]);
2032 while let Some(oid) = q.pop_front() {
2033 if !seen.insert(oid) {
2034 continue;
2035 }
2036 let obj = match repo.read_replaced(&oid) {
2037 Ok(o) => o,
2038 Err(_) => continue,
2039 };
2040 if obj.kind != ObjectKind::Commit {
2041 continue;
2042 }
2043 let commit = match parse_commit(&obj.data) {
2044 Ok(c) => c,
2045 Err(_) => continue,
2046 };
2047 for p in &commit.parents {
2048 q.push_back(*p);
2049 }
2050 }
2051 Ok(seen)
2052}
2053
2054fn describe_generation_count(
2056 repo: &Repository,
2057 head: ObjectId,
2058 tag_commit: ObjectId,
2059) -> Result<usize> {
2060 let from_tag = commit_reachable_closure(repo, tag_commit)?;
2061 let from_head = commit_reachable_closure(repo, head)?;
2062 Ok(from_head.difference(&from_tag).count())
2063}
2064
2065fn try_resolve_describe_name(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
2066 let re = Regex::new(r"(?i)^(.+)-(\d+)-g([0-9a-fA-F]+)$")
2067 .map_err(|_| Error::Message("internal: describe regex".to_owned()))?;
2068 let Some(caps) = re.captures(spec) else {
2069 return Ok(None);
2070 };
2071 let tag_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
2072 let gen: usize = caps
2073 .get(2)
2074 .and_then(|m| m.as_str().parse().ok())
2075 .unwrap_or(0);
2076 let hex_abbrev = caps.get(3).map(|m| m.as_str()).unwrap_or("");
2077 if tag_name.is_empty() || hex_abbrev.is_empty() {
2078 return Ok(None);
2079 }
2080 let hex_lower = hex_abbrev.to_ascii_lowercase();
2081 let mut commit_candidates: Vec<ObjectId> = find_abbrev_matches(repo, &hex_lower)?
2082 .into_iter()
2083 .filter(|oid| {
2084 repo.odb
2085 .read(oid)
2086 .is_ok_and(|o| o.kind == ObjectKind::Commit)
2087 })
2088 .collect();
2089 commit_candidates.sort_by_key(|o| o.to_hex());
2090 commit_candidates.dedup();
2091
2092 if let Ok(tag_oid) = refs::resolve_ref(&repo.git_dir, &format!("refs/tags/{tag_name}"))
2093 .or_else(|_| refs::resolve_ref(&repo.git_dir, tag_name))
2094 {
2095 let tag_commit = peel_to_commit_for_merge_base(repo, tag_oid)?;
2096 let mut strict_candidates = commit_candidates
2097 .iter()
2098 .copied()
2099 .filter(|oid| describe_generation_count(repo, *oid, tag_commit).ok() == Some(gen))
2100 .collect::<Vec<_>>();
2101 strict_candidates.sort_by_key(|o| o.to_hex());
2102 if strict_candidates.len() == 1 {
2103 return Ok(Some(strict_candidates[0]));
2104 }
2105 if strict_candidates.len() > 1 {
2106 return Err(Error::InvalidRef(format!(
2107 "short object ID {hex_abbrev} is ambiguous"
2108 )));
2109 }
2110 }
2111
2112 match commit_candidates.len() {
2113 0 => Err(Error::ObjectNotFound(spec.to_owned())),
2114 1 => Ok(Some(commit_candidates[0])),
2115 _ => Err(Error::InvalidRef(format!(
2116 "short object ID {hex_abbrev} is ambiguous"
2117 ))),
2118 }
2119}
2120
2121fn resolve_base(
2122 repo: &Repository,
2123 spec: &str,
2124 index_dwim: bool,
2125 commit_only_hex: bool,
2126 use_disambiguate_config: bool,
2127 peel_for_disambig: Option<&str>,
2128 implicit_tree_abbrev: bool,
2129 implicit_blob_abbrev: bool,
2130 remote_branch_name_guess: bool,
2131) -> Result<ObjectId> {
2132 if spec == "@" {
2134 return resolve_base(
2135 repo,
2136 "HEAD",
2137 index_dwim,
2138 commit_only_hex,
2139 use_disambiguate_config,
2140 peel_for_disambig,
2141 implicit_tree_abbrev,
2142 implicit_blob_abbrev,
2143 remote_branch_name_guess,
2144 );
2145 }
2146
2147 if spec == "FETCH_HEAD" {
2150 let path = repo.git_dir.join("FETCH_HEAD");
2151 let content = std::fs::read_to_string(&path)
2152 .map_err(|_| Error::ObjectNotFound("FETCH_HEAD".to_owned()))?;
2153 let mut first_oid = None;
2154 for line in content.lines() {
2155 let line = line.trim();
2156 if line.is_empty() {
2157 continue;
2158 }
2159 let mut parts = line.split('\t');
2160 let Some(oid_hex) = parts.next() else {
2161 continue;
2162 };
2163 if ObjectId::is_full_hex(oid_hex) {
2164 let oid = oid_hex
2165 .parse::<ObjectId>()
2166 .map_err(|_| Error::InvalidRef("invalid FETCH_HEAD object id".to_owned()))?;
2167 first_oid.get_or_insert(oid);
2168 let not_for_merge = parts.next().is_some_and(|v| v == "not-for-merge");
2169 if !not_for_merge {
2170 return Ok(oid);
2171 }
2172 }
2173 }
2174 return first_oid.ok_or_else(|| Error::ObjectNotFound("FETCH_HEAD".to_owned()));
2175 }
2176
2177 if spec.starts_with("@{-") {
2179 if let Some(close) = spec[3..].find('}') {
2180 let n_str = &spec[3..3 + close];
2181 if let Ok(n) = n_str.parse::<usize>() {
2182 if n >= 1 {
2183 let suffix = &spec[3 + close + 1..];
2184 if suffix.is_empty() {
2185 if let Some(oid) = try_resolve_at_minus(repo, spec)? {
2186 return Ok(oid);
2187 }
2188 } else {
2189 let branch = resolve_at_minus_to_branch(repo, n)?;
2190 let new_spec = format!("{branch}{suffix}");
2191 return resolve_base(
2192 repo,
2193 &new_spec,
2194 index_dwim,
2195 commit_only_hex,
2196 use_disambiguate_config,
2197 peel_for_disambig,
2198 implicit_tree_abbrev,
2199 implicit_blob_abbrev,
2200 remote_branch_name_guess,
2201 );
2202 }
2203 }
2204 }
2205 }
2206 }
2207
2208 if upstream_suffix_info(spec).is_some() {
2210 let full_ref = resolve_upstream_symbolic_name(repo, spec)?;
2211 return refs::resolve_ref(&repo.git_dir, &full_ref)
2212 .map_err(|_| Error::ObjectNotFound(spec.to_owned()));
2213 }
2214
2215 if let Some(oid) = try_resolve_reflog_index(repo, spec)? {
2217 return Ok(oid);
2218 }
2219
2220 if let Some(pattern) = spec.strip_prefix(":/") {
2222 if !pattern.is_empty() {
2223 return resolve_commit_message_search(repo, pattern);
2224 }
2225 }
2226
2227 if let Some(rest) = spec.strip_prefix(':') {
2230 if !rest.is_empty() && !rest.starts_with('/') {
2231 if rest.len() >= 3 && rest.as_bytes()[1] == b':' {
2233 if let Some(stage_char) = rest.chars().next() {
2234 if let Some(stage) = stage_char.to_digit(10) {
2235 if stage <= 3 {
2236 let raw_path = &rest[2..];
2237 let path = match normalize_colon_path_for_tree(repo, raw_path) {
2238 Ok(p) => p,
2239 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
2240 let wt = repo
2241 .work_tree
2242 .as_ref()
2243 .and_then(|p| p.canonicalize().ok())
2244 .map(|p| p.display().to_string())
2245 .unwrap_or_default();
2246 return Err(Error::Message(format!(
2247 "fatal: '{raw_path}' is outside repository at '{wt}'"
2248 )));
2249 }
2250 Err(e) => return Err(e),
2251 };
2252 return resolve_index_path_at_stage(repo, &path, stage as u8).map_err(
2253 |e| diagnose_index_path_error(repo, &path, stage as u8, e),
2254 );
2255 }
2256 }
2257 }
2258 }
2259 let clean_rest = match normalize_colon_path_for_tree(repo, rest) {
2260 Ok(p) => p,
2261 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
2262 let wt = repo
2263 .work_tree
2264 .as_ref()
2265 .and_then(|p| p.canonicalize().ok())
2266 .map(|p| p.display().to_string())
2267 .unwrap_or_default();
2268 return Err(Error::Message(format!(
2269 "fatal: '{rest}' is outside repository at '{wt}'"
2270 )));
2271 }
2272 Err(e) => return Err(e),
2273 };
2274 return resolve_index_path(repo, &clean_rest)
2275 .map_err(|e| diagnose_index_path_error(repo, &clean_rest, 0, e));
2276 }
2277 }
2278
2279 if let Some((treeish, path)) = split_treeish_spec(spec) {
2280 let root_oid = resolve_revision_impl(
2281 repo,
2282 treeish,
2283 index_dwim,
2284 commit_only_hex,
2285 use_disambiguate_config,
2286 false,
2287 false,
2288 false,
2289 false,
2290 )?;
2291 return resolve_treeish_path_to_object(repo, root_oid, path);
2292 }
2293
2294 if let Ok(oid) = spec.parse::<ObjectId>() {
2295 let rn = format!("refs/heads/{spec}");
2298 if refs::resolve_ref(&repo.git_dir, &rn).is_ok() {
2299 eprintln!("warning: refname '{spec}' is ambiguous.");
2300 }
2301 return Ok(oid);
2302 }
2303
2304 match try_resolve_describe_name(repo, spec) {
2305 Ok(Some(oid)) => return Ok(oid),
2306 Err(e) => return Err(e),
2307 Ok(None) => {}
2308 }
2309
2310 if is_hex_prefix(spec) && spec.len() < repo.odb.hash_algo().hex_len() {
2313 let tag_ref = format!("refs/tags/{spec}");
2314 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &tag_ref) {
2315 warn_if_hex_ref_collides_with_objects(repo, spec, oid);
2316 return Ok(oid);
2317 }
2318 let branch_ref = format!("refs/heads/{spec}");
2319 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &branch_ref) {
2320 warn_if_hex_ref_collides_with_objects(repo, spec, oid);
2321 return Ok(oid);
2322 }
2323 }
2324
2325 if is_hex_prefix(spec) {
2326 let matches = find_abbrev_matches(repo, spec)?;
2327 if matches.is_empty() {
2328 if (4..repo.odb.hash_algo().hex_len()).contains(&spec.len()) {
2332 return Err(Error::ObjectNotFound(spec.to_owned()));
2333 }
2334 } else if matches.len() == 1 {
2335 let oid = matches[0];
2336 warn_if_branch_refname_collides_with_abbrev_hex(repo, spec, oid);
2337 return Ok(oid);
2338 } else if matches.len() > 1 {
2339 if let Some(p) = peel_for_disambig {
2340 let oid = disambiguate_hex_by_peel(repo, spec, &matches, p)?;
2341 warn_if_branch_refname_collides_with_abbrev_hex(repo, spec, oid);
2342 return Ok(oid);
2343 }
2344 if commit_only_hex {
2345 let oid = disambiguate_hex_by_peel(repo, spec, &matches, "commit")?;
2346 warn_if_branch_refname_collides_with_abbrev_hex(repo, spec, oid);
2347 return Ok(oid);
2348 }
2349 if use_disambiguate_config {
2350 if let Some(pref) = read_core_disambiguate(repo) {
2351 if let Ok(oid) = disambiguate_hex_by_peel(repo, spec, &matches, pref) {
2352 warn_if_branch_refname_collides_with_abbrev_hex(repo, spec, oid);
2353 return Ok(oid);
2354 }
2355 }
2356 }
2357 return Err(Error::InvalidRef(format!(
2358 "short object ID {} is ambiguous",
2359 spec
2360 )));
2361 }
2362 }
2363
2364 let (dwim_count, dwim_oid) = resolve_ref_dwim_for_rev_parse(repo, spec);
2365 if dwim_count > 1 {
2366 eprintln!("warning: refname '{spec}' is ambiguous.");
2367 }
2368 if let Some(oid) = dwim_oid {
2369 return Ok(oid);
2370 }
2371 if let Some(rest) = spec.strip_prefix("remotes/") {
2373 let full = format!("refs/remotes/{rest}");
2374 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &full) {
2375 return Ok(oid);
2376 }
2377 }
2378 if !spec.contains('/')
2382 && !spec.starts_with('.')
2383 && spec != "HEAD"
2384 && spec != "FETCH_HEAD"
2385 && spec != "MERGE_HEAD"
2386 && spec != "CHERRY_PICK_HEAD"
2387 && spec != "REVERT_HEAD"
2388 && spec != "REBASE_HEAD"
2389 && spec != "AUTO_MERGE"
2390 && spec != "stash"
2391 {
2392 let local_branch = format!("refs/heads/{spec}");
2393 if refs::resolve_ref(&repo.git_dir, &local_branch).is_err() {
2394 let remote_head = format!("refs/remotes/{spec}/HEAD");
2395 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &remote_head) {
2396 return Ok(oid);
2397 }
2398 }
2399 }
2400 if spec == "stash" {
2402 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, "refs/stash") {
2403 return Ok(oid);
2404 }
2405 }
2406 let head_ref = format!("refs/heads/{spec}");
2410 let tag_ref = format!("refs/tags/{spec}");
2411 let head_oid = refs::resolve_ref(&repo.git_dir, &head_ref).ok();
2412 let tag_oid = refs::resolve_ref(&repo.git_dir, &tag_ref).ok();
2413 match (head_oid, tag_oid) {
2414 (Some(h), Some(t)) if h != t => {
2415 eprintln!("warning: refname '{spec}' is ambiguous.");
2416 return Ok(h);
2417 }
2418 (Some(h), _) => return Ok(h),
2419 (None, Some(t)) => return Ok(t),
2420 (None, None) => {}
2421 }
2422
2423 if !spec.contains('/')
2427 && !spec.contains(':')
2428 && !spec.starts_with('.')
2429 && spec != "HEAD"
2430 && spec.len() <= 255
2431 {
2432 let mut ref_match: Option<ObjectId> = None;
2433 for prefix in ["refs/heads/", "refs/tags/", "refs/remotes/", "refs/notes/"] {
2434 let full = format!("{prefix}{spec}");
2435 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &full) {
2436 ref_match = Some(oid);
2437 break;
2438 }
2439 }
2440 if let Some(oid) = ref_match {
2441 return Ok(oid);
2442 }
2443 }
2444 for candidate in &[format!("refs/remotes/{spec}"), format!("refs/notes/{spec}")] {
2445 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, candidate) {
2446 return Ok(oid);
2447 }
2448 }
2449
2450 if let Some(head_ref) = remote_tracking_head_symbolic_target(repo, spec) {
2452 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &head_ref) {
2453 return Ok(oid);
2454 }
2455 }
2456
2457 if remote_branch_name_guess
2459 && !spec.contains('/')
2460 && spec != "HEAD"
2461 && spec != "FETCH_HEAD"
2462 && spec != "MERGE_HEAD"
2463 {
2464 const REMOTES: &str = "refs/remotes/";
2465 if let Ok(remote_refs) = refs::list_refs(&repo.git_dir, REMOTES) {
2466 let matches: Vec<ObjectId> = remote_refs
2467 .into_iter()
2468 .filter(|(r, _)| {
2469 r.strip_prefix(REMOTES)
2470 .is_some_and(|rest| rest == spec || rest.ends_with(&format!("/{spec}")))
2471 })
2472 .map(|(_, oid)| oid)
2473 .collect();
2474 if matches.len() == 1 {
2475 return Ok(matches[0]);
2476 }
2477 if matches.len() > 1 {
2478 return Err(Error::InvalidRef(format!(
2479 "ambiguous refname '{spec}': matches multiple remote-tracking branches"
2480 )));
2481 }
2482 }
2483 }
2484
2485 if !spec.contains(':') && !spec.starts_with('-') {
2487 if index_dwim {
2488 if let Ok(oid) = resolve_index_path(repo, spec) {
2489 return Ok(oid);
2490 }
2491 }
2492 return Err(Error::Message(format!(
2493 "fatal: ambiguous argument '{spec}': unknown revision or path not in the working tree.\n\
2494Use '--' to separate paths from revisions, like this:\n\
2495'git <command> [<revision>...] -- [<file>...]'"
2496 )));
2497 }
2498 Err(Error::ObjectNotFound(spec.to_owned()))
2499}
2500
2501fn resolve_at_minus_to_branch(repo: &Repository, n: usize) -> Result<String> {
2503 let entries = read_reflog(&repo.git_dir, "HEAD")?;
2504 let mut count = 0usize;
2505 for entry in entries.iter().rev() {
2506 let msg = &entry.message;
2507 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
2508 count += 1;
2509 if count == n {
2510 if let Some(to_pos) = rest.find(" to ") {
2511 return Ok(rest[..to_pos].to_string());
2512 }
2513 }
2514 }
2515 }
2516 Err(Error::InvalidRef(format!(
2517 "@{{-{n}}}: only {count} checkout(s) in reflog"
2518 )))
2519}
2520
2521fn try_resolve_at_minus(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
2524 if !spec.starts_with("@{-") || !spec.ends_with('}') {
2526 return Ok(None);
2527 }
2528 let inner = &spec[3..spec.len() - 1];
2529 let n: usize = match inner.parse() {
2530 Ok(n) if n >= 1 => n,
2531 _ => return Ok(None),
2532 };
2533 let entries = read_reflog(&repo.git_dir, "HEAD")?;
2535 let mut count = 0usize;
2536 for entry in entries.iter().rev() {
2538 let msg = &entry.message;
2539 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
2540 count += 1;
2541 if count == n {
2542 if let Some(to_pos) = rest.find(" to ") {
2543 let from_branch = &rest[..to_pos];
2544 let ref_name = format!("refs/heads/{from_branch}");
2545 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &ref_name) {
2546 return Ok(Some(oid));
2547 }
2548 if let Ok(oid) = from_branch.parse::<ObjectId>() {
2549 if repo.odb.exists(&oid) {
2550 return Ok(Some(oid));
2551 }
2552 }
2553 if is_hex_prefix(from_branch) {
2554 if let Ok(oid) = resolve_revision_for_range_end(repo, from_branch)
2555 .and_then(|oid| peel_to_commit_for_merge_base(repo, oid))
2556 {
2557 return Ok(Some(oid));
2558 }
2559 }
2560 return Err(Error::InvalidRef(format!(
2561 "cannot resolve @{{-{n}}}: branch '{}' not found",
2562 from_branch
2563 )));
2564 }
2565 }
2566 }
2567 }
2568 Err(Error::InvalidRef(format!(
2569 "@{{-{n}}}: only {count} checkout(s) in reflog"
2570 )))
2571}
2572
2573#[derive(Debug, Clone)]
2574enum AtStep {
2575 Index(usize),
2576 Date(i64),
2577 Upstream,
2578 Push,
2579 Now,
2580}
2581
2582fn try_parse_at_step_inner(inner: &str) -> Option<AtStep> {
2583 if inner.eq_ignore_ascii_case("u") || inner.eq_ignore_ascii_case("upstream") {
2584 return Some(AtStep::Upstream);
2585 }
2586 if inner.eq_ignore_ascii_case("push") {
2587 return Some(AtStep::Push);
2588 }
2589 if inner.eq_ignore_ascii_case("now") {
2590 return Some(AtStep::Now);
2591 }
2592 if let Ok(n) = inner.parse::<usize>() {
2593 return Some(AtStep::Index(n));
2594 }
2595 approxidate(inner).map(AtStep::Date)
2596}
2597
2598fn next_reflog_at_open(spec: &str, mut from: usize) -> Option<usize> {
2599 let b = spec.as_bytes();
2600 while let Some(rel) = spec[from..].find("@{") {
2601 let i = from + rel;
2602 if b.get(i + 2) == Some(&b'-') {
2604 let after_open = i + 2;
2605 let close = spec[after_open..].find('}').map(|j| after_open + j)?;
2606 from = close + 1;
2607 continue;
2608 }
2609 return Some(i);
2610 }
2611 None
2612}
2613
2614fn split_reflog_at_chain(spec: &str) -> Option<(String, Vec<AtStep>)> {
2616 let at = next_reflog_at_open(spec, 0)?;
2617 let prefix = spec[..at].to_owned();
2618 let mut steps = Vec::new();
2619 let mut pos = at;
2620 while pos < spec.len() {
2621 let rest = &spec[pos..];
2622 if !rest.starts_with("@{") {
2623 return None;
2624 }
2625 if rest.as_bytes().get(2) == Some(&b'-') {
2626 return None;
2627 }
2628 let inner_start = pos + 2;
2629 let close = spec[inner_start..].find('}').map(|i| inner_start + i)?;
2630 let inner = &spec[inner_start..close];
2631 let step = try_parse_at_step_inner(inner)?;
2632 steps.push(step);
2633 pos = close + 1;
2634 }
2635 if steps.is_empty() {
2636 return None;
2637 }
2638 Some((prefix, steps))
2639}
2640
2641fn dwim_refname(repo: &Repository, raw: &str) -> String {
2642 if raw.is_empty() || raw == "HEAD" || raw.starts_with("refs/") {
2643 return raw.to_owned();
2644 }
2645 if raw == "stash" && refs::resolve_ref(&repo.git_dir, "refs/stash").is_ok() {
2647 return "refs/stash".to_owned();
2648 }
2649 let candidate = format!("refs/heads/{raw}");
2650 if refs::resolve_ref(&repo.git_dir, &candidate).is_ok() {
2651 candidate
2652 } else {
2653 raw.to_owned()
2654 }
2655}
2656
2657fn reflog_display_name(refname_raw: &str, refname: &str) -> String {
2658 if refname_raw.is_empty() {
2659 if let Some(b) = refname.strip_prefix("refs/heads/") {
2660 return b.to_owned();
2661 }
2662 return refname.to_owned();
2663 }
2664 refname_raw.to_owned()
2665}
2666
2667fn resolve_reflog_oid(
2668 repo: &Repository,
2669 refname: &str,
2670 refname_raw: &str,
2671 index_or_date: ReflogSelector,
2672) -> Result<ObjectId> {
2673 let mut entries = read_reflog(&repo.git_dir, refname)?;
2674 if refname == "HEAD" {
2675 if let ReflogSelector::Index(index) = index_or_date {
2676 if index >= entries.len() {
2677 if let Ok(Some(branch_ref)) = crate::refs::read_symbolic_ref(&repo.git_dir, "HEAD")
2678 {
2679 if let Ok(branch_entries) = read_reflog(&repo.git_dir, &branch_ref) {
2680 if index < branch_entries.len() {
2681 entries = branch_entries;
2682 }
2683 }
2684 }
2685 }
2686 }
2687 }
2688 let display = reflog_display_name(refname_raw, refname);
2689 match index_or_date {
2690 ReflogSelector::Index(index) => {
2691 let len = entries.len();
2692 if index == 0 {
2693 if len == 0 {
2694 if !crate::reflog::reflog_exists(&repo.git_dir, refname) {
2700 return Err(Error::Message(format!(
2701 "fatal: log for '{display}' is empty"
2702 )));
2703 }
2704 return refs::resolve_ref(&repo.git_dir, refname).map_err(|_| {
2705 Error::Message(format!("fatal: log for '{display}' is empty"))
2706 });
2707 }
2708 return Ok(entries[len - 1].new_oid);
2709 }
2710 if len == 0 {
2711 return Err(Error::Message(format!(
2712 "fatal: log for '{display}' is empty"
2713 )));
2714 }
2715 if index > len {
2716 return Err(Error::Message(format!(
2717 "fatal: log for '{display}' only has {len} entries"
2718 )));
2719 }
2720 let oid = entries[len - index].old_oid;
2721 if oid.is_zero() {
2722 return Err(Error::Message(format!(
2723 "fatal: log for '{display}' only has {len} entries"
2724 )));
2725 }
2726 Ok(oid)
2727 }
2728 ReflogSelector::Date(target_ts) => {
2729 if entries.is_empty() {
2730 return Err(Error::Message(format!(
2731 "fatal: log for '{display}' is empty"
2732 )));
2733 }
2734 for entry in entries.iter().rev() {
2735 let ts = parse_reflog_entry_timestamp(entry);
2736 if let Some(t) = ts {
2737 if t <= target_ts {
2738 return Ok(entry.new_oid);
2739 }
2740 }
2741 }
2742 Ok(entries[0].new_oid)
2743 }
2744 }
2745}
2746
2747fn resolve_at_minus_token_to_branch(repo: &Repository, token: &str) -> Result<Option<String>> {
2748 if !token.starts_with("@{-") || !token.ends_with('}') {
2749 return Ok(None);
2750 }
2751 let inner = &token[3..token.len() - 1];
2752 let n: usize = inner
2753 .parse()
2754 .map_err(|_| Error::InvalidRef(format!("invalid N in @{{-N}} for '{token}'")))?;
2755 if n < 1 {
2756 return Ok(None);
2757 }
2758 Ok(Some(resolve_at_minus_to_branch(repo, n)?))
2759}
2760
2761pub fn reflog_walk_refname(repo: &Repository, spec: &str) -> Result<Option<String>> {
2765 let Some((prefix, steps)) = split_reflog_at_chain(spec) else {
2766 return Ok(None);
2767 };
2768
2769 let prefix_resolved = if let Some(b) = resolve_at_minus_token_to_branch(repo, &prefix)? {
2770 b
2771 } else {
2772 prefix.clone()
2773 };
2774
2775 let mut current_spec = if prefix_resolved.is_empty() {
2776 if let Ok(Some(b)) = refs::read_head(&repo.git_dir) {
2777 if let Some(short) = b.strip_prefix("refs/heads/") {
2778 short.to_owned()
2779 } else {
2780 "HEAD".to_owned()
2781 }
2782 } else {
2783 "HEAD".to_owned()
2784 }
2785 } else {
2786 prefix_resolved
2787 };
2788
2789 let last_reflog_peel = steps
2790 .iter()
2791 .rposition(|s| matches!(s, AtStep::Index(_) | AtStep::Date(_) | AtStep::Now));
2792
2793 let limit = last_reflog_peel.unwrap_or(steps.len());
2794 for step in steps.iter().take(limit) {
2795 match step {
2796 AtStep::Upstream => {
2797 let base = if current_spec == "@" {
2798 "HEAD"
2799 } else {
2800 current_spec.as_str()
2801 };
2802 let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{u}}"))?;
2803 current_spec = full;
2804 }
2805 AtStep::Push => {
2806 let base = if current_spec == "@" {
2807 "HEAD"
2808 } else {
2809 current_spec.as_str()
2810 };
2811 let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{push}}"))?;
2812 current_spec = full;
2813 }
2814 AtStep::Now | AtStep::Index(_) | AtStep::Date(_) => {}
2815 }
2816 }
2817
2818 Ok(Some(dwim_refname(repo, current_spec.as_str())))
2819}
2820
2821pub fn resolve_reflog_walk_log_ref(repo: &Repository, r: &str) -> Result<String> {
2826 if let Ok(Some(w)) = reflog_walk_refname(repo, r) {
2827 return Ok(w);
2828 }
2829 if r == "HEAD" || r.starts_with("refs/") {
2830 return Ok(r.to_string());
2831 }
2832 if r.starts_with("@{") {
2833 if let Some(n_str) = r.strip_prefix("@{").and_then(|s| s.strip_suffix('}')) {
2834 if let Some(stripped) = n_str.strip_prefix('-') {
2835 if stripped.parse::<usize>().is_ok() {
2836 if let Ok(branch) = refs::resolve_at_n_branch(&repo.git_dir, r) {
2837 return Ok(format!("refs/heads/{branch}"));
2838 }
2839 }
2840 }
2841 }
2842 return Ok(r.to_string());
2843 }
2844 let candidate = format!("refs/heads/{r}");
2845 if refs::resolve_ref(&repo.git_dir, &candidate).is_ok() {
2846 Ok(candidate)
2847 } else {
2848 Ok(r.to_string())
2849 }
2850}
2851
2852fn try_resolve_reflog_index(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
2854 let Some((prefix, steps)) = split_reflog_at_chain(spec) else {
2855 return Ok(None);
2856 };
2857
2858 let prefix_resolved = if let Some(b) = resolve_at_minus_token_to_branch(repo, &prefix)? {
2859 b
2860 } else {
2861 prefix.clone()
2862 };
2863
2864 let mut current_spec = if prefix_resolved.is_empty() {
2865 if let Ok(Some(b)) = refs::read_head(&repo.git_dir) {
2866 if let Some(short) = b.strip_prefix("refs/heads/") {
2867 short.to_owned()
2868 } else {
2869 "HEAD".to_owned()
2870 }
2871 } else {
2872 "HEAD".to_owned()
2873 }
2874 } else {
2875 prefix_resolved
2876 };
2877
2878 for (i, step) in steps.iter().enumerate() {
2879 match step {
2880 AtStep::Upstream => {
2881 let base = if current_spec == "@" {
2882 "HEAD"
2883 } else {
2884 current_spec.as_str()
2885 };
2886 let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{u}}"))?;
2887 current_spec = full;
2888 }
2889 AtStep::Push => {
2890 let base = if current_spec == "@" {
2891 "HEAD"
2892 } else {
2893 current_spec.as_str()
2894 };
2895 let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{push}}"))?;
2896 current_spec = full;
2897 }
2898 AtStep::Now => {
2899 let refname_raw = current_spec.as_str();
2900 let refname = dwim_refname(repo, refname_raw);
2901 let oid =
2902 resolve_reflog_oid(repo, &refname, refname_raw, ReflogSelector::Index(0))?;
2903 if i + 1 == steps.len() {
2904 return Ok(Some(oid));
2905 }
2906 current_spec = oid.to_hex();
2907 }
2908 AtStep::Index(n) => {
2909 let refname_raw = current_spec.as_str();
2910 let refname = dwim_refname(repo, refname_raw);
2911 let oid =
2912 resolve_reflog_oid(repo, &refname, refname_raw, ReflogSelector::Index(*n))?;
2913 if i + 1 == steps.len() {
2914 return Ok(Some(oid));
2915 }
2916 current_spec = oid.to_hex();
2917 }
2918 AtStep::Date(ts) => {
2919 let refname_raw = current_spec.as_str();
2920 let refname = dwim_refname(repo, refname_raw);
2921 let oid =
2922 resolve_reflog_oid(repo, &refname, refname_raw, ReflogSelector::Date(*ts))?;
2923 if i + 1 == steps.len() {
2924 return Ok(Some(oid));
2925 }
2926 current_spec = oid.to_hex();
2927 }
2928 }
2929 }
2930
2931 let refname_raw = current_spec.as_str();
2932 let refname = dwim_refname(repo, refname_raw);
2933 refs::resolve_ref(&repo.git_dir, &refname)
2934 .map(Some)
2935 .map_err(|_| Error::ObjectNotFound(spec.to_owned()))
2936}
2937
2938enum ReflogSelector {
2939 Index(usize),
2940 Date(i64),
2941}
2942
2943fn parse_reflog_entry_timestamp(entry: &crate::reflog::ReflogEntry) -> Option<i64> {
2945 let parts: Vec<&str> = entry.identity.rsplitn(3, ' ').collect();
2947 if parts.len() >= 2 {
2948 parts[1].parse::<i64>().ok()
2949 } else {
2950 None
2951 }
2952}
2953
2954#[must_use]
2958pub fn reflog_date_selector_timestamp(s: &str) -> Option<i64> {
2959 approxidate(s)
2960}
2961
2962fn approxidate(s: &str) -> Option<i64> {
2965 let now_ts = std::time::SystemTime::now()
2966 .duration_since(std::time::UNIX_EPOCH)
2967 .ok()
2968 .map(|d| d.as_secs() as i64)
2969 .unwrap_or(0);
2970 let lower = s.trim().to_ascii_lowercase();
2971 if lower.split_whitespace().next() == Some("now") {
2972 if let Ok(raw) =
2975 std::env::var("GIT_COMMITTER_DATE").or_else(|_| std::env::var("GIT_AUTHOR_DATE"))
2976 {
2977 let mut it = raw.split_whitespace();
2978 if let Some(ts) = it.next().and_then(|p| p.parse::<i64>().ok()) {
2979 return Some(ts);
2980 }
2981 }
2982 return Some(now_ts);
2983 }
2984 if let Ok((ts, _offset)) = crate::git_date::parse::parse_date_basic(s.trim()) {
2985 return Some(ts as i64);
2986 }
2987 let relative = lower.replace('.', " ");
2990 let parts: Vec<&str> = relative.split_whitespace().collect();
2991 if parts.len() >= 2 {
2992 let (n_str, unit) = if parts.len() >= 3 && parts[2] == "ago" {
2996 (parts[0], parts[1])
2997 } else if parts.len() == 2 {
2998 (parts[0], parts[1])
2999 } else {
3000 ("", "")
3001 };
3002 if !n_str.is_empty() {
3003 if let Ok(n) = n_str.parse::<i64>() {
3004 let secs: Option<i64> = match unit.trim_end_matches('s') {
3005 "second" => Some(n),
3006 "minute" => Some(n * 60),
3007 "hour" => Some(n * 3600),
3008 "day" => Some(n * 86400),
3009 "week" => Some(n * 604800),
3010 "month" => Some(n * 2592000),
3011 "year" => Some(n * 31536000),
3012 _ => None,
3013 };
3014 if let Some(s) = secs {
3015 return Some(now_ts - s);
3016 }
3017 }
3018 }
3019 }
3020 let re_like = |input: &str| -> Option<i64> {
3022 for (i, _) in input.char_indices() {
3024 let rest = &input[i..];
3025 if rest.len() >= 10 {
3026 let bytes = rest.as_bytes();
3027 if bytes[4] == b'-'
3028 && bytes[7] == b'-'
3029 && bytes[0..4].iter().all(|b| b.is_ascii_digit())
3030 && bytes[5..7].iter().all(|b| b.is_ascii_digit())
3031 && bytes[8..10].iter().all(|b| b.is_ascii_digit())
3032 {
3033 let year: i32 = rest[0..4].parse().ok()?;
3034 let month: u8 = rest[5..7].parse().ok()?;
3035 let day: u8 = rest[8..10].parse().ok()?;
3036 let date = time::Date::from_calendar_date(
3037 year,
3038 time::Month::try_from(month).ok()?,
3039 day,
3040 )
3041 .ok()?;
3042 let dt = date.with_hms(0, 0, 0).ok()?;
3043 let odt = dt.assume_utc();
3044 return Some(odt.unix_timestamp());
3045 }
3046 }
3047 }
3048 None
3049 };
3050 re_like(s)
3051}
3052
3053fn head_tree_oid(repo: &Repository) -> Result<ObjectId> {
3054 let head_oid = refs::resolve_ref(&repo.git_dir, "HEAD")?;
3055 peel_to_tree(repo, head_oid)
3056}
3057
3058fn path_in_tree(repo: &Repository, tree_oid: ObjectId, path: &str) -> bool {
3059 resolve_tree_path(repo, &tree_oid, path).is_ok()
3060}
3061
3062fn path_in_index(repo: &Repository, path: &str, stage: u8) -> bool {
3063 resolve_index_path_at_stage(repo, path, stage).is_ok()
3064}
3065
3066fn diagnose_tree_path_error(
3067 repo: &Repository,
3068 rev_label: &str,
3069 raw_after_colon: &str,
3070 clean_path: &str,
3071 err: Error,
3072) -> Error {
3073 let Error::ObjectNotFound(msg) = err else {
3074 return err;
3075 };
3076 if !msg.contains("not found in tree") {
3077 return Error::ObjectNotFound(msg);
3078 }
3079 let rel_display: &str =
3080 if raw_after_colon.starts_with("./") || raw_after_colon.starts_with("../") {
3081 clean_path
3082 } else {
3083 raw_after_colon
3084 };
3085 if let Ok(head_tree) = head_tree_oid(repo) {
3086 if path_in_tree(repo, head_tree, clean_path) {
3087 return Error::Message(format!(
3088 "fatal: path '{rel_display}' exists on disk, but not in '{rev_label}'."
3089 ));
3090 }
3091 if let Ok(cwd) = std::env::current_dir() {
3092 let prefix = show_prefix(repo, &cwd);
3093 let pfx = prefix.trim_end_matches('/');
3094 if !pfx.is_empty() {
3095 let candidate = if clean_path.is_empty() {
3096 pfx.to_owned()
3097 } else {
3098 format!("{pfx}/{clean_path}")
3099 };
3100 if path_in_tree(repo, head_tree, &candidate) {
3101 return Error::Message(format!(
3102 "fatal: path '{candidate}' exists, but not '{rel_display}'\n\
3103hint: Did you mean '{rev_label}:{candidate}' aka '{rev_label}:./{rel_display}'?"
3104 ));
3105 }
3106 }
3107 }
3108 let on_disk = repo
3109 .work_tree
3110 .as_ref()
3111 .map(|wt| wt.join(clean_path))
3112 .is_some_and(|p| p.exists());
3113 let in_index = path_in_index(repo, clean_path, 0);
3114 if on_disk || in_index {
3115 return Error::Message(format!(
3116 "fatal: path '{rel_display}' exists on disk, but not in '{rev_label}'."
3117 ));
3118 }
3119 }
3120 Error::Message(format!(
3121 "fatal: path '{rel_display}' does not exist in '{rev_label}'"
3122 ))
3123}
3124
3125fn diagnose_index_path_error(repo: &Repository, path: &str, stage: u8, err: Error) -> Error {
3126 let Error::ObjectNotFound(_) = err else {
3127 return err;
3128 };
3129 let work_path = repo
3130 .work_tree
3131 .as_ref()
3132 .map(|wt| wt.join(path))
3133 .filter(|p| p.exists());
3134 let on_disk = work_path.is_some();
3135 let in_head = head_tree_oid(repo)
3136 .map(|t| path_in_tree(repo, t, path))
3137 .unwrap_or(false);
3138 let in_index = path_in_index(repo, path, 0);
3139 let at_stage = path_in_index(repo, path, stage);
3140
3141 if stage > 0 && !in_index {
3142 if let Ok(cwd) = std::env::current_dir() {
3143 let prefix = show_prefix(repo, &cwd);
3144 let pfx = prefix.trim_end_matches('/');
3145 if !pfx.is_empty() {
3146 let candidate = if path.is_empty() {
3147 pfx.to_owned()
3148 } else {
3149 format!("{pfx}/{path}")
3150 };
3151 if path_in_index(repo, &candidate, 0) && !path_in_index(repo, &candidate, stage) {
3152 return Error::Message(format!(
3153 "fatal: path '{candidate}' is in the index, but not '{path}'\n\
3154hint: Did you mean ':0:{candidate}' aka ':0:./{path}'?"
3155 ));
3156 }
3157 }
3158 }
3159 return Error::Message(format!(
3160 "fatal: path '{path}' does not exist (neither on disk nor in the index)"
3161 ));
3162 }
3163
3164 if stage > 0 && in_index && !at_stage {
3165 return Error::Message(format!(
3166 "fatal: path '{path}' is in the index, but not at stage {stage}\n\
3167hint: Did you mean ':0:{path}'?"
3168 ));
3169 }
3170
3171 if stage == 0 {
3172 if !on_disk && !in_index {
3173 if let Ok(cwd) = std::env::current_dir() {
3174 let prefix = show_prefix(repo, &cwd);
3175 let pfx = prefix.trim_end_matches('/');
3176 if !pfx.is_empty() {
3177 let candidate = if path.is_empty() {
3178 pfx.to_owned()
3179 } else {
3180 format!("{pfx}/{path}")
3181 };
3182 if path_in_index(repo, &candidate, 0) {
3183 return Error::Message(format!(
3184 "fatal: path '{candidate}' is in the index, but not '{path}'\n\
3185hint: Did you mean ':0:{candidate}' aka ':0:./{path}'?"
3186 ));
3187 }
3188 }
3189 }
3190 return Error::Message(format!(
3191 "fatal: path '{path}' does not exist (neither on disk nor in the index)"
3192 ));
3193 }
3194 if on_disk && !in_index && !in_head {
3195 return Error::Message(format!(
3196 "fatal: path '{path}' exists on disk, but not in the index"
3197 ));
3198 }
3199 }
3200 Error::Message(format!("fatal: path '{path}' does not exist in the index"))
3201}
3202
3203fn resolve_index_path(repo: &Repository, path: &str) -> Result<ObjectId> {
3205 resolve_index_path_at_stage(repo, path, 0)
3206}
3207
3208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3210pub struct IndexColonSpec<'a> {
3211 pub stage: u8,
3213 pub raw_path: &'a str,
3215}
3216
3217#[must_use]
3221pub fn parse_index_colon_spec(spec: &str) -> Option<IndexColonSpec<'_>> {
3222 if !spec.starts_with(':') || spec.starts_with(":/") || spec.len() <= 1 {
3223 return None;
3224 }
3225 let rest = &spec[1..];
3226 if rest.is_empty() {
3227 return None;
3228 }
3229 if rest.len() >= 3 && rest.as_bytes()[1] == b':' {
3230 if let Some(stage_char) = rest.chars().next() {
3231 if let Some(stage) = stage_char.to_digit(10) {
3232 if stage <= 3 {
3233 return Some(IndexColonSpec {
3234 stage: stage as u8,
3235 raw_path: &rest[2..],
3236 });
3237 }
3238 }
3239 }
3240 }
3241 Some(IndexColonSpec {
3242 stage: 0,
3243 raw_path: rest,
3244 })
3245}
3246
3247#[derive(Debug, Clone, PartialEq, Eq)]
3249pub struct IndexPathEntry {
3250 pub path: String,
3252 pub oid: ObjectId,
3254 pub mode: u32,
3256}
3257
3258pub fn resolve_index_path_entry(repo: &Repository, spec: &str) -> Result<Option<IndexPathEntry>> {
3266 let Some(colon) = parse_index_colon_spec(spec) else {
3267 return Ok(None);
3268 };
3269 let path = match normalize_colon_path_for_tree(repo, colon.raw_path) {
3270 Ok(p) => p,
3271 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
3272 let wt = repo
3273 .work_tree
3274 .as_ref()
3275 .and_then(|p| p.canonicalize().ok())
3276 .map(|p| p.display().to_string())
3277 .unwrap_or_default();
3278 return Err(Error::Message(format!(
3279 "fatal: '{}' is outside repository at '{wt}'",
3280 colon.raw_path
3281 )));
3282 }
3283 Err(e) => return Err(e),
3284 };
3285 let index_path = if let Ok(raw) = std::env::var("GIT_INDEX_FILE") {
3286 let p = std::path::PathBuf::from(raw);
3287 if p.is_absolute() {
3288 p
3289 } else if let Ok(cwd) = std::env::current_dir() {
3290 cwd.join(p)
3291 } else {
3292 p
3293 }
3294 } else {
3295 repo.index_path()
3296 };
3297 use crate::index::Index;
3298 let index = Index::load_expand_sparse(&index_path, &repo.odb)
3299 .map_err(|_| Error::ObjectNotFound(format!(":{}:{}", colon.stage, path)))?;
3300 let entry = index
3301 .get(path.as_bytes(), colon.stage)
3302 .ok_or_else(|| Error::ObjectNotFound(format!(":{}:{}", colon.stage, path)))?;
3303 Ok(Some(IndexPathEntry {
3304 path,
3305 oid: entry.oid,
3306 mode: entry.mode,
3307 }))
3308}
3309
3310fn resolve_index_path_at_stage(repo: &Repository, path: &str, stage: u8) -> Result<ObjectId> {
3312 use crate::index::Index;
3313 let index_path = if let Ok(raw) = std::env::var("GIT_INDEX_FILE") {
3314 let p = std::path::PathBuf::from(raw);
3315 if p.is_absolute() {
3316 p
3317 } else if let Ok(cwd) = std::env::current_dir() {
3318 cwd.join(p)
3319 } else {
3320 p
3321 }
3322 } else {
3323 repo.index_path()
3324 };
3325 let index = Index::load_expand_sparse(&index_path, &repo.odb)
3326 .map_err(|_| Error::ObjectNotFound(format!(":{stage}:{path}")))?;
3327 match index.get(path.as_bytes(), stage) {
3328 Some(entry) => Ok(entry.oid),
3329 None => Err(Error::ObjectNotFound(format!(":{stage}:{path}"))),
3330 }
3331}
3332
3333pub fn split_treeish_colon(spec: &str) -> Option<(&str, &str)> {
3338 if spec.starts_with(':') {
3339 return None;
3340 }
3341 let bytes = spec.as_bytes();
3342 let mut i = 0usize;
3343 let mut peel_depth = 0usize;
3344 while i < bytes.len() {
3345 if i + 1 < bytes.len() && bytes[i] == b'^' && bytes[i + 1] == b'{' {
3346 peel_depth += 1;
3347 i += 2;
3348 continue;
3349 }
3350 if peel_depth > 0 {
3351 if bytes[i] == b'}' {
3352 peel_depth -= 1;
3353 }
3354 i += 1;
3355 continue;
3356 }
3357 if bytes[i] == b':' && i > 0 {
3358 let before = &spec[..i];
3359 let after = &spec[i + 1..];
3360 if !before.is_empty() {
3361 return Some((before, after)); }
3363 }
3364 i += 1;
3365 }
3366 None
3367}
3368
3369pub(crate) fn split_treeish_spec(spec: &str) -> Option<(&str, &str)> {
3370 split_treeish_colon(spec)
3371}
3372
3373pub(crate) fn resolve_treeish_path_to_object(
3377 repo: &Repository,
3378 treeish: ObjectId,
3379 path: &str,
3380) -> Result<ObjectId> {
3381 let object = repo.read_replaced(&treeish)?;
3382 let mut current_tree = match object.kind {
3383 ObjectKind::Commit => parse_commit(&object.data)?.tree,
3384 ObjectKind::Tree => treeish,
3385 _ => {
3386 return Err(Error::InvalidRef(format!(
3387 "object {treeish} does not name a tree"
3388 )))
3389 }
3390 };
3391
3392 let parts_vec: Vec<&str> = path.split('/').filter(|p| !p.is_empty()).collect();
3393 if parts_vec.is_empty() {
3394 return Ok(current_tree);
3395 }
3396 for (idx, part) in parts_vec.iter().enumerate() {
3397 let tree_object = repo.read_replaced(¤t_tree)?;
3398 if tree_object.kind != ObjectKind::Tree {
3399 return Err(Error::CorruptObject(format!(
3400 "object {current_tree} is not a tree"
3401 )));
3402 }
3403 let entries = parse_tree(&tree_object.data)?;
3404 let Some(entry) = entries.iter().find(|entry| entry.name == part.as_bytes()) else {
3405 return Err(Error::ObjectNotFound(path.to_owned()));
3406 };
3407 if idx + 1 == parts_vec.len() {
3408 return Ok(entry.oid);
3409 }
3410 if entry.mode != crate::index::MODE_TREE {
3411 return Err(Error::ObjectNotFound(path.to_owned()));
3412 }
3413 current_tree = entry.oid;
3414 }
3415
3416 Err(Error::ObjectNotFound(path.to_owned()))
3417}
3418
3419pub(crate) fn resolve_treeish_path(
3420 repo: &Repository,
3421 treeish: ObjectId,
3422 path: &str,
3423) -> Result<ObjectId> {
3424 resolve_treeish_path_to_object(repo, treeish, path)
3425}
3426
3427fn apply_peel(repo: &Repository, mut oid: ObjectId, peel: Option<&str>) -> Result<ObjectId> {
3428 match peel {
3429 None => Ok(oid),
3430 Some(search) if search.starts_with('/') => {
3431 let pattern = &search[1..];
3432 if pattern.is_empty() {
3433 return Err(Error::InvalidRef(
3434 "empty commit message search pattern".to_owned(),
3435 ));
3436 }
3437 resolve_commit_message_search_from(repo, oid, pattern)
3438 }
3439 Some("") => {
3440 while let Ok(obj) = repo.read_replaced(&oid) {
3441 if obj.kind != ObjectKind::Tag {
3442 break;
3443 }
3444 oid = parse_tag_target(&obj.data)?;
3445 }
3446 Ok(oid)
3447 }
3448 Some("commit") => {
3449 oid = apply_peel(repo, oid, Some(""))?;
3450 let obj = repo.read_replaced(&oid)?;
3451 if obj.kind == ObjectKind::Commit {
3452 Ok(oid)
3453 } else {
3454 Err(Error::InvalidRef("expected commit".to_owned()))
3455 }
3456 }
3457 Some("tree") => {
3458 oid = apply_peel(repo, oid, Some(""))?;
3460 let obj = repo.read_replaced(&oid)?;
3461 match obj.kind {
3462 ObjectKind::Tree => Ok(oid),
3463 ObjectKind::Commit => Ok(parse_commit(&obj.data)?.tree),
3464 _ => Err(Error::InvalidRef("expected tree or commit".to_owned())),
3465 }
3466 }
3467 Some("blob") => {
3468 let mut cur = oid;
3470 loop {
3471 let obj = repo.read_replaced(&cur)?;
3472 match obj.kind {
3473 ObjectKind::Blob => return Ok(cur),
3474 ObjectKind::Tag => {
3475 cur = parse_tag_target(&obj.data)?;
3476 }
3477 _ => return Err(Error::InvalidRef("expected blob".to_owned())),
3478 }
3479 }
3480 }
3481 Some("object") => Ok(oid),
3482 Some("tag") => {
3483 let obj = repo.read_replaced(&oid)?;
3485 if obj.kind == ObjectKind::Tag {
3486 Ok(oid)
3487 } else {
3488 Err(Error::InvalidRef("expected tag".to_owned()))
3489 }
3490 }
3491 Some(other) => Err(Error::InvalidRef(format!(
3492 "unsupported peel operator '{{{other}}}'"
3493 ))),
3494 }
3495}
3496
3497pub fn expand_rev_token_circ_bang(repo: &Repository, token: &str) -> Result<Vec<String>> {
3508 let Some(base) = token.strip_suffix("^!") else {
3509 return Ok(vec![token.to_owned()]);
3510 };
3511 if base.is_empty() {
3512 return Err(Error::Message(format!(
3513 "fatal: ambiguous argument '{token}': unknown revision or path not in the working tree.\n\
3514Use '--' to separate paths from revisions, like this:\n\
3515'git <command> [<revision>...] -- [<file>...]'"
3516 )));
3517 }
3518 let oid = resolve_revision_for_range_end(repo, base)?;
3519 let commit_oid = peel_to_commit_for_merge_base(repo, oid)?;
3520 let parents = commit_parents_for_navigation(repo, commit_oid)?;
3521 let mut out = vec![base.to_owned()];
3522 for p in parents {
3523 out.push(format!("^{}", p.to_hex()));
3524 }
3525 Ok(out)
3526}
3527
3528#[must_use]
3530pub fn parse_peel_suffix(spec: &str) -> (&str, Option<&str>) {
3531 if let Some(base) = spec.strip_suffix("^{}") {
3532 return (base, Some(""));
3533 }
3534 if let Some(start) = spec.rfind("^{") {
3535 if spec.ends_with('}') {
3536 let base = &spec[..start];
3537 let op = &spec[start + 2..spec.len() - 1];
3538 return (base, Some(op));
3539 }
3540 }
3541 if let Some(base) = spec.strip_suffix("^0") {
3543 if !base.ends_with('^') {
3546 return (base, Some("commit"));
3547 }
3548 }
3549 (spec, None)
3550}
3551
3552fn parse_tag_target(data: &[u8]) -> Result<ObjectId> {
3553 let text = std::str::from_utf8(data)
3554 .map_err(|_| Error::CorruptObject("invalid tag object".to_owned()))?;
3555 let Some(line) = text.lines().find(|line| line.starts_with("object ")) else {
3556 return Err(Error::CorruptObject("tag missing object header".to_owned()));
3557 };
3558 let oid_text = line.trim_start_matches("object ").trim();
3559 oid_text.parse::<ObjectId>()
3560}
3561
3562fn parse_oneline_pattern(pattern: &str) -> Option<(bool, &str)> {
3574 let Some(after_bang) = pattern.strip_prefix('!') else {
3575 return Some((false, pattern));
3576 };
3577 if let Some(neg) = after_bang.strip_prefix('-') {
3578 return Some((true, neg));
3579 }
3580 if after_bang.starts_with('!') {
3581 return Some((false, after_bang));
3583 }
3584 None
3586}
3587
3588fn resolve_commit_message_search_from(
3589 repo: &Repository,
3590 start: ObjectId,
3591 pattern: &str,
3592) -> Result<ObjectId> {
3593 let Some((negate, effective_pattern)) = parse_oneline_pattern(pattern) else {
3594 return Err(Error::ObjectNotFound(format!(":/{pattern}")));
3595 };
3596 let regex = Regex::new(effective_pattern).ok();
3597 let mut visited = std::collections::HashSet::new();
3598 let mut queue = std::collections::VecDeque::new();
3599 queue.push_back(start);
3600 visited.insert(start);
3601
3602 while let Some(oid) = queue.pop_front() {
3603 let obj = match repo.read_replaced(&oid) {
3604 Ok(o) => o,
3605 Err(_) => continue,
3606 };
3607 if obj.kind != ObjectKind::Commit {
3608 continue;
3609 }
3610 let commit = match parse_commit(&obj.data) {
3611 Ok(c) => c,
3612 Err(_) => continue,
3613 };
3614
3615 let base_match = if let Some(re) = ®ex {
3616 re.is_match(&commit.message)
3617 } else {
3618 commit.message.contains(effective_pattern)
3619 };
3620 let is_match = negate ^ base_match;
3621 if is_match {
3622 return Ok(oid);
3623 }
3624
3625 for parent in &commit.parents {
3626 if visited.insert(*parent) {
3627 queue.push_back(*parent);
3628 }
3629 }
3630 }
3631
3632 Err(Error::ObjectNotFound(format!(":/{pattern}")))
3633}
3634
3635fn find_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
3636 let max_len = repo.odb.hash_algo().hex_len();
3637 if !is_hex_prefix(prefix) || !(4..=max_len).contains(&prefix.len()) {
3638 return Ok(Vec::new());
3639 }
3640 let mut seen = HashSet::new();
3641 let mut matches = Vec::new();
3642 for objects_dir in object_storage_dirs_for_abbrev(repo)? {
3643 for hex in collect_loose_object_ids_in_dir(&objects_dir)? {
3644 if hex.starts_with(prefix) {
3645 let oid = hex.parse::<ObjectId>()?;
3646 if seen.insert(oid) {
3647 matches.push(oid);
3648 }
3649 }
3650 }
3651 for oid in collect_pack_oids_with_prefix(&objects_dir, prefix)? {
3652 if seen.insert(oid) {
3653 matches.push(oid);
3654 }
3655 }
3656 }
3657 Ok(matches)
3658}
3659
3660fn collect_loose_object_ids(repo: &Repository) -> Result<Vec<String>> {
3661 collect_loose_object_ids_in_dir(repo.odb.objects_dir())
3662}
3663
3664fn collect_loose_object_ids_in_dir(objects_dir: &Path) -> Result<Vec<String>> {
3665 let mut ids = Vec::new();
3666 let read = match fs::read_dir(objects_dir) {
3667 Ok(read) => read,
3668 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(ids),
3669 Err(err) => return Err(Error::Io(err)),
3670 };
3671
3672 for dir_entry in read {
3673 let dir_entry = dir_entry?;
3674 let name = dir_entry.file_name();
3675 let Some(prefix) = name.to_str() else {
3676 continue;
3677 };
3678 if !is_two_hex(prefix) {
3679 continue;
3680 }
3681 if !dir_entry.file_type()?.is_dir() {
3682 continue;
3683 }
3684
3685 let files = fs::read_dir(dir_entry.path())?;
3686 for file_entry in files {
3687 let file_entry = file_entry?;
3688 if !file_entry.file_type()?.is_file() {
3689 continue;
3690 }
3691 let file_name = file_entry.file_name();
3692 let Some(suffix) = file_name.to_str() else {
3693 continue;
3694 };
3695 if ObjectId::is_loose_suffix_len(suffix.len())
3696 && suffix.chars().all(|ch| ch.is_ascii_hexdigit())
3697 {
3698 ids.push(format!("{prefix}{suffix}"));
3699 }
3700 }
3701 }
3702
3703 Ok(ids)
3704}
3705
3706fn is_two_hex(text: &str) -> bool {
3707 text.len() == 2 && text.chars().all(|ch| ch.is_ascii_hexdigit())
3708}
3709
3710fn is_hex_prefix(text: &str) -> bool {
3711 !text.is_empty() && text.chars().all(|ch| ch.is_ascii_hexdigit())
3712}
3713
3714fn path_is_within(path: &Path, container: &Path) -> bool {
3715 if path == container {
3716 return true;
3717 }
3718 path.starts_with(container)
3719}
3720
3721fn normalize_components(path: &Path) -> Vec<String> {
3722 path.components()
3723 .filter_map(|component| match component {
3724 Component::RootDir => Some(String::from("/")),
3725 Component::Normal(item) => Some(item.to_string_lossy().into_owned()),
3726 _ => None,
3727 })
3728 .collect()
3729}
3730
3731fn component_to_text(component: Component<'_>) -> Option<String> {
3732 match component {
3733 Component::Normal(item) => Some(os_to_string(item)),
3734 _ => None,
3735 }
3736}
3737
3738fn os_to_string(text: &OsStr) -> String {
3739 text.to_string_lossy().into_owned()
3740}
3741
3742fn resolve_commit_message_search(
3745 repo: &crate::repo::Repository,
3746 pattern: &str,
3747) -> Result<ObjectId> {
3748 let (negate, effective_pattern) = if pattern.starts_with('!') {
3750 if pattern.starts_with("!!") {
3751 (false, &pattern[1..]) } else {
3753 (true, &pattern[1..]) }
3755 } else {
3756 (false, pattern)
3757 };
3758 let regex = Regex::new(effective_pattern).ok();
3759 use crate::state::resolve_head;
3760 let head =
3761 resolve_head(&repo.git_dir).map_err(|_| Error::ObjectNotFound(format!(":/{pattern}")))?;
3762 let start_oid = match head.oid() {
3763 Some(oid) => *oid,
3764 None => return Err(Error::ObjectNotFound(format!(":/{pattern}"))),
3765 };
3766
3767 let mut visited = std::collections::HashSet::new();
3768 let mut queue = std::collections::VecDeque::new();
3769 queue.push_back(start_oid);
3770 visited.insert(start_oid);
3771 if let Ok(refs) = crate::refs::list_refs(&repo.git_dir, "refs/") {
3772 for (_name, oid) in refs {
3773 if visited.insert(oid) {
3774 queue.push_back(oid);
3775 }
3776 }
3777 }
3778
3779 while let Some(oid) = queue.pop_front() {
3780 let obj = match repo.read_replaced(&oid) {
3781 Ok(o) => o,
3782 Err(_) => continue,
3783 };
3784 if obj.kind != ObjectKind::Commit {
3786 continue;
3787 }
3788 let commit = match parse_commit(&obj.data) {
3789 Ok(c) => c,
3790 Err(_) => continue,
3791 };
3792
3793 let base_match = if let Some(re) = ®ex {
3795 re.is_match(&commit.message)
3796 } else {
3797 commit.message.contains(effective_pattern)
3798 };
3799 let is_match = if negate { !base_match } else { base_match };
3800 if is_match {
3801 return Ok(oid);
3802 }
3803
3804 for parent in &commit.parents {
3806 if visited.insert(*parent) {
3807 queue.push_back(*parent);
3808 }
3809 }
3810 }
3811
3812 Err(Error::ObjectNotFound(format!(":/{pattern}")))
3813}
3814
3815pub fn list_all_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
3817 find_abbrev_matches(repo, prefix)
3818}
3819
3820pub fn list_loose_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
3822 list_all_abbrev_matches(repo, prefix)
3823}
3824
3825#[cfg(test)]
3826mod superproject_path_tests {
3827 use super::superproject_work_tree_from_nested_git_modules;
3828 use std::path::PathBuf;
3829
3830 #[test]
3831 fn nested_modules_yields_superproject_work_tree() {
3832 let git_dir = PathBuf::from("/tmp/super/.git/modules/dir/modules/sub");
3833 assert_eq!(
3834 superproject_work_tree_from_nested_git_modules(&git_dir),
3835 Some(PathBuf::from("/tmp/super"))
3836 );
3837 }
3838
3839 #[test]
3840 fn non_nested_returns_none() {
3841 let git_dir = PathBuf::from("/tmp/repo/.git");
3842 assert!(superproject_work_tree_from_nested_git_modules(&git_dir).is_none());
3843 }
3844}