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