1use std::ffi::OsStr;
9use std::fs;
10use std::path::{Component, Path};
11
12use regex::Regex;
13
14use crate::error::{Error, Result};
15use crate::objects::{parse_commit, parse_tree, ObjectId, ObjectKind};
16use crate::reflog::read_reflog;
17use crate::refs;
18use crate::repo::Repository;
19
20pub fn discover_optional(start: Option<&Path>) -> Result<Option<Repository>> {
31 match Repository::discover(start) {
32 Ok(repo) => Ok(Some(repo)),
33 Err(Error::NotARepository(msg)) => {
34 if msg.contains("invalid gitfile format")
38 || msg.contains("gitfile does not contain 'gitdir:' line")
39 || msg.contains("not a regular file")
40 {
41 return Err(Error::NotARepository(msg));
42 }
43
44 if let Some(start) = start {
45 let start = if start.is_absolute() {
46 start.to_path_buf()
47 } else if let Ok(cwd) = std::env::current_dir() {
48 cwd.join(start)
49 } else {
50 start.to_path_buf()
51 };
52 let dot_git = start.join(".git");
53 if dot_git.is_file() || dot_git.is_symlink() {
54 return Err(Error::NotARepository(msg));
55 }
56 }
57
58 Ok(None)
59 }
60 Err(err) => Err(err),
61 }
62}
63
64#[must_use]
66pub fn is_inside_work_tree(repo: &Repository, cwd: &Path) -> bool {
67 let Some(work_tree) = &repo.work_tree else {
68 return false;
69 };
70 path_is_within(cwd, work_tree)
71}
72
73#[must_use]
75pub fn is_inside_git_dir(repo: &Repository, cwd: &Path) -> bool {
76 path_is_within(cwd, &repo.git_dir)
77}
78
79#[must_use]
84pub fn show_prefix(repo: &Repository, cwd: &Path) -> String {
85 let Some(work_tree) = &repo.work_tree else {
86 return String::new();
87 };
88 if !path_is_within(cwd, work_tree) {
89 return String::new();
90 }
91 if cwd == work_tree {
92 return String::new();
93 }
94 let Ok(rel) = cwd.strip_prefix(work_tree) else {
95 return String::new();
96 };
97 let mut out = rel
98 .components()
99 .filter_map(component_to_text)
100 .collect::<Vec<_>>()
101 .join("/");
102 if !out.is_empty() {
103 out.push('/');
104 }
105 out
106}
107
108#[must_use]
115pub fn symbolic_full_name(repo: &Repository, spec: &str) -> Option<String> {
116 if let Some(base) = spec
118 .strip_suffix("@{upstream}")
119 .or_else(|| spec.strip_suffix("@{u}"))
120 .or_else(|| spec.strip_suffix("@{UPSTREAM}"))
121 .or_else(|| spec.strip_suffix("@{U}"))
122 .or_else(|| spec.strip_suffix("@{UpSTReam}"))
123 {
124 return resolve_upstream_ref(repo, base);
125 }
126 if let Some(base) = spec.strip_suffix("@{push}") {
127 return resolve_push_ref(repo, base);
128 }
129
130 if let Ok(Some(branch)) = expand_at_minus_to_branch_name(repo, spec) {
131 let ref_name = format!("refs/heads/{branch}");
132 if refs::resolve_ref(&repo.git_dir, &ref_name).is_ok() {
133 return Some(ref_name);
134 }
135 return None;
136 }
137
138 if spec == "HEAD" {
139 if let Ok(Some(target)) = refs::read_symbolic_ref(&repo.git_dir, "HEAD") {
140 return Some(target);
141 }
142 return None;
143 }
144 if spec.starts_with("refs/") {
146 if refs::resolve_ref(&repo.git_dir, spec).is_ok() {
147 return Some(spec.to_owned());
148 }
149 return None;
150 }
151 for prefix in &["refs/heads/", "refs/tags/", "refs/remotes/"] {
153 let candidate = format!("{prefix}{spec}");
154 if refs::resolve_ref(&repo.git_dir, &candidate).is_ok() {
155 return Some(candidate);
156 }
157 }
158 None
159}
160
161pub fn expand_at_minus_to_branch_name(repo: &Repository, spec: &str) -> Result<Option<String>> {
169 if !spec.starts_with("@{-") || !spec.ends_with('}') {
170 return Ok(None);
171 }
172 let inner = &spec[3..spec.len() - 1];
173 let n: usize = inner
174 .parse()
175 .map_err(|_| Error::InvalidRef(format!("invalid N in @{{-N}} for '{spec}'")))?;
176 if n < 1 {
177 return Ok(None);
178 }
179 resolve_at_minus_to_branch(repo, n).map(Some)
180}
181
182pub fn resolve_at_minus_to_oid(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
184 try_resolve_at_minus(repo, spec)
185}
186
187#[must_use]
191pub fn abbreviate_ref_name(full_name: &str) -> String {
192 for prefix in &["refs/heads/", "refs/tags/", "refs/remotes/"] {
193 if let Some(short) = full_name.strip_prefix(prefix) {
194 return short.to_owned();
195 }
196 }
197 if let Some(short) = full_name.strip_prefix("refs/") {
198 return short.to_owned();
199 }
200 full_name.to_owned()
201}
202
203fn resolve_upstream_ref(repo: &Repository, branch: &str) -> Option<String> {
205 let branch_name = if branch.is_empty() {
207 match refs::read_head(&repo.git_dir) {
208 Ok(Some(target)) => target.strip_prefix("refs/heads/")?.to_owned(),
209 _ => return None,
210 }
211 } else {
212 branch.to_owned()
214 };
215
216 let config_path = repo.git_dir.join("config");
218 let config_content = fs::read_to_string(&config_path).ok()?;
219 let (remote, merge) = parse_branch_tracking(&config_content, &branch_name)?;
220
221 if remote == "." {
224 Some(merge.clone())
225 } else {
226 let merge_branch = merge.strip_prefix("refs/heads/")?;
227 Some(format!("refs/remotes/{remote}/{merge_branch}"))
228 }
229}
230
231fn resolve_push_ref(repo: &Repository, branch: &str) -> Option<String> {
233 let config_path = crate::refs::common_dir(&repo.git_dir)
235 .unwrap_or_else(|| repo.git_dir.clone())
236 .join("config");
237 let config_content = std::fs::read_to_string(&config_path).unwrap_or_default();
238 let push_default = parse_config_value(&config_content, "push", "default");
240 match push_default.as_deref().unwrap_or("simple") {
241 "nothing" => return None, _ => {}
243 }
244
245 let push_remote = parse_config_value(&config_content, "remote", "pushRemote").or_else(|| {
247 let section = format!("[branch \"{}\"]", branch);
248 let mut in_section = false;
249 for line in config_content.lines() {
250 let trimmed = line.trim();
251 if trimmed.starts_with('[') {
252 in_section = trimmed == section;
253 continue;
254 }
255 if in_section {
256 if let Some(v) = trimmed
257 .strip_prefix("pushremote = ")
258 .or_else(|| trimmed.strip_prefix("pushRemote = "))
259 {
260 return Some(v.trim().to_owned());
261 }
262 }
263 }
264 None
265 });
266
267 if let Some(remote) = push_remote {
268 let tracking_ref = format!("refs/remotes/{remote}/{branch}");
270 if crate::refs::resolve_ref(&repo.git_dir, &tracking_ref).is_ok() {
271 return Some(tracking_ref);
272 }
273 }
274
275 resolve_upstream_ref(repo, branch)
277}
278
279fn parse_config_value(config: &str, section: &str, key: &str) -> Option<String> {
280 let section_header = format!("[{}]", section);
281 let key_lower = key.to_ascii_lowercase();
282 let mut in_section = false;
283 for line in config.lines() {
284 let trimmed = line.trim();
285 if trimmed.starts_with('[') {
286 in_section = trimmed.eq_ignore_ascii_case(§ion_header);
287 continue;
288 }
289 if in_section {
290 let lower = trimmed.to_ascii_lowercase();
291 if lower.starts_with(&key_lower) {
292 let rest = lower[key_lower.len()..].trim_start().to_string();
293 if rest.starts_with('=') {
294 if let Some(eq_pos) = trimmed.find('=') {
295 return Some(trimmed[eq_pos + 1..].trim().to_owned());
296 }
297 }
298 }
299 }
300 }
301 None
302}
303
304fn parse_branch_tracking(config: &str, branch: &str) -> Option<(String, String)> {
306 let mut remote = None;
307 let mut merge = None;
308 let mut in_section = false;
309 let target_section = format!("[branch \"{}\"]", branch);
310
311 for line in config.lines() {
312 let trimmed = line.trim();
313 if trimmed.starts_with('[') {
314 in_section = trimmed == target_section
315 || trimmed.starts_with(&format!("[branch \"{}\"", branch));
316 continue;
317 }
318 if !in_section {
319 continue;
320 }
321 if let Some(value) = trimmed.strip_prefix("remote = ") {
322 remote = Some(value.trim().to_owned());
323 } else if let Some(value) = trimmed.strip_prefix("merge = ") {
324 merge = Some(value.trim().to_owned());
325 }
326 if let Some(value) = trimmed.strip_prefix("remote=") {
328 remote = Some(value.trim().to_owned());
329 } else if let Some(value) = trimmed.strip_prefix("merge=") {
330 merge = Some(value.trim().to_owned());
331 }
332 }
333
334 match (remote, merge) {
335 (Some(r), Some(m)) => Some((r, m)),
336 _ => None,
337 }
338}
339
340pub fn resolve_revision(repo: &Repository, spec: &str) -> Result<ObjectId> {
354 if let Some(pattern) = spec.strip_prefix(":/") {
357 if !pattern.is_empty() {
358 return resolve_commit_message_search(repo, pattern);
359 }
360 }
361
362 if let Some(idx) = spec.find("...") {
365 let left_raw = &spec[..idx];
366 let right_raw = &spec[idx + 3..];
367 if !left_raw.is_empty() || !right_raw.is_empty() {
368 let left_oid = if left_raw.is_empty() {
369 resolve_revision(repo, "HEAD")?
370 } else {
371 resolve_revision(repo, left_raw)?
372 };
373 let right_oid = if right_raw.is_empty() {
374 resolve_revision(repo, "HEAD")?
375 } else {
376 resolve_revision(repo, right_raw)?
377 };
378 let bases = crate::merge_base::merge_bases_first_vs_rest(repo, left_oid, &[right_oid])?;
379 return bases
380 .into_iter()
381 .next()
382 .ok_or_else(|| Error::ObjectNotFound(format!("no merge base for '{spec}'")));
383 }
384 }
385
386 if let Some((before, after)) = split_treeish_colon(spec) {
390 if !before.is_empty() && !spec.starts_with(":/") {
391 let rev_oid = resolve_revision(repo, before)?;
393 let tree_oid = peel_to_tree(repo, rev_oid)?;
394 if after.is_empty() {
395 return Ok(tree_oid);
397 }
398 let clean_path = after.strip_prefix("./").unwrap_or(after);
401 return resolve_tree_path(repo, &tree_oid, clean_path);
402 }
403 }
404
405 let (base_with_nav, peel) = parse_peel_suffix(spec);
406 let (base, nav_steps) = parse_nav_steps(base_with_nav);
407 let mut oid = resolve_base(repo, base)?;
408 for step in nav_steps {
409 oid = apply_nav_step(repo, oid, step)?;
410 }
411 apply_peel(repo, oid, peel)
412}
413
414fn peel_to_tree(repo: &Repository, oid: ObjectId) -> Result<ObjectId> {
416 let obj = repo.odb.read(&oid)?;
417 match obj.kind {
418 crate::objects::ObjectKind::Tree => Ok(oid),
419 crate::objects::ObjectKind::Commit => {
420 let commit = crate::objects::parse_commit(&obj.data)?;
421 Ok(commit.tree)
422 }
423 crate::objects::ObjectKind::Tag => {
424 let tag = crate::objects::parse_tag(&obj.data)?;
425 peel_to_tree(repo, tag.object)
426 }
427 _ => Err(Error::ObjectNotFound(format!(
428 "cannot peel {} to tree",
429 oid
430 ))),
431 }
432}
433
434fn resolve_tree_path(repo: &Repository, tree_oid: &ObjectId, path: &str) -> Result<ObjectId> {
436 let obj = repo.odb.read(tree_oid)?;
437 let entries = crate::objects::parse_tree(&obj.data)?;
438 let components: Vec<&str> = path.split('/').filter(|c| !c.is_empty()).collect();
439 if components.is_empty() {
440 return Ok(*tree_oid);
441 }
442 let first = components[0];
443 let rest: Vec<&str> = components[1..].to_vec();
444 for entry in entries {
445 let name = String::from_utf8_lossy(&entry.name);
446 if name == first {
447 if rest.is_empty() {
448 return Ok(entry.oid);
449 } else {
450 return resolve_tree_path(repo, &entry.oid, &rest.join("/"));
451 }
452 }
453 }
454 Err(Error::ObjectNotFound(format!(
455 "path '{}' not found in tree {}",
456 path, tree_oid
457 )))
458}
459
460#[derive(Debug, Clone, Copy)]
462enum NavStep {
463 ParentN(usize),
465 AncestorN(usize),
467}
468
469fn parse_nav_steps(spec: &str) -> (&str, Vec<NavStep>) {
473 let mut steps = Vec::new();
474 let mut remaining = spec;
475
476 loop {
477 if let Some(tilde_pos) = remaining.rfind('~') {
479 let after = &remaining[tilde_pos + 1..];
480 if after.is_empty() {
481 steps.push(NavStep::AncestorN(1));
483 remaining = &remaining[..tilde_pos];
484 continue;
485 }
486 if after.bytes().all(|b| b.is_ascii_digit()) {
487 let n: usize = after.parse().unwrap_or(1);
488 steps.push(NavStep::AncestorN(n));
489 remaining = &remaining[..tilde_pos];
490 continue;
491 }
492 }
493
494 if let Some(caret_pos) = remaining.rfind('^') {
496 let after = &remaining[caret_pos + 1..];
497 if after.is_empty() {
498 steps.push(NavStep::ParentN(1));
500 remaining = &remaining[..caret_pos];
501 continue;
502 }
503 if after.len() == 1 && after.as_bytes()[0].is_ascii_digit() {
504 let n = (after.as_bytes()[0] - b'0') as usize;
505 steps.push(NavStep::ParentN(n));
506 remaining = &remaining[..caret_pos];
507 continue;
508 }
509 }
510
511 break;
512 }
513
514 steps.reverse();
515 (remaining, steps)
516}
517
518fn apply_nav_step(repo: &Repository, oid: ObjectId, step: NavStep) -> Result<ObjectId> {
520 match step {
521 NavStep::ParentN(0) => Ok(oid),
522 NavStep::ParentN(n) => {
523 let obj = repo.odb.read(&oid)?;
524 if obj.kind != ObjectKind::Commit {
525 return Err(Error::InvalidRef(format!("{oid} is not a commit")));
526 }
527 let commit = parse_commit(&obj.data)?;
528 commit
529 .parents
530 .get(n - 1)
531 .copied()
532 .ok_or_else(|| Error::ObjectNotFound(format!("{oid}^{n}")))
533 }
534 NavStep::AncestorN(n) => {
535 let mut current = oid;
536 for _ in 0..n {
537 current = apply_nav_step(repo, current, NavStep::ParentN(1))?;
538 }
539 Ok(current)
540 }
541 }
542}
543
544pub fn abbreviate_object_id(repo: &Repository, oid: ObjectId, min_len: usize) -> Result<String> {
553 let min_len = min_len.clamp(4, 40);
554 let target = oid.to_hex();
555
556 if !repo.odb.exists(&oid) {
558 return Ok(target[..min_len].to_owned());
559 }
560
561 let all = collect_loose_object_ids(repo)?;
562
563 for len in min_len..=40 {
564 let prefix = &target[..len];
565 let matches = all
566 .iter()
567 .filter(|candidate| candidate.starts_with(prefix))
568 .count();
569 if matches <= 1 {
570 return Ok(prefix.to_owned());
571 }
572 }
573
574 Ok(target)
575}
576
577#[must_use]
579pub fn to_relative_path(path: &Path, cwd: &Path) -> String {
580 let path_components = normalize_components(path);
581 let cwd_components = normalize_components(cwd);
582
583 let mut common = 0usize;
584 let max_common = path_components.len().min(cwd_components.len());
585 while common < max_common && path_components[common] == cwd_components[common] {
586 common += 1;
587 }
588
589 let mut parts = Vec::new();
590 let up_count = cwd_components.len().saturating_sub(common);
591 for _ in 0..up_count {
592 parts.push("..".to_owned());
593 }
594 for item in path_components.iter().skip(common) {
595 parts.push(item.clone());
596 }
597
598 if parts.is_empty() {
599 ".".to_owned()
600 } else {
601 parts.join("/")
602 }
603}
604
605fn resolve_base(repo: &Repository, spec: &str) -> Result<ObjectId> {
606 if let Some(full_ref) = try_resolve_at_suffix(repo, spec) {
608 return refs::resolve_ref(&repo.git_dir, &full_ref)
609 .map_err(|_| Error::ObjectNotFound(spec.to_owned()));
610 }
611
612 if spec.starts_with("@{-") {
615 if let Some(close) = spec[3..].find('}') {
617 let n_str = &spec[3..3 + close];
618 if let Ok(n) = n_str.parse::<usize>() {
619 if n >= 1 {
620 let suffix = &spec[3 + close + 1..]; if suffix.is_empty() {
622 if let Some(oid) = try_resolve_at_minus(repo, spec)? {
624 return Ok(oid);
625 }
626 } else {
627 let branch = resolve_at_minus_to_branch(repo, n)?;
630 let new_spec = format!("{branch}{suffix}");
631 return resolve_base(repo, &new_spec);
632 }
633 }
634 }
635 }
636 }
637
638 if let Some(oid) = try_resolve_reflog_index(repo, spec)? {
640 return Ok(oid);
641 }
642
643 if let Some(pattern) = spec.strip_prefix(":/") {
645 if !pattern.is_empty() {
646 return resolve_commit_message_search(repo, pattern);
647 }
648 }
649
650 if let Some(rest) = spec.strip_prefix(':') {
653 if !rest.is_empty() && !rest.starts_with('/') {
654 if rest.len() >= 3 && rest.as_bytes()[1] == b':' {
656 if let Some(stage_char) = rest.chars().next() {
657 if let Some(stage) = stage_char.to_digit(10) {
658 if stage <= 3 {
659 let raw_path = &rest[2..];
660 let path = raw_path.strip_prefix("./").unwrap_or(raw_path);
661 return resolve_index_path_at_stage(repo, path, stage as u8);
662 }
663 }
664 }
665 }
666 let clean_rest = rest.strip_prefix("./").unwrap_or(rest);
668 return resolve_index_path(repo, clean_rest);
669 }
670 }
671
672 if let Some((treeish, path)) = split_treeish_spec(spec) {
673 let root_oid = resolve_revision(repo, treeish)?;
674 return resolve_treeish_path(repo, root_oid, path);
675 }
676
677 if let Ok(oid) = spec.parse::<ObjectId>() {
678 return Ok(oid);
681 }
682
683 if is_hex_prefix(spec) {
684 let matches = find_abbrev_matches(repo, spec)?;
685 if matches.len() == 1 {
686 return Ok(matches[0]);
687 }
688 if matches.len() > 1 {
689 return Err(Error::InvalidRef(format!(
690 "short object ID {} is ambiguous",
691 spec
692 )));
693 }
694 }
695
696 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, spec) {
697 return Ok(oid);
698 }
699 for candidate in &[
700 format!("refs/heads/{spec}"),
701 format!("refs/tags/{spec}"),
702 format!("refs/remotes/{spec}"),
703 ] {
704 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, candidate) {
705 return Ok(oid);
706 }
707 }
708
709 if !spec.contains(':') && !spec.starts_with('-') {
713 if let Ok(oid) = resolve_index_path(repo, spec) {
714 return Ok(oid);
715 }
716 }
717
718 Err(Error::ObjectNotFound(spec.to_owned()))
719}
720
721fn resolve_at_minus_to_branch(repo: &Repository, n: usize) -> Result<String> {
723 let entries = read_reflog(&repo.git_dir, "HEAD")?;
724 let mut count = 0usize;
725 for entry in entries.iter().rev() {
726 let msg = &entry.message;
727 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
728 count += 1;
729 if count == n {
730 if let Some(to_pos) = rest.find(" to ") {
731 return Ok(rest[..to_pos].to_string());
732 }
733 }
734 }
735 }
736 Err(Error::InvalidRef(format!(
737 "@{{-{n}}}: only {count} checkout(s) in reflog"
738 )))
739}
740
741fn try_resolve_at_minus(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
744 if !spec.starts_with("@{-") || !spec.ends_with('}') {
746 return Ok(None);
747 }
748 let inner = &spec[3..spec.len() - 1];
749 let n: usize = match inner.parse() {
750 Ok(n) if n >= 1 => n,
751 _ => return Ok(None),
752 };
753 let entries = read_reflog(&repo.git_dir, "HEAD")?;
755 let mut count = 0usize;
756 for entry in entries.iter().rev() {
758 let msg = &entry.message;
759 if let Some(rest) = msg.strip_prefix("checkout: moving from ") {
760 count += 1;
761 if count == n {
762 if let Some(to_pos) = rest.find(" to ") {
764 let from_branch = &rest[..to_pos];
765 let ref_name = format!("refs/heads/{from_branch}");
767 if let Ok(oid) = refs::resolve_ref(&repo.git_dir, &ref_name) {
768 return Ok(Some(oid));
769 }
770 if let Ok(oid) = from_branch.parse::<ObjectId>() {
772 if repo.odb.exists(&oid) {
773 return Ok(Some(oid));
774 }
775 }
776 return Err(Error::InvalidRef(format!(
777 "cannot resolve @{{-{n}}}: branch '{}' not found",
778 from_branch
779 )));
780 }
781 }
782 }
783 }
784 Err(Error::InvalidRef(format!(
785 "@{{-{n}}}: only {count} checkout(s) in reflog"
786 )))
787}
788
789fn try_resolve_reflog_index(repo: &Repository, spec: &str) -> Result<Option<ObjectId>> {
792 let at_pos = match spec.find("@{") {
794 Some(p) => p,
795 None => return Ok(None),
796 };
797 if !spec.ends_with('}') {
798 return Ok(None);
799 }
800 let inner = &spec[at_pos + 2..spec.len() - 1];
801 let index_or_date: ReflogSelector = if inner.eq_ignore_ascii_case("now") {
803 ReflogSelector::Index(0)
804 } else if let Ok(n) = inner.parse::<usize>() {
805 ReflogSelector::Index(n)
806 } else if let Some(ts) = approxidate(inner) {
807 ReflogSelector::Date(ts)
808 } else {
809 return Ok(None);
810 };
811 let refname_raw = &spec[..at_pos];
812 let refname = if refname_raw.is_empty() {
813 "HEAD".to_string()
814 } else if refname_raw == "HEAD" || refname_raw.starts_with("refs/") {
815 refname_raw.to_string()
816 } else {
817 let candidate = format!("refs/heads/{refname_raw}");
819 if refs::resolve_ref(&repo.git_dir, &candidate).is_ok() {
820 candidate
821 } else {
822 refname_raw.to_string()
823 }
824 };
825 let entries = read_reflog(&repo.git_dir, &refname)?;
826 if entries.is_empty() {
827 return Err(Error::InvalidRef(format!(
828 "log for '{}' is empty",
829 refname_raw
830 )));
831 }
832 match index_or_date {
833 ReflogSelector::Index(index) => {
834 let reversed_idx = entries.len().checked_sub(1 + index).ok_or_else(|| {
836 Error::InvalidRef(format!(
837 "log for '{}' only has {} entries",
838 refname_raw,
839 entries.len()
840 ))
841 })?;
842 Ok(Some(entries[reversed_idx].new_oid))
843 }
844 ReflogSelector::Date(target_ts) => {
845 for entry in entries.iter().rev() {
849 let ts = parse_reflog_entry_timestamp(entry);
850 if let Some(t) = ts {
851 if t <= target_ts {
852 return Ok(Some(entry.new_oid));
853 }
854 }
855 }
856 Ok(Some(entries[0].new_oid))
858 }
859 }
860}
861
862enum ReflogSelector {
863 Index(usize),
864 Date(i64),
865}
866
867fn parse_reflog_entry_timestamp(entry: &crate::reflog::ReflogEntry) -> Option<i64> {
869 let parts: Vec<&str> = entry.identity.rsplitn(3, ' ').collect();
871 if parts.len() >= 2 {
872 parts[1].parse::<i64>().ok()
873 } else {
874 None
875 }
876}
877
878fn approxidate(s: &str) -> Option<i64> {
881 let now_ts = std::time::SystemTime::now()
882 .duration_since(std::time::UNIX_EPOCH)
883 .ok()
884 .map(|d| d.as_secs() as i64)
885 .unwrap_or(0);
886 let lower = s.trim().to_ascii_lowercase();
887 if lower == "now" {
888 return Some(now_ts);
889 }
890 let relative = lower.replace('.', " ");
893 let parts: Vec<&str> = relative.split_whitespace().collect();
894 if parts.len() >= 2 {
895 let (n_str, unit, is_ago) = if parts.len() >= 3 && parts[2] == "ago" {
897 (parts[0], parts[1], true)
898 } else if parts.len() == 2 {
899 (parts[0], parts[1], false)
900 } else {
901 ("", "", false)
902 };
903 if !n_str.is_empty() {
904 if let Ok(n) = n_str.parse::<i64>() {
905 let secs: Option<i64> = match unit.trim_end_matches('s') {
906 "second" => Some(n),
907 "minute" => Some(n * 60),
908 "hour" => Some(n * 3600),
909 "day" => Some(n * 86400),
910 "week" => Some(n * 604800),
911 "month" => Some(n * 2592000),
912 "year" => Some(n * 31536000),
913 _ => None,
914 };
915 if let Some(s) = secs {
916 return Some(if is_ago || true {
917 now_ts - s
918 } else {
919 now_ts + s
920 });
921 }
922 }
923 }
924 }
925 let re_like = |input: &str| -> Option<i64> {
927 for (i, _) in input.char_indices() {
929 let rest = &input[i..];
930 if rest.len() >= 10 {
931 let bytes = rest.as_bytes();
932 if bytes[4] == b'-'
933 && bytes[7] == b'-'
934 && bytes[0..4].iter().all(|b| b.is_ascii_digit())
935 && bytes[5..7].iter().all(|b| b.is_ascii_digit())
936 && bytes[8..10].iter().all(|b| b.is_ascii_digit())
937 {
938 let year: i32 = rest[0..4].parse().ok()?;
939 let month: u8 = rest[5..7].parse().ok()?;
940 let day: u8 = rest[8..10].parse().ok()?;
941 let date = time::Date::from_calendar_date(
942 year,
943 time::Month::try_from(month).ok()?,
944 day,
945 )
946 .ok()?;
947 let dt = date.with_hms(0, 0, 0).ok()?;
948 let odt = dt.assume_utc();
949 return Some(odt.unix_timestamp());
950 }
951 }
952 }
953 None
954 };
955 re_like(s)
956}
957
958fn try_resolve_at_suffix(repo: &Repository, spec: &str) -> Option<String> {
961 let lower = spec.to_lowercase();
963 if lower.ends_with("@{upstream}") || lower.ends_with("@{u}") {
964 let suffix_len = if lower.ends_with("@{upstream}") {
965 11
966 } else {
967 4
968 };
969 let base = &spec[..spec.len() - suffix_len];
970 return resolve_upstream_ref(repo, base);
971 }
972 if lower.ends_with("@{push}") {
973 let base = &spec[..spec.len() - 7];
974 return resolve_push_ref(repo, base);
975 }
976 None
977}
978
979fn resolve_index_path(repo: &Repository, path: &str) -> Result<ObjectId> {
982 resolve_index_path_at_stage(repo, path, 0)
983}
984
985fn resolve_index_path_at_stage(repo: &Repository, path: &str, stage: u8) -> Result<ObjectId> {
987 use crate::index::Index;
988 let index_path = if let Ok(raw) = std::env::var("GIT_INDEX_FILE") {
989 let p = std::path::PathBuf::from(raw);
990 if p.is_absolute() {
991 p
992 } else if let Ok(cwd) = std::env::current_dir() {
993 cwd.join(p)
994 } else {
995 p
996 }
997 } else {
998 repo.index_path()
999 };
1000 let index =
1001 Index::load(&index_path).map_err(|_| Error::ObjectNotFound(format!(":{stage}:{path}")))?;
1002 match index.get(path.as_bytes(), stage) {
1003 Some(entry) => Ok(entry.oid),
1004 None => Err(Error::ObjectNotFound(format!(":{stage}:{path}"))),
1005 }
1006}
1007
1008fn split_treeish_colon(spec: &str) -> Option<(&str, &str)> {
1013 if spec.starts_with(':') {
1014 return None;
1015 }
1016 let bytes = spec.as_bytes();
1017 let mut i = 0usize;
1018 let mut peel_depth = 0usize;
1019 while i < bytes.len() {
1020 if i + 1 < bytes.len() && bytes[i] == b'^' && bytes[i + 1] == b'{' {
1021 peel_depth += 1;
1022 i += 2;
1023 continue;
1024 }
1025 if peel_depth > 0 {
1026 if bytes[i] == b'}' {
1027 peel_depth -= 1;
1028 }
1029 i += 1;
1030 continue;
1031 }
1032 if bytes[i] == b':' && i > 0 {
1033 let before = &spec[..i];
1034 let after = &spec[i + 1..];
1035 if !before.is_empty() {
1036 return Some((before, after)); }
1038 }
1039 i += 1;
1040 }
1041 None
1042}
1043
1044fn split_treeish_spec(spec: &str) -> Option<(&str, &str)> {
1045 split_treeish_colon(spec)
1046}
1047
1048fn resolve_treeish_path(repo: &Repository, treeish: ObjectId, path: &str) -> Result<ObjectId> {
1049 let object = repo.odb.read(&treeish)?;
1050 let mut current_tree = match object.kind {
1051 ObjectKind::Commit => parse_commit(&object.data)?.tree,
1052 ObjectKind::Tree => treeish,
1053 _ => {
1054 return Err(Error::InvalidRef(format!(
1055 "object {treeish} does not name a tree"
1056 )))
1057 }
1058 };
1059
1060 let mut parts = path.split('/').filter(|part| !part.is_empty()).peekable();
1061 if parts.peek().is_none() {
1062 return Ok(current_tree);
1063 }
1064 while let Some(part) = parts.next() {
1065 let tree_object = repo.odb.read(¤t_tree)?;
1066 if tree_object.kind != ObjectKind::Tree {
1067 return Err(Error::CorruptObject(format!(
1068 "object {current_tree} is not a tree"
1069 )));
1070 }
1071 let entries = parse_tree(&tree_object.data)?;
1072 let Some(entry) = entries.iter().find(|entry| entry.name == part.as_bytes()) else {
1073 return Err(Error::ObjectNotFound(path.to_owned()));
1074 };
1075 if parts.peek().is_none() {
1076 return Ok(entry.oid);
1077 }
1078 current_tree = entry.oid;
1079 }
1080
1081 Err(Error::ObjectNotFound(path.to_owned()))
1082}
1083
1084fn apply_peel(repo: &Repository, mut oid: ObjectId, peel: Option<&str>) -> Result<ObjectId> {
1085 match peel {
1086 None | Some("object") => Ok(oid),
1087 Some(search) if search.starts_with('/') => {
1088 let pattern = &search[1..];
1089 if pattern.is_empty() {
1090 return Err(Error::InvalidRef(
1091 "empty commit message search pattern".to_owned(),
1092 ));
1093 }
1094 resolve_commit_message_search_from(repo, oid, pattern)
1095 }
1096 Some("") => {
1097 while let Ok(obj) = repo.odb.read(&oid) {
1098 if obj.kind != ObjectKind::Tag {
1099 break;
1100 }
1101 oid = parse_tag_target(&obj.data)?;
1102 }
1103 Ok(oid)
1104 }
1105 Some("commit") => {
1106 oid = apply_peel(repo, oid, Some(""))?;
1107 let obj = repo.odb.read(&oid)?;
1108 if obj.kind == ObjectKind::Commit {
1109 Ok(oid)
1110 } else {
1111 Err(Error::InvalidRef("expected commit".to_owned()))
1112 }
1113 }
1114 Some("tree") => {
1115 oid = apply_peel(repo, oid, Some(""))?;
1117 let obj = repo.odb.read(&oid)?;
1118 match obj.kind {
1119 ObjectKind::Tree => Ok(oid),
1120 ObjectKind::Commit => Ok(parse_commit(&obj.data)?.tree),
1121 _ => Err(Error::InvalidRef("expected tree or commit".to_owned())),
1122 }
1123 }
1124 Some("blob") => {
1125 let mut cur = oid;
1127 loop {
1128 let obj = repo.odb.read(&cur)?;
1129 match obj.kind {
1130 ObjectKind::Blob => return Ok(cur),
1131 ObjectKind::Tag => {
1132 cur = parse_tag_target(&obj.data)?;
1133 }
1134 _ => return Err(Error::InvalidRef("expected blob".to_owned())),
1135 }
1136 }
1137 }
1138 Some("object") => {
1139 Ok(oid)
1141 }
1142 Some("tag") => {
1143 let obj = repo.odb.read(&oid)?;
1145 if obj.kind == ObjectKind::Tag {
1146 Ok(oid)
1147 } else {
1148 Err(Error::InvalidRef("expected tag".to_owned()))
1149 }
1150 }
1151 Some(other) => Err(Error::InvalidRef(format!(
1152 "unsupported peel operator '{{{other}}}'"
1153 ))),
1154 }
1155}
1156
1157fn parse_peel_suffix(spec: &str) -> (&str, Option<&str>) {
1158 if let Some(base) = spec.strip_suffix("^{}") {
1159 return (base, Some(""));
1160 }
1161 if let Some(start) = spec.rfind("^{") {
1162 if spec.ends_with('}') {
1163 let base = &spec[..start];
1164 let op = &spec[start + 2..spec.len() - 1];
1165 return (base, Some(op));
1166 }
1167 }
1168 if let Some(base) = spec.strip_suffix("^0") {
1170 if !base.ends_with('^') {
1173 return (base, Some("commit"));
1174 }
1175 }
1176 (spec, None)
1177}
1178
1179fn parse_tag_target(data: &[u8]) -> Result<ObjectId> {
1180 let text = std::str::from_utf8(data)
1181 .map_err(|_| Error::CorruptObject("invalid tag object".to_owned()))?;
1182 let Some(line) = text.lines().find(|line| line.starts_with("object ")) else {
1183 return Err(Error::CorruptObject("tag missing object header".to_owned()));
1184 };
1185 let oid_text = line.trim_start_matches("object ").trim();
1186 oid_text.parse::<ObjectId>()
1187}
1188
1189fn resolve_commit_message_search_from(
1192 repo: &Repository,
1193 start: ObjectId,
1194 pattern: &str,
1195) -> Result<ObjectId> {
1196 let regex = Regex::new(pattern).ok();
1198 let mut visited = std::collections::HashSet::new();
1199 let mut queue = std::collections::VecDeque::new();
1200 queue.push_back(start);
1201 visited.insert(start);
1202
1203 while let Some(oid) = queue.pop_front() {
1204 let obj = match repo.odb.read(&oid) {
1205 Ok(o) => o,
1206 Err(_) => continue,
1207 };
1208 if obj.kind != ObjectKind::Commit {
1209 continue;
1210 }
1211 let commit = match parse_commit(&obj.data) {
1212 Ok(c) => c,
1213 Err(_) => continue,
1214 };
1215
1216 let is_match = if let Some(re) = ®ex {
1217 re.is_match(&commit.message)
1218 } else {
1219 commit.message.contains(pattern)
1220 };
1221 if is_match {
1222 return Ok(oid);
1223 }
1224
1225 for parent in &commit.parents {
1226 if visited.insert(*parent) {
1227 queue.push_back(*parent);
1228 }
1229 }
1230 }
1231
1232 Err(Error::ObjectNotFound(format!(":/{pattern}")))
1233}
1234
1235fn find_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
1236 if !is_hex_prefix(prefix) || !(4..=40).contains(&prefix.len()) {
1237 return Ok(Vec::new());
1238 }
1239 let all = collect_loose_object_ids(repo)?;
1240 let mut matches = Vec::new();
1241 for candidate in all {
1242 if candidate.starts_with(prefix) {
1243 matches.push(candidate.parse::<ObjectId>()?);
1244 }
1245 }
1246 Ok(matches)
1247}
1248
1249fn collect_loose_object_ids(repo: &Repository) -> Result<Vec<String>> {
1250 let mut ids = Vec::new();
1251 let objects_dir = repo.odb.objects_dir();
1252 let read = match fs::read_dir(objects_dir) {
1253 Ok(read) => read,
1254 Err(err) => return Err(Error::Io(err)),
1255 };
1256
1257 for dir_entry in read {
1258 let dir_entry = dir_entry?;
1259 let name = dir_entry.file_name();
1260 let Some(prefix) = name.to_str() else {
1261 continue;
1262 };
1263 if !is_two_hex(prefix) {
1264 continue;
1265 }
1266 if !dir_entry.file_type()?.is_dir() {
1267 continue;
1268 }
1269
1270 let files = fs::read_dir(dir_entry.path())?;
1271 for file_entry in files {
1272 let file_entry = file_entry?;
1273 if !file_entry.file_type()?.is_file() {
1274 continue;
1275 }
1276 let file_name = file_entry.file_name();
1277 let Some(suffix) = file_name.to_str() else {
1278 continue;
1279 };
1280 if suffix.len() == 38 && suffix.chars().all(|ch| ch.is_ascii_hexdigit()) {
1281 ids.push(format!("{prefix}{suffix}"));
1282 }
1283 }
1284 }
1285
1286 Ok(ids)
1287}
1288
1289fn is_two_hex(text: &str) -> bool {
1290 text.len() == 2 && text.chars().all(|ch| ch.is_ascii_hexdigit())
1291}
1292
1293fn is_hex_prefix(text: &str) -> bool {
1294 !text.is_empty() && text.chars().all(|ch| ch.is_ascii_hexdigit())
1295}
1296
1297fn path_is_within(path: &Path, container: &Path) -> bool {
1298 if path == container {
1299 return true;
1300 }
1301 path.starts_with(container)
1302}
1303
1304fn normalize_components(path: &Path) -> Vec<String> {
1305 path.components()
1306 .filter_map(|component| match component {
1307 Component::RootDir => Some(String::from("/")),
1308 Component::Normal(item) => Some(item.to_string_lossy().into_owned()),
1309 _ => None,
1310 })
1311 .collect()
1312}
1313
1314fn component_to_text(component: Component<'_>) -> Option<String> {
1315 match component {
1316 Component::Normal(item) => Some(os_to_string(item)),
1317 _ => None,
1318 }
1319}
1320
1321fn os_to_string(text: &OsStr) -> String {
1322 text.to_string_lossy().into_owned()
1323}
1324
1325fn resolve_commit_message_search(
1328 repo: &crate::repo::Repository,
1329 pattern: &str,
1330) -> Result<ObjectId> {
1331 let (negate, effective_pattern) = if pattern.starts_with('!') {
1333 if pattern.starts_with("!!") {
1334 (false, &pattern[1..]) } else {
1336 (true, &pattern[1..]) }
1338 } else {
1339 (false, pattern)
1340 };
1341 let regex = Regex::new(effective_pattern).ok();
1342 use crate::state::resolve_head;
1343 let head =
1344 resolve_head(&repo.git_dir).map_err(|_| Error::ObjectNotFound(format!(":/{pattern}")))?;
1345 let start_oid = match head.oid() {
1346 Some(oid) => *oid,
1347 None => return Err(Error::ObjectNotFound(format!(":/{pattern}"))),
1348 };
1349
1350 let mut visited = std::collections::HashSet::new();
1351 let mut queue = std::collections::VecDeque::new();
1352 queue.push_back(start_oid);
1353 visited.insert(start_oid);
1354
1355 while let Some(oid) = queue.pop_front() {
1356 let obj = match repo.odb.read(&oid) {
1357 Ok(o) => o,
1358 Err(_) => continue,
1359 };
1360 if obj.kind != ObjectKind::Commit {
1362 continue;
1363 }
1364 let commit = match parse_commit(&obj.data) {
1365 Ok(c) => c,
1366 Err(_) => continue,
1367 };
1368
1369 let base_match = if let Some(re) = ®ex {
1371 re.is_match(&commit.message)
1372 } else {
1373 commit.message.contains(effective_pattern)
1374 };
1375 let is_match = if negate { !base_match } else { base_match };
1376 if is_match {
1377 return Ok(oid);
1378 }
1379
1380 for parent in &commit.parents {
1382 if visited.insert(*parent) {
1383 queue.push_back(*parent);
1384 }
1385 }
1386 }
1387
1388 Err(Error::ObjectNotFound(format!(":/{pattern}")))
1389}
1390
1391pub fn list_loose_abbrev_matches(repo: &Repository, prefix: &str) -> Result<Vec<ObjectId>> {
1393 find_abbrev_matches(repo, prefix)
1394}