Skip to main content

jj_cli/
commit_ref_list.rs

1// Copyright 2020-2025 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Types and functions for listing bookmark and tags.
16
17use std::cmp;
18use std::collections::HashMap;
19use std::collections::HashSet;
20use std::rc::Rc;
21use std::sync::Arc;
22
23use clap::ValueEnum;
24use itertools::Itertools as _;
25use jj_lib::backend;
26use jj_lib::backend::BackendResult;
27use jj_lib::backend::CommitId;
28use jj_lib::config::ConfigValue;
29use jj_lib::op_store::LocalRemoteRefTarget;
30use jj_lib::ref_name::RefName;
31use jj_lib::store::Store;
32use jj_lib::str_util::StringMatcher;
33
34use crate::commit_templater::CommitRef;
35
36/// Group of [`CommitRef`]s that should be sorted by the `primary` ref.
37#[derive(Clone, Debug)]
38pub struct RefListItem {
39    /// Local or untracked remote ref.
40    pub primary: Rc<CommitRef>,
41    /// Remote refs tracked by the primary (or local) ref.
42    pub tracked: Vec<Rc<CommitRef>>,
43}
44
45/// Conditions to select local/remote refs.
46pub struct RefFilterPredicates {
47    /// Matches local names.
48    pub name_matcher: StringMatcher,
49    /// Matches remote names.
50    pub remote_matcher: StringMatcher,
51    /// Matches any of the local targets.
52    pub matched_local_targets: HashSet<CommitId>,
53    /// Selects local refs having conflicted targets.
54    pub conflicted: bool,
55    /// Includes local-only refs.
56    pub include_local_only: bool,
57    /// Includes tracked remote refs pointing to the same local targets.
58    pub include_synced_remotes: bool,
59    /// Includes untracked remote refs.
60    pub include_untracked_remotes: bool,
61}
62
63/// Builds a list of local/remote refs matching the given predicates.
64pub fn collect_items<'a>(
65    all_refs: impl IntoIterator<Item = (&'a RefName, LocalRemoteRefTarget<'a>)>,
66    predicates: &RefFilterPredicates,
67) -> Vec<RefListItem> {
68    let mut list_items = Vec::new();
69    let refs_to_list = all_refs
70        .into_iter()
71        .filter(|(name, targets)| {
72            predicates.name_matcher.is_match(name.as_str())
73                || targets
74                    .local_target
75                    .added_ids()
76                    .any(|id| predicates.matched_local_targets.contains(id))
77        })
78        .filter(|(_, targets)| !predicates.conflicted || targets.local_target.has_conflict());
79    for (name, targets) in refs_to_list {
80        let LocalRemoteRefTarget {
81            local_target,
82            remote_refs,
83        } = targets;
84        let (mut tracked_remote_refs, untracked_remote_refs) = remote_refs
85            .iter()
86            .copied()
87            .filter(|(remote_name, _)| predicates.remote_matcher.is_match(remote_name.as_str()))
88            .partition::<Vec<_>, _>(|&(_, remote_ref)| remote_ref.is_tracked());
89        if !predicates.include_synced_remotes {
90            tracked_remote_refs.retain(|&(_, remote_ref)| remote_ref.target != *local_target);
91        }
92
93        if predicates.include_local_only && local_target.is_present()
94            || !tracked_remote_refs.is_empty()
95        {
96            let primary = CommitRef::local(
97                name,
98                local_target.clone(),
99                remote_refs.iter().map(|&(_, remote_ref)| remote_ref),
100            );
101            let tracked = tracked_remote_refs
102                .iter()
103                .map(|&(remote, remote_ref)| {
104                    CommitRef::remote(name, remote, remote_ref.clone(), local_target)
105                })
106                .collect();
107            list_items.push(RefListItem { primary, tracked });
108        }
109
110        if predicates.include_untracked_remotes {
111            list_items.extend(untracked_remote_refs.iter().map(|&(remote, remote_ref)| {
112                RefListItem {
113                    primary: CommitRef::remote_only(name, remote, remote_ref.target.clone()),
114                    tracked: vec![],
115                }
116            }));
117        }
118    }
119
120    list_items
121}
122
123/// Sort key for the `--sort` argument option.
124#[derive(Copy, Clone, PartialEq, Debug, ValueEnum)]
125pub enum SortKey {
126    Name,
127    #[value(name = "name-")]
128    NameDesc,
129    AuthorName,
130    #[value(name = "author-name-")]
131    AuthorNameDesc,
132    AuthorEmail,
133    #[value(name = "author-email-")]
134    AuthorEmailDesc,
135    AuthorDate,
136    #[value(name = "author-date-")]
137    AuthorDateDesc,
138    CommitterName,
139    #[value(name = "committer-name-")]
140    CommitterNameDesc,
141    CommitterEmail,
142    #[value(name = "committer-email-")]
143    CommitterEmailDesc,
144    CommitterDate,
145    #[value(name = "committer-date-")]
146    CommitterDateDesc,
147}
148
149impl SortKey {
150    fn is_commit_dependant(&self) -> bool {
151        match self {
152            Self::Name | Self::NameDesc => false,
153            Self::AuthorName
154            | Self::AuthorNameDesc
155            | Self::AuthorEmail
156            | Self::AuthorEmailDesc
157            | Self::AuthorDate
158            | Self::AuthorDateDesc
159            | Self::CommitterName
160            | Self::CommitterNameDesc
161            | Self::CommitterEmail
162            | Self::CommitterEmailDesc
163            | Self::CommitterDate
164            | Self::CommitterDateDesc => true,
165        }
166    }
167}
168
169pub fn parse_sort_keys(value: ConfigValue) -> Result<Vec<SortKey>, String> {
170    if let Some(array) = value.as_array() {
171        array
172            .iter()
173            .map(|item| {
174                item.as_str()
175                    .ok_or("Expected sort key as a string".to_owned())
176                    .and_then(|key| SortKey::from_str(key, false))
177            })
178            .try_collect()
179    } else {
180        Err("Expected an array of sort keys as strings".to_owned())
181    }
182}
183
184/// Sorts `items` by multiple `sort_keys`.
185///
186/// The first key is most significant. The input items should have been sorted
187/// by [`SortKey::Name`].
188pub fn sort(
189    store: &Arc<Store>,
190    items: &mut [RefListItem],
191    sort_keys: &[SortKey],
192) -> BackendResult<()> {
193    let mut commits: HashMap<CommitId, Arc<backend::Commit>> = HashMap::new();
194    if sort_keys.iter().any(|key| key.is_commit_dependant()) {
195        commits = items
196            .iter()
197            .filter_map(|item| item.primary.target().added_ids().next())
198            .map(|commit_id| {
199                store
200                    .get_commit(commit_id)
201                    .map(|commit| (commit_id.clone(), commit.store_commit().clone()))
202            })
203            .try_collect()?;
204    }
205    sort_inner(items, sort_keys, &commits);
206    Ok(())
207}
208
209fn sort_inner(
210    items: &mut [RefListItem],
211    sort_keys: &[SortKey],
212    commits: &HashMap<CommitId, Arc<backend::Commit>>,
213) {
214    let to_commit = |item: &RefListItem| {
215        let id = item.primary.target().added_ids().next()?;
216        commits.get(id)
217    };
218
219    // Multi-pass sorting, the first key is most significant. Skip first
220    // iteration if sort key is `Name`, since items are already sorted by name.
221    for sort_key in sort_keys
222        .iter()
223        .rev()
224        .skip_while(|key| *key == &SortKey::Name)
225    {
226        match sort_key {
227            SortKey::Name => {
228                items.sort_by_key(|item| {
229                    (
230                        item.primary.name().to_owned(),
231                        item.primary.remote_name().map(|name| name.to_owned()),
232                    )
233                });
234            }
235            SortKey::NameDesc => {
236                items.sort_by_key(|item| {
237                    cmp::Reverse((
238                        item.primary.name().to_owned(),
239                        item.primary.remote_name().map(|name| name.to_owned()),
240                    ))
241                });
242            }
243            SortKey::AuthorName => {
244                items.sort_by_key(|item| to_commit(item).map(|commit| commit.author.name.as_str()));
245            }
246            SortKey::AuthorNameDesc => {
247                items.sort_by_key(|item| {
248                    cmp::Reverse(to_commit(item).map(|commit| commit.author.name.as_str()))
249                });
250            }
251            SortKey::AuthorEmail => {
252                items
253                    .sort_by_key(|item| to_commit(item).map(|commit| commit.author.email.as_str()));
254            }
255            SortKey::AuthorEmailDesc => {
256                items.sort_by_key(|item| {
257                    cmp::Reverse(to_commit(item).map(|commit| commit.author.email.as_str()))
258                });
259            }
260            SortKey::AuthorDate => {
261                items.sort_by_key(|item| to_commit(item).map(|commit| commit.author.timestamp));
262            }
263            SortKey::AuthorDateDesc => {
264                items.sort_by_key(|item| {
265                    cmp::Reverse(to_commit(item).map(|commit| commit.author.timestamp))
266                });
267            }
268            SortKey::CommitterName => {
269                items.sort_by_key(|item| {
270                    to_commit(item).map(|commit| commit.committer.name.as_str())
271                });
272            }
273            SortKey::CommitterNameDesc => {
274                items.sort_by_key(|item| {
275                    cmp::Reverse(to_commit(item).map(|commit| commit.committer.name.as_str()))
276                });
277            }
278            SortKey::CommitterEmail => {
279                items.sort_by_key(|item| {
280                    to_commit(item).map(|commit| commit.committer.email.as_str())
281                });
282            }
283            SortKey::CommitterEmailDesc => {
284                items.sort_by_key(|item| {
285                    cmp::Reverse(to_commit(item).map(|commit| commit.committer.email.as_str()))
286                });
287            }
288            SortKey::CommitterDate => {
289                items.sort_by_key(|item| to_commit(item).map(|commit| commit.committer.timestamp));
290            }
291            SortKey::CommitterDateDesc => {
292                items.sort_by_key(|item| {
293                    cmp::Reverse(to_commit(item).map(|commit| commit.committer.timestamp))
294                });
295            }
296        }
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use jj_lib::backend::ChangeId;
303    use jj_lib::backend::MillisSinceEpoch;
304    use jj_lib::backend::Signature;
305    use jj_lib::backend::Timestamp;
306    use jj_lib::backend::TreeId;
307    use jj_lib::merge::Merge;
308    use jj_lib::op_store::RefTarget;
309
310    use super::*;
311
312    fn make_backend_commit(author: Signature, committer: Signature) -> Arc<backend::Commit> {
313        Arc::new(backend::Commit {
314            parents: vec![],
315            predecessors: vec![],
316            root_tree: Merge::resolved(TreeId::new(vec![])),
317            conflict_labels: Merge::resolved(String::new()),
318            change_id: ChangeId::new(vec![]),
319            description: String::new(),
320            author,
321            committer,
322            secure_sig: None,
323        })
324    }
325
326    fn make_default_signature() -> Signature {
327        Signature {
328            name: "Test User".to_owned(),
329            email: "test.user@g.com".to_owned(),
330            timestamp: Timestamp {
331                timestamp: MillisSinceEpoch(0),
332                tz_offset: 0,
333            },
334        }
335    }
336
337    fn commit_id_generator() -> impl FnMut() -> CommitId {
338        let mut iter = (1_u128..).map(|n| CommitId::new(n.to_le_bytes().into()));
339        move || iter.next().unwrap()
340    }
341
342    fn commit_ts_generator() -> impl FnMut() -> Timestamp {
343        // iter starts as 1, 1, 2, ... for test purposes
344        let mut iter = Some(1_i64).into_iter().chain(1_i64..).map(|ms| Timestamp {
345            timestamp: MillisSinceEpoch(ms),
346            tz_offset: 0,
347        });
348        move || iter.next().unwrap()
349    }
350
351    // Helper function to prepare test data, sort and prepare snapshot with relevant
352    // information.
353    fn prepare_data_sort_and_snapshot(sort_keys: &[SortKey]) -> String {
354        let mut new_commit_id = commit_id_generator();
355        let mut new_timestamp = commit_ts_generator();
356        let names = ["bob", "alice", "eve", "bob", "bob"];
357        let emails = [
358            "bob@g.com",
359            "alice@g.com",
360            "eve@g.com",
361            "bob@g.com",
362            "bob@g.com",
363        ];
364        let bookmark_names = ["feature", "bug-fix", "chore", "bug-fix", "feature"];
365        let remote_names = [None, Some("upstream"), None, Some("origin"), Some("origin")];
366        let deleted = [false, false, false, false, true];
367        let mut bookmark_items: Vec<RefListItem> = Vec::new();
368        let mut commits: HashMap<CommitId, Arc<backend::Commit>> = HashMap::new();
369        for (&name, &email, bookmark_name, remote_name, &is_deleted) in
370            itertools::izip!(&names, &emails, &bookmark_names, &remote_names, &deleted)
371        {
372            let commit_id = new_commit_id();
373            let mut b_name = "foo";
374            let mut author = make_default_signature();
375            let mut committer = make_default_signature();
376
377            if sort_keys.contains(&SortKey::Name) || sort_keys.contains(&SortKey::NameDesc) {
378                b_name = bookmark_name;
379            }
380            if sort_keys.contains(&SortKey::AuthorName)
381                || sort_keys.contains(&SortKey::AuthorNameDesc)
382            {
383                author.name = String::from(name);
384            }
385            if sort_keys.contains(&SortKey::AuthorEmail)
386                || sort_keys.contains(&SortKey::AuthorEmailDesc)
387            {
388                author.email = String::from(email);
389            }
390            if sort_keys.contains(&SortKey::AuthorDate)
391                || sort_keys.contains(&SortKey::AuthorDateDesc)
392            {
393                author.timestamp = new_timestamp();
394            }
395            if sort_keys.contains(&SortKey::CommitterName)
396                || sort_keys.contains(&SortKey::CommitterNameDesc)
397            {
398                committer.name = String::from(name);
399            }
400            if sort_keys.contains(&SortKey::CommitterEmail)
401                || sort_keys.contains(&SortKey::CommitterEmailDesc)
402            {
403                committer.email = String::from(email);
404            }
405            if sort_keys.contains(&SortKey::CommitterDate)
406                || sort_keys.contains(&SortKey::CommitterDateDesc)
407            {
408                committer.timestamp = new_timestamp();
409            }
410
411            if let Some(remote_name) = remote_name {
412                if is_deleted {
413                    bookmark_items.push(RefListItem {
414                        primary: CommitRef::remote_only(b_name, *remote_name, RefTarget::absent()),
415                        tracked: vec![CommitRef::local_only(
416                            b_name,
417                            RefTarget::normal(commit_id.clone()),
418                        )],
419                    });
420                } else {
421                    bookmark_items.push(RefListItem {
422                        primary: CommitRef::remote_only(
423                            b_name,
424                            *remote_name,
425                            RefTarget::normal(commit_id.clone()),
426                        ),
427                        tracked: vec![],
428                    });
429                }
430            } else {
431                bookmark_items.push(RefListItem {
432                    primary: CommitRef::local_only(b_name, RefTarget::normal(commit_id.clone())),
433                    tracked: vec![],
434                });
435            }
436
437            commits.insert(commit_id, make_backend_commit(author, committer));
438        }
439
440        // The sort function has an assumption that refs are sorted by name.
441        // Here we support this assumption.
442        bookmark_items.sort_by_key(|item| {
443            (
444                item.primary.name().to_owned(),
445                item.primary.remote_name().map(|name| name.to_owned()),
446            )
447        });
448
449        sort_and_snapshot(&mut bookmark_items, sort_keys, &commits)
450    }
451
452    // Helper function to sort refs and prepare snapshot with relevant information.
453    fn sort_and_snapshot(
454        items: &mut [RefListItem],
455        sort_keys: &[SortKey],
456        commits: &HashMap<CommitId, Arc<backend::Commit>>,
457    ) -> String {
458        sort_inner(items, sort_keys, commits);
459
460        let to_commit = |item: &RefListItem| {
461            let id = item.primary.target().added_ids().next()?;
462            commits.get(id)
463        };
464
465        macro_rules! row_format {
466            ($($args:tt)*) => {
467                format!("{:<20}{:<16}{:<17}{:<14}{:<16}{:<17}{}", $($args)*)
468            }
469        }
470
471        let header = row_format!(
472            "Name",
473            "AuthorName",
474            "AuthorEmail",
475            "AuthorDate",
476            "CommitterName",
477            "CommitterEmail",
478            "CommitterDate"
479        );
480
481        let rows: Vec<String> = items
482            .iter()
483            .map(|item| {
484                let name = [Some(item.primary.name()), item.primary.remote_name()]
485                    .iter()
486                    .flatten()
487                    .join("@");
488
489                let commit = to_commit(item);
490
491                let author_name = commit
492                    .map(|c| c.author.name.clone())
493                    .unwrap_or_else(|| String::from("-"));
494                let author_email = commit
495                    .map(|c| c.author.email.clone())
496                    .unwrap_or_else(|| String::from("-"));
497                let author_date = commit
498                    .map(|c| c.author.timestamp.timestamp.0.to_string())
499                    .unwrap_or_else(|| String::from("-"));
500
501                let committer_name = commit
502                    .map(|c| c.committer.name.clone())
503                    .unwrap_or_else(|| String::from("-"));
504                let committer_email = commit
505                    .map(|c| c.committer.email.clone())
506                    .unwrap_or_else(|| String::from("-"));
507                let committer_date = commit
508                    .map(|c| c.committer.timestamp.timestamp.0.to_string())
509                    .unwrap_or_else(|| String::from("-"));
510
511                row_format!(
512                    name,
513                    author_name,
514                    author_email,
515                    author_date,
516                    committer_name,
517                    committer_email,
518                    committer_date
519                )
520            })
521            .collect();
522
523        let mut result = vec![header];
524        result.extend(rows);
525        result.join("\n")
526    }
527
528    #[test]
529    fn test_sort_by_name() {
530        insta::assert_snapshot!(
531            prepare_data_sort_and_snapshot(&[SortKey::Name]), @r"
532        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
533        bug-fix@origin      Test User       test.user@g.com  0             Test User       test.user@g.com  0
534        bug-fix@upstream    Test User       test.user@g.com  0             Test User       test.user@g.com  0
535        chore               Test User       test.user@g.com  0             Test User       test.user@g.com  0
536        feature             Test User       test.user@g.com  0             Test User       test.user@g.com  0
537        feature@origin      -               -                -             -               -                -
538        ");
539    }
540
541    #[test]
542    fn test_sort_by_name_desc() {
543        insta::assert_snapshot!(
544            prepare_data_sort_and_snapshot(&[SortKey::NameDesc]), @r"
545        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
546        feature@origin      -               -                -             -               -                -
547        feature             Test User       test.user@g.com  0             Test User       test.user@g.com  0
548        chore               Test User       test.user@g.com  0             Test User       test.user@g.com  0
549        bug-fix@upstream    Test User       test.user@g.com  0             Test User       test.user@g.com  0
550        bug-fix@origin      Test User       test.user@g.com  0             Test User       test.user@g.com  0
551        ");
552    }
553
554    #[test]
555    fn test_sort_by_author_name() {
556        insta::assert_snapshot!(
557            prepare_data_sort_and_snapshot(&[SortKey::AuthorName]), @r"
558        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
559        foo@origin          -               -                -             -               -                -
560        foo@upstream        alice           test.user@g.com  0             Test User       test.user@g.com  0
561        foo                 bob             test.user@g.com  0             Test User       test.user@g.com  0
562        foo@origin          bob             test.user@g.com  0             Test User       test.user@g.com  0
563        foo                 eve             test.user@g.com  0             Test User       test.user@g.com  0
564        ");
565    }
566
567    #[test]
568    fn test_sort_by_author_name_desc() {
569        insta::assert_snapshot!(
570            prepare_data_sort_and_snapshot(&[SortKey::AuthorNameDesc]), @r"
571        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
572        foo                 eve             test.user@g.com  0             Test User       test.user@g.com  0
573        foo                 bob             test.user@g.com  0             Test User       test.user@g.com  0
574        foo@origin          bob             test.user@g.com  0             Test User       test.user@g.com  0
575        foo@upstream        alice           test.user@g.com  0             Test User       test.user@g.com  0
576        foo@origin          -               -                -             -               -                -
577        ");
578    }
579
580    #[test]
581    fn test_sort_by_author_email() {
582        insta::assert_snapshot!(
583            prepare_data_sort_and_snapshot(&[SortKey::AuthorEmail]), @r"
584        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
585        foo@origin          -               -                -             -               -                -
586        foo@upstream        Test User       alice@g.com      0             Test User       test.user@g.com  0
587        foo                 Test User       bob@g.com        0             Test User       test.user@g.com  0
588        foo@origin          Test User       bob@g.com        0             Test User       test.user@g.com  0
589        foo                 Test User       eve@g.com        0             Test User       test.user@g.com  0
590        ");
591    }
592
593    #[test]
594    fn test_sort_by_author_email_desc() {
595        insta::assert_snapshot!(
596            prepare_data_sort_and_snapshot(&[SortKey::AuthorEmailDesc]), @r"
597        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
598        foo                 Test User       eve@g.com        0             Test User       test.user@g.com  0
599        foo                 Test User       bob@g.com        0             Test User       test.user@g.com  0
600        foo@origin          Test User       bob@g.com        0             Test User       test.user@g.com  0
601        foo@upstream        Test User       alice@g.com      0             Test User       test.user@g.com  0
602        foo@origin          -               -                -             -               -                -
603        ");
604    }
605
606    #[test]
607    fn test_sort_by_author_date() {
608        insta::assert_snapshot!(
609            prepare_data_sort_and_snapshot(&[SortKey::AuthorDate]), @r"
610        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
611        foo@origin          -               -                -             -               -                -
612        foo                 Test User       test.user@g.com  1             Test User       test.user@g.com  0
613        foo@upstream        Test User       test.user@g.com  1             Test User       test.user@g.com  0
614        foo                 Test User       test.user@g.com  2             Test User       test.user@g.com  0
615        foo@origin          Test User       test.user@g.com  3             Test User       test.user@g.com  0
616        ");
617    }
618
619    #[test]
620    fn test_sort_by_author_date_desc() {
621        insta::assert_snapshot!(
622            prepare_data_sort_and_snapshot(&[SortKey::AuthorDateDesc]), @r"
623        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
624        foo@origin          Test User       test.user@g.com  3             Test User       test.user@g.com  0
625        foo                 Test User       test.user@g.com  2             Test User       test.user@g.com  0
626        foo                 Test User       test.user@g.com  1             Test User       test.user@g.com  0
627        foo@upstream        Test User       test.user@g.com  1             Test User       test.user@g.com  0
628        foo@origin          -               -                -             -               -                -
629        ");
630    }
631
632    #[test]
633    fn test_sort_by_committer_name() {
634        insta::assert_snapshot!(
635            prepare_data_sort_and_snapshot(&[SortKey::CommitterName]), @r"
636        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
637        foo@origin          -               -                -             -               -                -
638        foo@upstream        Test User       test.user@g.com  0             alice           test.user@g.com  0
639        foo                 Test User       test.user@g.com  0             bob             test.user@g.com  0
640        foo@origin          Test User       test.user@g.com  0             bob             test.user@g.com  0
641        foo                 Test User       test.user@g.com  0             eve             test.user@g.com  0
642        ");
643    }
644
645    #[test]
646    fn test_sort_by_committer_name_desc() {
647        insta::assert_snapshot!(
648            prepare_data_sort_and_snapshot(&[SortKey::CommitterNameDesc]), @r"
649        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
650        foo                 Test User       test.user@g.com  0             eve             test.user@g.com  0
651        foo                 Test User       test.user@g.com  0             bob             test.user@g.com  0
652        foo@origin          Test User       test.user@g.com  0             bob             test.user@g.com  0
653        foo@upstream        Test User       test.user@g.com  0             alice           test.user@g.com  0
654        foo@origin          -               -                -             -               -                -
655        ");
656    }
657
658    #[test]
659    fn test_sort_by_committer_email() {
660        insta::assert_snapshot!(
661            prepare_data_sort_and_snapshot(&[SortKey::CommitterEmail]), @r"
662        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
663        foo@origin          -               -                -             -               -                -
664        foo@upstream        Test User       test.user@g.com  0             Test User       alice@g.com      0
665        foo                 Test User       test.user@g.com  0             Test User       bob@g.com        0
666        foo@origin          Test User       test.user@g.com  0             Test User       bob@g.com        0
667        foo                 Test User       test.user@g.com  0             Test User       eve@g.com        0
668        ");
669    }
670
671    #[test]
672    fn test_sort_by_committer_email_desc() {
673        insta::assert_snapshot!(
674            prepare_data_sort_and_snapshot(&[SortKey::CommitterEmailDesc]), @r"
675        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
676        foo                 Test User       test.user@g.com  0             Test User       eve@g.com        0
677        foo                 Test User       test.user@g.com  0             Test User       bob@g.com        0
678        foo@origin          Test User       test.user@g.com  0             Test User       bob@g.com        0
679        foo@upstream        Test User       test.user@g.com  0             Test User       alice@g.com      0
680        foo@origin          -               -                -             -               -                -
681        ");
682    }
683
684    #[test]
685    fn test_sort_by_committer_date() {
686        insta::assert_snapshot!(
687            prepare_data_sort_and_snapshot(&[SortKey::CommitterDate]), @r"
688        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
689        foo@origin          -               -                -             -               -                -
690        foo                 Test User       test.user@g.com  0             Test User       test.user@g.com  1
691        foo@upstream        Test User       test.user@g.com  0             Test User       test.user@g.com  1
692        foo                 Test User       test.user@g.com  0             Test User       test.user@g.com  2
693        foo@origin          Test User       test.user@g.com  0             Test User       test.user@g.com  3
694        ");
695    }
696
697    #[test]
698    fn test_sort_by_committer_date_desc() {
699        insta::assert_snapshot!(
700            prepare_data_sort_and_snapshot(&[SortKey::CommitterDateDesc]), @r"
701        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
702        foo@origin          Test User       test.user@g.com  0             Test User       test.user@g.com  3
703        foo                 Test User       test.user@g.com  0             Test User       test.user@g.com  2
704        foo                 Test User       test.user@g.com  0             Test User       test.user@g.com  1
705        foo@upstream        Test User       test.user@g.com  0             Test User       test.user@g.com  1
706        foo@origin          -               -                -             -               -                -
707        ");
708    }
709
710    #[test]
711    fn test_sort_by_author_date_desc_and_name() {
712        insta::assert_snapshot!(
713            prepare_data_sort_and_snapshot(&[SortKey::AuthorDateDesc, SortKey::Name]), @r"
714        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
715        bug-fix@origin      Test User       test.user@g.com  3             Test User       test.user@g.com  0
716        chore               Test User       test.user@g.com  2             Test User       test.user@g.com  0
717        bug-fix@upstream    Test User       test.user@g.com  1             Test User       test.user@g.com  0
718        feature             Test User       test.user@g.com  1             Test User       test.user@g.com  0
719        feature@origin      -               -                -             -               -                -
720        ");
721    }
722
723    #[test]
724    fn test_sort_by_committer_name_and_name_desc() {
725        insta::assert_snapshot!(
726            prepare_data_sort_and_snapshot(&[SortKey::CommitterName, SortKey::NameDesc]), @r"
727        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
728        feature@origin      -               -                -             -               -                -
729        bug-fix@upstream    Test User       test.user@g.com  0             alice           test.user@g.com  0
730        feature             Test User       test.user@g.com  0             bob             test.user@g.com  0
731        bug-fix@origin      Test User       test.user@g.com  0             bob             test.user@g.com  0
732        chore               Test User       test.user@g.com  0             eve             test.user@g.com  0
733        ");
734    }
735
736    // Bookmarks are already sorted by name
737    // Test when sorting by name is not the only/last criteria
738    #[test]
739    fn test_sort_by_name_and_committer_date() {
740        insta::assert_snapshot!(
741            prepare_data_sort_and_snapshot(&[SortKey::Name, SortKey::AuthorDate]), @r"
742        Name                AuthorName      AuthorEmail      AuthorDate    CommitterName   CommitterEmail   CommitterDate
743        bug-fix@origin      Test User       test.user@g.com  3             Test User       test.user@g.com  0
744        bug-fix@upstream    Test User       test.user@g.com  1             Test User       test.user@g.com  0
745        chore               Test User       test.user@g.com  2             Test User       test.user@g.com  0
746        feature             Test User       test.user@g.com  1             Test User       test.user@g.com  0
747        feature@origin      -               -                -             -               -                -
748        ");
749    }
750}