1use std::cmp::Ordering;
29use std::collections::BTreeMap;
30use std::ops::ControlFlow;
31
32use thiserror::Error;
33
34use crate::error::Error;
35use crate::ident::committer_unix_seconds_for_ordering_bytes;
36use crate::objects::{ObjectId, ObjectKind};
37use crate::odb::Odb;
38use crate::refs::store::{RefEntry, RefStore};
39use crate::refs::version_sort::compare_refname_versions;
40use crate::refs::{query_reachability, PackedRefPeels};
41use crate::repo::Repository;
42use crate::wildmatch::{wildmatch, WM_CASEFOLD};
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum RefNamespace {
47 Heads,
49 Remotes,
51 Tags,
53 Custom(String),
55}
56
57impl RefNamespace {
58 fn prefix(&self) -> &str {
59 match self {
60 Self::Heads => "refs/heads/",
61 Self::Remotes => "refs/remotes/",
62 Self::Tags => "refs/tags/",
63 Self::Custom(p) => p.as_str(),
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
70pub enum SortKey {
71 #[default]
73 Name,
74 Version,
76 Date,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct RefSort {
83 pub key: SortKey,
85 pub reverse: bool,
87}
88
89impl Default for RefSort {
90 fn default() -> Self {
91 Self {
92 key: SortKey::Name,
93 reverse: false,
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Default)]
100pub struct RefQuery {
101 pub namespaces: Vec<RefNamespace>,
103 pub patterns: Vec<String>,
105 pub sort: RefSort,
107 pub limit: Option<usize>,
109 pub contains: Vec<ObjectId>,
111 pub no_contains: Vec<ObjectId>,
113 pub merged: Vec<ObjectId>,
115 pub no_merged: Vec<ObjectId>,
117 pub points_at: Vec<ObjectId>,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum RefKind {
124 Head,
126 Remote,
128 Tag,
130 Other,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct RefListing {
138 pub full_name: String,
140 pub short_name: String,
142 pub oid: ObjectId,
144 pub peeled: Option<ObjectId>,
146 pub kind: RefKind,
148}
149
150#[derive(Debug, Clone, Default)]
152pub struct RefQueryOptions {
153 pub versionsort_suffixes: Vec<String>,
155 pub ignore_case: bool,
157}
158
159#[derive(Debug, Error)]
161pub enum RefQueryError {
162 #[error("ref store: {0}")]
164 Store(#[from] Error),
165 #[error("creator-date sort requires an object database")]
167 MissingObjectDatabase,
168 #[error("object {0} not found")]
170 MissingObject(ObjectId),
171 #[error("object {0} is not a commit")]
173 NotCommit(ObjectId),
174}
175
176type QueryResult<T> = std::result::Result<T, RefQueryError>;
177
178#[derive(Clone)]
179struct CollectedRef {
180 full_name: String,
181 short_name: String,
182 oid: ObjectId,
183 peeled: Option<ObjectId>,
184 kind: RefKind,
185 creator_date: i64,
186}
187
188#[must_use]
190pub fn ref_kind_from_name(full_name: &str) -> RefKind {
191 if full_name.starts_with("refs/heads/") {
192 RefKind::Head
193 } else if full_name.starts_with("refs/remotes/") {
194 RefKind::Remote
195 } else if full_name.starts_with("refs/tags/") {
196 RefKind::Tag
197 } else {
198 RefKind::Other
199 }
200}
201
202#[must_use]
204pub fn short_name_in_namespace(full_name: &str, namespace: &RefNamespace) -> String {
205 let prefix = namespace.prefix();
206 full_name
207 .strip_prefix(prefix)
208 .unwrap_or(full_name)
209 .to_owned()
210}
211
212#[must_use]
214pub fn pattern_matches_ls_remote_refname(refname: &str, pattern: &str) -> bool {
215 if pattern.is_empty() {
216 return false;
217 }
218 let path = format!("/{refname}");
219 let full = format!("*/{pattern}");
220 ls_remote_glob_match(&full, &path)
221}
222
223#[must_use]
228pub fn patterns_match_ls_remote(refname: &str, patterns: &[String]) -> bool {
229 if patterns.is_empty() {
230 return true;
231 }
232 patterns
233 .iter()
234 .any(|p| pattern_matches_ls_remote_refname(refname, p))
235}
236
237fn ls_remote_glob_match(pattern: &str, text: &str) -> bool {
238 let pat: Vec<char> = pattern.chars().collect();
239 let txt: Vec<char> = text.chars().collect();
240 let (mut pi, mut ti) = (0, 0);
241 let (mut star_pi, mut star_ti) = (usize::MAX, 0);
242 while ti < txt.len() {
243 if pi < pat.len() && (pat[pi] == '?' || pat[pi] == txt[ti]) {
244 pi += 1;
245 ti += 1;
246 } else if pi < pat.len() && pat[pi] == '*' {
247 star_pi = pi;
248 star_ti = ti;
249 pi += 1;
250 } else if star_pi != usize::MAX {
251 pi = star_pi + 1;
252 star_ti += 1;
253 ti = star_ti;
254 } else {
255 return false;
256 }
257 }
258 while pi < pat.len() && pat[pi] == '*' {
259 pi += 1;
260 }
261 pi == pat.len()
262}
263
264#[must_use]
266pub fn pattern_matches_short_name(short_name: &str, pattern: &str, ignore_case: bool) -> bool {
267 if pattern.is_empty() {
268 return true;
269 }
270 let mut flags = 0u32;
271 if ignore_case {
272 flags |= WM_CASEFOLD;
273 }
274
275 if pattern.ends_with('/') {
276 let base = pattern.trim_end_matches('/');
277 if ascii_eq(short_name, base, ignore_case)
278 || ascii_starts_with(short_name, pattern, ignore_case)
279 {
280 return true;
281 }
282 }
283
284 if wildmatch(pattern.as_bytes(), short_name.as_bytes(), flags) {
285 return true;
286 }
287
288 if !pattern.contains(['*', '?', '[', '\\']) {
290 if ascii_eq(short_name, pattern, ignore_case) {
291 return true;
292 }
293 if ascii_starts_with_path_segment(short_name, pattern, ignore_case) {
294 return true;
295 }
296 }
297 false
298}
299
300fn ascii_eq(left: &str, right: &str, ignore_case: bool) -> bool {
301 ascii_eq_bytes(left.as_bytes(), right.as_bytes(), ignore_case)
302}
303
304fn ascii_eq_bytes(left: &[u8], right: &[u8], ignore_case: bool) -> bool {
305 if !ignore_case {
306 return left == right;
307 }
308 left.len() == right.len()
309 && left
310 .iter()
311 .zip(right.iter())
312 .all(|(a, b)| a.eq_ignore_ascii_case(b))
313}
314
315fn ascii_starts_with(name: &str, prefix: &str, ignore_case: bool) -> bool {
316 ascii_starts_with_bytes(name.as_bytes(), prefix.as_bytes(), ignore_case)
317}
318
319fn ascii_starts_with_bytes(name: &[u8], prefix: &[u8], ignore_case: bool) -> bool {
320 if name.len() < prefix.len() {
321 return false;
322 }
323 if !ignore_case {
324 return name.starts_with(prefix);
325 }
326 name[..prefix.len()]
327 .iter()
328 .zip(prefix.iter())
329 .all(|(a, b)| a.eq_ignore_ascii_case(b))
330}
331
332fn ascii_starts_with_path_segment(name: &str, pattern: &str, ignore_case: bool) -> bool {
333 let name = name.as_bytes();
334 let pattern = pattern.as_bytes();
335 if name.len() < pattern.len() + 1 || name[pattern.len()] != b'/' {
336 return false;
337 }
338 ascii_starts_with_bytes(name, pattern, ignore_case)
339}
340
341#[must_use]
345pub fn compare_version_names(a: &str, b: &str, versionsort_suffixes: &[String]) -> Ordering {
346 compare_refname_versions(a, b, versionsort_suffixes)
347}
348
349#[must_use]
351pub fn epoch_from_identity(ident: &str) -> i64 {
352 let parts: Vec<&str> = ident.rsplitn(3, ' ').collect();
353 if parts.len() >= 2 {
354 parts[1].parse().unwrap_or(0)
355 } else {
356 0
357 }
358}
359
360#[must_use]
365pub fn creator_date_from_bytes(kind: ObjectKind, data: &[u8]) -> i64 {
366 let header = object_header_bytes(data);
367 match kind {
368 ObjectKind::Tag => {
369 if let Some(line) = find_header_value(header, b"tagger") {
370 committer_unix_seconds_for_ordering_bytes(line)
371 } else {
372 0
373 }
374 }
375 ObjectKind::Commit => {
376 if let Some(line) = find_header_value(header, b"committer") {
377 committer_unix_seconds_for_ordering_bytes(line)
378 } else {
379 0
380 }
381 }
382 _ => 0,
383 }
384}
385
386pub fn creator_date(odb: &Odb, oid: &ObjectId) -> crate::error::Result<i64> {
392 let obj = odb.read(oid)?;
393 Ok(creator_date_from_bytes(obj.kind, &obj.data))
394}
395
396pub fn query_listings_from_pairs<I>(
406 entries: I,
407 query: &RefQuery,
408 options: &RefQueryOptions,
409 odb: Option<&Odb>,
410) -> QueryResult<Vec<RefListing>>
411where
412 I: IntoIterator<Item = (String, ObjectId, Option<ObjectId>)>,
413{
414 let namespaces = effective_namespaces(query);
415 let mut collected = BTreeMap::new();
416 for (full_name, oid, peeled) in entries {
417 if !namespaces
418 .iter()
419 .any(|ns| full_name.starts_with(ns.prefix()))
420 {
421 continue;
422 }
423 let short_name = short_name_for_query(&full_name, &namespaces);
424 if !patterns_match(&short_name, &query.patterns, options.ignore_case) {
425 continue;
426 }
427 let kind = ref_kind_from_name(&full_name);
428 collected.entry(full_name.clone()).or_insert(CollectedRef {
429 full_name,
430 short_name,
431 oid,
432 peeled,
433 kind,
434 creator_date: 0,
435 });
436 }
437 finish_query(
438 collected.into_values().collect(),
439 query,
440 options,
441 odb,
442 None,
443 PeelTips::Always,
444 )
445}
446
447pub fn query_listings_on_store(
453 store: &dyn RefStore,
454 query: &RefQuery,
455 options: &RefQueryOptions,
456 odb: Option<&Odb>,
457) -> QueryResult<Vec<RefListing>> {
458 let namespaces = effective_namespaces(query);
459 let mut collected = Vec::new();
460 for ns in &namespaces {
461 let prefix = ns.prefix();
462 store.for_each_ref(prefix, &mut |entry: &RefEntry| {
463 insert_collected_ref(&mut collected, entry, store, &namespaces, query, options);
464 ControlFlow::Continue(())
465 })?;
466 }
467 finish_query(
468 dedup_collected(collected),
469 query,
470 options,
471 odb,
472 None,
473 PeelTips::Always,
474 )
475}
476
477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
479pub(crate) enum PeelTips {
480 Always,
482 WhenFiltering,
485}
486
487pub fn query_refs(
498 repo: &Repository,
499 query: &RefQuery,
500 options: &RefQueryOptions,
501) -> QueryResult<Vec<RefListing>> {
502 query_refs_peeling(repo, query, options, PeelTips::Always)
503}
504
505pub(crate) fn query_refs_peeling(
512 repo: &Repository,
513 query: &RefQuery,
514 options: &RefQueryOptions,
515 peel: PeelTips,
516) -> QueryResult<Vec<RefListing>> {
517 let git_dir = repo.git_dir();
518 let store = repo.refs();
519 let namespaces = effective_namespaces(query);
520 let linked_worktree = crate::worktree_ref::is_linked_worktree_git_dir(git_dir);
521 let mut collected = Vec::new();
522 for ns in &namespaces {
523 let prefix = ns.prefix();
524 store.for_each_ref(prefix, &mut |entry: &RefEntry| {
525 if linked_worktree
526 && !crate::worktree_ref::ref_visible_from_worktree(git_dir, &entry.name)
527 {
528 return ControlFlow::Continue(());
529 }
530 insert_collected_ref(&mut collected, entry, store, &namespaces, query, options);
531 ControlFlow::Continue(())
532 })?;
533 }
534 finish_query(
535 dedup_collected(collected),
536 query,
537 options,
538 Some(repo.odb()),
539 Some(repo),
540 peel,
541 )
542}
543
544fn dedup_collected(mut collected: Vec<CollectedRef>) -> Vec<CollectedRef> {
548 collected.sort_by(|a, b| a.full_name.cmp(&b.full_name));
549 collected.dedup_by(|later, first| later.full_name == first.full_name);
550 collected
551}
552
553fn insert_collected_ref(
554 collected: &mut Vec<CollectedRef>,
555 entry: &RefEntry,
556 store: &dyn RefStore,
557 namespaces: &[RefNamespace],
558 query: &RefQuery,
559 options: &RefQueryOptions,
560) {
561 let resolved = entry
562 .peeled
563 .or_else(|| match &entry.value {
564 crate::refs::store::RawRef::Direct(oid) => Some(*oid),
565 crate::refs::store::RawRef::Symbolic(target) => store.resolve(target).ok(),
566 })
567 .or_else(|| store.resolve(&entry.name).ok());
568 let Some(resolved) = resolved else {
569 return;
570 };
571 let short_name = short_name_for_query(&entry.name, namespaces);
572 if !patterns_match(&short_name, &query.patterns, options.ignore_case) {
573 return;
574 }
575 let direct_oid = match &entry.value {
576 crate::refs::store::RawRef::Direct(oid) => *oid,
577 crate::refs::store::RawRef::Symbolic(_) => resolved,
578 };
579 let peeled = match &entry.value {
580 crate::refs::store::RawRef::Direct(_) => entry.peeled,
581 crate::refs::store::RawRef::Symbolic(_) => None,
582 };
583 let kind = ref_kind_from_name(&entry.name);
584 collected.push(CollectedRef {
585 full_name: entry.name.clone(),
586 short_name,
587 oid: direct_oid,
588 peeled,
589 kind,
590 creator_date: 0,
591 });
592}
593
594fn effective_namespaces(query: &RefQuery) -> Vec<RefNamespace> {
595 if query.namespaces.is_empty() {
596 vec![RefNamespace::Heads]
597 } else {
598 query.namespaces.clone()
599 }
600}
601
602fn short_name_for_query(full_name: &str, namespaces: &[RefNamespace]) -> String {
603 let mut best = 0usize;
604 for ns in namespaces {
605 let prefix = ns.prefix();
606 if full_name.starts_with(prefix) && prefix.len() > best {
607 best = prefix.len();
608 }
609 }
610 if best > 0 {
611 full_name[best..].to_owned()
612 } else {
613 short_name_for_kind(full_name, ref_kind_from_name(full_name))
614 }
615}
616
617fn short_name_for_kind(full_name: &str, kind: RefKind) -> String {
618 match kind {
619 RefKind::Head => full_name.strip_prefix("refs/heads/").unwrap_or(full_name),
620 RefKind::Remote => full_name.strip_prefix("refs/remotes/").unwrap_or(full_name),
621 RefKind::Tag => full_name.strip_prefix("refs/tags/").unwrap_or(full_name),
622 RefKind::Other => full_name.strip_prefix("refs/").unwrap_or(full_name),
623 }
624 .to_owned()
625}
626
627fn patterns_match(short_name: &str, patterns: &[String], ignore_case: bool) -> bool {
628 if patterns.is_empty() {
629 return true;
630 }
631 patterns
632 .iter()
633 .any(|p| pattern_matches_short_name(short_name, p, ignore_case))
634}
635
636fn query_has_reachability(query: &RefQuery) -> bool {
637 !query.contains.is_empty()
638 || !query.no_contains.is_empty()
639 || !query.merged.is_empty()
640 || !query.no_merged.is_empty()
641 || !query.points_at.is_empty()
642}
643
644fn finish_query(
645 mut collected: Vec<CollectedRef>,
646 query: &RefQuery,
647 options: &RefQueryOptions,
648 odb: Option<&Odb>,
649 repo: Option<&Repository>,
650 peel: PeelTips,
651) -> QueryResult<Vec<RefListing>> {
652 let bounded_contains = query_reachability::can_bound_contains(query);
653 let peel = (peel == PeelTips::Always || query_has_reachability(query)) && !bounded_contains;
654 if let Some(odb) = odb.filter(|_| peel) {
655 let to_peel = collected.iter().filter(|c| c.peeled.is_none()).count();
657 let packed = repo
658 .filter(|_| to_peel >= PACKED_PEELS_MIN_REFS)
659 .and_then(|r| PackedRefPeels::for_store(r.refs(), r.git_dir()).ok());
660 for entry in &mut collected {
661 fill_peeled(odb, packed.as_ref(), entry);
662 }
663 }
664 if query_has_reachability(query) {
665 let repo = repo.ok_or(RefQueryError::MissingObjectDatabase)?;
666 let mut reach_rows: Vec<(String, ObjectId, Option<ObjectId>)> = collected
667 .iter()
668 .map(|c| (c.full_name.clone(), c.oid, c.peeled))
669 .collect();
670 query_reachability::filter_listings_by_reachability(repo, query, &mut reach_rows)?;
671 let keep: std::collections::HashSet<String> =
672 reach_rows.into_iter().map(|(name, _, _)| name).collect();
673 collected.retain(|c| keep.contains(&c.full_name));
674 if bounded_contains {
675 for entry in &mut collected {
676 fill_peeled(repo.odb(), None, entry);
677 }
678 }
679 }
680 if matches!(query.sort.key, SortKey::Date) {
681 let odb = odb.ok_or(RefQueryError::MissingObjectDatabase)?;
682 for entry in &mut collected {
683 entry.creator_date = creator_date(odb, &entry.oid).unwrap_or(0);
684 }
685 }
686 let suffixes = &options.versionsort_suffixes;
687 collected.sort_by(|a, b| {
688 let primary = compare_entries(a, b, query.sort.key, suffixes);
689 let primary = if query.sort.reverse {
690 primary.reverse()
691 } else {
692 primary
693 };
694 if primary != Ordering::Equal {
695 return primary;
696 }
697 a.full_name.cmp(&b.full_name)
698 });
699 if let Some(limit) = query.limit {
700 collected.truncate(limit);
701 }
702 Ok(collected
703 .into_iter()
704 .map(|c| RefListing {
705 full_name: c.full_name,
706 short_name: c.short_name,
707 oid: c.oid,
708 peeled: c.peeled,
709 kind: c.kind,
710 })
711 .collect())
712}
713
714pub(crate) const PACKED_PEELS_MIN_REFS: usize = 64;
719
720fn fill_peeled(odb: &Odb, packed: Option<&PackedRefPeels>, entry: &mut CollectedRef) {
723 if entry.peeled.is_some() {
724 return;
725 }
726 if let Some(known) = packed.and_then(|p| p.peeled(&entry.full_name, &entry.oid)) {
727 entry.peeled = known;
728 return;
729 }
730 entry.peeled = odb.peel_tag_target(entry.oid).ok().flatten();
731}
732
733fn compare_entries(
734 a: &CollectedRef,
735 b: &CollectedRef,
736 key: SortKey,
737 suffixes: &[String],
738) -> Ordering {
739 match key {
740 SortKey::Name => a.full_name.cmp(&b.full_name),
741 SortKey::Version => compare_version_names(&a.full_name, &b.full_name, suffixes),
742 SortKey::Date => a.creator_date.cmp(&b.creator_date),
743 }
744}
745
746fn object_header_bytes(data: &[u8]) -> &[u8] {
747 if let Some(pos) = data.windows(2).position(|w| w == b"\n\n") {
748 &data[..pos]
749 } else {
750 data
751 }
752}
753
754fn find_header_value<'a>(header: &'a [u8], name: &[u8]) -> Option<&'a [u8]> {
755 let mut idx = 0;
756 while idx < header.len() {
757 let line_end = header[idx..]
758 .iter()
759 .position(|&b| b == b'\n')
760 .map(|p| idx + p)
761 .unwrap_or(header.len());
762 let line = &header[idx..line_end];
763 if line.is_empty() {
764 break;
765 }
766 if line.len() > name.len() + 1 && line.starts_with(name) && line[name.len()] == b' ' {
767 return Some(&line[name.len() + 1..]);
768 }
769 idx = line_end.saturating_add(1);
770 }
771 None
772}
773
774pub fn ref_query_options_for_repo(repo: &Repository) -> crate::error::Result<RefQueryOptions> {
780 let config = repo.config()?;
781 let versionsort_suffixes = config.get_all("versionsort.suffix");
782 let ignore_case = config
783 .get_bool("core.ignorecase")
784 .and_then(|r| r.ok())
785 .unwrap_or(false);
786 Ok(RefQueryOptions {
787 versionsort_suffixes,
788 ignore_case,
789 })
790}
791
792#[cfg(test)]
793mod tests {
794 #![allow(clippy::unwrap_used)]
795
796 use super::*;
797
798 #[test]
799 fn version_order_matches_git_tag_examples() {
800 assert_eq!(
801 compare_version_names("refs/tags/foo1.3", "refs/tags/foo1.10", &[]),
802 Ordering::Less
803 );
804 assert_eq!(
805 compare_version_names("refs/tags/foo1.10", "refs/tags/foo1.6", &[]),
806 Ordering::Greater
807 );
808 }
809
810 #[test]
811 fn pattern_prefix_slash_and_glob() {
812 assert!(pattern_matches_short_name("feature/x", "feature/", false));
813 assert!(pattern_matches_short_name("v1.9", "v1.*", false));
814 assert!(pattern_matches_short_name("release-rc1", "*rc*", false));
815 assert!(!pattern_matches_short_name("other", "feature/", false));
816 }
817
818 #[test]
819 fn versionsort_suffix_orders_prerelease() {
820 let suffixes = vec!["-rc".to_owned()];
821 assert_eq!(
822 compare_version_names("v1.0", "v1.0-rc1", &suffixes),
823 Ordering::Greater
824 );
825 }
826
827 #[test]
828 fn utf8_names_with_suffix_config_do_not_panic_in_compare() {
829 let suffixes = vec!["x".to_owned()];
830 let _ = compare_version_names("refs/tags/vα1", "refs/tags/vα2", &suffixes);
831 }
832
833 #[test]
834 fn ignore_case_prefix_directory_match() {
835 assert!(pattern_matches_short_name("Feature/x", "feature/", true));
836 }
837
838 #[test]
839 fn ignore_case_prefix_does_not_panic_on_utf8_short_names() {
840 assert!(!pattern_matches_short_name("€x", "a", true));
841 }
842
843 #[test]
844 fn ls_remote_empty_string_pattern_matches_nothing() {
845 assert!(!pattern_matches_ls_remote_refname("refs/heads/main", ""));
846 assert!(!patterns_match_ls_remote(
847 "refs/heads/main",
848 &[String::new()]
849 ));
850 assert!(patterns_match_ls_remote("refs/heads/main", &[]));
851 }
852
853 #[test]
854 fn ls_remote_pattern_tail_component() {
855 assert!(pattern_matches_ls_remote_refname("refs/heads/main", "main"));
856 assert!(!pattern_matches_ls_remote_refname(
857 "refs/heads/topic",
858 "main"
859 ));
860 assert!(pattern_matches_ls_remote_refname(
861 "refs/tags/v1^{}",
862 "refs/tags/v1*"
863 ));
864 assert!(!pattern_matches_ls_remote_refname("refs/tags/v1^{}", "v1"));
865 }
866
867 #[test]
868 fn peel_tips_skips_peeling_only_without_filters() {
869 use crate::objects::{serialize_commit, serialize_tag, CommitData, ObjectKind, TagData};
870
871 let dir = tempfile::tempdir().unwrap();
872 let repo = crate::repo::init_repository(
873 dir.path(),
874 false,
875 "main",
876 None,
877 crate::RefStorageFormat::Files,
878 )
879 .unwrap();
880 let tree = repo.odb().write(ObjectKind::Tree, b"").unwrap();
881 let commit = repo
882 .odb()
883 .write(
884 ObjectKind::Commit,
885 &serialize_commit(&CommitData {
886 tree,
887 parents: Vec::new(),
888 author: "T <t@example.com> 1 +0000".into(),
889 committer: "T <t@example.com> 1 +0000".into(),
890 author_raw: Vec::new(),
891 committer_raw: Vec::new(),
892 encoding: None,
893 message: "m\n".into(),
894 raw_message: None,
895 extra_headers: Vec::new(),
896 }),
897 )
898 .unwrap();
899 let tag = repo
900 .odb()
901 .write(
902 ObjectKind::Tag,
903 &serialize_tag(&TagData {
904 object: commit,
905 object_type: "commit".into(),
906 tag: "t".into(),
907 tagger: Some("T <t@example.com> 1 +0000".into()),
908 extra_headers: Vec::new(),
909 message: "t\n".into(),
910 }),
911 )
912 .unwrap();
913 crate::refs::write_ref(repo.git_dir(), "refs/heads/main", &commit).unwrap();
914 crate::refs::write_ref(repo.git_dir(), "refs/heads/tagged", &tag).unwrap();
915
916 let options = RefQueryOptions::default();
917 let peeled = |query: &RefQuery, peel: PeelTips| -> Vec<(String, Option<ObjectId>)> {
918 query_refs_peeling(&repo, query, &options, peel)
919 .unwrap()
920 .into_iter()
921 .map(|l| (l.short_name, l.peeled))
922 .collect()
923 };
924 let all = RefQuery::default();
925 let both_peeled = vec![
926 ("main".to_owned(), None),
927 ("tagged".to_owned(), Some(commit)),
928 ];
929 assert_eq!(peeled(&all, PeelTips::Always), both_peeled);
930 assert_eq!(
931 query_refs(&repo, &all, &options)
932 .unwrap()
933 .into_iter()
934 .map(|l| (l.short_name, l.peeled))
935 .collect::<Vec<_>>(),
936 both_peeled
937 );
938 assert_eq!(
939 peeled(&all, PeelTips::WhenFiltering),
940 vec![("main".to_owned(), None), ("tagged".to_owned(), None)]
941 );
942
943 let points_at = RefQuery {
944 points_at: vec![commit],
945 ..RefQuery::default()
946 };
947 assert_eq!(peeled(&points_at, PeelTips::WhenFiltering), both_peeled);
948 let contains = RefQuery {
949 contains: vec![commit],
950 ..RefQuery::default()
951 };
952 assert_eq!(peeled(&contains, PeelTips::WhenFiltering), both_peeled);
953 }
954}