Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mailmap: add support for .mailmap files #3951

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
address unconditionally. Only ASCII case folding is currently implemented,
but this will likely change in the future.

* Support for [`.mailmap`](https://git-scm.com/docs/gitmailmap) files has
been added.

### Fixed bugs

## [0.19.0] - 2024-07-03
Expand Down
18 changes: 16 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ gix = { version = "0.63.0", default-features = false, features = [
"blob-diff",
] }
gix-filter = "0.11.2"
# We list `gix-{actor,mailmap}` separately, as they are used by
# `jj_lib::mailmap` even when the Git backend is disabled.
gix-actor = { version = "0.31.3" }
gix-mailmap = { version = "0.23.4" }
glob = "0.3.1"
hex = "0.4.3"
ignore = "0.4.20"
Expand Down
9 changes: 9 additions & 0 deletions cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{read_current_mailmap, Mailmap};
use jj_lib::matchers::Matcher;
use jj_lib::merge::MergedTreeValue;
use jj_lib::merged_tree::MergedTree;
Expand Down Expand Up @@ -73,6 +74,7 @@ use jj_lib::workspace::{
};
use jj_lib::{dag_walk, fileset, git, op_heads_store, op_walk, revset};
use once_cell::unsync::OnceCell;
use pollster::FutureExt;
use tracing::instrument;
use tracing_chrome::ChromeLayerBuilder;
use tracing_subscriber::prelude::*;
Expand Down Expand Up @@ -712,6 +714,11 @@ impl WorkspaceCommandHelper {
self.repo().view().get_wc_commit_id(self.workspace_id())
}

pub fn current_mailmap(&self) -> Result<Mailmap, CommandError> {
// TODO: Consider figuring out a caching strategy for this.
Ok(read_current_mailmap(self.repo().as_ref(), self.workspace.workspace_id()).block_on()?)
}

pub fn working_copy_shared_with_git(&self) -> bool {
self.working_copy_shared_with_git
}
Expand Down Expand Up @@ -996,6 +1003,8 @@ impl WorkspaceCommandHelper {
self.settings.user_email(),
&self.revset_extensions,
Some(workspace_context),
// TODO: Consider handling errors here.
Rc::new(self.current_mailmap().unwrap_or_default()),
)
}

Expand Down
16 changes: 8 additions & 8 deletions cli/src/commands/git/push.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,14 +289,14 @@ pub fn cmd_git_push(
if commit.description().is_empty() && !args.allow_empty_description {
reasons.push("it has no description");
}
if commit.author().name.is_empty()
|| commit.author().name == UserSettings::USER_NAME_PLACEHOLDER
|| commit.author().email.is_empty()
|| commit.author().email == UserSettings::USER_EMAIL_PLACEHOLDER
|| commit.committer().name.is_empty()
|| commit.committer().name == UserSettings::USER_NAME_PLACEHOLDER
|| commit.committer().email.is_empty()
|| commit.committer().email == UserSettings::USER_EMAIL_PLACEHOLDER
if commit.author_raw().name.is_empty()
|| commit.author_raw().name == UserSettings::USER_NAME_PLACEHOLDER
|| commit.author_raw().email.is_empty()
|| commit.author_raw().email == UserSettings::USER_EMAIL_PLACEHOLDER
|| commit.committer_raw().name.is_empty()
|| commit.committer_raw().name == UserSettings::USER_NAME_PLACEHOLDER
|| commit.committer_raw().email.is_empty()
|| commit.committer_raw().email == UserSettings::USER_EMAIL_PLACEHOLDER
{
reasons.push("it has no author and/or committer set");
}
Expand Down
25 changes: 21 additions & 4 deletions cli/src/commit_templater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,26 +468,43 @@ 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.revset_parse_context.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().clone());
let out_property = self_property.map(|commit| commit.author_raw().clone());
Ok(L::wrap_signature(out_property))
},
);
map.insert(
"committer",
|language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let mailmap = language.revset_parse_context.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().clone());
let out_property = self_property.map(|commit| commit.committer_raw().clone());
Ok(L::wrap_signature(out_property))
},
);
map.insert("mine", |language, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let mailmap = language.revset_parse_context.mailmap().clone();
let user_email = language.revset_parse_context.user_email().to_owned();
let out_property = self_property.map(move |commit| commit.author().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(
Expand Down
1 change: 1 addition & 0 deletions cli/tests/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,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;
Expand Down
168 changes: 168 additions & 0 deletions cli/tests/test_mailmap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// 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::{get_stdout_string, 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 run_as = |name: &str, email: &str, args: &[&str]| {
test_env
.jj_cmd(&repo_path, args)
.env("JJ_USER", name)
.env("JJ_EMAIL", email)
.assert()
.success()
};

append_mailmap("# test comment\n");

let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "author"]);
insta::assert_snapshot!(stdout, @r###"
@ Test User <[email protected]>
"###);

// Map an email address without any name change.
run_as("Test Üser", "[email protected]", &["new"]);
append_mailmap("<[email protected]> <[email protected]>\n");

let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "author"]);
insta::assert_snapshot!(stdout, @r###"
@ Test Üser <[email protected]>
◉ Test User <[email protected]>
"###);

// Map an email address to a new name.
run_as("West User", "[email protected]", &["new"]);
append_mailmap("Fest User <[email protected]>\n");

let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "author"]);
insta::assert_snapshot!(stdout, @r###"
@ Fest User <[email protected]>
◉ Test Üser <[email protected]>
◉ Test User <[email protected]>
"###);

// Map an email address to a new name and email address.
run_as("Pest User", "[email protected]", &["new"]);
append_mailmap("Best User <[email protected]> <[email protected]>\n");

let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "author"]);
insta::assert_snapshot!(stdout, @r###"
@ Best User <[email protected]>
◉ Fest User <[email protected]>
◉ Test Üser <[email protected]>
◉ Test User <[email protected]>
"###);

// Map an ambiguous email address using names for disambiguation.
run_as("Rest User", "user@test", &["new"]);
run_as("Vest User", "user@test", &["new"]);
append_mailmap(
&[
"Jest User <[email protected]> ReSt UsEr <UsEr@TeSt>\n",
"Zest User <[email protected]> vEsT uSeR <uSeR@tEsT>\n",
]
.concat(),
);

let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "author"]);
insta::assert_snapshot!(stdout, @r###"
@ Zest User <[email protected]>
◉ Jest User <[email protected]>
◉ Best User <[email protected]>
◉ Fest User <[email protected]>
◉ Test Üser <[email protected]>
◉ Test User <[email protected]>
"###);

// 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 <user@test>
◉ Rest User <user@test>
◉ Best User <[email protected]>
◉ Fest User <[email protected]>
◉ Test Üser <[email protected]>
◉ Test User <[email protected]>
"###);

// The `author(pattern)` revset function should find mapped committers.
let stdout = test_env.jj_cmd_success(
&repo_path,
&["log", "-T", "author", "-r", "author(substring-i:bEsT)"],
);
insta::assert_snapshot!(stdout, @r###"
◉ Best User <[email protected]>
~
"###);

// The `author(pattern)` revset function should only search the mapped form.
// This matches Git’s behaviour and the principle of not surfacing raw
// signatures by default.
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 <[email protected]>
◉ Jest User <[email protected]>
~
"###);

// `mine()` should find commits that map to the current `user.email`.
let assert = run_as(
"Tëst Üser",
"[email protected]",
&["log", "-T", "author", "-r", "mine()"],
);
insta::assert_snapshot!(get_stdout_string(&assert), @r###"
◉ Test Üser <[email protected]>
◉ Test User <[email protected]>
~
"###);

// `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: String =
test_env.jj_cmd_success(&repo_path, &["log", "-T", "author", "-r", "mine()"]);
insta::assert_snapshot!(stdout, @r###"
"###);
}
2 changes: 1 addition & 1 deletion cli/tests/test_revset_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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###"
Expand Down
6 changes: 6 additions & 0 deletions docs/revsets.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,12 +251,18 @@ 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_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()`.

Expand Down
4 changes: 4 additions & 0 deletions docs/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ This type cannot be printed. The following methods are defined.
* `commit_id() -> CommitId`
* `parents() -> List<Commit>`
* `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
Expand Down
2 changes: 2 additions & 0 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ futures = { workspace = true }
git2 = { workspace = true, optional = true }
gix = { workspace = true, optional = true }
gix-filter = { workspace = true, optional = true }
gix-actor = { workspace = true }
gix-mailmap = { workspace = true }
glob = { workspace = true }
hex = { workspace = true }
ignore = { workspace = true }
Expand Down
Loading