Skip to content

Commit

Permalink
annotate: make AnnotationLine template type
Browse files Browse the repository at this point in the history
Allows:
* self.commit()
* self.line_number()
* self.first_line_in_hunk()

Certain pagers (like `delta`), when used for `git blame`, only show the
commit information for the first line in a hunk. This would be a nice
addition to `jj file annotate`.

`jj file annotate` already uses a template to control the rendering of
commit information --- `templates.annotate_commit_summary`. Instead of
a custom CLI flag, the tools necessary to do this should be available in
the template language.

If `1 % 2` or `1.is_even()` was available in the template language, this
would also allow alternating colors (using `raw_escape_sequence`).

Example:

```toml
[templates]
# only show commit info for the first line of each hunk
annotate_commit_summary = '''
if(first_line_in_hunk,
  show_commit_info(commit),
  pad_end(20, " "),
)
'''
```
  • Loading branch information
bryceberger committed Feb 15, 2025
1 parent 6ec337e commit 3bc111e
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 16 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* The `ui.allow-filesets` configuration option has been removed.
[The "fileset" language](docs/filesets.md) has been enabled by default since v0.20.

* `templates.annotate_commit_summary` is renamed to `templates.file_annotate`,
and now has an implicit `self` parameter of type `AnnotationLine`, instead of
`Commit`. All methods on `Commit` can be accessed with `commit.method()`, or
`self.commit().method()`.

### Deprecations

* This release takes the first steps to make target revision required in
Expand Down Expand Up @@ -57,6 +62,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* The description of commits backed out by `jj backout` can now be configured
using `templates.backout_description`.

* New `AnnotationLine` templater type. Used in `templates.file_annotate`.
Provides `self.commit()`, `self.line_number()`, and `self.first_line_in_hunk()`.

### Fixed bugs

* `jj status` now shows untracked files under untracked directories.
Expand Down
52 changes: 41 additions & 11 deletions cli/src/commands/file/annotate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter;
use jj_lib::annotate::get_annotation_for_file;
use jj_lib::annotate::FileAnnotation;
use jj_lib::commit::Commit;
use jj_lib::repo::Repo;
use jj_lib::revset::RevsetExpression;
use tracing::instrument;
Expand All @@ -25,6 +24,8 @@ use crate::cli_util::CommandHelper;
use crate::cli_util::RevisionArg;
use crate::command_error::user_error;
use crate::command_error::CommandError;
use crate::commit_templater::AnnotationLine;
use crate::commit_templater::CommitTemplateLanguage;
use crate::complete;
use crate::templater::TemplateRenderer;
use crate::ui::Ui;
Expand All @@ -33,8 +34,6 @@ use crate::ui::Ui;
///
/// Annotates a revision line by line. Each line includes the source change that
/// introduced the associated line. A path to the desired file must be provided.
/// The per-line prefix for each line can be customized via
/// template with the `templates.annotate_commit_summary` config variable.
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct FileAnnotateArgs {
/// the file to annotate
Expand All @@ -51,6 +50,21 @@ pub(crate) struct FileAnnotateArgs {
add = ArgValueCandidates::new(complete::all_revisions)
)]
revision: Option<RevisionArg>,
/// Render a prefix for each line using the given template
///
/// All 0-argument methods of the [`AnnotationLine` type] are available as
/// keywords in the [template expression].
///
/// If not specified, this defaults to the
/// `templates.file_annotate` setting.
///
/// [template expression]:
/// https://jj-vcs.github.io/jj/latest/templates/
///
/// [`AnnotationLine` type]:
/// https://jj-vcs.github.io/jj/latest/templates/#annotationline-type
#[arg(long, short = 'T', add = ArgValueCandidates::new(complete::template_aliases))]
template: Option<String>,
}

#[instrument(skip_all)]
Expand All @@ -75,10 +89,19 @@ pub(crate) fn cmd_file_annotate(
)));
}

let annotate_commit_summary_text = workspace_command
.settings()
.get_string("templates.annotate_commit_summary")?;
let template = workspace_command.parse_commit_template(ui, &annotate_commit_summary_text)?;
let template_text = match &args.template {
Some(value) => value.clone(),
None => workspace_command
.settings()
.get_string("templates.file_annotate")?,
};
let language = workspace_command.commit_template_language();
let template = workspace_command.parse_template(
ui,
&language,
&template_text,
CommitTemplateLanguage::wrap_annotation_line,
)?;

// TODO: Should we add an option to limit the domain to e.g. recent commits?
// Note that this is probably different from "--skip REVS", which won't
Expand All @@ -94,17 +117,24 @@ pub(crate) fn cmd_file_annotate(
fn render_file_annotation(
repo: &dyn Repo,
ui: &mut Ui,
template_render: &TemplateRenderer<Commit>,
template_render: &TemplateRenderer<AnnotationLine>,
annotation: &FileAnnotation,
) -> Result<(), CommandError> {
ui.request_pager();
let mut formatter = ui.stdout_formatter();
for (line_no, (commit_id, line)) in annotation.lines().enumerate() {
let mut last_id = None;
for (line_number, (commit_id, line)) in annotation.lines().enumerate() {
let commit_id = commit_id.expect("should reached to the empty ancestor");
let commit = repo.store().get_commit(commit_id)?;
template_render.format(&commit, formatter.as_mut())?;
write!(formatter, " {:>4}: ", line_no + 1)?;
let first_line_in_hunk = last_id != Some(commit_id);
let annotation_line = AnnotationLine {
commit,
line_number: line_number + 1,
first_line_in_hunk,
};
template_render.format(&annotation_line, formatter.as_mut())?;
formatter.write_all(line)?;
last_id = Some(commit_id);
}

Ok(())
Expand Down
62 changes: 62 additions & 0 deletions cli/src/commit_templater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,12 @@ impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> {
function,
)
}
CommitTemplatePropertyKind::AnnotationLine(property) => {
let type_name = "AnnotationLine";
let table = &self.build_fn_table.annotation_line_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(self, diagnostics, build_ctx, property, function)
}
}
}
}
Expand Down Expand Up @@ -441,6 +447,12 @@ impl<'repo> CommitTemplateLanguage<'repo> {
) -> CommitTemplatePropertyKind<'repo> {
CommitTemplatePropertyKind::CryptographicSignatureOpt(Box::new(property))
}

pub fn wrap_annotation_line(
property: impl TemplateProperty<Output = AnnotationLine> + 'repo,
) -> CommitTemplatePropertyKind<'repo> {
CommitTemplatePropertyKind::AnnotationLine(Box::new(property))
}
}

pub enum CommitTemplatePropertyKind<'repo> {
Expand All @@ -463,6 +475,7 @@ pub enum CommitTemplatePropertyKind<'repo> {
CryptographicSignatureOpt(
Box<dyn TemplateProperty<Output = Option<CryptographicSignature>> + 'repo>,
),
AnnotationLine(Box<dyn TemplateProperty<Output = AnnotationLine> + 'repo>),
}

impl<'repo> IntoTemplateProperty<'repo> for CommitTemplatePropertyKind<'repo> {
Expand All @@ -487,6 +500,7 @@ impl<'repo> IntoTemplateProperty<'repo> for CommitTemplatePropertyKind<'repo> {
CommitTemplatePropertyKind::CryptographicSignatureOpt(_) => {
"Option<CryptographicSignature>"
}
CommitTemplatePropertyKind::AnnotationLine(_) => "AnnotationLine",
}
}

Expand Down Expand Up @@ -525,6 +539,7 @@ impl<'repo> IntoTemplateProperty<'repo> for CommitTemplatePropertyKind<'repo> {
CommitTemplatePropertyKind::CryptographicSignatureOpt(property) => {
Some(Box::new(property.map(|sig| sig.is_some())))
}
CommitTemplatePropertyKind::AnnotationLine(_) => None,
}
}

Expand Down Expand Up @@ -568,6 +583,7 @@ impl<'repo> IntoTemplateProperty<'repo> for CommitTemplatePropertyKind<'repo> {
CommitTemplatePropertyKind::TreeEntry(_) => None,
CommitTemplatePropertyKind::DiffStats(property) => Some(property.into_template()),
CommitTemplatePropertyKind::CryptographicSignatureOpt(_) => None,
CommitTemplatePropertyKind::AnnotationLine(_) => None,
}
}

Expand All @@ -593,6 +609,7 @@ impl<'repo> IntoTemplateProperty<'repo> for CommitTemplatePropertyKind<'repo> {
(CommitTemplatePropertyKind::TreeEntry(_), _) => None,
(CommitTemplatePropertyKind::DiffStats(_), _) => None,
(CommitTemplatePropertyKind::CryptographicSignatureOpt(_), _) => None,
(CommitTemplatePropertyKind::AnnotationLine(_), _) => None,
}
}

Expand Down Expand Up @@ -621,6 +638,7 @@ impl<'repo> IntoTemplateProperty<'repo> for CommitTemplatePropertyKind<'repo> {
(CommitTemplatePropertyKind::TreeEntry(_), _) => None,
(CommitTemplatePropertyKind::DiffStats(_), _) => None,
(CommitTemplatePropertyKind::CryptographicSignatureOpt(_), _) => None,
(CommitTemplatePropertyKind::AnnotationLine(_), _) => None,
}
}
}
Expand All @@ -643,6 +661,7 @@ pub struct CommitTemplateBuildFnTable<'repo> {
pub diff_stats_methods: CommitTemplateBuildMethodFnMap<'repo, DiffStats>,
pub cryptographic_signature_methods:
CommitTemplateBuildMethodFnMap<'repo, CryptographicSignature>,
pub annotation_line_methods: CommitTemplateBuildMethodFnMap<'repo, AnnotationLine>,
}

impl<'repo> CommitTemplateBuildFnTable<'repo> {
Expand All @@ -660,6 +679,7 @@ impl<'repo> CommitTemplateBuildFnTable<'repo> {
tree_entry_methods: builtin_tree_entry_methods(),
diff_stats_methods: builtin_diff_stats_methods(),
cryptographic_signature_methods: builtin_cryptographic_signature_methods(),
annotation_line_methods: builtin_annotation_line_methods(),
}
}

Expand All @@ -676,6 +696,7 @@ impl<'repo> CommitTemplateBuildFnTable<'repo> {
tree_entry_methods: HashMap::new(),
diff_stats_methods: HashMap::new(),
cryptographic_signature_methods: HashMap::new(),
annotation_line_methods: HashMap::new(),
}
}

Expand All @@ -692,6 +713,7 @@ impl<'repo> CommitTemplateBuildFnTable<'repo> {
tree_entry_methods,
diff_stats_methods,
cryptographic_signature_methods,
annotation_line_methods,
} = extension;

self.core.merge(core);
Expand All @@ -714,6 +736,7 @@ impl<'repo> CommitTemplateBuildFnTable<'repo> {
&mut self.cryptographic_signature_methods,
cryptographic_signature_methods,
);
merge_fn_map(&mut self.annotation_line_methods, annotation_line_methods);
}
}

Expand Down Expand Up @@ -2194,3 +2217,42 @@ pub fn builtin_cryptographic_signature_methods<'repo>(
);
map
}

// TODO: add `line: BString` field when available in template language
#[derive(Debug, Clone)]
pub struct AnnotationLine {
pub commit: Commit,
pub line_number: usize,
pub first_line_in_hunk: bool,
}

pub fn builtin_annotation_line_methods<'repo>(
) -> CommitTemplateBuildMethodFnMap<'repo, AnnotationLine> {
type L<'repo> = CommitTemplateLanguage<'repo>;
let mut map = CommitTemplateBuildMethodFnMap::<AnnotationLine>::new();
map.insert(
"commit",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.and_then(|data| Ok(data.commit));
Ok(L::wrap_commit(out_property))
},
);
map.insert(
"line_number",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.and_then(|data| Ok(data.line_number.try_into()?));
Ok(L::wrap_integer(out_property))
},
);
map.insert(
"first_line_in_hunk",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.and_then(|data| Ok(data.first_line_in_hunk));
Ok(L::wrap_boolean(out_property))
},
);
map
}
9 changes: 5 additions & 4 deletions cli/src/config/templates.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ if(remote,

commit_summary = 'format_commit_summary_with_refs(self, bookmarks)'

annotate_commit_summary = '''
file_annotate = '''
separate(" ",
change_id.shortest(8),
pad_end(8, truncate_end(8, author.email().local())),
commit_timestamp(self).local().format('%Y-%m-%d %H:%M:%S'),
commit.change_id().shortest(8),
pad_end(8, truncate_end(8, commit.author().email().local())),
commit_timestamp(commit).local().format('%Y-%m-%d %H:%M:%S'),
pad_start(4, line_number) ++ ": ",
)
'''

Expand Down
11 changes: 10 additions & 1 deletion cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -887,7 +887,7 @@ File operations

Show the source change for each line of the target file.

Annotates a revision line by line. Each line includes the source change that introduced the associated line. A path to the desired file must be provided. The per-line prefix for each line can be customized via template with the `templates.annotate_commit_summary` config variable.
Annotates a revision line by line. Each line includes the source change that introduced the associated line. A path to the desired file must be provided.

**Usage:** `jj file annotate [OPTIONS] <PATH>`

Expand All @@ -898,6 +898,15 @@ Annotates a revision line by line. Each line includes the source change that int
###### **Options:**

* `-r`, `--revision <REVSET>` — an optional revision to start at
* `-T`, `--template <TEMPLATE>` — Render a prefix for each line using the given template

All 0-argument methods of the [`AnnotationLine` type] are available as keywords in the [template expression].

If not specified, this defaults to the `templates.file_annotate` setting.

[template expression]: https://jj-vcs.github.io/jj/latest/templates/

[`AnnotationLine` type]: https://jj-vcs.github.io/jj/latest/templates/#annotationline-type



Expand Down
54 changes: 54 additions & 0 deletions cli/tests/test_file_annotate_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,57 @@ fn test_annotate_merge_one_sided_conflict_resolution() {
zsuskuln test.use 2001-02-03 08:05:11 2: new text from new commit 1
");
}

#[test]
fn test_annotate_with_template() {
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");

std::fs::write(repo_path.join("file.txt"), "line1\n").unwrap();
test_env.jj_cmd_ok(&repo_path, &["commit", "-m=initial"]);

append_to_file(
&repo_path.join("file.txt"),
"new text from new commit 1\nthat splits into multiple lines",
);
test_env.jj_cmd_ok(&repo_path, &["commit", "-m=commit1"]);

append_to_file(
&repo_path.join("file.txt"),
"new text from new commit 2\nalso continuing on a second line\nand a third!",
);
test_env.jj_cmd_ok(&repo_path, &["describe", "-m=commit2"]);

let template = indoc::indoc! {r#"
if(first_line_in_hunk, "\n" ++ separate("\n",
commit.change_id().shortest(8)
++ " "
++ commit.description().first_line(),
commit_timestamp(commit).local().format('%Y-%m-%d %H:%M:%S')
++ " "
++ commit.author(),
) ++ "\n") ++ pad_start(4, line_number) ++ ": "
"#};

let stdout = test_env.jj_cmd_success(
&repo_path,
&["file", "annotate", "file.txt", "-T", template],
);
insta::assert_snapshot!(stdout, @r#"
qpvuntsm initial
2001-02-03 08:05:08 Test User <[email protected]>
1: line1
rlvkpnrz commit1
2001-02-03 08:05:09 Test User <[email protected]>
2: new text from new commit 1
3: that splits into multiple lines
kkmpptxz commit2
2001-02-03 08:05:10 Test User <[email protected]>
4: new text from new commit 2
5: also continuing on a second line
6: and a third!
"#);
}
9 changes: 9 additions & 0 deletions docs/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ The following functions are defined.

## Types

### AnnotationLine type

The following methods are defined.

* `.commit() -> Commit`: Commit responsible for changing the relevant line.
* `.line_number() -> Integer`: 1-based line number.
* `.first_line_in_hunk() -> Boolean`: False when the directly preceding line
references the same commit.

### Boolean type

No methods are defined. Can be constructed with `false` or `true` literal.
Expand Down

0 comments on commit 3bc111e

Please sign in to comment.