750 lines · 32.6 KB · Rust
RawDownload
// Copyright 2020-2025 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. //! Types and functions for listing bookmark and tags. use std::cmp;use std::collections::HashMap;use std::collections::HashSet;use std::rc::Rc;use std::sync::Arc; use clap::ValueEnum;use itertools::Itertools as _;use jj_lib::backend;use jj_lib::backend::BackendResult;use jj_lib::backend::CommitId;use jj_lib::config::ConfigValue;use jj_lib::op_store::LocalRemoteRefTarget;use jj_lib::ref_name::RefName;use jj_lib::store::Store;use jj_lib::str_util::StringMatcher; use crate::commit_templater::CommitRef; /// Group of [`CommitRef`]s that should be sorted by the `primary` ref.#[derive(Clone, Debug)]pub struct RefListItem {    /// Local or untracked remote ref.    pub primary: Rc<CommitRef>,    /// Remote refs tracked by the primary (or local) ref.    pub tracked: Vec<Rc<CommitRef>>,} /// Conditions to select local/remote refs.pub struct RefFilterPredicates {    /// Matches local names.    pub name_matcher: StringMatcher,    /// Matches remote names.    pub remote_matcher: StringMatcher,    /// Matches any of the local targets.    pub matched_local_targets: HashSet<CommitId>,    /// Selects local refs having conflicted targets.    pub conflicted: bool,    /// Includes local-only refs.    pub include_local_only: bool,    /// Includes tracked remote refs pointing to the same local targets.    pub include_synced_remotes: bool,    /// Includes untracked remote refs.    pub include_untracked_remotes: bool,} /// Builds a list of local/remote refs matching the given predicates.pub fn collect_items<'a>(    all_refs: impl IntoIterator<Item = (&'a RefName, LocalRemoteRefTarget<'a>)>,    predicates: &RefFilterPredicates,) -> Vec<RefListItem> {    let mut list_items = Vec::new();    let refs_to_list = all_refs        .into_iter()        .filter(|(name, targets)| {            predicates.name_matcher.is_match(name.as_str())                || targets                    .local_target                    .added_ids()                    .any(|id| predicates.matched_local_targets.contains(id))        })        .filter(|(_, targets)| !predicates.conflicted || targets.local_target.has_conflict());    for (name, targets) in refs_to_list {        let LocalRemoteRefTarget {            local_target,            remote_refs,        } = targets;        let (mut tracked_remote_refs, untracked_remote_refs) = remote_refs            .iter()            .copied()            .filter(|(remote_name, _)| predicates.remote_matcher.is_match(remote_name.as_str()))            .partition::<Vec<_>, _>(|&(_, remote_ref)| remote_ref.is_tracked());        if !predicates.include_synced_remotes {            tracked_remote_refs.retain(|&(_, remote_ref)| remote_ref.target != *local_target);        }         if predicates.include_local_only && local_target.is_present()            || !tracked_remote_refs.is_empty()        {            let primary = CommitRef::local(                name,                local_target.clone(),                remote_refs.iter().map(|&(_, remote_ref)| remote_ref),            );            let tracked = tracked_remote_refs                .iter()                .map(|&(remote, remote_ref)| {                    CommitRef::remote(name, remote, remote_ref.clone(), local_target)                })                .collect();            list_items.push(RefListItem { primary, tracked });        }         if predicates.include_untracked_remotes {            list_items.extend(untracked_remote_refs.iter().map(|&(remote, remote_ref)| {                RefListItem {                    primary: CommitRef::remote_only(name, remote, remote_ref.target.clone()),                    tracked: vec![],                }            }));        }    }     list_items} /// Sort key for the `--sort` argument option.#[derive(Copy, Clone, PartialEq, Debug, ValueEnum)]pub enum SortKey {    Name,    #[value(name = "name-")]    NameDesc,    AuthorName,    #[value(name = "author-name-")]    AuthorNameDesc,    AuthorEmail,    #[value(name = "author-email-")]    AuthorEmailDesc,    AuthorDate,    #[value(name = "author-date-")]    AuthorDateDesc,    CommitterName,    #[value(name = "committer-name-")]    CommitterNameDesc,    CommitterEmail,    #[value(name = "committer-email-")]    CommitterEmailDesc,    CommitterDate,    #[value(name = "committer-date-")]    CommitterDateDesc,} impl SortKey {    fn is_commit_dependant(&self) -> bool {        match self {            Self::Name | Self::NameDesc => false,            Self::AuthorName            | Self::AuthorNameDesc            | Self::AuthorEmail            | Self::AuthorEmailDesc            | Self::AuthorDate            | Self::AuthorDateDesc            | Self::CommitterName            | Self::CommitterNameDesc            | Self::CommitterEmail            | Self::CommitterEmailDesc            | Self::CommitterDate            | Self::CommitterDateDesc => true,        }    }} pub fn parse_sort_keys(value: ConfigValue) -> Result<Vec<SortKey>, String> {    if let Some(array) = value.as_array() {        array            .iter()            .map(|item| {                item.as_str()                    .ok_or("Expected sort key as a string".to_owned())                    .and_then(|key| SortKey::from_str(key, false))            })            .try_collect()    } else {        Err("Expected an array of sort keys as strings".to_owned())    }} /// Sorts `items` by multiple `sort_keys`.////// The first key is most significant. The input items should have been sorted/// by [`SortKey::Name`].pub fn sort(    store: &Arc<Store>,    items: &mut [RefListItem],    sort_keys: &[SortKey],) -> BackendResult<()> {    let mut commits: HashMap<CommitId, Arc<backend::Commit>> = HashMap::new();    if sort_keys.iter().any(|key| key.is_commit_dependant()) {        commits = items            .iter()            .filter_map(|item| item.primary.target().added_ids().next())            .map(|commit_id| {                store                    .get_commit(commit_id)                    .map(|commit| (commit_id.clone(), commit.store_commit().clone()))            })            .try_collect()?;    }    sort_inner(items, sort_keys, &commits);    Ok(())} fn sort_inner(    items: &mut [RefListItem],    sort_keys: &[SortKey],    commits: &HashMap<CommitId, Arc<backend::Commit>>,) {    let to_commit = |item: &RefListItem| {        let id = item.primary.target().added_ids().next()?;        commits.get(id)    };     // Multi-pass sorting, the first key is most significant. Skip first    // iteration if sort key is `Name`, since items are already sorted by name.    for sort_key in sort_keys        .iter()        .rev()        .skip_while(|key| *key == &SortKey::Name)    {        match sort_key {            SortKey::Name => {                items.sort_by_key(|item| {                    (                        item.primary.name().to_owned(),                        item.primary.remote_name().map(|name| name.to_owned()),                    )                });            }            SortKey::NameDesc => {                items.sort_by_key(|item| {                    cmp::Reverse((                        item.primary.name().to_owned(),                        item.primary.remote_name().map(|name| name.to_owned()),                    ))                });            }            SortKey::AuthorName => {                items.sort_by_key(|item| to_commit(item).map(|commit| commit.author.name.as_str()));            }            SortKey::AuthorNameDesc => {                items.sort_by_key(|item| {                    cmp::Reverse(to_commit(item).map(|commit| commit.author.name.as_str()))                });            }            SortKey::AuthorEmail => {                items                    .sort_by_key(|item| to_commit(item).map(|commit| commit.author.email.as_str()));            }            SortKey::AuthorEmailDesc => {                items.sort_by_key(|item| {                    cmp::Reverse(to_commit(item).map(|commit| commit.author.email.as_str()))                });            }            SortKey::AuthorDate => {                items.sort_by_key(|item| to_commit(item).map(|commit| commit.author.timestamp));            }            SortKey::AuthorDateDesc => {                items.sort_by_key(|item| {                    cmp::Reverse(to_commit(item).map(|commit| commit.author.timestamp))                });            }            SortKey::CommitterName => {                items.sort_by_key(|item| {                    to_commit(item).map(|commit| commit.committer.name.as_str())                });            }            SortKey::CommitterNameDesc => {                items.sort_by_key(|item| {                    cmp::Reverse(to_commit(item).map(|commit| commit.committer.name.as_str()))                });            }            SortKey::CommitterEmail => {                items.sort_by_key(|item| {                    to_commit(item).map(|commit| commit.committer.email.as_str())                });            }            SortKey::CommitterEmailDesc => {                items.sort_by_key(|item| {                    cmp::Reverse(to_commit(item).map(|commit| commit.committer.email.as_str()))                });            }            SortKey::CommitterDate => {                items.sort_by_key(|item| to_commit(item).map(|commit| commit.committer.timestamp));            }            SortKey::CommitterDateDesc => {                items.sort_by_key(|item| {                    cmp::Reverse(to_commit(item).map(|commit| commit.committer.timestamp))                });            }        }    }} #[cfg(test)]mod tests {    use jj_lib::backend::ChangeId;    use jj_lib::backend::MillisSinceEpoch;    use jj_lib::backend::Signature;    use jj_lib::backend::Timestamp;    use jj_lib::backend::TreeId;    use jj_lib::merge::Merge;    use jj_lib::op_store::RefTarget;     use super::*;     fn make_backend_commit(author: Signature, committer: Signature) -> Arc<backend::Commit> {        Arc::new(backend::Commit {            parents: vec![],            predecessors: vec![],            root_tree: Merge::resolved(TreeId::new(vec![])),            conflict_labels: Merge::resolved(String::new()),            change_id: ChangeId::new(vec![]),            description: String::new(),            author,            committer,            secure_sig: None,        })    }     fn make_default_signature() -> Signature {        Signature {            name: "Test User".to_owned(),            email: "test.user@g.com".to_owned(),            timestamp: Timestamp {                timestamp: MillisSinceEpoch(0),                tz_offset: 0,            },        }    }     fn commit_id_generator() -> impl FnMut() -> CommitId {        let mut iter = (1_u128..).map(|n| CommitId::new(n.to_le_bytes().into()));        move || iter.next().unwrap()    }     fn commit_ts_generator() -> impl FnMut() -> Timestamp {        // iter starts as 1, 1, 2, ... for test purposes        let mut iter = Some(1_i64).into_iter().chain(1_i64..).map(|ms| Timestamp {            timestamp: MillisSinceEpoch(ms),            tz_offset: 0,        });        move || iter.next().unwrap()    }     // Helper function to prepare test data, sort and prepare snapshot with relevant    // information.    fn prepare_data_sort_and_snapshot(sort_keys: &[SortKey]) -> String {        let mut new_commit_id = commit_id_generator();        let mut new_timestamp = commit_ts_generator();        let names = ["bob", "alice", "eve", "bob", "bob"];        let emails = [            "bob@g.com",            "alice@g.com",            "eve@g.com",            "bob@g.com",            "bob@g.com",        ];        let bookmark_names = ["feature", "bug-fix", "chore", "bug-fix", "feature"];        let remote_names = [None, Some("upstream"), None, Some("origin"), Some("origin")];        let deleted = [false, false, false, false, true];        let mut bookmark_items: Vec<RefListItem> = Vec::new();        let mut commits: HashMap<CommitId, Arc<backend::Commit>> = HashMap::new();        for (&name, &email, bookmark_name, remote_name, &is_deleted) in            itertools::izip!(&names, &emails, &bookmark_names, &remote_names, &deleted)        {            let commit_id = new_commit_id();            let mut b_name = "foo";            let mut author = make_default_signature();            let mut committer = make_default_signature();             if sort_keys.contains(&SortKey::Name) || sort_keys.contains(&SortKey::NameDesc) {                b_name = bookmark_name;            }            if sort_keys.contains(&SortKey::AuthorName)                || sort_keys.contains(&SortKey::AuthorNameDesc)            {                author.name = String::from(name);            }            if sort_keys.contains(&SortKey::AuthorEmail)                || sort_keys.contains(&SortKey::AuthorEmailDesc)            {                author.email = String::from(email);            }            if sort_keys.contains(&SortKey::AuthorDate)                || sort_keys.contains(&SortKey::AuthorDateDesc)            {                author.timestamp = new_timestamp();            }            if sort_keys.contains(&SortKey::CommitterName)                || sort_keys.contains(&SortKey::CommitterNameDesc)            {                committer.name = String::from(name);            }            if sort_keys.contains(&SortKey::CommitterEmail)                || sort_keys.contains(&SortKey::CommitterEmailDesc)            {                committer.email = String::from(email);            }            if sort_keys.contains(&SortKey::CommitterDate)                || sort_keys.contains(&SortKey::CommitterDateDesc)            {                committer.timestamp = new_timestamp();            }             if let Some(remote_name) = remote_name {                if is_deleted {                    bookmark_items.push(RefListItem {                        primary: CommitRef::remote_only(b_name, *remote_name, RefTarget::absent()),                        tracked: vec![CommitRef::local_only(                            b_name,                            RefTarget::normal(commit_id.clone()),                        )],                    });                } else {                    bookmark_items.push(RefListItem {                        primary: CommitRef::remote_only(                            b_name,                            *remote_name,                            RefTarget::normal(commit_id.clone()),                        ),                        tracked: vec![],                    });                }            } else {                bookmark_items.push(RefListItem {                    primary: CommitRef::local_only(b_name, RefTarget::normal(commit_id.clone())),                    tracked: vec![],                });            }             commits.insert(commit_id, make_backend_commit(author, committer));        }         // The sort function has an assumption that refs are sorted by name.        // Here we support this assumption.        bookmark_items.sort_by_key(|item| {            (                item.primary.name().to_owned(),                item.primary.remote_name().map(|name| name.to_owned()),            )        });         sort_and_snapshot(&mut bookmark_items, sort_keys, &commits)    }     // Helper function to sort refs and prepare snapshot with relevant information.    fn sort_and_snapshot(        items: &mut [RefListItem],        sort_keys: &[SortKey],        commits: &HashMap<CommitId, Arc<backend::Commit>>,    ) -> String {        sort_inner(items, sort_keys, commits);         let to_commit = |item: &RefListItem| {            let id = item.primary.target().added_ids().next()?;            commits.get(id)        };         macro_rules! row_format {            ($($args:tt)*) => {                format!("{:<20}{:<16}{:<17}{:<14}{:<16}{:<17}{}", $($args)*)            }        }         let header = row_format!(            "Name",            "AuthorName",            "AuthorEmail",            "AuthorDate",            "CommitterName",            "CommitterEmail",            "CommitterDate"        );         let rows: Vec<String> = items            .iter()            .map(|item| {                let name = [Some(item.primary.name()), item.primary.remote_name()]                    .iter()                    .flatten()                    .join("@");                 let commit = to_commit(item);                 let author_name = commit                    .map(|c| c.author.name.clone())                    .unwrap_or_else(|| String::from("-"));                let author_email = commit                    .map(|c| c.author.email.clone())                    .unwrap_or_else(|| String::from("-"));                let author_date = commit                    .map(|c| c.author.timestamp.timestamp.0.to_string())                    .unwrap_or_else(|| String::from("-")); 

Lines 1–500 of 750.