728 lines · 26.5 KB · Rust
RawDownload
// Copyright 2020-2023 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. use std::cmp::max;use std::collections::HashMap;use std::io;use std::rc::Rc; use itertools::Itertools as _;use jj_lib::backend::{ChangeId, CommitId};use jj_lib::commit::Commit;use jj_lib::hex_util::to_reverse_hex;use jj_lib::id_prefix::IdPrefixContext;use jj_lib::object_id::ObjectId as _;use jj_lib::op_store::{RefTarget, WorkspaceId};use jj_lib::repo::Repo;use jj_lib::{git, rewrite};use once_cell::unsync::OnceCell; use crate::formatter::Formatter;use crate::template_builder::{    self, BuildContext, CoreTemplatePropertyKind, IntoTemplateProperty, TemplateLanguage,};use crate::template_parser::{    self, FunctionCallNode, TemplateAliasesMap, TemplateParseError, TemplateParseResult,};use crate::templater::{    self, IntoTemplate, PlainTextFormattedProperty, Template, TemplateFunction, TemplateProperty,    TemplatePropertyFn,};use crate::text_util; struct CommitTemplateLanguage<'repo, 'b> {    repo: &'repo dyn Repo,    workspace_id: &'b WorkspaceId,    id_prefix_context: &'repo IdPrefixContext,    keyword_cache: CommitKeywordCache,} impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo, '_> {    type Context = Commit;    type Property = CommitTemplatePropertyKind<'repo>;     template_builder::impl_core_wrap_property_fns!('repo, CommitTemplatePropertyKind::Core);     fn build_keyword(&self, name: &str, span: pest::Span) -> TemplateParseResult<Self::Property> {        build_commit_keyword(self, name, span)    }     fn build_method(        &self,        build_ctx: &BuildContext<Self::Property>,        property: Self::Property,        function: &FunctionCallNode,    ) -> TemplateParseResult<Self::Property> {        match property {            CommitTemplatePropertyKind::Core(property) => {                template_builder::build_core_method(self, build_ctx, property, function)            }            CommitTemplatePropertyKind::Commit(property) => {                build_commit_method(self, build_ctx, property, function)            }            CommitTemplatePropertyKind::CommitList(property) => {                template_builder::build_unformattable_list_method(                    self,                    build_ctx,                    property,                    function,                    |item| self.wrap_commit(item),                )            }            CommitTemplatePropertyKind::RefName(property) => {                build_ref_name_method(self, build_ctx, property, function)            }            CommitTemplatePropertyKind::RefNameList(property) => {                template_builder::build_formattable_list_method(                    self,                    build_ctx,                    property,                    function,                    |item| self.wrap_ref_name(item),                )            }            CommitTemplatePropertyKind::CommitOrChangeId(property) => {                build_commit_or_change_id_method(self, build_ctx, property, function)            }            CommitTemplatePropertyKind::ShortestIdPrefix(property) => {                build_shortest_id_prefix_method(self, build_ctx, property, function)            }        }    }} // If we need to add multiple languages that support Commit types, this can be// turned into a trait which extends TemplateLanguage.impl<'repo> CommitTemplateLanguage<'repo, '_> {    fn wrap_commit(        &self,        property: impl TemplateProperty<Commit, Output = Commit> + 'repo,    ) -> CommitTemplatePropertyKind<'repo> {        CommitTemplatePropertyKind::Commit(Box::new(property))    }     fn wrap_commit_list(        &self,        property: impl TemplateProperty<Commit, Output = Vec<Commit>> + 'repo,    ) -> CommitTemplatePropertyKind<'repo> {        CommitTemplatePropertyKind::CommitList(Box::new(property))    }     fn wrap_ref_name(        &self,        property: impl TemplateProperty<Commit, Output = RefName> + 'repo,    ) -> CommitTemplatePropertyKind<'repo> {        CommitTemplatePropertyKind::RefName(Box::new(property))    }     fn wrap_ref_name_list(        &self,        property: impl TemplateProperty<Commit, Output = Vec<RefName>> + 'repo,    ) -> CommitTemplatePropertyKind<'repo> {        CommitTemplatePropertyKind::RefNameList(Box::new(property))    }     fn wrap_commit_or_change_id(        &self,        property: impl TemplateProperty<Commit, Output = CommitOrChangeId> + 'repo,    ) -> CommitTemplatePropertyKind<'repo> {        CommitTemplatePropertyKind::CommitOrChangeId(Box::new(property))    }     fn wrap_shortest_id_prefix(        &self,        property: impl TemplateProperty<Commit, Output = ShortestIdPrefix> + 'repo,    ) -> CommitTemplatePropertyKind<'repo> {        CommitTemplatePropertyKind::ShortestIdPrefix(Box::new(property))    }} enum CommitTemplatePropertyKind<'repo> {    Core(CoreTemplatePropertyKind<'repo, Commit>),    Commit(Box<dyn TemplateProperty<Commit, Output = Commit> + 'repo>),    CommitList(Box<dyn TemplateProperty<Commit, Output = Vec<Commit>> + 'repo>),    RefName(Box<dyn TemplateProperty<Commit, Output = RefName> + 'repo>),    RefNameList(Box<dyn TemplateProperty<Commit, Output = Vec<RefName>> + 'repo>),    CommitOrChangeId(Box<dyn TemplateProperty<Commit, Output = CommitOrChangeId> + 'repo>),    ShortestIdPrefix(Box<dyn TemplateProperty<Commit, Output = ShortestIdPrefix> + 'repo>),} impl<'repo> IntoTemplateProperty<'repo, Commit> for CommitTemplatePropertyKind<'repo> {    fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Commit, Output = bool> + 'repo>> {        match self {            CommitTemplatePropertyKind::Core(property) => property.try_into_boolean(),            CommitTemplatePropertyKind::Commit(_) => None,            CommitTemplatePropertyKind::CommitList(property) => {                Some(Box::new(TemplateFunction::new(property, |l| !l.is_empty())))            }            CommitTemplatePropertyKind::RefName(_) => None,            CommitTemplatePropertyKind::RefNameList(property) => {                Some(Box::new(TemplateFunction::new(property, |l| !l.is_empty())))            }            CommitTemplatePropertyKind::CommitOrChangeId(_) => None,            CommitTemplatePropertyKind::ShortestIdPrefix(_) => None,        }    }     fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Commit, Output = i64> + 'repo>> {        match self {            CommitTemplatePropertyKind::Core(property) => property.try_into_integer(),            _ => None,        }    }     fn try_into_plain_text(        self,    ) -> Option<Box<dyn TemplateProperty<Commit, Output = String> + 'repo>> {        match self {            CommitTemplatePropertyKind::Core(property) => property.try_into_plain_text(),            _ => {                let template = self.try_into_template()?;                Some(Box::new(PlainTextFormattedProperty::new(template)))            }        }    }     fn try_into_template(self) -> Option<Box<dyn Template<Commit> + 'repo>> {        match self {            CommitTemplatePropertyKind::Core(property) => property.try_into_template(),            CommitTemplatePropertyKind::Commit(_) => None,            CommitTemplatePropertyKind::CommitList(_) => None,            CommitTemplatePropertyKind::RefName(property) => Some(property.into_template()),            CommitTemplatePropertyKind::RefNameList(property) => Some(property.into_template()),            CommitTemplatePropertyKind::CommitOrChangeId(property) => {                Some(property.into_template())            }            CommitTemplatePropertyKind::ShortestIdPrefix(property) => {                Some(property.into_template())            }        }    }} #[derive(Debug, Default)]struct CommitKeywordCache {    // Build index lazily, and Rc to get away from &self lifetime.    branches_index: OnceCell<Rc<RefNamesIndex>>,    tags_index: OnceCell<Rc<RefNamesIndex>>,    git_refs_index: OnceCell<Rc<RefNamesIndex>>,} impl CommitKeywordCache {    fn branches_index(&self, repo: &dyn Repo) -> &Rc<RefNamesIndex> {        self.branches_index            .get_or_init(|| Rc::new(build_branches_index(repo)))    }     fn tags_index(&self, repo: &dyn Repo) -> &Rc<RefNamesIndex> {        self.tags_index            .get_or_init(|| Rc::new(build_ref_names_index(repo.view().tags())))    }     fn git_refs_index(&self, repo: &dyn Repo) -> &Rc<RefNamesIndex> {        self.git_refs_index            .get_or_init(|| Rc::new(build_ref_names_index(repo.view().git_refs())))    }} fn build_commit_keyword<'repo>(    language: &CommitTemplateLanguage<'repo, '_>,    name: &str,    span: pest::Span,) -> TemplateParseResult<CommitTemplatePropertyKind<'repo>> {    // Commit object is lightweight (a few Arc + CommitId), so just clone it    // to turn into a property type. Abstraction over "for<'a> (&'a T) -> &'a T"    // and "(&T) -> T" wouldn't be simple. If we want to remove Clone/Rc/Arc,    // maybe we can add an abstraction that takes "Fn(&Commit) -> O" and returns    // "TemplateProperty<Commit, Output = O>".    let property = TemplatePropertyFn(|commit: &Commit| commit.clone());    build_commit_keyword_opt(language, property, name)        .ok_or_else(|| TemplateParseError::no_such_keyword(name, span))} fn build_commit_method<'repo>(    language: &CommitTemplateLanguage<'repo, '_>,    _build_ctx: &BuildContext<CommitTemplatePropertyKind<'repo>>,    self_property: impl TemplateProperty<Commit, Output = Commit> + 'repo,    function: &FunctionCallNode,) -> TemplateParseResult<CommitTemplatePropertyKind<'repo>> {    if let Some(property) = build_commit_keyword_opt(language, self_property, function.name) {        template_parser::expect_no_arguments(function)?;        Ok(property)    } else {        Err(TemplateParseError::no_such_method("Commit", function))    }} fn build_commit_keyword_opt<'repo>(    language: &CommitTemplateLanguage<'repo, '_>,    property: impl TemplateProperty<Commit, Output = Commit> + 'repo,    name: &str,) -> Option<CommitTemplatePropertyKind<'repo>> {    fn wrap_fn<'repo, O>(        property: impl TemplateProperty<Commit, Output = Commit> + 'repo,        f: impl Fn(&Commit) -> O + 'repo,    ) -> impl TemplateProperty<Commit, Output = O> + 'repo {        TemplateFunction::new(property, move |commit| f(&commit))    }    fn wrap_repo_fn<'repo, O>(        repo: &'repo dyn Repo,        property: impl TemplateProperty<Commit, Output = Commit> + 'repo,        f: impl Fn(&dyn Repo, &Commit) -> O + 'repo,    ) -> impl TemplateProperty<Commit, Output = O> + 'repo {        TemplateFunction::new(property, move |commit| f(repo, &commit))    }     let repo = language.repo;    let cache = &language.keyword_cache;    let property = match name {        "description" => language.wrap_string(wrap_fn(property, |commit| {            text_util::complete_newline(commit.description())        })),        "change_id" => language.wrap_commit_or_change_id(wrap_fn(property, |commit| {            CommitOrChangeId::Change(commit.change_id().to_owned())        })),        "commit_id" => language.wrap_commit_or_change_id(wrap_fn(property, |commit| {            CommitOrChangeId::Commit(commit.id().to_owned())        })),        "parents" => language.wrap_commit_list(wrap_fn(property, |commit| commit.parents())),        "author" => language.wrap_signature(wrap_fn(property, |commit| commit.author().clone())),        "committer" => {            language.wrap_signature(wrap_fn(property, |commit| commit.committer().clone()))        }        "working_copies" => {            language.wrap_string(wrap_repo_fn(repo, property, extract_working_copies))        }        "current_working_copy" => {            let workspace_id = language.workspace_id.clone();            language.wrap_boolean(wrap_fn(property, move |commit| {                Some(commit.id()) == repo.view().get_wc_commit_id(&workspace_id)            }))        }        "branches" => {            let index = cache.branches_index(repo).clone();            language.wrap_ref_name_list(wrap_fn(property, move |commit| {                index                    .get(commit.id())                    .iter()                    .filter(|ref_name| ref_name.is_local() || !ref_name.synced)                    .cloned()                    .collect()            }))        }        "local_branches" => {            let index = cache.branches_index(repo).clone();            language.wrap_ref_name_list(wrap_fn(property, move |commit| {                index                    .get(commit.id())                    .iter()                    .filter(|ref_name| ref_name.is_local())                    .cloned()                    .collect()            }))        }        "remote_branches" => {            let index = cache.branches_index(repo).clone();            language.wrap_ref_name_list(wrap_fn(property, move |commit| {                index                    .get(commit.id())                    .iter()                    .filter(|ref_name| ref_name.is_remote())                    .cloned()                    .collect()            }))        }        "tags" => {            let index = cache.tags_index(repo).clone();            language.wrap_ref_name_list(wrap_fn(property, move |commit| {                index.get(commit.id()).to_vec()            }))        }        "git_refs" => {            let index = cache.git_refs_index(repo).clone();            language.wrap_ref_name_list(wrap_fn(property, move |commit| {                index.get(commit.id()).to_vec()            }))        }        "git_head" => language.wrap_ref_name_list(wrap_repo_fn(repo, property, extract_git_head)),        "divergent" => language.wrap_boolean(wrap_fn(property, |commit| {            // The given commit could be hidden in e.g. obslog.            let maybe_entries = repo.resolve_change_id(commit.change_id());            maybe_entries.map_or(0, |entries| entries.len()) > 1        })),        "hidden" => language.wrap_boolean(wrap_fn(property, |commit| {            let maybe_entries = repo.resolve_change_id(commit.change_id());            maybe_entries.map_or(true, |entries| !entries.contains(commit.id()))        })),        "conflict" => {            language.wrap_boolean(wrap_fn(property, |commit| commit.has_conflict().unwrap()))        }        "empty" => language.wrap_boolean(wrap_fn(property, |commit| {            if let [parent] = &commit.parents()[..] {                return parent.tree_id() == commit.tree_id();            }            let parent_tree = rewrite::merge_commit_trees(repo, &commit.parents()).unwrap();            *commit.tree_id() == parent_tree.id()        })),        "root" => language.wrap_boolean(wrap_fn(property, move |commit| {            commit.id() == repo.store().root_commit_id()        })),        _ => return None,    };    Some(property)} // TODO: return Vec<String>fn extract_working_copies(repo: &dyn Repo, commit: &Commit) -> String {    let wc_commit_ids = repo.view().wc_commit_ids();    if wc_commit_ids.len() <= 1 {        return "".to_string();    }    let mut names = vec![];    for (workspace_id, wc_commit_id) in wc_commit_ids.iter().sorted() {        if wc_commit_id == commit.id() {            names.push(format!("{}@", workspace_id.as_str()));        }    }    names.join(" ")} /// Branch or tag name with metadata.#[derive(Clone, Debug, Eq, PartialEq)]struct RefName {    /// Local name.    name: String,    /// Remote name if this is a remote or Git-tracking ref.    remote: Option<String>,    /// Ref target has conflicts.    conflict: bool,    /// Local ref is synchronized with all tracking remotes, or tracking remote    /// ref is synchronized with the local.    synced: bool,} impl RefName {    fn is_local(&self) -> bool {        self.remote.is_none()    }     fn is_remote(&self) -> bool {        self.remote.is_some()    }} impl Template<()> for RefName {    fn format(&self, _: &(), formatter: &mut dyn Formatter) -> io::Result<()> {        write!(formatter.labeled("name"), "{}", self.name)?;        if let Some(remote) = &self.remote {            write!(formatter, "@")?;            write!(formatter.labeled("remote"), "{remote}")?;        }        // Don't show both conflict and unsynced sigils as conflicted ref wouldn't        // be pushed.        if self.conflict {            write!(formatter, "??")?;        } else if self.is_local() && !self.synced {            write!(formatter, "*")?;        }        Ok(())    }} impl Template<()> for Vec<RefName> {    fn format(&self, _: &(), formatter: &mut dyn Formatter) -> io::Result<()> {        templater::format_joined(&(), formatter, self, " ")    }} fn build_ref_name_method<'repo>(    language: &CommitTemplateLanguage<'repo, '_>,    _build_ctx: &BuildContext<CommitTemplatePropertyKind<'repo>>,    self_property: impl TemplateProperty<Commit, Output = RefName> + 'repo,    function: &FunctionCallNode,) -> TemplateParseResult<CommitTemplatePropertyKind<'repo>> {    let property = match function.name {        "name" => {            template_parser::expect_no_arguments(function)?;            language.wrap_string(TemplateFunction::new(self_property, |ref_name| {                ref_name.name            }))        }        "remote" => {            template_parser::expect_no_arguments(function)?;            language.wrap_string(TemplateFunction::new(self_property, |ref_name| {                ref_name.remote.unwrap_or_default()            }))        }        // TODO: expose conflict, synced, remote.is_some()        _ => return Err(TemplateParseError::no_such_method("RefName", function)),    };    Ok(property)} /// Cache for reverse lookup refs.#[derive(Clone, Debug, Default)]struct RefNamesIndex {    index: HashMap<CommitId, Vec<RefName>>,} impl RefNamesIndex {    fn insert<'a>(&mut self, ids: impl IntoIterator<Item = &'a CommitId>, name: RefName) {        for id in ids {            let ref_names = self.index.entry(id.clone()).or_default();            ref_names.push(name.clone());        }    }     fn get(&self, id: &CommitId) -> &[RefName] {        if let Some(names) = self.index.get(id) {            names        } else {            &[]        }    }} fn build_branches_index(repo: &dyn Repo) -> RefNamesIndex {    let mut index = RefNamesIndex::default();    for (branch_name, branch_target) in repo.view().branches() {        let local_target = branch_target.local_target;

Lines 1–500 of 728.