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::query_reachability::{self, ReachRow};
39use crate::refs::store::{RefEntry, RefStore};
40use crate::refs::version_sort::compare_refname_versions;
41use crate::refs::PackedRefPeels;
42use crate::repo::Repository;
43use crate::wildmatch::{wildmatch, WM_CASEFOLD};
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum RefNamespace {
48 Heads,
50 Remotes,
52 Tags,
54 Custom(String),
56}
57
58impl RefNamespace {
59 fn prefix(&self) -> &str {
60 match self {
61 Self::Heads => "refs/heads/",
62 Self::Remotes => "refs/remotes/",
63 Self::Tags => "refs/tags/",
64 Self::Custom(p) => p.as_str(),
65 }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
71pub enum SortKey {
72 #[default]
74 Name,
75 Version,
77 Date,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct RefSort {
84 pub key: SortKey,
86 pub reverse: bool,
88}
89
90impl Default for RefSort {
91 fn default() -> Self {
92 Self {
93 key: SortKey::Name,
94 reverse: false,
95 }
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Default)]
101pub struct RefQuery {
102 pub namespaces: Vec<RefNamespace>,
104 pub patterns: Vec<String>,
106 pub sort: RefSort,
108 pub limit: Option<usize>,
110 pub contains: Vec<ObjectId>,
112 pub no_contains: Vec<ObjectId>,
114 pub merged: Vec<ObjectId>,
116 pub no_merged: Vec<ObjectId>,
118 pub points_at: Vec<ObjectId>,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum RefKind {
125 Head,
127 Remote,
129 Tag,
131 Other,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct RefListing {
139 pub full_name: String,
141 pub short_name: String,
143 pub oid: ObjectId,
145 pub peeled: Option<ObjectId>,
147 pub kind: RefKind,
149}
150
151#[derive(Debug, Clone, Default)]
153pub struct RefQueryOptions {
154 pub versionsort_suffixes: Vec<String>,
156 pub ignore_case: bool,
158}
159
160#[derive(Debug, Error)]
162pub enum RefQueryError {
163 #[error("ref store: {0}")]
165 Store(#[from] Error),
166 #[error("creator-date sort requires an object database")]
168 MissingObjectDatabase,
169 #[error("object {0} not found")]
171 MissingObject(ObjectId),
172 #[error("object {0} is not a commit")]
174 NotCommit(ObjectId),
175}
176
177type QueryResult<T> = std::result::Result<T, RefQueryError>;
178
179#[derive(Clone)]
180struct CollectedRef {
181 full_name: String,
182 short_name: String,
183 oid: ObjectId,
184 peeled: Option<ObjectId>,
185 kind: RefKind,
186 creator_date: i64,
187}
188
189#[must_use]
191pub fn ref_kind_from_name(full_name: &str) -> RefKind {
192 if full_name.starts_with("refs/heads/") {
193 RefKind::Head
194 } else if full_name.starts_with("refs/remotes/") {
195 RefKind::Remote
196 } else if full_name.starts_with("refs/tags/") {
197 RefKind::Tag
198 } else {
199 RefKind::Other
200 }
201}
202
203#[must_use]
205pub fn short_name_in_namespace(full_name: &str, namespace: &RefNamespace) -> String {
206 let prefix = namespace.prefix();
207 full_name
208 .strip_prefix(prefix)
209 .unwrap_or(full_name)
210 .to_owned()
211}
212
213#[must_use]
215pub fn pattern_matches_ls_remote_refname(refname: &str, pattern: &str) -> bool {
216 if pattern.is_empty() {
217 return false;
218 }
219 let path = format!("/{refname}");
220 let full = format!("*/{pattern}");
221 ls_remote_glob_match(&full, &path)
222}
223
224#[must_use]
229pub fn patterns_match_ls_remote(refname: &str, patterns: &[String]) -> bool {
230 if patterns.is_empty() {
231 return true;
232 }
233 patterns
234 .iter()
235 .any(|p| pattern_matches_ls_remote_refname(refname, p))
236}
237
238fn ls_remote_glob_match(pattern: &str, text: &str) -> bool {
239 let pat: Vec<char> = pattern.chars().collect();
240 let txt: Vec<char> = text.chars().collect();
241 let (mut pi, mut ti) = (0, 0);
242 let (mut star_pi, mut star_ti) = (usize::MAX, 0);
243 while ti < txt.len() {
244 if pi < pat.len() && (pat[pi] == '?' || pat[pi] == txt[ti]) {
245 pi += 1;
246 ti += 1;
247 } else if pi < pat.len() && pat[pi] == '*' {
248 star_pi = pi;
249 star_ti = ti;
250 pi += 1;
251 } else if star_pi != usize::MAX {
252 pi = star_pi + 1;
253 star_ti += 1;
254 ti = star_ti;
255 } else {
256 return false;
257 }
258 }
259 while pi < pat.len() && pat[pi] == '*' {
260 pi += 1;
261 }
262 pi == pat.len()
263}
264
265#[must_use]
267pub fn pattern_matches_short_name(short_name: &str, pattern: &str, ignore_case: bool) -> bool {
268 if pattern.is_empty() {
269 return true;
270 }
271 let mut flags = 0u32;
272 if ignore_case {
273 flags |= WM_CASEFOLD;
274 }
275
276 if pattern.ends_with('/') {
277 let base = pattern.trim_end_matches('/');
278 if ascii_eq(short_name, base, ignore_case)
279 || ascii_starts_with(short_name, pattern, ignore_case)
280 {
281 return true;
282 }
283 }
284
285 if wildmatch(pattern.as_bytes(), short_name.as_bytes(), flags) {
286 return true;
287 }
288
289 if !pattern.contains(['*', '?', '[', '\\']) {
291 if ascii_eq(short_name, pattern, ignore_case) {
292 return true;
293 }
294 if ascii_starts_with_path_segment(short_name, pattern, ignore_case) {
295 return true;
296 }
297 }
298 false
299}
300
301fn ascii_eq(left: &str, right: &str, ignore_case: bool) -> bool {
302 ascii_eq_bytes(left.as_bytes(), right.as_bytes(), ignore_case)
303}
304
305fn ascii_eq_bytes(left: &[u8], right: &[u8], ignore_case: bool) -> bool {
306 if !ignore_case {
307 return left == right;
308 }
309 left.len() == right.len()
310 && left
311 .iter()
312 .zip(right.iter())
313 .all(|(a, b)| a.eq_ignore_ascii_case(b))
314}
315
316fn ascii_starts_with(name: &str, prefix: &str, ignore_case: bool) -> bool {
317 ascii_starts_with_bytes(name.as_bytes(), prefix.as_bytes(), ignore_case)
318}
319
320fn ascii_starts_with_bytes(name: &[u8], prefix: &[u8], ignore_case: bool) -> bool {
321 if name.len() < prefix.len() {
322 return false;
323 }
324 if !ignore_case {
325 return name.starts_with(prefix);
326 }
327 name[..prefix.len()]
328 .iter()
329 .zip(prefix.iter())
330 .all(|(a, b)| a.eq_ignore_ascii_case(b))
331}
332
333fn ascii_starts_with_path_segment(name: &str, pattern: &str, ignore_case: bool) -> bool {
334 let name = name.as_bytes();
335 let pattern = pattern.as_bytes();
336 if name.len() < pattern.len() + 1 || name[pattern.len()] != b'/' {
337 return false;
338 }
339 ascii_starts_with_bytes(name, pattern, ignore_case)
340}
341
342#[must_use]
346pub fn compare_version_names(a: &str, b: &str, versionsort_suffixes: &[String]) -> Ordering {
347 compare_refname_versions(a, b, versionsort_suffixes)
348}
349
350#[must_use]
352pub fn epoch_from_identity(ident: &str) -> i64 {
353 let parts: Vec<&str> = ident.rsplitn(3, ' ').collect();
354 if parts.len() >= 2 {
355 parts[1].parse().unwrap_or(0)
356 } else {
357 0
358 }
359}
360
361#[must_use]
366pub fn creator_date_from_bytes(kind: ObjectKind, data: &[u8]) -> i64 {
367 let header = object_header_bytes(data);
368 match kind {
369 ObjectKind::Tag => {
370 if let Some(line) = find_header_value(header, b"tagger") {
371 committer_unix_seconds_for_ordering_bytes(line)
372 } else {
373 0
374 }
375 }
376 ObjectKind::Commit => {
377 if let Some(line) = find_header_value(header, b"committer") {
378 committer_unix_seconds_for_ordering_bytes(line)
379 } else {
380 0
381 }
382 }
383 _ => 0,
384 }
385}
386
387pub fn creator_date(odb: &Odb, oid: &ObjectId) -> crate::error::Result<i64> {
393 let obj = odb.read(oid)?;
394 Ok(creator_date_from_bytes(obj.kind, &obj.data))
395}
396
397pub fn query_listings_from_pairs<I>(
407 entries: I,
408 query: &RefQuery,
409 options: &RefQueryOptions,
410 odb: Option<&Odb>,
411) -> QueryResult<Vec<RefListing>>
412where
413 I: IntoIterator<Item = (String, ObjectId, Option<ObjectId>)>,
414{
415 let namespaces = effective_namespaces(query);
416 let mut collected = BTreeMap::new();
417 for (full_name, oid, peeled) in entries {
418 if !namespaces
419 .iter()
420 .any(|ns| full_name.starts_with(ns.prefix()))
421 {
422 continue;
423 }
424 let short_name = short_name_for_query(&full_name, &namespaces);
425 if !patterns_match(&short_name, &query.patterns, options.ignore_case) {
426 continue;
427 }
428 let kind = ref_kind_from_name(&full_name);
429 collected.entry(full_name.clone()).or_insert(CollectedRef {
430 full_name,
431 short_name,
432 oid,
433 peeled,
434 kind,
435 creator_date: 0,
436 });
437 }
438 finish_query(
439 collected.into_values().collect(),
440 query,
441 options,
442 odb,
443 None,
444 PeelTips::Always,
445 )
446}
447
448pub fn query_listings_on_store(
454 store: &dyn RefStore,
455 query: &RefQuery,
456 options: &RefQueryOptions,
457 odb: Option<&Odb>,
458) -> QueryResult<Vec<RefListing>> {
459 let namespaces = effective_namespaces(query);
460 let mut collected = Vec::new();
461 for ns in &namespaces {
462 let prefix = ns.prefix();
463 store.for_each_ref(prefix, &mut |entry: &RefEntry| {
464 insert_collected_ref(&mut collected, entry, store, &namespaces, query, options);
465 ControlFlow::Continue(())
466 })?;
467 }
468 finish_query(
469 dedup_collected(collected),
470 query,
471 options,
472 odb,
473 None,
474 PeelTips::Always,
475 )
476}
477
478#[derive(Debug, Clone, Copy, PartialEq, Eq)]
480pub(crate) enum PeelTips {
481 Always,
483 WhenFiltering,
486}
487
488pub fn query_refs(
499 repo: &Repository,
500 query: &RefQuery,
501 options: &RefQueryOptions,
502) -> QueryResult<Vec<RefListing>> {
503 query_refs_peeling(repo, query, options, PeelTips::Always)
504}
505
506pub(crate) fn query_refs_peeling(
513 repo: &Repository,
514 query: &RefQuery,
515 options: &RefQueryOptions,
516 peel: PeelTips,
517) -> QueryResult<Vec<RefListing>> {
518 let git_dir = repo.git_dir();
519 let store = repo.refs();
520 let namespaces = effective_namespaces(query);
521 let linked_worktree = crate::worktree_ref::is_linked_worktree_git_dir(git_dir);
522 let early_limit = early_ref_collection_limit(query, options);
523 let mut collected = Vec::new();
524 for ns in &namespaces {
525 let prefix = ns.prefix();
526 store.for_each_ref(prefix, &mut |entry: &RefEntry| {
527 if linked_worktree
528 && !crate::worktree_ref::ref_visible_from_worktree(git_dir, &entry.name)
529 {
530 return ControlFlow::Continue(());
531 }
532 insert_collected_ref(&mut collected, entry, store, &namespaces, query, options);
533 if early_limit.is_some_and(|limit| collected.len() >= limit) {
534 return ControlFlow::Break(());
535 }
536 ControlFlow::Continue(())
537 })?;
538 if early_limit.is_some_and(|limit| collected.len() >= limit) {
539 break;
540 }
541 }
542 finish_query(
543 dedup_collected(collected),
544 query,
545 options,
546 Some(repo.odb()),
547 Some(repo),
548 peel,
549 )
550}
551
552fn dedup_collected(mut collected: Vec<CollectedRef>) -> Vec<CollectedRef> {
556 collected.sort_by(|a, b| a.full_name.cmp(&b.full_name));
557 collected.dedup_by(|later, first| later.full_name == first.full_name);
558 collected
559}
560
561fn insert_collected_ref(
562 collected: &mut Vec<CollectedRef>,
563 entry: &RefEntry,
564 store: &dyn RefStore,
565 namespaces: &[RefNamespace],
566 query: &RefQuery,
567 options: &RefQueryOptions,
568) {
569 let resolved = entry
570 .peeled
571 .or_else(|| match &entry.value {
572 crate::refs::store::RawRef::Direct(oid) => Some(*oid),
573 crate::refs::store::RawRef::Symbolic(target) => store.resolve(target).ok(),
574 })
575 .or_else(|| store.resolve(&entry.name).ok());
576 let Some(resolved) = resolved else {
577 return;
578 };
579 let short_name = short_name_for_query(&entry.name, namespaces);
580 if !patterns_match(&short_name, &query.patterns, options.ignore_case) {
581 return;
582 }
583 let direct_oid = match &entry.value {
584 crate::refs::store::RawRef::Direct(oid) => *oid,
585 crate::refs::store::RawRef::Symbolic(_) => resolved,
586 };
587 let peeled = match &entry.value {
588 crate::refs::store::RawRef::Direct(_) => entry.peeled,
589 crate::refs::store::RawRef::Symbolic(_) => None,
590 };
591 let kind = ref_kind_from_name(&entry.name);
592 collected.push(CollectedRef {
593 full_name: entry.name.clone(),
594 short_name,
595 oid: direct_oid,
596 peeled,
597 kind,
598 creator_date: 0,
599 });
600}
601
602fn effective_namespaces(query: &RefQuery) -> Vec<RefNamespace> {
603 if query.namespaces.is_empty() {
604 vec![RefNamespace::Heads]
605 } else {
606 query.namespaces.clone()
607 }
608}
609
610fn short_name_for_query(full_name: &str, namespaces: &[RefNamespace]) -> String {
611 let mut best = 0usize;
612 for ns in namespaces {
613 let prefix = ns.prefix();
614 if full_name.starts_with(prefix) && prefix.len() > best {
615 best = prefix.len();
616 }
617 }
618 if best > 0 {
619 full_name[best..].to_owned()
620 } else {
621 short_name_for_kind(full_name, ref_kind_from_name(full_name))
622 }
623}
624
625fn short_name_for_kind(full_name: &str, kind: RefKind) -> String {
626 match kind {
627 RefKind::Head => full_name.strip_prefix("refs/heads/").unwrap_or(full_name),
628 RefKind::Remote => full_name.strip_prefix("refs/remotes/").unwrap_or(full_name),
629 RefKind::Tag => full_name.strip_prefix("refs/tags/").unwrap_or(full_name),
630 RefKind::Other => full_name.strip_prefix("refs/").unwrap_or(full_name),
631 }
632 .to_owned()
633}
634
635fn patterns_match(short_name: &str, patterns: &[String], ignore_case: bool) -> bool {
636 if patterns.is_empty() {
637 return true;
638 }
639 patterns
640 .iter()
641 .any(|p| pattern_matches_short_name(short_name, p, ignore_case))
642}
643
644fn query_has_reachability(query: &RefQuery) -> bool {
645 !query.contains.is_empty()
646 || !query.no_contains.is_empty()
647 || !query.merged.is_empty()
648 || !query.no_merged.is_empty()
649 || !query.points_at.is_empty()
650}
651
652fn early_ref_collection_limit(query: &RefQuery, options: &RefQueryOptions) -> Option<usize> {
654 let limit = query.limit?;
655 if query.sort.key != SortKey::Name || query.sort.reverse {
656 return None;
657 }
658 if !query.patterns.is_empty() || options.ignore_case {
659 return None;
660 }
661 if query_has_reachability(query) {
662 return None;
663 }
664 if effective_namespaces(query).len() != 1 {
665 return None;
666 }
667 Some(limit)
668}
669
670fn finish_query(
671 mut collected: Vec<CollectedRef>,
672 query: &RefQuery,
673 options: &RefQueryOptions,
674 odb: Option<&Odb>,
675 repo: Option<&Repository>,
676 peel: PeelTips,
677) -> QueryResult<Vec<RefListing>> {
678 if query_has_reachability(query) {
679 let repo = repo.ok_or(RefQueryError::MissingObjectDatabase)?;
682 let packed = packed_peels_for(repo, &collected);
683 let mut rows: Vec<ReachRow> = collected
684 .iter()
685 .map(|c| ReachRow {
686 oid: c.oid,
687 peeled: c
688 .peeled
689 .map(Some)
690 .or_else(|| packed.as_ref().and_then(|p| p.peeled(&c.full_name, &c.oid))),
691 })
692 .collect();
693 let keep = query_reachability::filter_listings_by_reachability(repo, query, &mut rows)?;
694 let mut verdicts = keep.into_iter().zip(rows);
695 collected.retain_mut(|entry| {
696 let Some((keep, row)) = verdicts.next() else {
697 return false;
698 };
699 if keep {
700 match row.peeled {
701 Some(peeled) => entry.peeled = peeled,
702 None => fill_peeled(repo.odb(), None, entry),
703 }
704 }
705 keep
706 });
707 } else if let Some(odb) = odb.filter(|_| peel == PeelTips::Always) {
708 let packed = repo.and_then(|r| packed_peels_for(r, &collected));
709 for entry in &mut collected {
710 fill_peeled(odb, packed.as_ref(), entry);
711 }
712 }
713 if matches!(query.sort.key, SortKey::Date) {
714 let odb = odb.ok_or(RefQueryError::MissingObjectDatabase)?;
715 for entry in &mut collected {
716 entry.creator_date = creator_date(odb, &entry.oid).unwrap_or(0);
717 }
718 }
719 let suffixes = &options.versionsort_suffixes;
720 collected.sort_by(|a, b| {
721 let primary = compare_entries(a, b, query.sort.key, suffixes);
722 let primary = if query.sort.reverse {
723 primary.reverse()
724 } else {
725 primary
726 };
727 if primary != Ordering::Equal {
728 return primary;
729 }
730 a.full_name.cmp(&b.full_name)
731 });
732 if let Some(limit) = query.limit {
733 collected.truncate(limit);
734 }
735 Ok(collected
736 .into_iter()
737 .map(|c| RefListing {
738 full_name: c.full_name,
739 short_name: c.short_name,
740 oid: c.oid,
741 peeled: c.peeled,
742 kind: c.kind,
743 })
744 .collect())
745}
746
747pub(crate) const PACKED_PEELS_MIN_REFS: usize = 64;
752
753fn packed_peels_for(repo: &Repository, collected: &[CollectedRef]) -> Option<PackedRefPeels> {
757 let to_peel = collected.iter().filter(|c| c.peeled.is_none()).count();
758 if to_peel < PACKED_PEELS_MIN_REFS {
759 return None;
760 }
761 PackedRefPeels::for_store(repo.refs(), repo.git_dir()).ok()
762}
763
764fn fill_peeled(odb: &Odb, packed: Option<&PackedRefPeels>, entry: &mut CollectedRef) {
767 if entry.peeled.is_some() {
768 return;
769 }
770 if let Some(known) = packed.and_then(|p| p.peeled(&entry.full_name, &entry.oid)) {
771 entry.peeled = known;
772 return;
773 }
774 entry.peeled = odb.peel_tag_target(entry.oid).ok().flatten();
775}
776
777fn compare_entries(
778 a: &CollectedRef,
779 b: &CollectedRef,
780 key: SortKey,
781 suffixes: &[String],
782) -> Ordering {
783 match key {
784 SortKey::Name => a.full_name.cmp(&b.full_name),
785 SortKey::Version => compare_version_names(&a.full_name, &b.full_name, suffixes),
786 SortKey::Date => a.creator_date.cmp(&b.creator_date),
787 }
788}
789
790fn object_header_bytes(data: &[u8]) -> &[u8] {
791 if let Some(pos) = data.windows(2).position(|w| w == b"\n\n") {
792 &data[..pos]
793 } else {
794 data
795 }
796}
797
798fn find_header_value<'a>(header: &'a [u8], name: &[u8]) -> Option<&'a [u8]> {
799 let mut idx = 0;
800 while idx < header.len() {
801 let line_end = header[idx..]
802 .iter()
803 .position(|&b| b == b'\n')
804 .map(|p| idx + p)
805 .unwrap_or(header.len());
806 let line = &header[idx..line_end];
807 if line.is_empty() {
808 break;
809 }
810 if line.len() > name.len() + 1 && line.starts_with(name) && line[name.len()] == b' ' {
811 return Some(&line[name.len() + 1..]);
812 }
813 idx = line_end.saturating_add(1);
814 }
815 None
816}
817
818pub fn ref_query_options_for_repo(repo: &Repository) -> crate::error::Result<RefQueryOptions> {
824 let config = repo.config()?;
825 let versionsort_suffixes = config.get_all("versionsort.suffix");
826 let ignore_case = config
827 .get_bool("core.ignorecase")
828 .and_then(|r| r.ok())
829 .unwrap_or(false);
830 Ok(RefQueryOptions {
831 versionsort_suffixes,
832 ignore_case,
833 })
834}
835
836#[cfg(test)]
837mod tests {
838 #![allow(clippy::unwrap_used)]
839
840 use super::*;
841
842 #[test]
843 fn version_order_matches_git_tag_examples() {
844 assert_eq!(
845 compare_version_names("refs/tags/foo1.3", "refs/tags/foo1.10", &[]),
846 Ordering::Less
847 );
848 assert_eq!(
849 compare_version_names("refs/tags/foo1.10", "refs/tags/foo1.6", &[]),
850 Ordering::Greater
851 );
852 }
853
854 #[test]
855 fn pattern_prefix_slash_and_glob() {
856 assert!(pattern_matches_short_name("feature/x", "feature/", false));
857 assert!(pattern_matches_short_name("v1.9", "v1.*", false));
858 assert!(pattern_matches_short_name("release-rc1", "*rc*", false));
859 assert!(!pattern_matches_short_name("other", "feature/", false));
860 }
861
862 #[test]
863 fn versionsort_suffix_orders_prerelease() {
864 let suffixes = vec!["-rc".to_owned()];
865 assert_eq!(
866 compare_version_names("v1.0", "v1.0-rc1", &suffixes),
867 Ordering::Greater
868 );
869 }
870
871 #[test]
872 fn utf8_names_with_suffix_config_do_not_panic_in_compare() {
873 let suffixes = vec!["x".to_owned()];
874 let _ = compare_version_names("refs/tags/vα1", "refs/tags/vα2", &suffixes);
875 }
876
877 #[test]
878 fn ignore_case_prefix_directory_match() {
879 assert!(pattern_matches_short_name("Feature/x", "feature/", true));
880 }
881
882 #[test]
883 fn ignore_case_prefix_does_not_panic_on_utf8_short_names() {
884 assert!(!pattern_matches_short_name("€x", "a", true));
885 }
886
887 #[test]
888 fn ls_remote_empty_string_pattern_matches_nothing() {
889 assert!(!pattern_matches_ls_remote_refname("refs/heads/main", ""));
890 assert!(!patterns_match_ls_remote(
891 "refs/heads/main",
892 &[String::new()]
893 ));
894 assert!(patterns_match_ls_remote("refs/heads/main", &[]));
895 }
896
897 #[test]
898 fn ls_remote_pattern_tail_component() {
899 assert!(pattern_matches_ls_remote_refname("refs/heads/main", "main"));
900 assert!(!pattern_matches_ls_remote_refname(
901 "refs/heads/topic",
902 "main"
903 ));
904 assert!(pattern_matches_ls_remote_refname(
905 "refs/tags/v1^{}",
906 "refs/tags/v1*"
907 ));
908 assert!(!pattern_matches_ls_remote_refname("refs/tags/v1^{}", "v1"));
909 }
910
911 #[test]
912 fn peel_tips_skips_peeling_only_without_filters() {
913 use crate::objects::{serialize_commit, serialize_tag, CommitData, ObjectKind, TagData};
914
915 let dir = tempfile::tempdir().unwrap();
916 let repo = crate::repo::init_repository(
917 dir.path(),
918 false,
919 "main",
920 None,
921 crate::RefStorageFormat::Files,
922 )
923 .unwrap();
924 let tree = repo.odb().write(ObjectKind::Tree, b"").unwrap();
925 let commit = repo
926 .odb()
927 .write(
928 ObjectKind::Commit,
929 &serialize_commit(&CommitData {
930 tree,
931 parents: Vec::new(),
932 author: "T <t@example.com> 1 +0000".into(),
933 committer: "T <t@example.com> 1 +0000".into(),
934 author_raw: Vec::new(),
935 committer_raw: Vec::new(),
936 encoding: None,
937 message: "m\n".into(),
938 raw_message: None,
939 extra_headers: Vec::new(),
940 }),
941 )
942 .unwrap();
943 let tag = repo
944 .odb()
945 .write(
946 ObjectKind::Tag,
947 &serialize_tag(&TagData {
948 object: commit,
949 object_type: "commit".into(),
950 tag: "t".into(),
951 tagger: Some("T <t@example.com> 1 +0000".into()),
952 extra_headers: Vec::new(),
953 message: "t\n".into(),
954 }),
955 )
956 .unwrap();
957 crate::refs::write_ref(repo.git_dir(), "refs/heads/main", &commit).unwrap();
958 crate::refs::write_ref(repo.git_dir(), "refs/tags/tagged", &tag).unwrap();
959
960 let options = RefQueryOptions::default();
961 let peeled = |query: &RefQuery, peel: PeelTips| -> Vec<(String, Option<ObjectId>)> {
962 query_refs_peeling(&repo, query, &options, peel)
963 .unwrap()
964 .into_iter()
965 .map(|l| (l.short_name, l.peeled))
966 .collect()
967 };
968 let all = RefQuery {
969 namespaces: vec![RefNamespace::Heads, RefNamespace::Tags],
970 ..RefQuery::default()
971 };
972 let both_peeled = vec![
973 ("main".to_owned(), None),
974 ("tagged".to_owned(), Some(commit)),
975 ];
976 assert_eq!(peeled(&all, PeelTips::Always), both_peeled);
977 assert_eq!(
978 query_refs(&repo, &all, &options)
979 .unwrap()
980 .into_iter()
981 .map(|l| (l.short_name, l.peeled))
982 .collect::<Vec<_>>(),
983 both_peeled
984 );
985 assert_eq!(
986 peeled(&all, PeelTips::WhenFiltering),
987 vec![("main".to_owned(), None), ("tagged".to_owned(), None)]
988 );
989
990 let points_at = RefQuery {
991 points_at: vec![commit],
992 ..all.clone()
993 };
994 assert_eq!(peeled(&points_at, PeelTips::WhenFiltering), both_peeled);
995 let contains = RefQuery {
996 contains: vec![commit],
997 ..all.clone()
998 };
999 assert_eq!(peeled(&contains, PeelTips::WhenFiltering), both_peeled);
1000 }
1001
1002 #[test]
1003 fn limit_one_with_multiple_namespaces_uses_global_name_order() {
1004 let dir = tempfile::tempdir().unwrap();
1005 let repo = crate::repo::init_repository(
1006 dir.path(),
1007 false,
1008 "main",
1009 None,
1010 crate::RefStorageFormat::Files,
1011 )
1012 .unwrap();
1013 let tree = repo
1014 .odb()
1015 .write(crate::objects::ObjectKind::Tree, b"")
1016 .unwrap();
1017 let commit = repo
1018 .odb()
1019 .write(
1020 crate::objects::ObjectKind::Commit,
1021 &crate::objects::serialize_commit(&crate::objects::CommitData {
1022 tree,
1023 parents: Vec::new(),
1024 author: "T <t@example.com> 1 +0000".into(),
1025 committer: "T <t@example.com> 1 +0000".into(),
1026 author_raw: Vec::new(),
1027 committer_raw: Vec::new(),
1028 encoding: None,
1029 message: "m\n".into(),
1030 raw_message: None,
1031 extra_headers: Vec::new(),
1032 }),
1033 )
1034 .unwrap();
1035 crate::refs::write_ref(repo.git_dir(), "refs/heads/a", &commit).unwrap();
1036 crate::refs::write_ref(repo.git_dir(), "refs/tags/z", &commit).unwrap();
1037
1038 let query = RefQuery {
1039 namespaces: vec![RefNamespace::Tags, RefNamespace::Heads],
1040 limit: Some(1),
1041 sort: RefSort {
1042 key: SortKey::Name,
1043 reverse: false,
1044 },
1045 ..RefQuery::default()
1046 };
1047 let names: Vec<String> = query_refs(&repo, &query, &RefQueryOptions::default())
1048 .unwrap()
1049 .into_iter()
1050 .map(|l| l.full_name)
1051 .collect();
1052 assert_eq!(names, vec!["refs/heads/a".to_owned()]);
1053 }
1054}