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::HashSet;
16
17use crate::config::ConfigSet;
18use crate::error::{Error, Result};
19use crate::objects::{parse_commit, parse_tree, ObjectId, ObjectKind};
20use crate::pack;
21use crate::reflog::read_reflog;
22use crate::refs;
23use crate::repo::Repository;
24
25pub fn discover_optional(start: Option<&Path>) -> Result<Option<Repository>> {
36 match Repository::discover(start) {
37 Ok(repo) => Ok(Some(repo)),
38 Err(Error::NotARepository(msg)) => {
39 if msg.contains("invalid gitfile format")
43 || msg.contains("gitfile does not contain 'gitdir:' line")
44 || msg.contains("not a regular file")
45 {
46 return Err(Error::NotARepository(msg));
47 }
48
49 if let Some(start) = start {
50 let start = if start.is_absolute() {
51 start.to_path_buf()
52 } else if let Ok(cwd) = std::env::current_dir() {
53 cwd.join(start)
54 } else {
55 start.to_path_buf()
56 };
57 let dot_git = start.join(".git");
58 if dot_git.is_file() || dot_git.is_symlink() {
59 return Err(Error::NotARepository(msg));
60 }
61 }
62
63 Ok(None)
64 }
65 Err(err) => Err(err),
66 }
67}
68
69#[must_use]
71pub fn is_inside_work_tree(repo: &Repository, cwd: &Path) -> bool {
72 let Some(work_tree) = &repo.work_tree else {
73 return false;
74 };
75 path_is_within(cwd, work_tree)
76}
77
78#[must_use]
80pub fn is_inside_git_dir(repo: &Repository, cwd: &Path) -> bool {
81 path_is_within(cwd, &repo.git_dir)
82}
83
84#[must_use]
89pub fn show_prefix(repo: &Repository, cwd: &Path) -> String {
90 let Some(work_tree) = &repo.work_tree else {
91 return String::new();
92 };
93 if !path_is_within(cwd, work_tree) {
94 return String::new();
95 }
96 if cwd == work_tree {
97 return String::new();
98 }
99 let Ok(rel) = cwd.strip_prefix(work_tree) else {
100 return String::new();
101 };
102 let mut out = rel
103 .components()
104 .filter_map(component_to_text)
105 .collect::<Vec<_>>()
106 .join("/");
107 if !out.is_empty() {
108 out.push('/');
109 }
110 out
111}
112
113#[must_use]
120pub fn symbolic_full_name(repo: &Repository, spec: &str) -> Option<String> {
121 if upstream_suffix_info(spec).is_some() {
123 return resolve_upstream_symbolic_name(repo, spec).ok();
124 }
125
126 if let Ok(Some(branch)) = expand_at_minus_to_branch_name(repo, spec) {
127 let ref_name = format!("refs/heads/{branch}");
128 if refs::resolve_ref(&repo.git_dir, &ref_name).is_ok() {
129 return Some(ref_name);
130 }
131 return None;
132 }
133
134 if spec == "HEAD" {
135 if let Ok(Some(target)) = refs::read_symbolic_ref(&repo.git_dir, "HEAD") {
136 return Some(target);
137 }
138 return None;
139 }
140 if spec.starts_with("refs/") {
142 if refs::resolve_ref(&repo.git_dir, spec).is_ok() {
143 return Some(spec.to_owned());
144 }
145 return None;
146 }
147 for prefix in &["refs/heads/", "refs/tags/", "refs/remotes/"] {
149 let candidate = format!("{prefix}{spec}");
150 if refs::resolve_ref(&repo.git_dir, &candidate).is_ok() {
151 return Some(candidate);
152 }
153 }
154 if let Some(full) = remote_tracking_head_symbolic_target(repo, spec) {
156 return Some(full);
157 }
158 None
159}
160
161fn remote_tracking_head_symbolic_target(repo: &Repository, name: &str) -> Option<String> {
163 if name.contains('/')
164 || matches!(
165 name,
166 "HEAD" | "FETCH_HEAD" | "MERGE_HEAD" | "CHERRY_PICK_HEAD" | "REVERT_HEAD"
167 )
168 {
169 return None;
170 }
171 let config = ConfigSet::load(Some(&repo.git_dir), true).ok()?;
172 let url_key = format!("remote.{name}.url");
173 config.get(&url_key)?;
174 let head_ref = format!("refs/remotes/{name}/HEAD");
175 let target = refs::read_symbolic_ref(&repo.git_dir, &head_ref).ok()??;
176 Some(target)
177}
178
179pub fn expand_at_minus_to_branch_name(repo: &Repository, spec: &str) -> Result<Option<String>> {
187 if !spec.starts_with("@{-") || !spec.ends_with('}') {
188 return Ok(None);
189 }
190 let inner = &spec[3..spec.len() - 1];
191 let n: usize = inner
192 .parse()
193 .map_err(|_| Error::InvalidRef(format!("invalid N in @{{-N}} for '{spec}'")))?;
194 if n < 1 {
195 return Ok(None);
196 }
197 resolve_at_minus_to_branch(repo, n).map(Some)
198}
199
200pub fn resolve_at_minus_to_oid(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
202 try_resolve_at_minus(repo, spec)
203}
204
205#[must_use]
209pub fn abbreviate_ref_name(full_name: &str) -> String {
210 for prefix in &["refs/heads/", "refs/tags/", "refs/remotes/"] {
211 if let Some(short) = full_name.strip_prefix(prefix) {
212 return short.to_owned();
213 }
214 }
215 if let Some(short) = full_name.strip_prefix("refs/") {
216 return short.to_owned();
217 }
218 full_name.to_owned()
219}
220
221#[must_use]
224pub fn upstream_suffix_info(spec: &str) -> Option<(&str, bool)> {
225 let lower = spec.to_ascii_lowercase();
226 if lower.ends_with("@{push}") {
227 let base = &spec[..spec.len() - 7];
228 return Some((base, true));
229 }
230 if lower.ends_with("@{upstream}") {
231 let base = &spec[..spec.len() - 11];
232 return Some((base, false));
233 }
234 if lower.ends_with("@{u}") {
235 let base = &spec[..spec.len() - 4];
236 return Some((base, false));
237 }
238 None
239}
240
241pub fn resolve_upstream_symbolic_name(repo: &Repository, spec: &str) -> Result<String> {
243 let Some((base, is_push)) = upstream_suffix_info(spec) else {
244 return Err(Error::InvalidRef(format!("not an upstream spec: {spec}")));
245 };
246 resolve_upstream_full_ref_name(repo, base, is_push)
247}
248
249fn resolve_upstream_full_ref_name(repo: &Repository, base: &str, is_push: bool) -> Result<String> {
250 if is_push {
251 return resolve_push_ref_name(repo, base);
252 }
253 let (branch_key, display_branch) = resolve_upstream_branch_context(repo, base)?;
254 let config_path = repo.git_dir.join("config");
255 let config_content = fs::read_to_string(&config_path).map_err(Error::Io)?;
256 let Some((remote, merge)) = parse_branch_tracking(&config_content, &branch_key) else {
257 return Err(Error::Message(format!(
258 "fatal: no upstream configured for branch '{display_branch}'"
259 )));
260 };
261 if remote == "." {
262 return Ok(merge);
263 }
264 let merge_branch = merge
265 .strip_prefix("refs/heads/")
266 .ok_or_else(|| Error::InvalidRef(format!("invalid merge ref: {merge}")))?;
267 let tracking = format!("refs/remotes/{remote}/{merge_branch}");
268 if refs::resolve_ref(&repo.git_dir, &tracking).is_err() {
269 return Err(Error::Message(format!(
270 "fatal: upstream branch '{merge}' not stored as a remote-tracking branch"
271 )));
272 }
273 Ok(tracking)
274}
275
276pub fn resolve_push_full_ref_for_branch(repo: &Repository, branch_short: &str) -> Result<String> {
281 let config_path = crate::refs::common_dir(&repo.git_dir)
282 .unwrap_or_else(|| repo.git_dir.clone())
283 .join("config");
284 let config_content = fs::read_to_string(&config_path).map_err(Error::Io)?;
285
286 let upstream_tracking =
287 parse_branch_tracking(&config_content, branch_short).and_then(|(remote, merge)| {
288 if remote == "." {
289 return None;
290 }
291 let mb = merge.strip_prefix("refs/heads/").unwrap_or(&merge);
292 let tr = format!("refs/remotes/{remote}/{mb}");
293 if refs::resolve_ref(&repo.git_dir, &tr).is_ok() {
294 Some(tr)
295 } else {
296 None
297 }
298 });
299
300 let push_remote = parse_config_value(&config_content, "remote", "pushRemote")
301 .or_else(|| parse_config_value(&config_content, "remote", "pushDefault"))
302 .or_else(|| {
303 let section = format!("[branch \"{}\"]", branch_short);
304 let mut in_section = false;
305 for line in config_content.lines() {
306 let trimmed = line.trim();
307 if trimmed.starts_with('[') {
308 in_section = trimmed == section;
309 continue;
310 }
311 if in_section {
312 if let Some(v) = trimmed
313 .strip_prefix("pushremote = ")
314 .or_else(|| trimmed.strip_prefix("pushRemote = "))
315 {
316 return Some(v.trim().to_owned());
317 }
318 }
319 }
320 None
321 })
322 .or_else(|| {
323 parse_branch_tracking(&config_content, branch_short)
324 .map(|(r, _)| r)
325 .filter(|r| r != ".")
326 });
327
328 let Some(push_remote_name) = push_remote else {
329 return upstream_tracking.ok_or_else(|| {
330 Error::Message("fatal: branch has no configured push remote".to_owned())
331 });
332 };
333
334 let push_default = parse_config_value(&config_content, "push", "default");
335 let push_default = push_default.as_deref().unwrap_or("simple");
336
337 if push_default == "nothing" {
338 return Err(Error::Message(
339 "fatal: push.default is nothing; no push destination".to_owned(),
340 ));
341 }
342
343 if let Some(mapped) =
344 push_refspec_mapped_tracking(&config_content, &push_remote_name, branch_short)
345 {
346 if refs::resolve_ref(&repo.git_dir, &mapped).is_ok() {
347 return Ok(mapped);
348 }
349 }
350
351 let current_tracking = format!("refs/remotes/{push_remote_name}/{branch_short}");
352
353 match push_default {
354 "upstream" => upstream_tracking.ok_or_else(|| {
355 Error::Message(format!(
356 "fatal: branch '{branch_short}' has no upstream for push.default upstream"
357 ))
358 }),
359 "simple" => {
360 if let Some(ref up) = upstream_tracking {
361 if up == ¤t_tracking
362 && refs::resolve_ref(&repo.git_dir, ¤t_tracking).is_ok()
363 {
364 return Ok(current_tracking);
365 }
366 }
367 Err(Error::Message(
368 "fatal: push.default simple: upstream and push ref differ".to_owned(),
369 ))
370 }
371 "current" | "matching" | _ => {
372 if refs::resolve_ref(&repo.git_dir, ¤t_tracking).is_ok() {
373 Ok(current_tracking)
374 } else if let Some(up) = upstream_tracking {
375 Ok(up)
376 } else {
377 Err(Error::Message(format!(
378 "fatal: no push tracking ref for branch '{branch_short}'"
379 )))
380 }
381 }
382 }
383}
384
385fn push_refspec_mapped_tracking(
386 config_content: &str,
387 remote_name: &str,
388 branch_short: &str,
389) -> Option<String> {
390 let section = format!("[remote \"{remote_name}\"]");
391 let mut in_section = false;
392 let src_want = format!("refs/heads/{branch_short}");
393 for line in config_content.lines() {
394 let trimmed = line.trim();
395 if trimmed.starts_with('[') {
396 in_section = trimmed == section;
397 continue;
398 }
399 if !in_section {
400 continue;
401 }
402 let Some(val) = trimmed
403 .strip_prefix("push = ")
404 .or_else(|| trimmed.strip_prefix("push="))
405 else {
406 continue;
407 };
408 let Some(spec) = val.split_whitespace().next() else {
409 continue;
410 };
411 let spec = spec.trim().strip_prefix('+').unwrap_or(spec);
412 let Some((left, right)) = spec.split_once(':') else {
413 continue;
414 };
415 let left = left.trim();
416 let right = right.trim();
417 if left != src_want {
418 continue;
419 }
420 let Some(dest_branch) = right.strip_prefix("refs/heads/") else {
421 continue;
422 };
423 return Some(format!("refs/remotes/{remote_name}/{dest_branch}"));
424 }
425 None
426}
427
428fn resolve_push_ref_name(repo: &Repository, base: &str) -> Result<String> {
429 let (branch_key, _display) = resolve_upstream_branch_context(repo, base)?;
430 resolve_push_full_ref_for_branch(repo, &branch_key)
431}
432
433fn resolve_upstream_branch_context(repo: &Repository, base: &str) -> Result<(String, String)> {
435 let base = if base == "HEAD" {
436 Cow::Borrowed("")
437 } else if base.starts_with("@{-") && base.ends_with('}') {
438 if let Ok(Some(b)) = expand_at_minus_to_branch_name(repo, base) {
439 Cow::Owned(b)
440 } else {
441 Cow::Borrowed(base)
442 }
443 } else {
444 Cow::Borrowed(base)
445 };
446 let base = base.as_ref();
447 let base = if base == "@" { "" } else { base };
448
449 if base.is_empty() {
450 let Some(head) = refs::read_head(&repo.git_dir)? else {
451 return Err(Error::Message(
452 "fatal: HEAD does not point to a branch".to_owned(),
453 ));
454 };
455 let Some(short) = head.strip_prefix("refs/heads/") else {
456 return Err(Error::Message(
457 "fatal: HEAD does not point to a branch".to_owned(),
458 ));
459 };
460 return Ok((short.to_owned(), short.to_owned()));
461 }
462 let head_branch = refs::read_head(&repo.git_dir)?.and_then(|h| {
463 h.strip_prefix("refs/heads/")
464 .map(std::borrow::ToOwned::to_owned)
465 });
466 if head_branch.as_deref() == Some(base) {
467 return Ok((base.to_owned(), base.to_owned()));
468 }
469 let refname = format!("refs/heads/{base}");
470 if refs::resolve_ref(&repo.git_dir, &refname).is_err() {
471 return Err(Error::Message(format!("fatal: no such branch: '{base}'")));
472 }
473 Ok((base.to_owned(), base.to_owned()))
474}
475
476fn parse_config_value(config: &str, section: &str, key: &str) -> Option<String> {
477 let section_header = format!("[{}]", section);
478 let key_lower = key.to_ascii_lowercase();
479 let mut in_section = false;
480 for line in config.lines() {
481 let trimmed = line.trim();
482 if trimmed.starts_with('[') {
483 in_section = trimmed.eq_ignore_ascii_case(§ion_header);
484 continue;
485 }
486 if in_section {
487 let lower = trimmed.to_ascii_lowercase();
488 if lower.starts_with(&key_lower) {
489 let rest = lower[key_lower.len()..].trim_start().to_string();
490 if rest.starts_with('=') {
491 if let Some(eq_pos) = trimmed.find('=') {
492 return Some(trimmed[eq_pos + 1..].trim().to_owned());
493 }
494 }
495 }
496 }
497 }
498 None
499}
500
501fn parse_branch_tracking(config: &str, branch: &str) -> Option<(String, String)> {
503 let mut remote = None;
504 let mut merge = None;
505 let mut in_section = false;
506 let target_section = format!("[branch \"{}\"]", branch);
507
508 for line in config.lines() {
509 let trimmed = line.trim();
510 if trimmed.starts_with('[') {
511 in_section = trimmed == target_section
512 || trimmed.starts_with(&format!("[branch \"{}\"", branch));
513 continue;
514 }
515 if !in_section {
516 continue;
517 }
518 if let Some(value) = trimmed.strip_prefix("remote = ") {
519 remote = Some(value.trim().to_owned());
520 } else if let Some(value) = trimmed.strip_prefix("merge = ") {
521 merge = Some(value.trim().to_owned());
522 }
523 if let Some(value) = trimmed.strip_prefix("remote=") {
525 remote = Some(value.trim().to_owned());
526 } else if let Some(value) = trimmed.strip_prefix("merge=") {
527 merge = Some(value.trim().to_owned());
528 }
529 }
530
531 match (remote, merge) {
532 (Some(r), Some(m)) => Some((r, m)),
533 _ => None,
534 }
535}
536
537#[must_use]
554pub fn split_double_dot_range(spec: &str) -> Option<(&str, &str)> {
555 if spec == ".." {
556 return Some(("", ""));
557 }
558 let bytes = spec.as_bytes();
559 let mut search = 0usize;
560 while let Some(rel) = spec[search..].find("..") {
561 let idx = search + rel;
562 let touches_dot_before = idx > 0 && bytes[idx - 1] == b'.';
564 let touches_dot_after = idx + 2 < bytes.len() && bytes[idx + 2] == b'.';
565 if touches_dot_before || touches_dot_after {
566 search = idx + 1;
567 continue;
568 }
569 if idx + 2 < bytes.len() && (bytes[idx + 2] == b'/' || bytes[idx + 2] == b'\\') {
571 search = idx + 1;
572 continue;
573 }
574 let left = &spec[..idx];
575 let right = &spec[idx + 2..];
576 return Some((left, right));
577 }
578 None
579}
580
581#[must_use]
585pub fn split_triple_dot_range(spec: &str) -> Option<(&str, &str)> {
586 if spec == "..." {
587 return Some(("", ""));
588 }
589 let bytes = spec.as_bytes();
590 let mut search = 0usize;
591 while let Some(rel) = spec[search..].find("...") {
592 let idx = search + rel;
593 let four_before = idx >= 1 && bytes[idx - 1] == b'.';
594 let four_after = idx + 3 < bytes.len() && bytes[idx + 3] == b'.';
595 if four_before || four_after {
596 search = idx + 1;
597 continue;
598 }
599 let left = &spec[..idx];
600 let right = &spec[idx + 3..];
601 return Some((left, right));
602 }
603 None
604}
605
606pub fn resolve_revision_without_index_dwim(repo: &Repository, spec: &str) -> Result<ObjectId> {
609 resolve_revision_impl(repo, spec, false, false, true, false, false, false)
610}
611
612pub fn resolve_revision(repo: &Repository, spec: &str) -> Result<ObjectId> {
614 resolve_revision_impl(repo, spec, true, false, true, false, false, false)
615}
616
617pub fn resolve_revision_for_range_end(repo: &Repository, spec: &str) -> Result<ObjectId> {
620 resolve_revision_impl(repo, spec, true, true, true, false, false, false)
621}
622
623pub fn resolve_revision_for_commit_tree_tree(repo: &Repository, spec: &str) -> Result<ObjectId> {
625 resolve_revision_impl(repo, spec, true, false, true, false, true, false)
626}
627
628pub fn resolve_revision_for_patch_old_blob(repo: &Repository, spec: &str) -> Result<ObjectId> {
630 resolve_revision_impl(repo, spec, true, false, true, false, false, true)
631}
632
633pub fn try_parse_double_dot_log_range(
643 repo: &Repository,
644 spec: &str,
645) -> Result<Option<(ObjectId, ObjectId)>> {
646 let Some((left, right)) = split_double_dot_range(spec) else {
647 return Ok(None);
648 };
649 let left_tip = if left.is_empty() {
650 resolve_revision_for_range_end(repo, "HEAD")?
651 } else {
652 resolve_revision_for_range_end(repo, left)?
653 };
654 let right_tip = if right.is_empty() {
655 resolve_revision_for_range_end(repo, "HEAD")?
656 } else {
657 resolve_revision_for_range_end(repo, right)?
658 };
659 let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
660 let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
661 Ok(Some((left_c, right_c)))
662}
663
664#[must_use]
674pub fn revision_spec_contains_ancestry_navigation(spec: &str) -> bool {
675 let (_, steps) = parse_nav_steps(spec);
676 !steps.is_empty()
677}
678
679pub fn resolve_revision_as_commit(repo: &Repository, spec: &str) -> Result<ObjectId> {
680 if let Some((left, right)) = split_triple_dot_range(spec) {
681 let left_tip = if left.is_empty() {
682 resolve_revision_for_range_end(repo, "HEAD")?
683 } else {
684 resolve_revision_for_range_end(repo, left)?
685 };
686 let right_tip = if right.is_empty() {
687 resolve_revision_for_range_end(repo, "HEAD")?
688 } else {
689 resolve_revision_for_range_end(repo, right)?
690 };
691 let left_c = peel_to_commit_for_merge_base(repo, left_tip)?;
692 let right_c = peel_to_commit_for_merge_base(repo, right_tip)?;
693 let bases = crate::merge_base::merge_bases_first_vs_rest(repo, left_c, &[right_c])?;
694 return bases
695 .into_iter()
696 .next()
697 .ok_or_else(|| Error::ObjectNotFound(format!("no merge base for '{spec}'")));
698 }
699 if let Some((_excl, tip)) = try_parse_double_dot_log_range(repo, spec)? {
700 return Ok(tip);
701 }
702 let oid = resolve_revision_for_range_end(repo, spec)?;
703 peel_to_commit_for_merge_base(repo, oid)
704}
705
706fn resolve_revision_impl(
707 repo: &Repository,
708 spec: &str,
709 index_dwim: bool,
710 commit_only_hex: bool,
711 use_disambiguate_config: bool,
712 treeish_colon_lhs: bool,
713 implicit_tree_abbrev: bool,
714 implicit_blob_abbrev: bool,
715) -> Result<ObjectId> {
716 if let Some(pattern) = spec.strip_prefix(":/") {
719 if !pattern.is_empty() {
720 return resolve_commit_message_search(repo, pattern);
721 }
722 }
723
724 if spec == "AUTO_MERGE" {
726 let raw = fs::read_to_string(repo.git_dir.join("AUTO_MERGE"))
727 .map_err(|e| Error::Message(format!("failed to read AUTO_MERGE: {e}")))?;
728 let line = raw.lines().next().unwrap_or("").trim();
729 return line
730 .parse::<ObjectId>()
731 .map_err(|_| Error::InvalidRef("AUTO_MERGE: invalid object id".to_owned()));
732 }
733
734 if let Some(idx) = spec.find("...") {
737 let left_raw = &spec[..idx];
738 let right_raw = &spec[idx + 3..];
739 if !left_raw.is_empty() || !right_raw.is_empty() {
740 let left_oid = peel_to_commit_for_merge_base(
741 repo,
742 if left_raw.is_empty() {
743 resolve_revision_impl(
744 repo,
745 "HEAD",
746 index_dwim,
747 commit_only_hex,
748 use_disambiguate_config,
749 false,
750 false,
751 false,
752 )?
753 } else {
754 resolve_revision_impl(
755 repo,
756 left_raw,
757 index_dwim,
758 commit_only_hex,
759 use_disambiguate_config,
760 false,
761 false,
762 false,
763 )?
764 },
765 )?;
766 let right_oid = peel_to_commit_for_merge_base(
767 repo,
768 if right_raw.is_empty() {
769 resolve_revision_impl(
770 repo,
771 "HEAD",
772 index_dwim,
773 commit_only_hex,
774 use_disambiguate_config,
775 false,
776 false,
777 false,
778 )?
779 } else {
780 resolve_revision_impl(
781 repo,
782 right_raw,
783 index_dwim,
784 commit_only_hex,
785 use_disambiguate_config,
786 false,
787 false,
788 false,
789 )?
790 },
791 )?;
792 let bases = crate::merge_base::merge_bases_first_vs_rest(repo, left_oid, &[right_oid])?;
793 return bases
794 .into_iter()
795 .next()
796 .ok_or_else(|| Error::ObjectNotFound(format!("no merge base for '{spec}'")));
797 }
798 }
799
800 if let Some((before, after)) = split_treeish_colon(spec) {
804 if !before.is_empty() && !spec.starts_with(":/") {
805 let rev_oid = match resolve_revision_impl(
807 repo,
808 before,
809 index_dwim,
810 commit_only_hex,
811 use_disambiguate_config,
812 true,
813 false,
814 false,
815 ) {
816 Ok(o) => o,
817 Err(Error::ObjectNotFound(s)) if s == before => {
818 return Err(Error::Message(format!(
819 "fatal: invalid object name '{before}'."
820 )));
821 }
822 Err(Error::Message(msg)) if msg.contains("ambiguous argument") => {
823 return Err(Error::Message(format!(
824 "fatal: invalid object name '{before}'."
825 )));
826 }
827 Err(e) => return Err(e),
828 };
829 let tree_oid = peel_to_tree(repo, rev_oid)?;
830 if after.is_empty() {
831 return Ok(tree_oid);
833 }
834 let clean_path = match normalize_colon_path_for_tree(repo, after) {
835 Ok(p) => p,
836 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
837 let wt = repo
838 .work_tree
839 .as_ref()
840 .and_then(|p| p.canonicalize().ok())
841 .map(|p| p.display().to_string())
842 .unwrap_or_default();
843 return Err(Error::Message(format!(
844 "fatal: '{after}' is outside repository at '{wt}'"
845 )));
846 }
847 Err(e) => return Err(e),
848 };
849 return resolve_tree_path(repo, &tree_oid, &clean_path)
850 .map_err(|e| diagnose_tree_path_error(repo, before, after, &clean_path, e));
851 }
852 }
853
854 let (base_with_nav, peel) = parse_peel_suffix(spec);
855 let (base, nav_steps) = parse_nav_steps(base_with_nav);
856 let peel_for_hex = peel
857 .or(((treeish_colon_lhs || implicit_tree_abbrev) && peel.is_none()).then_some("tree"))
858 .or((implicit_blob_abbrev && peel.is_none()).then_some("blob"));
859 let mut oid = resolve_base(
860 repo,
861 base,
862 index_dwim,
863 commit_only_hex,
864 use_disambiguate_config,
865 peel_for_hex,
866 implicit_tree_abbrev,
867 implicit_blob_abbrev,
868 )?;
869 for step in nav_steps {
870 oid = apply_nav_step(repo, oid, step).map_err(|e| {
871 if matches!(e, Error::ObjectNotFound(_)) {
872 Error::Message(format!(
873 "fatal: ambiguous argument '{spec}': unknown revision or path not in the working tree.\n\
874Use '--' to separate paths from revisions, like this:\n\
875'git <command> [<revision>...] -- [<file>...]'"
876 ))
877 } else {
878 e
879 }
880 })?;
881 }
882 apply_peel(repo, oid, peel)
883}
884
885fn normalize_path_components(path: PathBuf) -> PathBuf {
888 let mut out = PathBuf::new();
889 for c in path.components() {
890 match c {
891 Component::Prefix(_) | Component::RootDir => out.push(c),
892 Component::CurDir => {}
893 Component::ParentDir => {
894 let _ = out.pop();
895 }
896 Component::Normal(x) => out.push(x),
897 }
898 }
899 out
900}
901
902fn normalize_colon_path_for_tree(repo: &Repository, raw_path: &str) -> Result<String> {
903 let work_tree = repo.work_tree.as_ref().ok_or_else(|| {
904 Error::InvalidRef("relative path syntax can't be used outside working tree".to_owned())
905 })?;
906
907 let cwd = std::env::current_dir().map_err(Error::Io)?;
908 let wt_canon = work_tree.canonicalize().map_err(Error::Io)?;
909
910 let cwd_relative = raw_path.starts_with("./") || raw_path.starts_with("../") || raw_path == ".";
911 if cwd_relative && !path_is_within(&cwd, work_tree) {
912 return Err(Error::InvalidRef(
913 "relative path syntax can't be used outside working tree".to_owned(),
914 ));
915 }
916
917 let full = if raw_path.starts_with('/') {
919 PathBuf::from(raw_path)
920 } else if cwd_relative {
921 cwd.join(raw_path)
922 } else {
923 work_tree.join(raw_path)
924 };
925 let full = normalize_path_components(full);
926
927 if !path_is_within(&full, &wt_canon) {
928 return Err(Error::InvalidRef("outside repository".to_owned()));
929 }
930 let rel = full
931 .strip_prefix(&wt_canon)
932 .map_err(|_| Error::InvalidRef("outside repository".to_owned()))?;
933 let s = rel.to_string_lossy().replace('\\', "/");
934 Ok(s.trim_end_matches('/').to_owned())
935}
936
937pub fn peel_to_commit_for_merge_base(repo: &Repository, mut oid: ObjectId) -> Result<ObjectId> {
939 oid = apply_peel(repo, oid, Some(""))?;
940 let obj = repo.odb.read(&oid)?;
941 match obj.kind {
942 ObjectKind::Commit => Ok(oid),
943 ObjectKind::Tree => Err(Error::InvalidRef(format!(
944 "object {oid} does not name a commit"
945 ))),
946 ObjectKind::Blob => Err(Error::InvalidRef(format!(
947 "object {oid} does not name a commit"
948 ))),
949 ObjectKind::Tag => Err(Error::InvalidRef("unexpected tag after peel".to_owned())),
950 }
951}
952
953fn peel_to_tree(repo: &Repository, oid: ObjectId) -> Result<ObjectId> {
955 let obj = repo.odb.read(&oid)?;
956 match obj.kind {
957 crate::objects::ObjectKind::Tree => Ok(oid),
958 crate::objects::ObjectKind::Commit => {
959 let commit = crate::objects::parse_commit(&obj.data)?;
960 Ok(commit.tree)
961 }
962 crate::objects::ObjectKind::Tag => {
963 let tag = crate::objects::parse_tag(&obj.data)?;
964 peel_to_tree(repo, tag.object)
965 }
966 _ => Err(Error::ObjectNotFound(format!(
967 "cannot peel {} to tree",
968 oid
969 ))),
970 }
971}
972
973fn resolve_tree_path(repo: &Repository, tree_oid: &ObjectId, path: &str) -> Result<ObjectId> {
975 let obj = repo.odb.read(tree_oid)?;
976 let entries = crate::objects::parse_tree(&obj.data)?;
977 let components: Vec<&str> = path.split('/').filter(|c| !c.is_empty()).collect();
978 if components.is_empty() {
979 return Ok(*tree_oid);
980 }
981 let first = components[0];
982 let rest: Vec<&str> = components[1..].to_vec();
983 for entry in entries {
984 let name = String::from_utf8_lossy(&entry.name);
985 if name == first {
986 if rest.is_empty() {
987 return Ok(entry.oid);
988 } else {
989 return resolve_tree_path(repo, &entry.oid, &rest.join("/"));
990 }
991 }
992 }
993 Err(Error::ObjectNotFound(format!(
994 "path '{}' not found in tree {}",
995 path, tree_oid
996 )))
997}
998
999#[derive(Debug, Clone, Copy)]
1001enum NavStep {
1002 ParentN(usize),
1004 AncestorN(usize),
1006}
1007
1008fn parse_nav_steps(spec: &str) -> (&str, Vec<NavStep>) {
1012 let mut steps = Vec::new();
1013 let mut remaining = spec;
1014
1015 loop {
1016 if let Some(tilde_pos) = remaining.rfind('~') {
1018 let after = &remaining[tilde_pos + 1..];
1019 if after.is_empty() {
1020 steps.push(NavStep::AncestorN(1));
1022 remaining = &remaining[..tilde_pos];
1023 continue;
1024 }
1025 if after.bytes().all(|b| b.is_ascii_digit()) {
1026 let n: usize = after.parse().unwrap_or(1);
1027 steps.push(NavStep::AncestorN(n));
1028 remaining = &remaining[..tilde_pos];
1029 continue;
1030 }
1031 }
1032
1033 if let Some(caret_pos) = remaining.rfind('^') {
1035 let after = &remaining[caret_pos + 1..];
1036 if after.is_empty() {
1037 steps.push(NavStep::ParentN(1));
1039 remaining = &remaining[..caret_pos];
1040 continue;
1041 }
1042 if after.bytes().all(|b| b.is_ascii_digit()) && !after.is_empty() {
1043 let n: usize = after.parse().unwrap_or(usize::MAX);
1044 steps.push(NavStep::ParentN(n));
1045 remaining = &remaining[..caret_pos];
1046 continue;
1047 }
1048 }
1049
1050 break;
1051 }
1052
1053 steps.reverse();
1054 (remaining, steps)
1055}
1056
1057fn apply_nav_step(repo: &Repository, oid: ObjectId, step: NavStep) -> Result<ObjectId> {
1059 match step {
1060 NavStep::ParentN(0) => Ok(oid),
1061 NavStep::ParentN(n) => {
1062 let obj = repo.odb.read(&oid)?;
1063 if obj.kind != ObjectKind::Commit {
1064 return Err(Error::InvalidRef(format!("{oid} is not a commit")));
1065 }
1066 let commit = parse_commit(&obj.data)?;
1067 commit
1068 .parents
1069 .get(n - 1)
1070 .copied()
1071 .ok_or_else(|| Error::ObjectNotFound(format!("{oid}^{n}")))
1072 }
1073 NavStep::AncestorN(n) => {
1074 let mut current = oid;
1075 for _ in 0..n {
1076 current = apply_nav_step(repo, current, NavStep::ParentN(1))?;
1077 }
1078 Ok(current)
1079 }
1080 }
1081}
1082
1083pub fn abbreviate_object_id(repo: &Repository, oid: ObjectId, min_len: usize) -> Result<String> {
1092 let min_len = min_len.clamp(4, 40);
1093 let target = oid.to_hex();
1094
1095 if !repo.odb.exists(&oid) {
1097 return Ok(target[..min_len].to_owned());
1098 }
1099
1100 let all = collect_loose_object_ids(repo)?;
1101
1102 for len in min_len..=40 {
1103 let prefix = &target[..len];
1104 let matches = all
1105 .iter()
1106 .filter(|candidate| candidate.starts_with(prefix))
1107 .count();
1108 if matches <= 1 {
1109 return Ok(prefix.to_owned());
1110 }
1111 }
1112
1113 Ok(target)
1114}
1115
1116#[must_use]
1118pub fn to_relative_path(path: &Path, cwd: &Path) -> String {
1119 let path_components = normalize_components(path);
1120 let cwd_components = normalize_components(cwd);
1121
1122 let mut common = 0usize;
1123 let max_common = path_components.len().min(cwd_components.len());
1124 while common < max_common && path_components[common] == cwd_components[common] {
1125 common += 1;
1126 }
1127
1128 let mut parts = Vec::new();
1129 let up_count = cwd_components.len().saturating_sub(common);
1130 for _ in 0..up_count {
1131 parts.push("..".to_owned());
1132 }
1133 for item in path_components.iter().skip(common) {
1134 parts.push(item.clone());
1135 }
1136
1137 if parts.is_empty() {
1138 ".".to_owned()
1139 } else {
1140 parts.join("/")
1141 }
1142}
1143
1144fn object_storage_dirs_for_abbrev(repo: &Repository) -> Result<Vec<PathBuf>> {
1145 let mut dirs = Vec::new();
1146 let primary = repo.odb.objects_dir().to_path_buf();
1147 dirs.push(primary.clone());
1148 if let Ok(alts) = pack::read_alternates_recursive(&primary) {
1149 for alt in alts {
1150 if !dirs.iter().any(|d| d == &alt) {
1151 dirs.push(alt);
1152 }
1153 }
1154 }
1155 Ok(dirs)
1156}
1157
1158fn collect_pack_oids_with_prefix(objects_dir: &Path, prefix: &str) -> Result<Vec<ObjectId>> {
1159 let mut out = Vec::new();
1160 for idx in pack::read_local_pack_indexes(objects_dir)? {
1161 for e in idx.entries {
1162 if e.oid.to_hex().starts_with(prefix) {
1163 out.push(e.oid);
1164 }
1165 }
1166 }
1167 Ok(out)
1168}
1169
1170fn disambiguate_kind_rank(kind: ObjectKind) -> u8 {
1171 match kind {
1172 ObjectKind::Tag => 0,
1173 ObjectKind::Commit => 1,
1174 ObjectKind::Tree => 2,
1175 ObjectKind::Blob => 3,
1176 }
1177}
1178
1179fn oid_satisfies_peel_filter(repo: &Repository, oid: ObjectId, peel_inner: &str) -> bool {
1180 apply_peel(repo, oid, Some(peel_inner)).is_ok()
1181}
1182
1183pub fn ambiguous_object_hint_lines(
1185 repo: &Repository,
1186 short_prefix: &str,
1187 peel_filter: Option<&str>,
1188) -> Result<Vec<String>> {
1189 let mut typed: Vec<(u8, String, &'static str)> = Vec::new();
1190 let mut bad_hex: Vec<String> = Vec::new();
1191 for oid in list_all_abbrev_matches(repo, short_prefix)? {
1192 let hex = oid.to_hex();
1193 match repo.odb.read(&oid) {
1194 Ok(obj) => {
1195 let ok = peel_filter.is_none_or(|p| oid_satisfies_peel_filter(repo, oid, p));
1196 if ok {
1197 typed.push((disambiguate_kind_rank(obj.kind), hex, obj.kind.as_str()));
1198 }
1199 }
1200 Err(_) => bad_hex.push(hex),
1201 }
1202 }
1203 if typed.is_empty() && peel_filter.is_some() {
1204 return ambiguous_object_hint_lines(repo, short_prefix, None);
1205 }
1206 bad_hex.sort();
1207 typed.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
1208 let mut out = Vec::new();
1209 for h in bad_hex {
1210 out.push(format!("hint: {h} [bad object]"));
1211 }
1212 for (_, hex, kind) in typed {
1213 out.push(format!("hint: {hex} {kind}"));
1214 }
1215 Ok(out)
1216}
1217
1218fn read_core_disambiguate(repo: &Repository) -> Option<&'static str> {
1219 let config = ConfigSet::load(Some(&repo.git_dir), true).unwrap_or_else(|_| ConfigSet::new());
1220 let v = config.get("core.disambiguate")?;
1221 match v.to_ascii_lowercase().as_str() {
1222 "committish" | "commit" => Some("commit"),
1223 "treeish" | "tree" => Some("tree"),
1224 "blob" => Some("blob"),
1225 "tag" => Some("tag"),
1226 "none" => None,
1227 _ => None,
1228 }
1229}
1230
1231fn disambiguate_hex_by_peel(
1232 repo: &Repository,
1233 spec: &str,
1234 matches: &[ObjectId],
1235 peel: &str,
1236) -> Result<ObjectId> {
1237 let peel_some = Some(peel);
1238 let filtered: Vec<ObjectId> = matches
1239 .iter()
1240 .copied()
1241 .filter(|oid| apply_peel(repo, *oid, peel_some).is_ok())
1242 .collect();
1243 if filtered.len() == 1 {
1244 return Ok(filtered[0]);
1245 }
1246 if filtered.is_empty() {
1247 return Err(Error::InvalidRef(format!(
1248 "short object ID {spec} is ambiguous"
1249 )));
1250 }
1251 let mut peeled_targets: HashSet<ObjectId> = HashSet::new();
1252 for oid in &filtered {
1253 if let Ok(p) = apply_peel(repo, *oid, peel_some) {
1254 peeled_targets.insert(p);
1255 }
1256 }
1257 if peeled_targets.len() == 1 {
1258 let mut sorted = filtered;
1261 sorted.sort_by_key(|o| o.to_hex());
1262 return Ok(sorted[0]);
1263 }
1264 Err(Error::InvalidRef(format!(
1265 "short object ID {spec} is ambiguous"
1266 )))
1267}
1268
1269fn commit_reachable_closure(repo: &Repository, start: ObjectId) -> Result<HashSet<ObjectId>> {
1270 use std::collections::VecDeque;
1271 let mut seen = HashSet::new();
1272 let mut q = VecDeque::from([start]);
1273 while let Some(oid) = q.pop_front() {
1274 if !seen.insert(oid) {
1275 continue;
1276 }
1277 let obj = match repo.odb.read(&oid) {
1278 Ok(o) => o,
1279 Err(_) => continue,
1280 };
1281 if obj.kind != ObjectKind::Commit {
1282 continue;
1283 }
1284 let commit = match parse_commit(&obj.data) {
1285 Ok(c) => c,
1286 Err(_) => continue,
1287 };
1288 for p in &commit.parents {
1289 q.push_back(*p);
1290 }
1291 }
1292 Ok(seen)
1293}
1294
1295fn describe_generation_count(
1297 repo: &Repository,
1298 head: ObjectId,
1299 tag_commit: ObjectId,
1300) -> Result<usize> {
1301 let from_tag = commit_reachable_closure(repo, tag_commit)?;
1302 let from_head = commit_reachable_closure(repo, head)?;
1303 Ok(from_head.difference(&from_tag).count())
1304}
1305
1306fn try_resolve_describe_name(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
1307 let re = Regex::new(r"(?i)^(.+)-(\d+)-g([0-9a-fA-F]+)$")
1308 .map_err(|_| Error::Message("internal: describe regex".to_owned()))?;
1309 let Some(caps) = re.captures(spec) else {
1310 return Ok(None);
1311 };
1312 let tag_name = caps.get(1).map(|m| m.as_str()).unwrap_or("");
1313 let gen: usize = caps
1314 .get(2)
1315 .and_then(|m| m.as_str().parse().ok())
1316 .unwrap_or(0);
1317 let hex_abbrev = caps.get(3).map(|m| m.as_str()).unwrap_or("");
1318 if tag_name.is_empty() || hex_abbrev.is_empty() {
1319 return Ok(None);
1320 }
1321 let hex_lower = hex_abbrev.to_ascii_lowercase();
1322 let tag_oid = match refs::resolve_ref(&repo.git_dir, &format!("refs/tags/{tag_name}"))
1323 .or_else(|_| refs::resolve_ref(&repo.git_dir, tag_name))
1324 {
1325 Ok(o) => o,
1326 Err(_) => return Ok(None),
1327 };
1328 let tag_commit = peel_to_commit_for_merge_base(repo, tag_oid)?;
1329 let mut candidates: Vec<ObjectId> = find_abbrev_matches(repo, &hex_lower)?
1330 .into_iter()
1331 .filter(|oid| {
1332 repo.odb
1333 .read(oid)
1334 .map(|o| o.kind == ObjectKind::Commit)
1335 .unwrap_or(false)
1336 && describe_generation_count(repo, *oid, tag_commit).ok() == Some(gen)
1337 })
1338 .collect();
1339 candidates.sort_by_key(|o| o.to_hex());
1340 match candidates.len() {
1341 0 => Err(Error::ObjectNotFound(spec.to_owned())),
1342 1 => Ok(Some(candidates[0])),
1343 _ => Err(Error::InvalidRef(format!(
1344 "short object ID {hex_abbrev} is ambiguous"
1345 ))),
1346 }
1347}
1348
1349fn resolve_base(
1350 repo: &Repository,
1351 spec: &str,
1352 index_dwim: bool,
1353 commit_only_hex: bool,
1354 use_disambiguate_config: bool,
1355 peel_for_disambig: Option<&str>,
1356 implicit_tree_abbrev: bool,
1357 implicit_blob_abbrev: bool,
1358) -> Result<ObjectId> {
1359 if spec == "@" {
1361 return resolve_base(
1362 repo,
1363 "HEAD",
1364 index_dwim,
1365 commit_only_hex,
1366 use_disambiguate_config,
1367 peel_for_disambig,
1368 implicit_tree_abbrev,
1369 implicit_blob_abbrev,
1370 );
1371 }
1372
1373 if spec.starts_with("@{-") {
1375 if let Some(close) = spec[3..].find('}') {
1376 let n_str = &spec[3..3 + close];
1377 if let Ok(n) = n_str.parse::<usize>() {
1378 if n >= 1 {
1379 let suffix = &spec[3 + close + 1..];
1380 if suffix.is_empty() {
1381 if let Some(oid) = try_resolve_at_minus(repo, spec)? {
1382 return Ok(oid);
1383 }
1384 } else {
1385 let branch = resolve_at_minus_to_branch(repo, n)?;
1386 let new_spec = format!("{branch}{suffix}");
1387 return resolve_base(
1388 repo,
1389 &new_spec,
1390 index_dwim,
1391 commit_only_hex,
1392 use_disambiguate_config,
1393 peel_for_disambig,
1394 implicit_tree_abbrev,
1395 implicit_blob_abbrev,
1396 );
1397 }
1398 }
1399 }
1400 }
1401 }
1402
1403 if upstream_suffix_info(spec).is_some() {
1405 let full_ref = resolve_upstream_symbolic_name(repo, spec)?;
1406 return refs::resolve_ref(&repo.git_dir, &full_ref)
1407 .map_err(|_| Error::ObjectNotFound(spec.to_owned()));
1408 }
1409
1410 if let Some(oid) = try_resolve_reflog_index(repo, spec)? {
1412 return Ok(oid);
1413 }
1414
1415 if let Some(pattern) = spec.strip_prefix(":/") {
1417 if !pattern.is_empty() {
1418 return resolve_commit_message_search(repo, pattern);
1419 }
1420 }
1421
1422 if let Some(rest) = spec.strip_prefix(':') {
1425 if !rest.is_empty() && !rest.starts_with('/') {
1426 if rest.len() >= 3 && rest.as_bytes()[1] == b':' {
1428 if let Some(stage_char) = rest.chars().next() {
1429 if let Some(stage) = stage_char.to_digit(10) {
1430 if stage <= 3 {
1431 let raw_path = &rest[2..];
1432 let path = match normalize_colon_path_for_tree(repo, raw_path) {
1433 Ok(p) => p,
1434 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
1435 let wt = repo
1436 .work_tree
1437 .as_ref()
1438 .and_then(|p| p.canonicalize().ok())
1439 .map(|p| p.display().to_string())
1440 .unwrap_or_default();
1441 return Err(Error::Message(format!(
1442 "fatal: '{raw_path}' is outside repository at '{wt}'"
1443 )));
1444 }
1445 Err(e) => return Err(e),
1446 };
1447 return resolve_index_path_at_stage(repo, &path, stage as u8).map_err(
1448 |e| diagnose_index_path_error(repo, &path, stage as u8, e),
1449 );
1450 }
1451 }
1452 }
1453 }
1454 let clean_rest = match normalize_colon_path_for_tree(repo, rest) {
1455 Ok(p) => p,
1456 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
1457 let wt = repo
1458 .work_tree
1459 .as_ref()
1460 .and_then(|p| p.canonicalize().ok())
1461 .map(|p| p.display().to_string())
1462 .unwrap_or_default();
1463 return Err(Error::Message(format!(
1464 "fatal: '{rest}' is outside repository at '{wt}'"
1465 )));
1466 }
1467 Err(e) => return Err(e),
1468 };
1469 return resolve_index_path(repo, &clean_rest)
1470 .map_err(|e| diagnose_index_path_error(repo, &clean_rest, 0, e));
1471 }
1472 }
1473
1474 if let Some((treeish, path)) = split_treeish_spec(spec) {
1475 let root_oid = resolve_revision_impl(
1476 repo,
1477 treeish,
1478 index_dwim,
1479 commit_only_hex,
1480 use_disambiguate_config,
1481 false,
1482 false,
1483 false,
1484 )?;
1485 return resolve_treeish_path(repo, root_oid, path);
1486 }
1487
1488 if let Ok(oid) = spec.parse::<ObjectId>() {
1489 let rn = format!("refs/heads/{spec}");
1492 if refs::resolve_ref(&repo.git_dir, &rn).is_ok() {
1493 eprintln!("warning: refname '{spec}' is ambiguous.");
1494 }
1495 return Ok(oid);
1496 }
1497
1498 match try_resolve_describe_name(repo, spec) {
1499 Ok(Some(oid)) => return Ok(oid),
1500 Err(e) => return Err(e),
1501 Ok(None) => {}
1502 }
1503
1504 if is_hex_prefix(spec) && spec.len() < 40 {
1507 let branch_ref = format!("refs/heads/{spec}");
1508 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &branch_ref) {
1509 return Ok(oid);
1510 }
1511 }
1512
1513 if is_hex_prefix(spec) {
1514 let matches = find_abbrev_matches(repo, spec)?;
1515 if matches.len() == 1 {
1516 return Ok(matches[0]);
1517 }
1518 if matches.len() > 1 {
1519 if commit_only_hex {
1520 return disambiguate_hex_by_peel(repo, spec, &matches, "commit");
1521 }
1522 if let Some(p) = peel_for_disambig {
1523 return disambiguate_hex_by_peel(repo, spec, &matches, p);
1524 }
1525 if use_disambiguate_config {
1526 if let Some(pref) = read_core_disambiguate(repo) {
1527 if let Ok(oid) = disambiguate_hex_by_peel(repo, spec, &matches, pref) {
1528 return Ok(oid);
1529 }
1530 }
1531 }
1532 return Err(Error::InvalidRef(format!(
1533 "short object ID {} is ambiguous",
1534 spec
1535 )));
1536 }
1537 }
1538
1539 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, spec) {
1540 return Ok(oid);
1541 }
1542 if !spec.contains('/')
1546 && !spec.starts_with('.')
1547 && spec != "HEAD"
1548 && spec != "FETCH_HEAD"
1549 && spec != "MERGE_HEAD"
1550 && spec != "CHERRY_PICK_HEAD"
1551 && spec != "REVERT_HEAD"
1552 && spec != "REBASE_HEAD"
1553 && spec != "AUTO_MERGE"
1554 && spec != "stash"
1555 {
1556 let local_branch = format!("refs/heads/{spec}");
1557 if refs::resolve_ref(&repo.git_dir, &local_branch).is_err() {
1558 let remote_head = format!("refs/remotes/{spec}/HEAD");
1559 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &remote_head) {
1560 return Ok(oid);
1561 }
1562 }
1563 }
1564 if spec == "stash" {
1566 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, "refs/stash") {
1567 return Ok(oid);
1568 }
1569 }
1570 let head_ref = format!("refs/heads/{spec}");
1574 let tag_ref = format!("refs/tags/{spec}");
1575 let head_oid = refs::resolve_ref(&repo.git_dir, &head_ref).ok();
1576 let tag_oid = refs::resolve_ref(&repo.git_dir, &tag_ref).ok();
1577 match (head_oid, tag_oid) {
1578 (Some(h), Some(t)) if h != t => {
1579 eprintln!("warning: refname '{spec}' is ambiguous.");
1580 return Ok(h);
1581 }
1582 (Some(h), _) => return Ok(h),
1583 (None, Some(t)) => return Ok(t),
1584 (None, None) => {}
1585 }
1586 for candidate in &[format!("refs/remotes/{spec}"), format!("refs/notes/{spec}")] {
1587 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, candidate) {
1588 return Ok(oid);
1589 }
1590 }
1591
1592 if let Some(head_ref) = remote_tracking_head_symbolic_target(repo, spec) {
1594 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &head_ref) {
1595 return Ok(oid);
1596 }
1597 }
1598
1599 if !spec.contains('/') && spec != "HEAD" && spec != "FETCH_HEAD" && spec != "MERGE_HEAD" {
1601 const REMOTES: &str = "refs/remotes/";
1602 if let Ok(remote_refs) = refs::list_refs(&repo.git_dir, REMOTES) {
1603 let matches: Vec<ObjectId> = remote_refs
1604 .into_iter()
1605 .filter(|(r, _)| {
1606 r.strip_prefix(REMOTES)
1607 .is_some_and(|rest| rest == spec || rest.ends_with(&format!("/{spec}")))
1608 })
1609 .map(|(_, oid)| oid)
1610 .collect();
1611 if matches.len() == 1 {
1612 return Ok(matches[0]);
1613 }
1614 if matches.len() > 1 {
1615 return Err(Error::InvalidRef(format!(
1616 "ambiguous refname '{spec}': matches multiple remote-tracking branches"
1617 )));
1618 }
1619 }
1620 }
1621
1622 if !spec.contains(':') && !spec.starts_with('-') {
1624 if index_dwim {
1625 if let Ok(oid) = resolve_index_path(repo, spec) {
1626 return Ok(oid);
1627 }
1628 }
1629 return Err(Error::Message(format!(
1630 "fatal: ambiguous argument '{spec}': unknown revision or path not in the working tree.\n\
1631Use '--' to separate paths from revisions, like this:\n\
1632'git <command> [<revision>...] -- [<file>...]'"
1633 )));
1634 }
1635 Err(Error::ObjectNotFound(spec.to_owned()))
1636}
1637
1638fn resolve_at_minus_to_branch(repo: &Repository, n: usize) -> Result<String> {
1640 let entries = read_reflog(&repo.git_dir, "HEAD")?;
1641 let mut count = 0usize;
1642 for entry in entries.iter().rev() {
1643 let msg = &entry.message;
1644 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
1645 count += 1;
1646 if count == n {
1647 if let Some(to_pos) = rest.find(" to ") {
1648 return Ok(rest[..to_pos].to_string());
1649 }
1650 }
1651 }
1652 }
1653 Err(Error::InvalidRef(format!(
1654 "@{{-{n}}}: only {count} checkout(s) in reflog"
1655 )))
1656}
1657
1658fn try_resolve_at_minus(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
1661 if !spec.starts_with("@{-") || !spec.ends_with('}') {
1663 return Ok(None);
1664 }
1665 let inner = &spec[3..spec.len() - 1];
1666 let n: usize = match inner.parse() {
1667 Ok(n) if n >= 1 => n,
1668 _ => return Ok(None),
1669 };
1670 let entries = read_reflog(&repo.git_dir, "HEAD")?;
1672 let mut count = 0usize;
1673 for entry in entries.iter().rev() {
1675 let msg = &entry.message;
1676 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
1677 count += 1;
1678 if count == n {
1679 if let Some(to_pos) = rest.find(" to ") {
1681 let from_branch = &rest[..to_pos];
1682 let ref_name = format!("refs/heads/{from_branch}");
1684 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &ref_name) {
1685 return Ok(Some(oid));
1686 }
1687 if let Ok(oid) = from_branch.parse::<ObjectId>() {
1689 if repo.odb.exists(&oid) {
1690 return Ok(Some(oid));
1691 }
1692 }
1693 return Err(Error::InvalidRef(format!(
1694 "cannot resolve @{{-{n}}}: branch '{}' not found",
1695 from_branch
1696 )));
1697 }
1698 }
1699 }
1700 }
1701 Err(Error::InvalidRef(format!(
1702 "@{{-{n}}}: only {count} checkout(s) in reflog"
1703 )))
1704}
1705
1706#[derive(Debug, Clone)]
1707enum AtStep {
1708 Index(usize),
1709 Date(i64),
1710 Upstream,
1711 Push,
1712 Now,
1713}
1714
1715fn try_parse_at_step_inner(inner: &str) -> Option<AtStep> {
1716 if inner.eq_ignore_ascii_case("u") || inner.eq_ignore_ascii_case("upstream") {
1717 return Some(AtStep::Upstream);
1718 }
1719 if inner.eq_ignore_ascii_case("push") {
1720 return Some(AtStep::Push);
1721 }
1722 if inner.eq_ignore_ascii_case("now") {
1723 return Some(AtStep::Now);
1724 }
1725 if let Ok(n) = inner.parse::<usize>() {
1726 return Some(AtStep::Index(n));
1727 }
1728 approxidate(inner).map(AtStep::Date)
1729}
1730
1731fn next_reflog_at_open(spec: &str, mut from: usize) -> Option<usize> {
1732 let b = spec.as_bytes();
1733 while let Some(rel) = spec[from..].find("@{") {
1734 let i = from + rel;
1735 if b.get(i + 2) == Some(&b'-') {
1737 let after_open = i + 2;
1738 let close = spec[after_open..].find('}').map(|j| after_open + j)?;
1739 from = close + 1;
1740 continue;
1741 }
1742 return Some(i);
1743 }
1744 None
1745}
1746
1747fn split_reflog_at_chain(spec: &str) -> Option<(String, Vec<AtStep>)> {
1749 let at = next_reflog_at_open(spec, 0)?;
1750 let prefix = spec[..at].to_owned();
1751 let mut steps = Vec::new();
1752 let mut pos = at;
1753 while pos < spec.len() {
1754 let rest = &spec[pos..];
1755 if !rest.starts_with("@{") {
1756 return None;
1757 }
1758 if rest.as_bytes().get(2) == Some(&b'-') {
1759 return None;
1760 }
1761 let inner_start = pos + 2;
1762 let close = spec[inner_start..].find('}').map(|i| inner_start + i)?;
1763 let inner = &spec[inner_start..close];
1764 let step = try_parse_at_step_inner(inner)?;
1765 steps.push(step);
1766 pos = close + 1;
1767 }
1768 if steps.is_empty() {
1769 return None;
1770 }
1771 Some((prefix, steps))
1772}
1773
1774fn dwim_refname(repo: &Repository, raw: &str) -> String {
1775 if raw.is_empty() || raw == "HEAD" || raw.starts_with("refs/") {
1776 return raw.to_owned();
1777 }
1778 let candidate = format!("refs/heads/{raw}");
1779 if refs::resolve_ref(&repo.git_dir, &candidate).is_ok() {
1780 candidate
1781 } else {
1782 raw.to_owned()
1783 }
1784}
1785
1786fn reflog_display_name(refname_raw: &str, refname: &str) -> String {
1787 if refname_raw.is_empty() {
1788 if let Some(b) = refname.strip_prefix("refs/heads/") {
1789 return b.to_owned();
1790 }
1791 return refname.to_owned();
1792 }
1793 refname_raw.to_owned()
1794}
1795
1796fn resolve_reflog_oid(
1797 repo: &Repository,
1798 refname: &str,
1799 refname_raw: &str,
1800 index_or_date: ReflogSelector,
1801) -> Result<ObjectId> {
1802 let entries = read_reflog(&repo.git_dir, refname)?;
1803 let display = reflog_display_name(refname_raw, refname);
1804 if entries.is_empty() {
1805 return Err(Error::Message(format!(
1806 "fatal: log for '{display}' is empty"
1807 )));
1808 }
1809 match index_or_date {
1810 ReflogSelector::Index(index) => {
1811 let reversed_idx = entries.len().checked_sub(1 + index).ok_or_else(|| {
1812 Error::Message(format!(
1813 "fatal: log for '{display}' only has {} entries",
1814 entries.len()
1815 ))
1816 })?;
1817 Ok(entries[reversed_idx].new_oid)
1818 }
1819 ReflogSelector::Date(target_ts) => {
1820 for entry in entries.iter().rev() {
1821 let ts = parse_reflog_entry_timestamp(entry);
1822 if let Some(t) = ts {
1823 if t <= target_ts {
1824 return Ok(entry.new_oid);
1825 }
1826 }
1827 }
1828 Ok(entries[0].new_oid)
1829 }
1830 }
1831}
1832
1833fn resolve_at_minus_token_to_branch(repo: &Repository, token: &str) -> Result<Option<String>> {
1834 if !token.starts_with("@{-") || !token.ends_with('}') {
1835 return Ok(None);
1836 }
1837 let inner = &token[3..token.len() - 1];
1838 let n: usize = inner
1839 .parse()
1840 .map_err(|_| Error::InvalidRef(format!("invalid N in @{{-N}} for '{token}'")))?;
1841 if n < 1 {
1842 return Ok(None);
1843 }
1844 Ok(Some(resolve_at_minus_to_branch(repo, n)?))
1845}
1846
1847pub fn reflog_walk_refname(repo: &Repository, spec: &str) -> Result<Option<String>> {
1851 let Some((prefix, steps)) = split_reflog_at_chain(spec) else {
1852 return Ok(None);
1853 };
1854
1855 let prefix_resolved = if let Some(b) = resolve_at_minus_token_to_branch(repo, &prefix)? {
1856 b
1857 } else {
1858 prefix.clone()
1859 };
1860
1861 let mut current_spec = if prefix_resolved.is_empty() {
1862 if let Ok(Some(b)) = refs::read_head(&repo.git_dir) {
1863 if let Some(short) = b.strip_prefix("refs/heads/") {
1864 short.to_owned()
1865 } else {
1866 "HEAD".to_owned()
1867 }
1868 } else {
1869 "HEAD".to_owned()
1870 }
1871 } else {
1872 prefix_resolved
1873 };
1874
1875 let last_reflog_peel = steps
1876 .iter()
1877 .rposition(|s| matches!(s, AtStep::Index(_) | AtStep::Date(_) | AtStep::Now));
1878
1879 let limit = last_reflog_peel.unwrap_or(steps.len());
1880 for step in steps.iter().take(limit) {
1881 match step {
1882 AtStep::Upstream => {
1883 let base = if current_spec == "@" {
1884 "HEAD"
1885 } else {
1886 current_spec.as_str()
1887 };
1888 let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{u}}"))?;
1889 current_spec = full;
1890 }
1891 AtStep::Push => {
1892 let base = if current_spec == "@" {
1893 "HEAD"
1894 } else {
1895 current_spec.as_str()
1896 };
1897 let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{push}}"))?;
1898 current_spec = full;
1899 }
1900 AtStep::Now | AtStep::Index(_) | AtStep::Date(_) => {}
1901 }
1902 }
1903
1904 Ok(Some(dwim_refname(repo, current_spec.as_str())))
1905}
1906
1907fn try_resolve_reflog_index(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
1909 let Some((prefix, steps)) = split_reflog_at_chain(spec) else {
1910 return Ok(None);
1911 };
1912
1913 let prefix_resolved = if let Some(b) = resolve_at_minus_token_to_branch(repo, &prefix)? {
1914 b
1915 } else {
1916 prefix.clone()
1917 };
1918
1919 let mut current_spec = if prefix_resolved.is_empty() {
1920 if let Ok(Some(b)) = refs::read_head(&repo.git_dir) {
1921 if let Some(short) = b.strip_prefix("refs/heads/") {
1922 short.to_owned()
1923 } else {
1924 "HEAD".to_owned()
1925 }
1926 } else {
1927 "HEAD".to_owned()
1928 }
1929 } else {
1930 prefix_resolved
1931 };
1932
1933 for (i, step) in steps.iter().enumerate() {
1934 match step {
1935 AtStep::Upstream => {
1936 let base = if current_spec == "@" {
1937 "HEAD"
1938 } else {
1939 current_spec.as_str()
1940 };
1941 let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{u}}"))?;
1942 current_spec = full;
1943 }
1944 AtStep::Push => {
1945 let base = if current_spec == "@" {
1946 "HEAD"
1947 } else {
1948 current_spec.as_str()
1949 };
1950 let full = resolve_upstream_symbolic_name(repo, &format!("{base}@{{push}}"))?;
1951 current_spec = full;
1952 }
1953 AtStep::Now => {
1954 let refname_raw = current_spec.as_str();
1955 let refname = dwim_refname(repo, refname_raw);
1956 let oid =
1957 resolve_reflog_oid(repo, &refname, refname_raw, ReflogSelector::Index(0))?;
1958 if i + 1 == steps.len() {
1959 return Ok(Some(oid));
1960 }
1961 current_spec = oid.to_hex();
1962 }
1963 AtStep::Index(n) => {
1964 let refname_raw = current_spec.as_str();
1965 let refname = dwim_refname(repo, refname_raw);
1966 let oid =
1967 resolve_reflog_oid(repo, &refname, refname_raw, ReflogSelector::Index(*n))?;
1968 if i + 1 == steps.len() {
1969 return Ok(Some(oid));
1970 }
1971 current_spec = oid.to_hex();
1972 }
1973 AtStep::Date(ts) => {
1974 let refname_raw = current_spec.as_str();
1975 let refname = dwim_refname(repo, refname_raw);
1976 let oid =
1977 resolve_reflog_oid(repo, &refname, refname_raw, ReflogSelector::Date(*ts))?;
1978 if i + 1 == steps.len() {
1979 return Ok(Some(oid));
1980 }
1981 current_spec = oid.to_hex();
1982 }
1983 }
1984 }
1985
1986 let refname_raw = current_spec.as_str();
1987 let refname = dwim_refname(repo, refname_raw);
1988 refs::resolve_ref(&repo.git_dir, &refname)
1989 .map(Some)
1990 .map_err(|_| Error::ObjectNotFound(spec.to_owned()))
1991}
1992
1993enum ReflogSelector {
1994 Index(usize),
1995 Date(i64),
1996}
1997
1998fn parse_reflog_entry_timestamp(entry: &crate::reflog::ReflogEntry) -> Option<i64> {
2000 let parts: Vec<&str> = entry.identity.rsplitn(3, ' ').collect();
2002 if parts.len() >= 2 {
2003 parts[1].parse::<i64>().ok()
2004 } else {
2005 None
2006 }
2007}
2008
2009#[must_use]
2013pub fn reflog_date_selector_timestamp(s: &str) -> Option<i64> {
2014 approxidate(s)
2015}
2016
2017fn approxidate(s: &str) -> Option<i64> {
2020 let now_ts = std::time::SystemTime::now()
2021 .duration_since(std::time::UNIX_EPOCH)
2022 .ok()
2023 .map(|d| d.as_secs() as i64)
2024 .unwrap_or(0);
2025 let lower = s.trim().to_ascii_lowercase();
2026 if lower == "now" {
2027 if let Ok(raw) =
2030 std::env::var("GIT_COMMITTER_DATE").or_else(|_| std::env::var("GIT_AUTHOR_DATE"))
2031 {
2032 let mut it = raw.split_whitespace();
2033 if let Some(ts) = it.next().and_then(|p| p.parse::<i64>().ok()) {
2034 return Some(ts);
2035 }
2036 }
2037 return Some(now_ts);
2038 }
2039 let relative = lower.replace('.', " ");
2042 let parts: Vec<&str> = relative.split_whitespace().collect();
2043 if parts.len() >= 2 {
2044 let (n_str, unit, is_ago) = if parts.len() >= 3 && parts[2] == "ago" {
2046 (parts[0], parts[1], true)
2047 } else if parts.len() == 2 {
2048 (parts[0], parts[1], false)
2049 } else {
2050 ("", "", false)
2051 };
2052 if !n_str.is_empty() {
2053 if let Ok(n) = n_str.parse::<i64>() {
2054 let secs: Option<i64> = match unit.trim_end_matches('s') {
2055 "second" => Some(n),
2056 "minute" => Some(n * 60),
2057 "hour" => Some(n * 3600),
2058 "day" => Some(n * 86400),
2059 "week" => Some(n * 604800),
2060 "month" => Some(n * 2592000),
2061 "year" => Some(n * 31536000),
2062 _ => None,
2063 };
2064 if let Some(s) = secs {
2065 return Some(if is_ago || true {
2066 now_ts - s
2067 } else {
2068 now_ts + s
2069 });
2070 }
2071 }
2072 }
2073 }
2074 let re_like = |input: &str| -> Option<i64> {
2076 for (i, _) in input.char_indices() {
2078 let rest = &input[i..];
2079 if rest.len() >= 10 {
2080 let bytes = rest.as_bytes();
2081 if bytes[4] == b'-'
2082 && bytes[7] == b'-'
2083 && bytes[0..4].iter().all(|b| b.is_ascii_digit())
2084 && bytes[5..7].iter().all(|b| b.is_ascii_digit())
2085 && bytes[8..10].iter().all(|b| b.is_ascii_digit())
2086 {
2087 let year: i32 = rest[0..4].parse().ok()?;
2088 let month: u8 = rest[5..7].parse().ok()?;
2089 let day: u8 = rest[8..10].parse().ok()?;
2090 let date = time::Date::from_calendar_date(
2091 year,
2092 time::Month::try_from(month).ok()?,
2093 day,
2094 )
2095 .ok()?;
2096 let dt = date.with_hms(0, 0, 0).ok()?;
2097 let odt = dt.assume_utc();
2098 return Some(odt.unix_timestamp());
2099 }
2100 }
2101 }
2102 None
2103 };
2104 re_like(s)
2105}
2106
2107fn head_tree_oid(repo: &Repository) -> Result<ObjectId> {
2108 let head_oid = refs::resolve_ref(&repo.git_dir, "HEAD")?;
2109 peel_to_tree(repo, head_oid)
2110}
2111
2112fn path_in_tree(repo: &Repository, tree_oid: ObjectId, path: &str) -> bool {
2113 resolve_tree_path(repo, &tree_oid, path).is_ok()
2114}
2115
2116fn path_in_index(repo: &Repository, path: &str, stage: u8) -> bool {
2117 resolve_index_path_at_stage(repo, path, stage).is_ok()
2118}
2119
2120fn diagnose_tree_path_error(
2121 repo: &Repository,
2122 rev_label: &str,
2123 raw_after_colon: &str,
2124 clean_path: &str,
2125 err: Error,
2126) -> Error {
2127 let Error::ObjectNotFound(msg) = err else {
2128 return err;
2129 };
2130 if !msg.contains("not found in tree") {
2131 return Error::ObjectNotFound(msg);
2132 }
2133 let rel_display: &str =
2134 if raw_after_colon.starts_with("./") || raw_after_colon.starts_with("../") {
2135 clean_path
2136 } else {
2137 raw_after_colon
2138 };
2139 if let Ok(head_tree) = head_tree_oid(repo) {
2140 if path_in_tree(repo, head_tree, clean_path) {
2141 return Error::Message(format!(
2142 "fatal: path '{rel_display}' exists on disk, but not in '{rev_label}'."
2143 ));
2144 }
2145 if let Ok(cwd) = std::env::current_dir() {
2146 let prefix = show_prefix(repo, &cwd);
2147 let pfx = prefix.trim_end_matches('/');
2148 if !pfx.is_empty() {
2149 let candidate = if clean_path.is_empty() {
2150 pfx.to_owned()
2151 } else {
2152 format!("{pfx}/{clean_path}")
2153 };
2154 if path_in_tree(repo, head_tree, &candidate) {
2155 return Error::Message(format!(
2156 "fatal: path '{candidate}' exists, but not '{rel_display}'\n\
2157hint: Did you mean '{rev_label}:{candidate}' aka '{rev_label}:./{rel_display}'?"
2158 ));
2159 }
2160 }
2161 }
2162 let on_disk = repo
2163 .work_tree
2164 .as_ref()
2165 .map(|wt| wt.join(clean_path))
2166 .is_some_and(|p| p.exists());
2167 let in_index = path_in_index(repo, clean_path, 0);
2168 if on_disk || in_index {
2169 return Error::Message(format!(
2170 "fatal: path '{rel_display}' exists on disk, but not in '{rev_label}'."
2171 ));
2172 }
2173 }
2174 Error::Message(format!(
2175 "fatal: path '{rel_display}' does not exist in '{rev_label}'"
2176 ))
2177}
2178
2179fn diagnose_index_path_error(repo: &Repository, path: &str, stage: u8, err: Error) -> Error {
2180 let Error::ObjectNotFound(_) = err else {
2181 return err;
2182 };
2183 let work_path = repo
2184 .work_tree
2185 .as_ref()
2186 .map(|wt| wt.join(path))
2187 .filter(|p| p.exists());
2188 let on_disk = work_path.is_some();
2189 let in_head = head_tree_oid(repo)
2190 .map(|t| path_in_tree(repo, t, path))
2191 .unwrap_or(false);
2192 let in_index = path_in_index(repo, path, 0);
2193 let at_stage = path_in_index(repo, path, stage);
2194
2195 if stage > 0 && !in_index {
2196 if let Ok(cwd) = std::env::current_dir() {
2197 let prefix = show_prefix(repo, &cwd);
2198 let pfx = prefix.trim_end_matches('/');
2199 if !pfx.is_empty() {
2200 let candidate = if path.is_empty() {
2201 pfx.to_owned()
2202 } else {
2203 format!("{pfx}/{path}")
2204 };
2205 if path_in_index(repo, &candidate, 0) && !path_in_index(repo, &candidate, stage) {
2206 return Error::Message(format!(
2207 "fatal: path '{candidate}' is in the index, but not '{path}'\n\
2208hint: Did you mean ':0:{candidate}' aka ':0:./{path}'?"
2209 ));
2210 }
2211 }
2212 }
2213 return Error::Message(format!(
2214 "fatal: path '{path}' does not exist (neither on disk nor in the index)"
2215 ));
2216 }
2217
2218 if stage > 0 && in_index && !at_stage {
2219 return Error::Message(format!(
2220 "fatal: path '{path}' is in the index, but not at stage {stage}\n\
2221hint: Did you mean ':0:{path}'?"
2222 ));
2223 }
2224
2225 if stage == 0 {
2226 if !on_disk && !in_index {
2227 if let Ok(cwd) = std::env::current_dir() {
2228 let prefix = show_prefix(repo, &cwd);
2229 let pfx = prefix.trim_end_matches('/');
2230 if !pfx.is_empty() {
2231 let candidate = if path.is_empty() {
2232 pfx.to_owned()
2233 } else {
2234 format!("{pfx}/{path}")
2235 };
2236 if path_in_index(repo, &candidate, 0) {
2237 return Error::Message(format!(
2238 "fatal: path '{candidate}' is in the index, but not '{path}'\n\
2239hint: Did you mean ':0:{candidate}' aka ':0:./{path}'?"
2240 ));
2241 }
2242 }
2243 }
2244 return Error::Message(format!(
2245 "fatal: path '{path}' does not exist (neither on disk nor in the index)"
2246 ));
2247 }
2248 if on_disk && !in_index && !in_head {
2249 return Error::Message(format!(
2250 "fatal: path '{path}' exists on disk, but not in the index"
2251 ));
2252 }
2253 }
2254 Error::Message(format!("fatal: path '{path}' does not exist in the index"))
2255}
2256
2257fn resolve_index_path(repo: &Repository, path: &str) -> Result<ObjectId> {
2259 resolve_index_path_at_stage(repo, path, 0)
2260}
2261
2262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2264pub struct IndexColonSpec<'a> {
2265 pub stage: u8,
2267 pub raw_path: &'a str,
2269}
2270
2271#[must_use]
2275pub fn parse_index_colon_spec(spec: &str) -> Option<IndexColonSpec<'_>> {
2276 if !spec.starts_with(':') || spec.starts_with(":/") || spec.len() <= 1 {
2277 return None;
2278 }
2279 let rest = &spec[1..];
2280 if rest.is_empty() {
2281 return None;
2282 }
2283 if rest.len() >= 3 && rest.as_bytes()[1] == b':' {
2284 if let Some(stage_char) = rest.chars().next() {
2285 if let Some(stage) = stage_char.to_digit(10) {
2286 if stage <= 3 {
2287 return Some(IndexColonSpec {
2288 stage: stage as u8,
2289 raw_path: &rest[2..],
2290 });
2291 }
2292 }
2293 }
2294 }
2295 Some(IndexColonSpec {
2296 stage: 0,
2297 raw_path: rest,
2298 })
2299}
2300
2301#[derive(Debug, Clone, PartialEq, Eq)]
2303pub struct IndexPathEntry {
2304 pub path: String,
2306 pub oid: ObjectId,
2308 pub mode: u32,
2310}
2311
2312pub fn resolve_index_path_entry(repo: &Repository, spec: &str) -> Result<Option<IndexPathEntry>> {
2320 let Some(colon) = parse_index_colon_spec(spec) else {
2321 return Ok(None);
2322 };
2323 let path = match normalize_colon_path_for_tree(repo, colon.raw_path) {
2324 Ok(p) => p,
2325 Err(Error::InvalidRef(msg)) if msg == "outside repository" => {
2326 let wt = repo
2327 .work_tree
2328 .as_ref()
2329 .and_then(|p| p.canonicalize().ok())
2330 .map(|p| p.display().to_string())
2331 .unwrap_or_default();
2332 return Err(Error::Message(format!(
2333 "fatal: '{}' is outside repository at '{wt}'",
2334 colon.raw_path
2335 )));
2336 }
2337 Err(e) => return Err(e),
2338 };
2339 let index_path = if let Ok(raw) = std::env::var("GIT_INDEX_FILE") {
2340 let p = std::path::PathBuf::from(raw);
2341 if p.is_absolute() {
2342 p
2343 } else if let Ok(cwd) = std::env::current_dir() {
2344 cwd.join(p)
2345 } else {
2346 p
2347 }
2348 } else {
2349 repo.index_path()
2350 };
2351 use crate::index::Index;
2352 let index = Index::load_expand_sparse(&index_path, &repo.odb)
2353 .map_err(|_| Error::ObjectNotFound(format!(":{}:{}", colon.stage, path)))?;
2354 let entry = index
2355 .get(path.as_bytes(), colon.stage)
2356 .ok_or_else(|| Error::ObjectNotFound(format!(":{}:{}", colon.stage, path)))?;
2357 Ok(Some(IndexPathEntry {
2358 path,
2359 oid: entry.oid,
2360 mode: entry.mode,
2361 }))
2362}
2363
2364fn resolve_index_path_at_stage(repo: &Repository, path: &str, stage: u8) -> Result<ObjectId> {
2366 use crate::index::Index;
2367 let index_path = if let Ok(raw) = std::env::var("GIT_INDEX_FILE") {
2368 let p = std::path::PathBuf::from(raw);
2369 if p.is_absolute() {
2370 p
2371 } else if let Ok(cwd) = std::env::current_dir() {
2372 cwd.join(p)
2373 } else {
2374 p
2375 }
2376 } else {
2377 repo.index_path()
2378 };
2379 let index = Index::load_expand_sparse(&index_path, &repo.odb)
2380 .map_err(|_| Error::ObjectNotFound(format!(":{stage}:{path}")))?;
2381 match index.get(path.as_bytes(), stage) {
2382 Some(entry) => Ok(entry.oid),
2383 None => Err(Error::ObjectNotFound(format!(":{stage}:{path}"))),
2384 }
2385}
2386
2387pub fn split_treeish_colon(spec: &str) -> Option<(&str, &str)> {
2392 if spec.starts_with(':') {
2393 return None;
2394 }
2395 let bytes = spec.as_bytes();
2396 let mut i = 0usize;
2397 let mut peel_depth = 0usize;
2398 while i < bytes.len() {
2399 if i + 1 < bytes.len() && bytes[i] == b'^' && bytes[i + 1] == b'{' {
2400 peel_depth += 1;
2401 i += 2;
2402 continue;
2403 }
2404 if peel_depth > 0 {
2405 if bytes[i] == b'}' {
2406 peel_depth -= 1;
2407 }
2408 i += 1;
2409 continue;
2410 }
2411 if bytes[i] == b':' && i > 0 {
2412 let before = &spec[..i];
2413 let after = &spec[i + 1..];
2414 if !before.is_empty() {
2415 return Some((before, after)); }
2417 }
2418 i += 1;
2419 }
2420 None
2421}
2422
2423pub(crate) fn split_treeish_spec(spec: &str) -> Option<(&str, &str)> {
2424 split_treeish_colon(spec)
2425}
2426
2427pub(crate) fn resolve_treeish_path(
2428 repo: &Repository,
2429 treeish: ObjectId,
2430 path: &str,
2431) -> Result<ObjectId> {
2432 let object = repo.odb.read(&treeish)?;
2433 let mut current_tree = match object.kind {
2434 ObjectKind::Commit => parse_commit(&object.data)?.tree,
2435 ObjectKind::Tree => treeish,
2436 _ => {
2437 return Err(Error::InvalidRef(format!(
2438 "object {treeish} does not name a tree"
2439 )))
2440 }
2441 };
2442
2443 let mut parts = path.split('/').filter(|part| !part.is_empty()).peekable();
2444 if parts.peek().is_none() {
2445 return Ok(current_tree);
2446 }
2447 while let Some(part) = parts.next() {
2448 let tree_object = repo.odb.read(¤t_tree)?;
2449 if tree_object.kind != ObjectKind::Tree {
2450 return Err(Error::CorruptObject(format!(
2451 "object {current_tree} is not a tree"
2452 )));
2453 }
2454 let entries = parse_tree(&tree_object.data)?;
2455 let Some(entry) = entries.iter().find(|entry| entry.name == part.as_bytes()) else {
2456 return Err(Error::ObjectNotFound(path.to_owned()));
2457 };
2458 if parts.peek().is_none() {
2459 return Ok(entry.oid);
2460 }
2461 current_tree = entry.oid;
2462 }
2463
2464 Err(Error::ObjectNotFound(path.to_owned()))
2465}
2466
2467fn apply_peel(repo: &Repository, mut oid: ObjectId, peel: Option<&str>) -> Result<ObjectId> {
2468 match peel {
2469 None => Ok(oid),
2470 Some(search) if search.starts_with('/') => {
2471 let pattern = &search[1..];
2472 if pattern.is_empty() {
2473 return Err(Error::InvalidRef(
2474 "empty commit message search pattern".to_owned(),
2475 ));
2476 }
2477 resolve_commit_message_search_from(repo, oid, pattern)
2478 }
2479 Some("") => {
2480 while let Ok(obj) = repo.odb.read(&oid) {
2481 if obj.kind != ObjectKind::Tag {
2482 break;
2483 }
2484 oid = parse_tag_target(&obj.data)?;
2485 }
2486 Ok(oid)
2487 }
2488 Some("commit") => {
2489 oid = apply_peel(repo, oid, Some(""))?;
2490 let obj = repo.odb.read(&oid)?;
2491 if obj.kind == ObjectKind::Commit {
2492 Ok(oid)
2493 } else {
2494 Err(Error::InvalidRef("expected commit".to_owned()))
2495 }
2496 }
2497 Some("tree") => {
2498 oid = apply_peel(repo, oid, Some(""))?;
2500 let obj = repo.odb.read(&oid)?;
2501 match obj.kind {
2502 ObjectKind::Tree => Ok(oid),
2503 ObjectKind::Commit => Ok(parse_commit(&obj.data)?.tree),
2504 _ => Err(Error::InvalidRef("expected tree or commit".to_owned())),
2505 }
2506 }
2507 Some("blob") => {
2508 let mut cur = oid;
2510 loop {
2511 let obj = repo.odb.read(&cur)?;
2512 match obj.kind {
2513 ObjectKind::Blob => return Ok(cur),
2514 ObjectKind::Tag => {
2515 cur = parse_tag_target(&obj.data)?;
2516 }
2517 _ => return Err(Error::InvalidRef("expected blob".to_owned())),
2518 }
2519 }
2520 }
2521 Some("object") => Ok(oid),
2522 Some("tag") => {
2523 let obj = repo.odb.read(&oid)?;
2525 if obj.kind == ObjectKind::Tag {
2526 Ok(oid)
2527 } else {
2528 Err(Error::InvalidRef("expected tag".to_owned()))
2529 }
2530 }
2531 Some(other) => Err(Error::InvalidRef(format!(
2532 "unsupported peel operator '{{{other}}}'"
2533 ))),
2534 }
2535}
2536
2537pub fn expand_rev_token_circ_bang(repo: &Repository, token: &str) -> Result<Vec<String>> {
2548 let Some(base) = token.strip_suffix("^!") else {
2549 return Ok(vec![token.to_owned()]);
2550 };
2551 if base.is_empty() {
2552 return Err(Error::Message(format!(
2553 "fatal: ambiguous argument '{token}': unknown revision or path not in the working tree.\n\
2554Use '--' to separate paths from revisions, like this:\n\
2555'git <command> [<revision>...] -- [<file>...]'"
2556 )));
2557 }
2558 let oid = resolve_revision_for_range_end(repo, base)?;
2559 let commit_oid = peel_to_commit_for_merge_base(repo, oid)?;
2560 let obj = repo.odb.read(&commit_oid)?;
2561 let commit = parse_commit(&obj.data)?;
2562 if commit.parents.len() != 1 {
2563 return Err(Error::Message(format!(
2564 "fatal: ambiguous argument '{token}': unknown revision or path not in the working tree.\n\
2565Use '--' to separate paths from revisions, like this:\n\
2566'git <command> [<revision>...] -- [<file>...]'"
2567 )));
2568 }
2569 Ok(vec![
2570 base.to_owned(),
2571 format!("^{}", commit.parents[0].to_hex()),
2572 ])
2573}
2574
2575#[must_use]
2577pub fn parse_peel_suffix(spec: &str) -> (&str, Option<&str>) {
2578 if let Some(base) = spec.strip_suffix("^{}") {
2579 return (base, Some(""));
2580 }
2581 if let Some(start) = spec.rfind("^{") {
2582 if spec.ends_with('}') {
2583 let base = &spec[..start];
2584 let op = &spec[start + 2..spec.len() - 1];
2585 return (base, Some(op));
2586 }
2587 }
2588 if let Some(base) = spec.strip_suffix("^0") {
2590 if !base.ends_with('^') {
2593 return (base, Some("commit"));
2594 }
2595 }
2596 (spec, None)
2597}
2598
2599fn parse_tag_target(data: &[u8]) -> Result<ObjectId> {
2600 let text = std::str::from_utf8(data)
2601 .map_err(|_| Error::CorruptObject("invalid tag object".to_owned()))?;
2602 let Some(line) = text.lines().find(|line| line.starts_with("object ")) else {
2603 return Err(Error::CorruptObject("tag missing object header".to_owned()));
2604 };
2605 let oid_text = line.trim_start_matches("object ").trim();
2606 oid_text.parse::<ObjectId>()
2607}
2608
2609fn resolve_commit_message_search_from(
2612 repo: &Repository,
2613 start: ObjectId,
2614 pattern: &str,
2615) -> Result<ObjectId> {
2616 let regex = Regex::new(pattern).ok();
2618 let mut visited = std::collections::HashSet::new();
2619 let mut queue = std::collections::VecDeque::new();
2620 queue.push_back(start);
2621 visited.insert(start);
2622
2623 while let Some(oid) = queue.pop_front() {
2624 let obj = match repo.odb.read(&oid) {
2625 Ok(o) => o,
2626 Err(_) => continue,
2627 };
2628 if obj.kind != ObjectKind::Commit {
2629 continue;
2630 }
2631 let commit = match parse_commit(&obj.data) {
2632 Ok(c) => c,
2633 Err(_) => continue,
2634 };
2635
2636 let is_match = if let Some(re) = ®ex {
2637 re.is_match(&commit.message)
2638 } else {
2639 commit.message.contains(pattern)
2640 };
2641 if is_match {
2642 return Ok(oid);
2643 }
2644
2645 for parent in &commit.parents {
2646 if visited.insert(*parent) {
2647 queue.push_back(*parent);
2648 }
2649 }
2650 }
2651
2652 Err(Error::ObjectNotFound(format!(":/{pattern}")))
2653}
2654
2655fn find_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
2656 if !is_hex_prefix(prefix) || !(4..=40).contains(&prefix.len()) {
2657 return Ok(Vec::new());
2658 }
2659 let mut seen = HashSet::new();
2660 let mut matches = Vec::new();
2661 for objects_dir in object_storage_dirs_for_abbrev(repo)? {
2662 for hex in collect_loose_object_ids_in_dir(&objects_dir)? {
2663 if hex.starts_with(prefix) {
2664 let oid = hex.parse::<ObjectId>()?;
2665 if seen.insert(oid) {
2666 matches.push(oid);
2667 }
2668 }
2669 }
2670 for oid in collect_pack_oids_with_prefix(&objects_dir, prefix)? {
2671 if seen.insert(oid) {
2672 matches.push(oid);
2673 }
2674 }
2675 }
2676 Ok(matches)
2677}
2678
2679fn collect_loose_object_ids(repo: &Repository) -> Result<Vec<String>> {
2680 collect_loose_object_ids_in_dir(repo.odb.objects_dir())
2681}
2682
2683fn collect_loose_object_ids_in_dir(objects_dir: &Path) -> Result<Vec<String>> {
2684 let mut ids = Vec::new();
2685 let read = match fs::read_dir(objects_dir) {
2686 Ok(read) => read,
2687 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(ids),
2688 Err(err) => return Err(Error::Io(err)),
2689 };
2690
2691 for dir_entry in read {
2692 let dir_entry = dir_entry?;
2693 let name = dir_entry.file_name();
2694 let Some(prefix) = name.to_str() else {
2695 continue;
2696 };
2697 if !is_two_hex(prefix) {
2698 continue;
2699 }
2700 if !dir_entry.file_type()?.is_dir() {
2701 continue;
2702 }
2703
2704 let files = fs::read_dir(dir_entry.path())?;
2705 for file_entry in files {
2706 let file_entry = file_entry?;
2707 if !file_entry.file_type()?.is_file() {
2708 continue;
2709 }
2710 let file_name = file_entry.file_name();
2711 let Some(suffix) = file_name.to_str() else {
2712 continue;
2713 };
2714 if suffix.len() == 38 && suffix.chars().all(|ch| ch.is_ascii_hexdigit()) {
2715 ids.push(format!("{prefix}{suffix}"));
2716 }
2717 }
2718 }
2719
2720 Ok(ids)
2721}
2722
2723fn is_two_hex(text: &str) -> bool {
2724 text.len() == 2 && text.chars().all(|ch| ch.is_ascii_hexdigit())
2725}
2726
2727fn is_hex_prefix(text: &str) -> bool {
2728 !text.is_empty() && text.chars().all(|ch| ch.is_ascii_hexdigit())
2729}
2730
2731fn path_is_within(path: &Path, container: &Path) -> bool {
2732 if path == container {
2733 return true;
2734 }
2735 path.starts_with(container)
2736}
2737
2738fn normalize_components(path: &Path) -> Vec<String> {
2739 path.components()
2740 .filter_map(|component| match component {
2741 Component::RootDir => Some(String::from("/")),
2742 Component::Normal(item) => Some(item.to_string_lossy().into_owned()),
2743 _ => None,
2744 })
2745 .collect()
2746}
2747
2748fn component_to_text(component: Component<'_>) -> Option<String> {
2749 match component {
2750 Component::Normal(item) => Some(os_to_string(item)),
2751 _ => None,
2752 }
2753}
2754
2755fn os_to_string(text: &OsStr) -> String {
2756 text.to_string_lossy().into_owned()
2757}
2758
2759fn resolve_commit_message_search(
2762 repo: &crate::repo::Repository,
2763 pattern: &str,
2764) -> Result<ObjectId> {
2765 let (negate, effective_pattern) = if pattern.starts_with('!') {
2767 if pattern.starts_with("!!") {
2768 (false, &pattern[1..]) } else {
2770 (true, &pattern[1..]) }
2772 } else {
2773 (false, pattern)
2774 };
2775 let regex = Regex::new(effective_pattern).ok();
2776 use crate::state::resolve_head;
2777 let head =
2778 resolve_head(&repo.git_dir).map_err(|_| Error::ObjectNotFound(format!(":/{pattern}")))?;
2779 let start_oid = match head.oid() {
2780 Some(oid) => *oid,
2781 None => return Err(Error::ObjectNotFound(format!(":/{pattern}"))),
2782 };
2783
2784 let mut visited = std::collections::HashSet::new();
2785 let mut queue = std::collections::VecDeque::new();
2786 queue.push_back(start_oid);
2787 visited.insert(start_oid);
2788
2789 while let Some(oid) = queue.pop_front() {
2790 let obj = match repo.odb.read(&oid) {
2791 Ok(o) => o,
2792 Err(_) => continue,
2793 };
2794 if obj.kind != ObjectKind::Commit {
2796 continue;
2797 }
2798 let commit = match parse_commit(&obj.data) {
2799 Ok(c) => c,
2800 Err(_) => continue,
2801 };
2802
2803 let base_match = if let Some(re) = ®ex {
2805 re.is_match(&commit.message)
2806 } else {
2807 commit.message.contains(effective_pattern)
2808 };
2809 let is_match = if negate { !base_match } else { base_match };
2810 if is_match {
2811 return Ok(oid);
2812 }
2813
2814 for parent in &commit.parents {
2816 if visited.insert(*parent) {
2817 queue.push_back(*parent);
2818 }
2819 }
2820 }
2821
2822 Err(Error::ObjectNotFound(format!(":/{pattern}")))
2823}
2824
2825pub fn list_all_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
2827 find_abbrev_matches(repo, prefix)
2828}
2829
2830pub fn list_loose_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
2832 list_all_abbrev_matches(repo, prefix)
2833}