Skip to content

Commit

Permalink
Add command jj annotate
Browse files Browse the repository at this point in the history
A new module is added to jj_lib which exposes a function
get_annotation_for_file. This annotates the given file line by line with
commit information according to the commit that made the most recent
change to the line.
Similarly, a new command is added to the CLI called `jj annotate` which
accepts a file path. It then prints out line by line the commit
information for the line and the line itself. Specific commit
information can be configured via the templates.annotate_commit_summary
config variable
  • Loading branch information
allonsy committed Jul 31, 2024
1 parent ce29824 commit 684840d
Show file tree
Hide file tree
Showing 9 changed files with 604 additions and 0 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### New features

* Adds a new annotate command to annotate files line by line. This is similar
in functionality to git's blame. Invoke the command with `jj annotate <file_path>`.
The output can be customized via the `templates.annotate_commit_summary`
configi variable.

* Support background filesystem monitoring via watchman triggers enabled with
the `core.watchman.register_snapshot_trigger = true` config.

Expand Down
85 changes: 85 additions & 0 deletions cli/src/commands/annotate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// 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 jj_lib::annotate::{get_annotation_for_file, AnnotateResults};
use jj_lib::repo::{ReadonlyRepo, Repo};
use jj_lib::repo_path::RepoPathBuf;
use jj_lib::settings::UserSettings;
use tracing::instrument;

use crate::cli_util::{CommandHelper, WorkspaceCommandHelper};
use crate::command_error::CommandError;
use crate::ui::Ui;

/// Display information for each line of file, showing the source change of each
/// line.
///
/// Annotates a revision line by line. Each line includes the source change that
/// committed the associated line. A path to the desired file must be provided
/// and this command will fail if the file is in a conflicted state currently or
/// in previous changes. 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 AnnotateArgs {
/// the file to annotate
#[arg(value_hint = clap::ValueHint::AnyPath)]
path: String,
}

#[instrument(skip_all)]
pub(crate) fn cmd_annotate(
ui: &mut Ui,
command: &CommandHelper,
args: &AnnotateArgs,
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let starting_commit_id = workspace_command.get_wc_commit_id().unwrap();
let repo = workspace_command.repo();
let file_path = RepoPathBuf::from_relative_path(&args.path);
if file_path.is_err() {
eprintln!("Unable to locate file: {}", args.path);
return Ok(());
}

let res = get_annotation_for_file(&file_path.unwrap(), repo, starting_commit_id);
if let Err(e) = res {
eprintln!("{}", e);
return Ok(());
}
let res = res.unwrap();
render_annotations(repo, ui, command.settings(), &workspace_command, &res)?;
Ok(())
}

fn render_annotations(
repo: &ReadonlyRepo,
ui: &mut Ui,
settings: &UserSettings,
workspace_command: &WorkspaceCommandHelper,
results: &AnnotateResults,
) -> Result<(), CommandError> {
let annotate_commit_summary_text = settings
.config()
.get_string("templates.annotate_commit_summary")?;
let template = workspace_command.parse_commit_template(&annotate_commit_summary_text)?;
ui.request_pager();
let mut formatter = ui.stdout_formatter();
for (line_no, (commit_id, line)) in results.file_annotations.iter().enumerate() {
let commit = repo.store().get_commit(commit_id)?;
template.format(&commit, formatter.as_mut())?;
writeln!(formatter, " {}: {}", line_no + 1, line)?;
}

Ok(())
}
3 changes: 3 additions & 0 deletions cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

mod abandon;
mod annotate;
mod backout;
#[cfg(feature = "bench")]
mod bench;
Expand Down Expand Up @@ -69,6 +70,7 @@ use crate::ui::Ui;
#[derive(clap::Parser, Clone, Debug)]
enum Command {
Abandon(abandon::AbandonArgs),
Annotate(annotate::AnnotateArgs),
Backout(backout::BackoutArgs),
#[cfg(feature = "bench")]
#[command(subcommand)]
Expand Down Expand Up @@ -174,6 +176,7 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co
#[cfg(feature = "bench")]
Command::Bench(args) => bench::cmd_bench(ui, command_helper, args),
Command::Branch(args) => branch::cmd_branch(ui, command_helper, args),
Command::Annotate(args) => annotate::cmd_annotate(ui, command_helper, args),
Command::Cat(args) => file::show::deprecated_cmd_cat(ui, command_helper, args),
Command::Checkout(args) => checkout::cmd_checkout(ui, command_helper, args),
Command::Chmod(args) => file::chmod::deprecated_cmd_chmod(ui, command_helper, args),
Expand Down
6 changes: 6 additions & 0 deletions cli/src/config/templates.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ if(remote,

commit_summary = 'format_commit_summary_with_refs(self, branches)'

annotate_commit_summary = '''
self.change_id().shortest(8) ++ " " ++
self.commit_id().shortest(8) ++ " " ++
self.author().name() ++ " " ++ format_timestamp(self.author().timestamp())
'''

config_list = '''
if(overridden,
label("overridden", indent("# ", name ++ " = " ++ value)),
Expand Down
16 changes: 16 additions & 0 deletions cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This document contains the help content for the `jj` command-line program.

* [`jj`↴](#jj)
* [`jj abandon`↴](#jj-abandon)
* [`jj annotate`↴](#jj-annotate)
* [`jj backout`↴](#jj-backout)
* [`jj branch`↴](#jj-branch)
* [`jj branch create`↴](#jj-branch-create)
Expand Down Expand Up @@ -111,6 +112,7 @@ To get started, see the tutorial at https://github.com/martinvonz/jj/blob/main/d
###### **Subcommands:**

* `abandon`Abandon a revision
* `annotate`Display information for each line of file, showing the source change of each line
* `backout`Apply the reverse of a revision on top of another revision
* `branch`Manage branches
* `commit`Update the description and create a new change on top
Expand Down Expand Up @@ -209,6 +211,20 @@ If a working-copy commit gets abandoned, it will be given a new, empty commit. T
## `jj annotate`
Display information for each line of file, showing the source change of each line.
Annotates a revision line by line. Each line includes the source change that committed the associated line. A path to the desired file must be provided and this command will fail if the file is in a conflicted state currently or in previous changes. The per line prefix for each line can be customized via template with the `templates.annotate_commit_summary` config variable
**Usage:** `jj annotate <PATH>`
###### **Arguments:**
* `<PATH>` — the file to annotate
## `jj backout`
Apply the reverse of a revision on top of another revision
Expand Down
1 change: 1 addition & 0 deletions cli/tests/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod test_abandon_command;
mod test_acls;
mod test_advance_branches;
mod test_alias;
mod test_annotate_command;
mod test_backout_command;
mod test_branch_command;
mod test_builtin_aliases;
Expand Down
114 changes: 114 additions & 0 deletions cli/tests/test_annotate_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// 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 std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;

use crate::common::TestEnvironment;

fn append_to_file(file_path: &Path, contents: &str) {
let mut options = OpenOptions::new();
options.append(true);
let mut file = options.open(file_path).unwrap();
writeln!(file, "{}", contents).unwrap();
}

#[test]
fn test_annotate_linear() {
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, &["describe", "-m=initial"]);

test_env.jj_cmd_ok(&repo_path, &["new", "-m=next"]);
append_to_file(&repo_path.join("file.txt"), "new text from new commit");

let stdout = test_env.jj_cmd_success(&repo_path, &["annotate", "file.txt"]);
insta::assert_snapshot!(stdout, @r###"
qpvuntsm 8934c772 Test User 2001-02-03 08:05:08 1: line1
kkmpptxz 41ae16e6 Test User 2001-02-03 08:05:09 2: new text from new commit
"###);
}

#[test]
fn test_annotate_merge() {
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, &["describe", "-m=initial"]);
test_env.jj_cmd_ok(&repo_path, &["branch", "create", "initial"]);

test_env.jj_cmd_ok(&repo_path, &["new", "-m=commit1"]);
append_to_file(&repo_path.join("file.txt"), "new text from new commit 1");
test_env.jj_cmd_ok(&repo_path, &["branch", "create", "commit1"]);

test_env.jj_cmd_ok(&repo_path, &["new", "-m=commit2", "initial"]);
append_to_file(&repo_path.join("file.txt"), "new text from new commit 2");
test_env.jj_cmd_ok(&repo_path, &["branch", "create", "commit2"]);

// create a (conflicted) merge
test_env.jj_cmd_ok(&repo_path, &["new", "-m=merged", "commit1", "commit2"]);
// resolve conflicts
std::fs::write(
repo_path.join("file.txt"),
"line1\nnew text from new commit 1\nnew text from new commit 2\n",
)
.unwrap();

let stdout = test_env.jj_cmd_success(&repo_path, &["annotate", "file.txt"]);
insta::assert_snapshot!(stdout, @r###"
qpvuntsm 8934c772 Test User 2001-02-03 08:05:08 1: line1
zsuskuln 712ba14a Test User 2001-02-03 08:05:10 2: new text from new commit 1
royxmykx b0571bd9 Test User 2001-02-03 08:05:12 3: new text from new commit 2
"###);
}

#[test]
fn test_annotate_merge_one_sided_conflict_resolution() {
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, &["describe", "-m=initial"]);
test_env.jj_cmd_ok(&repo_path, &["branch", "create", "initial"]);

test_env.jj_cmd_ok(&repo_path, &["new", "-m=commit1"]);
append_to_file(&repo_path.join("file.txt"), "new text from new commit 1");
test_env.jj_cmd_ok(&repo_path, &["branch", "create", "commit1"]);

test_env.jj_cmd_ok(&repo_path, &["new", "-m=commit2", "initial"]);
append_to_file(&repo_path.join("file.txt"), "new text from new commit 2");
test_env.jj_cmd_ok(&repo_path, &["branch", "create", "commit2"]);

// create a (conflicted) merge
test_env.jj_cmd_ok(&repo_path, &["new", "-m=merged", "commit1", "commit2"]);
// resolve conflicts
std::fs::write(
repo_path.join("file.txt"),
"line1\nnew text from new commit 1\n",
)
.unwrap();

let stdout = test_env.jj_cmd_success(&repo_path, &["annotate", "file.txt"]);
insta::assert_snapshot!(stdout, @r###"
qpvuntsm 8934c772 Test User 2001-02-03 08:05:08 1: line1
zsuskuln 712ba14a Test User 2001-02-03 08:05:10 2: new text from new commit 1
"###);
}
Loading

0 comments on commit 684840d

Please sign in to comment.