diff --git a/CHANGELOG.md b/CHANGELOG.md index 24c05d90c5..d8eff703a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * New command `jj git remote set-url` that sets the url of a git remote. +* Support for [`.mailmap`](https://git-scm.com/docs/gitmailmap) files has + been added. + ### Fixed bugs * `jj git push` now ignores immutable commits when checking whether a diff --git a/Cargo.lock b/Cargo.lock index 0677eb222e..013ce804db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1080,9 +1080,9 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "367ee9093b0c2b04fd04c5c7c8b6a1082713534eab537597ae343663a518fa99" +checksum = "9eed6931f21491ee0aeb922751bd7ec97b4b2fe8fbfedcb678e2a2dce5f3b8c0" dependencies = [ "bstr", "itoa", @@ -1234,6 +1234,18 @@ dependencies = [ "syn", ] +[[package]] +name = "gix-mailmap" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30d52feec20210e102ef0dd53b7e3265a6ac8ccd492b2d67f0e357d7a8d8439" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "thiserror", +] + [[package]] name = "gix-object" version = "0.42.2" @@ -1736,6 +1748,9 @@ dependencies = [ "futures 0.3.30", "git2", "gix", + "gix-actor", + "gix-date", + "gix-mailmap", "glob", "hex", "ignore", diff --git a/Cargo.toml b/Cargo.toml index 182d48c880..3420d76516 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,11 @@ gix = { version = "0.63.0", default-features = false, features = [ "index", "max-performance-safe", ] } +# We list `gix-{actor,date,mailmap}` separately, as they are required +# even when the Git backend is disabled. +gix-actor = { version = "0.31.2" } +gix-date = { version = "0.8.7" } +gix-mailmap = { version = "0.23.2" } glob = "0.3.1" hex = "0.4.3" ignore = "0.4.20" diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index a72e89e9ff..3a6a96ca25 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -41,6 +41,7 @@ use jj_lib::git_backend::GitBackend; use jj_lib::gitignore::{GitIgnoreError, GitIgnoreFile}; use jj_lib::hex_util::to_reverse_hex; use jj_lib::id_prefix::IdPrefixContext; +use jj_lib::mailmap::get_current_mailmap; use jj_lib::matchers::Matcher; use jj_lib::merge::MergedTreeValue; use jj_lib::merged_tree::MergedTree; @@ -991,11 +992,16 @@ impl WorkspaceCommandHelper { path_converter: &self.path_converter, workspace_id: self.workspace_id(), }; + let mailmap = Rc::new(get_current_mailmap( + self.repo().as_ref(), + self.workspace_id(), + )); RevsetParseContext::new( &self.revset_aliases_map, self.settings.user_email(), &self.revset_extensions, Some(workspace_context), + mailmap, ) } diff --git a/cli/src/commit_templater.rs b/cli/src/commit_templater.rs index df276bdfa3..fb3aec486e 100644 --- a/cli/src/commit_templater.rs +++ b/cli/src/commit_templater.rs @@ -25,6 +25,7 @@ use jj_lib::extensions_map::ExtensionsMap; use jj_lib::git; use jj_lib::hex_util::to_reverse_hex; use jj_lib::id_prefix::IdPrefixContext; +use jj_lib::mailmap::{get_current_mailmap, Mailmap}; use jj_lib::object_id::ObjectId as _; use jj_lib::op_store::{RefTarget, RemoteRef, WorkspaceId}; use jj_lib::repo::Repo; @@ -61,6 +62,7 @@ pub struct CommitTemplateLanguage<'repo> { build_fn_table: CommitTemplateBuildFnTable<'repo>, keyword_cache: CommitKeywordCache<'repo>, cache_extensions: ExtensionsMap, + mailmap: Rc, } impl<'repo> CommitTemplateLanguage<'repo> { @@ -83,6 +85,8 @@ impl<'repo> CommitTemplateLanguage<'repo> { .build_cache_extensions(&mut cache_extensions); } + let mailmap = Rc::new(get_current_mailmap(repo, workspace_id)); + CommitTemplateLanguage { repo, workspace_id: workspace_id.clone(), @@ -91,6 +95,7 @@ impl<'repo> CommitTemplateLanguage<'repo> { build_fn_table, keyword_cache: CommitKeywordCache::default(), cache_extensions, + mailmap, } } } @@ -468,8 +473,14 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm Ok(L::wrap_commit_list(out_property)) }, ); + map.insert("author", |language, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let mailmap = language.mailmap.clone(); + let out_property = self_property.map(move |commit| mailmap.author(&commit)); + Ok(L::wrap_signature(out_property)) + }); map.insert( - "author", + "author_raw", |_language, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit| commit.author_raw().clone()); @@ -478,6 +489,15 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm ); map.insert( "committer", + |language, _build_ctx, self_property, function| { + function.expect_no_arguments()?; + let mailmap = language.mailmap.clone(); + let out_property = self_property.map(move |commit| mailmap.committer(&commit)); + Ok(L::wrap_signature(out_property)) + }, + ); + map.insert( + "committer_raw", |_language, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit| commit.committer_raw().clone()); @@ -486,8 +506,10 @@ fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Comm ); map.insert("mine", |language, _build_ctx, self_property, function| { function.expect_no_arguments()?; + let mailmap = language.mailmap.clone(); let user_email = language.revset_parse_context.user_email().to_owned(); - let out_property = self_property.map(move |commit| commit.author_raw().email == user_email); + let out_property = + self_property.map(move |commit| mailmap.author(&commit).email == user_email); Ok(L::wrap_boolean(out_property)) }); map.insert( diff --git a/cli/tests/common/mod.rs b/cli/tests/common/mod.rs index 72f4eb5a51..1bb47bef99 100644 --- a/cli/tests/common/mod.rs +++ b/cli/tests/common/mod.rs @@ -160,7 +160,7 @@ impl TestEnvironment { cmd } - fn get_ok(&self, mut cmd: assert_cmd::Command) -> (String, String) { + pub(crate) fn get_ok(&self, mut cmd: assert_cmd::Command) -> (String, String) { let assert = cmd.assert().success(); let stdout = self.normalize_output(&get_stdout_string(&assert)); let stderr = self.normalize_output(&get_stderr_string(&assert)); diff --git a/cli/tests/runner.rs b/cli/tests/runner.rs index c143db0975..86d487c2dd 100644 --- a/cli/tests/runner.rs +++ b/cli/tests/runner.rs @@ -43,6 +43,7 @@ mod test_immutable_commits; mod test_init_command; mod test_interdiff_command; mod test_log_command; +mod test_mailmap; mod test_move_command; mod test_new_command; mod test_next_prev_commands; diff --git a/cli/tests/test_mailmap.rs b/cli/tests/test_mailmap.rs new file mode 100644 index 0000000000..c689729448 --- /dev/null +++ b/cli/tests/test_mailmap.rs @@ -0,0 +1,162 @@ +// Copyright 2024 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 crate::common::TestEnvironment; + +#[test] +fn test_mailmap() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + let repo_path = test_env.env_root().join("repo"); + + let mut mailmap = String::new(); + let mailmap_path = repo_path.join(".mailmap"); + let mut append_mailmap = move |extra| { + mailmap.push_str(extra); + std::fs::write(&mailmap_path, &mailmap).unwrap() + }; + + let jj_cmd_ok_as = |name: &str, email: &str, args: &[&str]| { + let mut cmd = test_env.jj_cmd(&repo_path, args); + cmd.env("JJ_USER", name); + cmd.env("JJ_EMAIL", email); + test_env.get_ok(cmd) + }; + + append_mailmap("# test comment\n"); + + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "author"]); + insta::assert_snapshot!(stdout, @r###" + @ Test User + ◉ + "###); + + // Map an email address without any name change. + jj_cmd_ok_as("Test Üser", "test.user@example.com", &["new"]); + append_mailmap(" \n"); + + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "author"]); + insta::assert_snapshot!(stdout, @r###" + @ Test Üser + ◉ Test User + ◉ + "###); + + // Map an email address to a new name. + jj_cmd_ok_as("West User", "xest.user@example.com", &["new"]); + append_mailmap("Fest User \n"); + + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "author"]); + insta::assert_snapshot!(stdout, @r###" + @ Fest User + ◉ Test Üser + ◉ Test User + ◉ + "###); + + // Map an email address to a new name and email address. + jj_cmd_ok_as("Pest User", "pest.user@example.com", &["new"]); + append_mailmap("Best User \n"); + + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "author"]); + insta::assert_snapshot!(stdout, @r###" + @ Best User + ◉ Fest User + ◉ Test Üser + ◉ Test User + ◉ + "###); + + // Map an ambiguous email address using names for disambiguation. + jj_cmd_ok_as("Rest User", "user@test", &["new"]); + jj_cmd_ok_as("Vest User", "user@test", &["new"]); + append_mailmap( + &[ + "Jest User Rest User \n", + "Zest User Vest User \n", + ] + .concat(), + ); + + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "author"]); + insta::assert_snapshot!(stdout, @r###" + @ Zest User + ◉ Jest User + ◉ Best User + ◉ Fest User + ◉ Test Üser + ◉ Test User + ◉ + "###); + + // The `.mailmap` file in the current workspace’s @ commit should be used. + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "author", "--at-operation=@-"]); + insta::assert_snapshot!(stdout, @r###" + @ Vest User + ◉ Rest User + ◉ Best User + ◉ Fest User + ◉ Test Üser + ◉ Test User + ◉ + "###); + + // The `author(pattern)` revset function should find mapped committers. + let stdout = + test_env.jj_cmd_success(&repo_path, &["log", "-T", "author", "-r", "author(best)"]); + insta::assert_snapshot!(stdout, @r###" + ◉ Best User + │ + ~ + "###); + + // The `author(pattern)` revset function should only search the mapped form. + // This matches Git’s behaviour and avoids some tricky implementation questions. + let stdout = + test_env.jj_cmd_success(&repo_path, &["log", "-T", "author", "-r", "author(pest)"]); + insta::assert_snapshot!(stdout, @r###" + "###); + + // The `author_raw(pattern)` revset function should search the unmapped + // commit data. + let stdout = test_env.jj_cmd_success( + &repo_path, + &["log", "-T", "author", "-r", "author_raw(\"user@test\")"], + ); + insta::assert_snapshot!(stdout, @r###" + @ Zest User + ◉ Jest User + │ + ~ + "###); + + // `mine()` should only search the mapped author; this may be confusing in this + // case, but matches the semantics of it expanding to `author(‹user.email›)`. + let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "author", "-r", "mine()"]); + insta::assert_snapshot!(stdout, @r###" + "###); + + // `mine()` should find commits that map to the current `user.email`. + let (stdout, _stderr) = jj_cmd_ok_as( + "Tëst Üser", + "test.user@example.net", + &["log", "-T", "author", "-r", "mine()"], + ); + insta::assert_snapshot!(stdout, @r###" + ◉ Test Üser + ◉ Test User + │ + ~ + "###); +} diff --git a/cli/tests/test_revset_output.rs b/cli/tests/test_revset_output.rs index df723efbb2..7d6d8b1867 100644 --- a/cli/tests/test_revset_output.rs +++ b/cli/tests/test_revset_output.rs @@ -290,7 +290,7 @@ fn test_function_name_hint() { | ^-----^ | = Function "author_" doesn't exist - Hint: Did you mean "author", "my_author"? + Hint: Did you mean "author", "author_raw", "my_author"? "###); insta::assert_snapshot!(evaluate_err("my_branches"), @r###" diff --git a/docs/revsets.md b/docs/revsets.md index b7676ec561..e1a5327bd8 100644 --- a/docs/revsets.md +++ b/docs/revsets.md @@ -251,11 +251,17 @@ revsets (expressions) as arguments. * `author(pattern)`: Commits with the author's name or email matching the given [string pattern](#string-patterns). +* `author_raw(pattern)`: Like `author(pattern)`, but ignoring any mappings in + the [`.mailmap` file](https://git-scm.com/docs/gitmailmap). + * `mine()`: Commits where the author's email matches the email of the current user. -* `committer(pattern)`: Commits with the committer's name or email matching the -given [string pattern](#string-patterns). +* `committer(pattern)`: Commits with the committer's name or email matching the + given [string pattern](#string-patterns). + +* `committer_raw(pattern)`: Like `committer(pattern)`, but ignoring any + mappings in the [`.mailmap` file](https://git-scm.com/docs/gitmailmap). * `empty()`: Commits modifying no files. This also includes `merges()` without user modifications and `root()`. diff --git a/docs/templates.md b/docs/templates.md index 6317925514..5b4e441538 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -73,7 +73,11 @@ This type cannot be printed. The following methods are defined. * `commit_id() -> CommitId` * `parents() -> List` * `author() -> Signature` +* `author_raw() -> Signature`: Like `author()`, but ignoring any mappings in + the [`.mailmap` file](https://git-scm.com/docs/gitmailmap). * `committer() -> Signature` +* `committer_raw() -> Signature`: Like `committer()`, but ignoring any mappings + in the [`.mailmap` file](https://git-scm.com/docs/gitmailmap). * `mine() -> Boolean`: Commits where the author's email matches the email of the current user. * `working_copies() -> String`: For multi-workspace repository, indicate diff --git a/lib/Cargo.toml b/lib/Cargo.toml index e6a47dede4..b192c36d35 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -44,6 +44,9 @@ either = { workspace = true } futures = { workspace = true } git2 = { workspace = true, optional = true } gix = { workspace = true, optional = true } +gix-actor = { workspace = true } +gix-date = { workspace = true } +gix-mailmap = { workspace = true } glob = { workspace = true } hex = { workspace = true } ignore = { workspace = true } diff --git a/lib/src/commit.rs b/lib/src/commit.rs index 30ce536524..00e3226b15 100644 --- a/lib/src/commit.rs +++ b/lib/src/commit.rs @@ -146,11 +146,25 @@ impl Commit { } /// Returns the raw author signature from the commit data. + /// + /// **Note:** You usually **should not** directly process or display this + /// information before canonicalizing it. Prefer + /// [`Mailmap::author`][`crate::mailmap::Mailmap::author`] unless you + /// care specficially about the potentially‐outdated immutable commit data, + /// or are performing low‐level operations in a context that can’t obtain a + /// [`Mailmap`][`crate::mailmap::Mailmap`]. pub fn author_raw(&self) -> &Signature { &self.data.author } /// Returns the raw committer signature from the commit data. + /// + /// **Note:** You usually **should not** directly process or display this + /// information before canonicalizing it. Prefer + /// [`Mailmap::committer`][`crate::mailmap::Mailmap::committer`] unless you + /// care specficially about the potentially‐outdated immutable commit + /// data, or are performing low‐level operations in a context that can’t + /// obtain a [`Mailmap`][`crate::mailmap::Mailmap`]. pub fn committer_raw(&self) -> &Signature { &self.data.committer } diff --git a/lib/src/default_index/revset_engine.rs b/lib/src/default_index/revset_engine.rs index f5f78edce9..cdf57ef19c 100644 --- a/lib/src/default_index/revset_engine.rs +++ b/lib/src/default_index/revset_engine.rs @@ -1049,11 +1049,21 @@ fn build_predicate_fn( pattern.matches(commit.description()) }) } - RevsetFilterPredicate::Author(pattern) => { + RevsetFilterPredicate::Author(pattern, mailmap) => { let pattern = pattern.clone(); + let mailmap = mailmap.clone(); // TODO: Make these functions that take a needle to search for accept some // syntax for specifying whether it's a regex and whether it's // case-sensitive. + box_pure_predicate_fn(move |index, pos| { + let entry = index.entry_by_pos(pos); + let commit = store.get_commit(&entry.commit_id()).unwrap(); + let author = mailmap.author(&commit); + pattern.matches(&author.name) || pattern.matches(&author.email) + }) + } + RevsetFilterPredicate::AuthorRaw(pattern) => { + let pattern = pattern.clone(); box_pure_predicate_fn(move |index, pos| { let entry = index.entry_by_pos(pos); let commit = store.get_commit(&entry.commit_id()).unwrap(); @@ -1061,7 +1071,17 @@ fn build_predicate_fn( || pattern.matches(&commit.author_raw().email) }) } - RevsetFilterPredicate::Committer(pattern) => { + RevsetFilterPredicate::Committer(pattern, mailmap) => { + let pattern = pattern.clone(); + let mailmap = mailmap.clone(); + box_pure_predicate_fn(move |index, pos| { + let entry = index.entry_by_pos(pos); + let commit = store.get_commit(&entry.commit_id()).unwrap(); + let committer = mailmap.committer(&commit); + pattern.matches(&committer.name) || pattern.matches(&committer.email) + }) + } + RevsetFilterPredicate::CommitterRaw(pattern) => { let pattern = pattern.clone(); box_pure_predicate_fn(move |index, pos| { let entry = index.entry_by_pos(pos); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 149956ad2b..160b9fb4db 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -57,6 +57,7 @@ pub mod index; pub mod local_backend; pub mod local_working_copy; pub mod lock; +pub mod mailmap; pub mod matchers; pub mod merge; pub mod merged_tree; diff --git a/lib/src/mailmap.rs b/lib/src/mailmap.rs new file mode 100644 index 0000000000..c291fe821b --- /dev/null +++ b/lib/src/mailmap.rs @@ -0,0 +1,125 @@ +// Copyright 2024 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. + +//! Support for `.mailmap` files. + +use std::fmt::Debug; +use std::io::{self, Read}; + +use pollster::FutureExt; + +use crate::backend::Signature; +use crate::commit::Commit; +use crate::conflicts::{materialize_tree_value, MaterializedTreeValue}; +use crate::op_store::WorkspaceId; +use crate::repo::Repo; +use crate::repo_path::RepoPath; + +/// Models a `.mailmap` file, mapping email addresses and names to +/// canonical ones. +/// +/// The syntax and semantics are as described in +/// [`gitmailmap(5)`](https://git-scm.com/docs/gitmailmap). +/// +/// You can obtain the currently‐applicable [`Mailmap`] using +/// [`get_current_mailmap`]. +/// +/// An empty [`Mailmap`] does not use any heap allocations, and an absent +/// `.mailmap` file is semantically equivalent to an empty one, so there is +/// usually no need to wrap this type in [`Option`]. +#[derive(Clone, Default)] +pub struct Mailmap(gix_mailmap::Snapshot); + +impl Debug for Mailmap { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("Mailmap").field(&self.0.entries()).finish() + } +} + +impl PartialEq for Mailmap { + fn eq(&self, other: &Self) -> bool { + self.0.entries() == other.0.entries() + } +} + +impl Eq for Mailmap {} + +impl Mailmap { + /// Parses a `.mailmap` file, ignoring parse errors. + pub fn from_bytes(bytes: &[u8]) -> Self { + Self(gix_mailmap::Snapshot::from_bytes(bytes)) + } + + /// Reads and parses a `.mailmap` file from a reader, ignoring parse errors. + pub fn from_reader(reader: &mut R) -> io::Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes)?; + Ok(Self::from_bytes(&bytes)) + } + + /// Returns the canonical signature corresponding to the raw `signature`. + /// The timestamp is left untouched. Signatures with no corresponding entry + /// are returned as‐is. + pub fn resolve(&self, signature: &Signature) -> Signature { + let result = self.0.try_resolve(gix_actor::SignatureRef { + name: signature.name.as_bytes().into(), + email: signature.email.as_bytes().into(), + time: Default::default(), + }); + match result { + Some(canonical) => Signature { + name: String::from_utf8_lossy(&canonical.name).into(), + email: String::from_utf8_lossy(&canonical.email).into(), + timestamp: signature.timestamp.clone(), + }, + None => signature.clone(), + } + } + + /// Returns the canonical author signature of `commit`. + pub fn author(&self, commit: &Commit) -> Signature { + self.resolve(commit.author_raw()) + } + + /// Returns the canonical committer signature of `commit`. + pub fn committer(&self, commit: &Commit) -> Signature { + self.resolve(commit.committer_raw()) + } +} + +/// Reads and parses the `.mailmap` file from the working‐copy commit of the +/// specified workspace. An absent `.mailmap` is treated the same way +/// as an empty file, and any errors finding or materializing the file are +/// treated the same way. Parse errors when reading the file are ignored, but +/// the rest of the file will still be processed. +pub fn get_current_mailmap(repo: &dyn Repo, workspace_id: &WorkspaceId) -> Mailmap { + let inner = || { + let commit_id = repo.view().get_wc_commit_id(workspace_id)?; + let commit = repo.store().get_commit(commit_id).ok()?; + let tree = commit.tree().ok()?; + let path = RepoPath::from_internal_string(".mailmap"); + let value = tree.path_value(path).ok()?; + // We ignore symbolic links, as per `gitmailmap(5)`. + // + // TODO: Figure out how conflicts should be handled here. + let materialized = materialize_tree_value(repo.store(), path, value) + .block_on() + .ok()?; + let MaterializedTreeValue::File { mut reader, .. } = materialized else { + return None; + }; + Mailmap::from_reader(&mut reader).ok() + }; + inner().unwrap_or_default() +} diff --git a/lib/src/revset.rs b/lib/src/revset.rs index ddf5dc397e..963eca3ad6 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -33,6 +33,7 @@ use crate::fileset::{FilePattern, FilesetExpression}; use crate::graph::GraphEdge; use crate::hex_util::to_forward_hex; use crate::id_prefix::IdPrefixContext; +use crate::mailmap::Mailmap; use crate::object_id::{HexPrefix, PrefixResolution}; use crate::op_store::WorkspaceId; use crate::repo::Repo; @@ -141,9 +142,13 @@ pub enum RevsetFilterPredicate { /// Commits with description containing the needle. Description(StringPattern), /// Commits with author's name or email containing the needle. - Author(StringPattern), + Author(StringPattern, Rc), + /// Commits with author's unmapped name or email containing the needle. + AuthorRaw(StringPattern), /// Commits with committer's name or email containing the needle. - Committer(StringPattern), + Committer(StringPattern, Rc), + /// Commits with committer's unmapped name or email containing the needle. + CommitterRaw(StringPattern), /// Commits modifying the paths specified by the fileset. File(FilesetExpression), /// Commits with conflicts @@ -691,26 +696,43 @@ static BUILTIN_FUNCTION_MAP: Lazy> = Lazy: RevsetFilterPredicate::Description(pattern), )) }); - map.insert("author", |function, _context| { + map.insert("author", |function, context| { let [arg] = function.expect_exact_arguments()?; let pattern = expect_string_pattern(arg)?; Ok(RevsetExpression::filter(RevsetFilterPredicate::Author( pattern, + context.mailmap.clone(), + ))) + }); + map.insert("author_raw", |function, _context| { + let [arg] = function.expect_exact_arguments()?; + let pattern = expect_string_pattern(arg)?; + Ok(RevsetExpression::filter(RevsetFilterPredicate::AuthorRaw( + pattern, ))) }); map.insert("mine", |function, context| { function.expect_no_arguments()?; Ok(RevsetExpression::filter(RevsetFilterPredicate::Author( StringPattern::Exact(context.user_email.to_owned()), + context.mailmap.clone(), ))) }); - map.insert("committer", |function, _context| { + map.insert("committer", |function, context| { let [arg] = function.expect_exact_arguments()?; let pattern = expect_string_pattern(arg)?; Ok(RevsetExpression::filter(RevsetFilterPredicate::Committer( pattern, + context.mailmap.clone(), ))) }); + map.insert("committer_raw", |function, _context| { + let [arg] = function.expect_exact_arguments()?; + let pattern = expect_string_pattern(arg)?; + Ok(RevsetExpression::filter( + RevsetFilterPredicate::CommitterRaw(pattern), + )) + }); map.insert("empty", |function, _context| { function.expect_no_arguments()?; Ok(RevsetExpression::is_empty()) @@ -1992,6 +2014,7 @@ pub struct RevsetParseContext<'a> { user_email: String, extensions: &'a RevsetExtensions, workspace: Option>, + mailmap: Rc, } impl<'a> RevsetParseContext<'a> { @@ -2000,12 +2023,14 @@ impl<'a> RevsetParseContext<'a> { user_email: String, extensions: &'a RevsetExtensions, workspace: Option>, + mailmap: Rc, ) -> Self { Self { aliases_map, user_email, extensions, workspace, + mailmap, } } @@ -2063,6 +2088,7 @@ mod tests { "test.user@example.com".to_string(), &extensions, None, + Default::default(), ); // Map error to comparable object super::parse(revset_str, &context).map_err(|e| e.kind) @@ -2092,6 +2118,7 @@ mod tests { "test.user@example.com".to_string(), &extensions, Some(workspace_ctx), + Default::default(), ); // Map error to comparable object super::parse(revset_str, &context).map_err(|e| e.kind) @@ -2117,6 +2144,7 @@ mod tests { "test.user@example.com".to_string(), &extensions, None, + Default::default(), ); // Map error to comparable object super::parse_with_modifier(revset_str, &context).map_err(|e| e.kind) @@ -2296,6 +2324,7 @@ mod tests { parse(r#"author("foo@")"#), Ok(RevsetExpression::filter(RevsetFilterPredicate::Author( StringPattern::Substring("foo@".to_string()), + Default::default(), ))) ); // Parse a single symbol @@ -2456,7 +2485,8 @@ mod tests { assert_eq!( parse("mine()"), Ok(RevsetExpression::filter(RevsetFilterPredicate::Author( - StringPattern::Exact("test.user@example.com".to_string()) + StringPattern::Exact("test.user@example.com".to_string()), + Default::default(), ))) ); assert_eq!( @@ -3047,6 +3077,9 @@ mod tests { Substring( "foo", ), + Mailmap( + [], + ), ), ), ) @@ -3068,6 +3101,9 @@ mod tests { Substring( "bar", ), + Mailmap( + [], + ), ), ), ) @@ -3089,6 +3125,9 @@ mod tests { Substring( "bar", ), + Mailmap( + [], + ), ), ), CommitRef( @@ -3117,6 +3156,9 @@ mod tests { Substring( "foo", ), + Mailmap( + [], + ), ), ), ) @@ -3131,6 +3173,9 @@ mod tests { Substring( "foo", ), + Mailmap( + [], + ), ), ) "###); @@ -3163,6 +3208,9 @@ mod tests { Substring( "foo", ), + Mailmap( + [], + ), ), ), ) @@ -3175,6 +3223,9 @@ mod tests { Substring( "foo", ), + Mailmap( + [], + ), ), ), Filter( @@ -3182,6 +3233,9 @@ mod tests { Substring( "bar", ), + Mailmap( + [], + ), ), ), ) @@ -3209,6 +3263,9 @@ mod tests { Substring( "baz", ), + Mailmap( + [], + ), ), ), ) @@ -3227,6 +3284,9 @@ mod tests { Substring( "foo", ), + Mailmap( + [], + ), ), ), ), @@ -3235,6 +3295,9 @@ mod tests { Substring( "baz", ), + Mailmap( + [], + ), ), ), ) @@ -3253,6 +3316,9 @@ mod tests { Substring( "foo", ), + Mailmap( + [], + ), ), ), ), @@ -3276,6 +3342,9 @@ mod tests { Substring( "foo", ), + Mailmap( + [], + ), ), ), Filter( @@ -3293,6 +3362,9 @@ mod tests { Substring( "baz", ), + Mailmap( + [], + ), ), ), ) @@ -3352,6 +3424,9 @@ mod tests { Substring( "baz", ), + Mailmap( + [], + ), ), ), ) @@ -3372,6 +3447,9 @@ mod tests { Substring( "baz", ), + Mailmap( + [], + ), ), ), generation: 1..2, @@ -3413,6 +3491,9 @@ mod tests { Substring( "baz", ), + Mailmap( + [], + ), ), ), ), @@ -3459,6 +3540,9 @@ mod tests { Substring( "A", ), + Mailmap( + [], + ), ), ), ), @@ -3467,6 +3551,9 @@ mod tests { Substring( "B", ), + Mailmap( + [], + ), ), ), ), @@ -3475,6 +3562,9 @@ mod tests { Substring( "C", ), + Mailmap( + [], + ), ), ), ) @@ -3516,6 +3606,9 @@ mod tests { Substring( "A", ), + Mailmap( + [], + ), ), ), ), @@ -3524,6 +3617,9 @@ mod tests { Substring( "B", ), + Mailmap( + [], + ), ), ), ), @@ -3532,6 +3628,9 @@ mod tests { Substring( "C", ), + Mailmap( + [], + ), ), ), ) @@ -3561,6 +3660,9 @@ mod tests { Substring( "baz", ), + Mailmap( + [], + ), ), ), ) @@ -3584,6 +3686,9 @@ mod tests { Substring( "foo", ), + Mailmap( + [], + ), ), ), CommitRef( @@ -3617,6 +3722,9 @@ mod tests { Substring( "bar", ), + Mailmap( + [], + ), ), ), ), @@ -3657,6 +3765,9 @@ mod tests { Substring( "foo", ), + Mailmap( + [], + ), ), ), ), @@ -3708,6 +3819,9 @@ mod tests { Substring( "A", ), + Mailmap( + [], + ), ), ), CommitRef( @@ -3725,6 +3839,9 @@ mod tests { Substring( "B", ), + Mailmap( + [], + ), ), ), CommitRef( @@ -3742,6 +3859,9 @@ mod tests { Substring( "C", ), + Mailmap( + [], + ), ), ), CommitRef( diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index 8341837ff1..f43995f92d 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -13,6 +13,7 @@ // limitations under the License. use std::path::Path; +use std::rc::Rc; use assert_matches::assert_matches; use itertools::Itertools; @@ -22,6 +23,7 @@ use jj_lib::fileset::FilesetExpression; use jj_lib::git; use jj_lib::git_backend::GitBackend; use jj_lib::graph::{GraphEdge, ReverseGraphIterator}; +use jj_lib::mailmap::{get_current_mailmap, Mailmap}; use jj_lib::object_id::ObjectId; use jj_lib::op_store::{RefTarget, RemoteRef, RemoteRefState, WorkspaceId}; use jj_lib::repo::Repo; @@ -45,7 +47,13 @@ fn resolve_symbol_with_extensions( symbol: &str, ) -> Result, RevsetResolutionError> { let aliases_map = RevsetAliasesMap::default(); - let context = RevsetParseContext::new(&aliases_map, String::new(), extensions, None); + let context = RevsetParseContext::new( + &aliases_map, + String::new(), + extensions, + None, + Default::default(), + ); let expression = parse(symbol, &context).unwrap(); assert_matches!(*expression, RevsetExpression::CommitRef(_)); let symbol_resolver = DefaultSymbolResolver::new(repo, extensions.symbol_resolvers()); @@ -179,7 +187,13 @@ fn test_resolve_symbol_commit_id() { ); let aliases_map = RevsetAliasesMap::default(); let extensions = RevsetExtensions::default(); - let context = RevsetParseContext::new(&aliases_map, settings.user_email(), &extensions, None); + let context = RevsetParseContext::new( + &aliases_map, + settings.user_email(), + &extensions, + None, + Default::default(), + ); assert_matches!( optimize(parse("present(04)", &context).unwrap()).resolve_user_expression(repo.as_ref(), &symbol_resolver), Err(RevsetResolutionError::AmbiguousCommitIdPrefix(s)) if s == "04" @@ -830,7 +844,11 @@ fn test_resolve_symbol_git_refs() { ); } -fn resolve_commit_ids(repo: &dyn Repo, revset_str: &str) -> Vec { +fn resolve_commit_ids_with_mailmap( + repo: &dyn Repo, + mailmap_source: &str, + revset_str: &str, +) -> Vec { let settings = testutils::user_settings(); let aliases_map = RevsetAliasesMap::default(); let revset_extensions = RevsetExtensions::default(); @@ -839,6 +857,7 @@ fn resolve_commit_ids(repo: &dyn Repo, revset_str: &str) -> Vec { settings.user_email(), &revset_extensions, None, + Rc::new(Mailmap::from_bytes(mailmap_source.as_bytes())), ); let expression = optimize(parse(revset_str, &context).unwrap()); let symbol_resolver = DefaultSymbolResolver::new(repo, revset_extensions.symbol_resolvers()); @@ -848,6 +867,10 @@ fn resolve_commit_ids(repo: &dyn Repo, revset_str: &str) -> Vec { expression.evaluate(repo).unwrap().iter().collect() } +fn resolve_commit_ids(repo: &dyn Repo, revset_str: &str) -> Vec { + resolve_commit_ids_with_mailmap(repo, "", revset_str) +} + fn resolve_commit_ids_in_workspace( repo: &dyn Repo, revset_str: &str, @@ -865,11 +888,13 @@ fn resolve_commit_ids_in_workspace( }; let aliases_map = RevsetAliasesMap::default(); let extensions = RevsetExtensions::default(); + let mailmap = Rc::new(get_current_mailmap(repo, workspace_ctx.workspace_id)); let context = RevsetParseContext::new( &aliases_map, settings.user_email(), &extensions, Some(workspace_ctx), + mailmap, ); let expression = optimize(parse(revset_str, &context).unwrap()); let symbol_resolver = @@ -2411,6 +2436,20 @@ fn test_evaluate_expression_author() { ), vec![commit3.id().clone(), commit1.id().clone()] ); + // Signatures are treated as their mailmapped forms + let mailmap = "nameone "; + assert_eq!( + resolve_commit_ids_with_mailmap(mut_repo, mailmap, "author(\"nameone\")"), + vec![commit1.id().clone()] + ); + assert_eq!( + resolve_commit_ids_with_mailmap(mut_repo, mailmap, "author(\"name1\")"), + vec![] + ); + assert_eq!( + resolve_commit_ids_with_mailmap(mut_repo, mailmap, "author_raw(\"name1\")"), + vec![commit1.id().clone()] + ); } #[test] @@ -2479,6 +2518,16 @@ fn test_evaluate_expression_mine() { commit1.id().clone() ] ); + // Signatures are treated as their mailmapped forms + let user_email = settings.user_email(); + assert_eq!( + resolve_commit_ids_with_mailmap( + mut_repo, + format!("<{user_email}> \n name2 <{user_email}>").as_ref(), + "mine()" + ), + vec![commit3.id().clone(), commit1.id().clone()] + ); } #[test] @@ -2544,6 +2593,20 @@ fn test_evaluate_expression_committer() { resolve_commit_ids(mut_repo, "visible_heads() & committer(\"name2\")"), vec![] ); + // Signatures are treated as their mailmapped forms + let mailmap = "nameone "; + assert_eq!( + resolve_commit_ids_with_mailmap(mut_repo, mailmap, "committer(\"nameone\")"), + vec![commit1.id().clone()] + ); + assert_eq!( + resolve_commit_ids_with_mailmap(mut_repo, mailmap, "committer(\"name1\")"), + vec![] + ); + assert_eq!( + resolve_commit_ids_with_mailmap(mut_repo, mailmap, "committer_raw(\"name1\")"), + vec![commit1.id().clone()] + ); } #[test]