This commit is contained in:
Alec Snyder 2024-10-03 09:17:18 +00:00 committed by GitHub
commit 2e1ae12c16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 677 additions and 0 deletions

View File

@ -309,6 +309,11 @@ Thanks to the people who made this release happen!
* `jj describe` can now update the description of multiple commits.
* New command `jj annotate` that annotates 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`
config variable.
### Fixed bugs
* `jj status` will show different messages in a conflicted tree, depending

View File

@ -0,0 +1,96 @@
// 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;
use jj_lib::annotate::AnnotateResults;
use jj_lib::commit::Commit;
use jj_lib::repo::Repo;
use tracing::instrument;
use crate::cli_util::CommandHelper;
use crate::cli_util::RevisionArg;
use crate::command_error::user_error;
use crate::command_error::CommandError;
use crate::command_error::CommandErrorKind;
use crate::templater::TemplateRenderer;
use crate::ui::Ui;
/// 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.
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct AnnotateArgs {
/// the file to annotate
#[arg(value_hint = clap::ValueHint::AnyPath)]
path: String,
/// an optional revision to start at
#[arg(long, short)]
revision: Option<RevisionArg>,
}
#[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 repo = workspace_command.repo();
let starting_commit =
workspace_command.resolve_single_rev(args.revision.as_ref().unwrap_or(&RevisionArg::AT))?;
let file_path = workspace_command.parse_file_path(&args.path)?;
let file_value = starting_commit.tree()?.path_value(&file_path)?;
let ui_path = workspace_command.format_file_path(&file_path);
if file_value.is_absent() {
return Err(user_error(format!("No such path: {ui_path}")));
}
if file_value.is_tree() {
return Err(user_error(format!(
"Path exists but is not a regular file: {ui_path}"
)));
}
let annotate_commit_summary_text = command
.settings()
.config()
.get_string("templates.annotate_commit_summary")?;
let template = workspace_command.parse_commit_template(&annotate_commit_summary_text)?;
let annotations = get_annotation_for_file(repo.as_ref(), &starting_commit, &file_path)
.map_err(|e| CommandError::new(CommandErrorKind::Internal, e))?;
render_annotations(repo.as_ref(), ui, &template, &annotations)?;
Ok(())
}
fn render_annotations(
repo: &dyn Repo,
ui: &mut Ui,
template_render: &TemplateRenderer<Commit>,
results: &AnnotateResults,
) -> Result<(), CommandError> {
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_render.format(&commit, formatter.as_mut())?;
write!(formatter, " {:>4}: ", line_no + 1)?;
formatter.write_all(line)?;
}
Ok(())
}

View File

@ -13,6 +13,7 @@
// limitations under the License.
mod abandon;
mod annotate;
mod backout;
#[cfg(feature = "bench")]
mod bench;
@ -72,6 +73,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)]
@ -183,6 +185,7 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co
let subcommand = Command::from_arg_matches(command_helper.matches()).unwrap();
match &subcommand {
Command::Abandon(args) => abandon::cmd_abandon(ui, command_helper, args),
Command::Annotate(args) => annotate::cmd_annotate(ui, command_helper, args),
Command::Backout(args) => backout::cmd_backout(ui, command_helper, args),
#[cfg(feature = "bench")]
Command::Bench(args) => bench::cmd_bench(ui, command_helper, args),

View File

@ -14,6 +14,15 @@ if(remote,
commit_summary = 'format_commit_summary_with_refs(self, bookmarks)'
annotate_commit_summary = '''
separate(" ",
format_short_id(change_id),
format_short_id(commit_id),
format_short_signature(author),
format_timestamp(committer.timestamp()),
)
'''
config_list = '''
if(overridden,
label("overridden", indent("# ", name ++ " = " ++ value)),

View File

@ -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 bookmark`↴](#jj-bookmark)
* [`jj bookmark create`↴](#jj-bookmark-create)
@ -112,6 +113,7 @@ To get started, see the tutorial at https://martinvonz.github.io/jj/latest/tutor
###### **Subcommands:**
* `abandon` — Abandon a revision
* `annotate` — Show the source change for each line of the target file
* `backout` — Apply the reverse of a revision on top of another revision
* `bookmark` — Manage bookmarks
* `commit` — Update the description and create a new change on top
@ -209,6 +211,24 @@ If a working-copy commit gets abandoned, it will be given a new, empty commit. T
## `jj annotate`
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.
**Usage:** `jj annotate [OPTIONS] <PATH>`
###### **Arguments:**
* `<PATH>` — the file to annotate
###### **Options:**
* `-r`, `--revision <REVISION>` — an optional revision to start at
## `jj backout`
Apply the reverse of a revision on top of another revision

View File

@ -12,6 +12,7 @@ mod test_abandon_command;
mod test_acls;
mod test_advance_bookmarks;
mod test_alias;
mod test_annotate_command;
mod test_backout_command;
mod test_bookmark_command;
mod test_builtin_aliases;

View File

@ -0,0 +1,148 @@
// 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@example.com 2001-02-03 08:05:08 1: line1
kkmpptxz 41ae16e6 test.user@example.com 2001-02-03 08:05:10 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@example.com 2001-02-03 08:05:08 1: line1
zsuskuln 712ba14a test.user@example.com 2001-02-03 08:05:11 2: new text from new commit 1
royxmykx b0571bd9 test.user@example.com 2001-02-03 08:05:13 3: new text from new commit 2
"###);
}
#[test]
fn test_annotate_conflicted() {
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"]);
test_env.jj_cmd_ok(&repo_path, &["new"]);
let stdout = test_env.jj_cmd_success(&repo_path, &["annotate", "file.txt"]);
insta::assert_snapshot!(stdout, @r###"
qpvuntsm 8934c772 test.user@example.com 2001-02-03 08:05:08 1: line1
yostqsxw 7b90c9f6 test.user@example.com 2001-02-03 08:05:15 2: <<<<<<< Conflict 1 of 1
yostqsxw 7b90c9f6 test.user@example.com 2001-02-03 08:05:15 3: %%%%%%% Changes from base to side #1
yostqsxw 7b90c9f6 test.user@example.com 2001-02-03 08:05:15 4: +new text from new commit 1
yostqsxw 7b90c9f6 test.user@example.com 2001-02-03 08:05:15 5: +++++++ Contents of side #2
royxmykx b0571bd9 test.user@example.com 2001-02-03 08:05:13 6: new text from new commit 2
yostqsxw 7b90c9f6 test.user@example.com 2001-02-03 08:05:15 7: >>>>>>> Conflict 1 of 1 ends
"###);
}
#[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@example.com 2001-02-03 08:05:08 1: line1
zsuskuln 712ba14a test.user@example.com 2001-02-03 08:05:11 2: new text from new commit 1
"###);
}

394
lib/src/annotate.rs Normal file
View File

@ -0,0 +1,394 @@
// 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.
//! Methods that allow annotation (attribution and blame) for a file in a
//! repository.
//!
//! TODO: Add support for different blame layers with a trait in the future.
//! Like commit metadata and more.
use std::collections::HashMap;
use pollster::FutureExt;
use crate::backend::BackendError;
use crate::backend::CommitId;
use crate::commit::Commit;
use crate::conflicts::materialize_merge_result;
use crate::conflicts::materialize_tree_value;
use crate::conflicts::MaterializedTreeValue;
use crate::diff::Diff;
use crate::diff::DiffHunk;
use crate::fileset::FilesetExpression;
use crate::graph::GraphEdge;
use crate::graph::GraphEdgeType;
use crate::merged_tree::MergedTree;
use crate::repo::Repo;
use crate::repo_path::RepoPath;
use crate::revset::RevsetEvaluationError;
use crate::revset::RevsetExpression;
use crate::revset::RevsetFilterPredicate;
use crate::store::Store;
/// Annotation results for a specific file
pub struct AnnotateResults {
/// An array of annotation results ordered by line.
/// For each value in the array, the commit_id is the commit id of the
/// originator of the line and the string is the actual line itself (without
/// newline terminators). The vector is ordered by appearance in the
/// file
pub file_annotations: Vec<(CommitId, Vec<u8>)>,
}
/// A map from commits to line mappings.
/// Namely, for a given commit A, the value is the mapping of lines in the file
/// at commit A to line numbers in the original file
type CommitLineMap = HashMap<CommitId, HashMap<usize, usize>>;
/// Memoizes the file contents for a given version to save time
type FileCache = HashMap<CommitId, Vec<u8>>;
/// A map from line numbers in the original file to the commit that originated
/// that line
type OriginalLineMap = HashMap<usize, CommitId>;
fn get_initial_commit_line_map(commit_id: &CommitId, num_lines: usize) -> CommitLineMap {
let mut starting_commit_map = HashMap::new();
for i in 0..num_lines {
starting_commit_map.insert(i, i);
}
let mut starting_line_map = HashMap::new();
starting_line_map.insert(commit_id.clone(), starting_commit_map);
starting_line_map
}
/// Add a new line mapping from a commit to an original line number.
/// For example, if we figure out that line 2 in commit A maps to line 7 in
/// the original, we update the mapping so line 3 maps to line 7 in the
/// original.
fn remap_line(
commit_line_map: &mut CommitLineMap,
new_commit_id: &CommitId,
commit_line_number: usize,
original_line_number: usize,
) {
if commit_line_map.contains_key(new_commit_id) {
commit_line_map
.get_mut(new_commit_id)
.unwrap()
.insert(commit_line_number, original_line_number);
} else {
let mut new_map = HashMap::new();
new_map.insert(commit_line_number, original_line_number);
commit_line_map.insert(new_commit_id.clone(), new_map);
}
}
/// Once we've looked at all parents of a commit, any leftover lines must be
/// original to the current commit, so we save this information in
/// original_line_map.
fn mark_lines_from_original(
original_line_map: &mut OriginalLineMap,
commit_id: &CommitId,
commit_lines: HashMap<usize, usize>,
) {
for (_, original_line_number) in commit_lines {
original_line_map.insert(original_line_number, commit_id.clone());
}
}
/// Takes in an original line map and the original contents and annotates each
/// line according to the contents of the provided OriginalLineMap
fn convert_to_results(
original_line_map: OriginalLineMap,
original_contents: &[u8],
) -> AnnotateResults {
let mut result_lines = Vec::new();
original_contents
.split_inclusive(|b| *b == b'\n')
.enumerate()
.for_each(|(idx, line)| {
result_lines.push((
original_line_map.get(&idx).unwrap().clone(),
line.to_owned(),
));
});
AnnotateResults {
file_annotations: result_lines,
}
}
/// loads a given file into the cache under a specific commit id.
/// If there is already a file for a given commit, it is a no-op.
fn load_file_into_cache(
file_cache: &mut FileCache,
store: &Store,
commit_id: &CommitId,
file_path: &RepoPath,
tree: &MergedTree,
) -> Result<(), BackendError> {
if file_cache.contains_key(commit_id) {
return Ok(());
}
if let Some(file_contents) = get_file_contents(store, file_path, tree)? {
file_cache.insert(commit_id.clone(), file_contents);
}
Ok(())
}
/// Get line by line annotations for a specific file path in the repo.
/// If the file is not found, returns empty results.
pub fn get_annotation_for_file(
repo: &dyn Repo,
starting_commit: &Commit,
file_path: &RepoPath,
) -> Result<AnnotateResults, BackendError> {
if let Some(original_contents) =
get_file_contents(starting_commit.store(), file_path, &starting_commit.tree()?)?
{
let num_lines = original_contents.split_inclusive(|b| *b == b'\n').count();
let mut file_cache = HashMap::new();
file_cache.insert(starting_commit.id().clone(), original_contents.clone());
let original_line_map =
process_commits(repo, file_cache, starting_commit.id(), file_path, num_lines)?;
Ok(convert_to_results(original_line_map, &original_contents))
} else {
Ok(AnnotateResults {
file_annotations: Vec::new(),
})
}
}
/// Starting at the starting commit, compute changes at that commit relative to
/// it's direct parents, updating the mappings as we go. We return the final
/// original line map that represents where each line of the original came from.
fn process_commits(
repo: &dyn Repo,
mut file_cache: FileCache,
starting_commit_id: &CommitId,
file_name: &RepoPath,
num_lines: usize,
) -> Result<OriginalLineMap, BackendError> {
let predicate = RevsetFilterPredicate::File(FilesetExpression::file_path(file_name.to_owned()));
let revset = RevsetExpression::commit(starting_commit_id.clone())
.union(
&RevsetExpression::commit(starting_commit_id.clone())
.ancestors()
.filtered(predicate),
)
.evaluate_programmatic(repo)
.map_err(|e| match e {
RevsetEvaluationError::StoreError(backend_error) => backend_error,
RevsetEvaluationError::Other(_) => {
panic!("Unable to evaluate internal revset")
}
})?;
let mut commit_line_map = get_initial_commit_line_map(starting_commit_id, num_lines);
let mut original_line_map = HashMap::new();
for (cid, edge_list) in revset.iter_graph() {
let current_commit_line_map = commit_line_map.remove(&cid);
if let Some(current_commit_line_map) = current_commit_line_map {
let current_commit = repo.store().get_commit(&cid)?;
let new_commit_lines = process_commit(
repo,
file_name,
&mut original_line_map,
&current_commit,
&mut file_cache,
current_commit_line_map,
&edge_list,
)?;
commit_line_map.extend(new_commit_lines);
if original_line_map.len() >= num_lines {
break;
}
}
}
Ok(original_line_map)
}
/// For a given commit, for each parent, we compare the version in the parent
/// tree with the current version, updating the mappings for any lines in
/// common. If the parent doesn't have the file, we skip it.
/// After iterating through all the parents, any leftover lines unmapped means
/// that those lines are original in the current commit. In that case,
/// original_line_map is updated for the leftover lines.
/// We return the lines that are the same in the child commit and
/// any parent. Namely, if line x is found in parent Y, we record the mapping
/// that parent Y has line x. The line mappings for all parents are returned
/// along with any lines originated in the current commit
fn process_commit(
repo: &dyn Repo,
file_name: &RepoPath,
original_line_map: &mut HashMap<usize, CommitId>,
current_commit: &Commit,
file_cache: &mut FileCache,
mut current_commit_line_map: HashMap<usize, usize>,
edges: &Vec<GraphEdge<CommitId>>,
) -> Result<CommitLineMap, BackendError> {
let mut commit_line_map: CommitLineMap = HashMap::new();
for parent_edge in edges {
if parent_edge.edge_type != GraphEdgeType::Missing {
let parent_commit = repo.store().get_commit(&parent_edge.target)?;
let same_line_map = process_files_in_commits(
repo.store(),
file_name,
file_cache,
current_commit,
&parent_commit,
)?;
for (current_line_number, parent_line_number) in same_line_map {
if let Some(original_line_number) =
current_commit_line_map.remove(&current_line_number)
{
remap_line(
&mut commit_line_map,
parent_commit.id(),
parent_line_number,
original_line_number,
);
}
}
}
}
if !current_commit_line_map.is_empty() {
mark_lines_from_original(
original_line_map,
current_commit.id(),
current_commit_line_map,
);
}
let _ = file_cache.remove(current_commit.id());
Ok(commit_line_map)
}
/// For two versions of the same file, for all the lines in common, overwrite
/// the new mapping in the results for the new commit. Let's say I have
/// a file in commit A and commit B. We know that according to local_line_map,
/// in commit A, line 3 corresponds to line 7 of the original file. Now, line 3
/// in Commit A corresponds to line 6 in commit B. Then, we update
/// local_line_map to say that "Commit B line 6 goes to line 7 of the original
/// file". We repeat this for all lines in common in the two commits. For 2
/// identical files, we bulk replace all mappings from commit A to commit B in
/// local_line_map
fn process_files_in_commits(
store: &Store,
file_name: &RepoPath,
file_cache: &mut FileCache,
current_commit: &Commit,
parent_commit: &Commit,
) -> Result<HashMap<usize, usize>, BackendError> {
load_file_into_cache(
file_cache,
store,
current_commit.id(),
file_name,
&current_commit.tree()?,
)?;
load_file_into_cache(
file_cache,
store,
parent_commit.id(),
file_name,
&parent_commit.tree()?,
)?;
let current_contents = file_cache.get(current_commit.id()).unwrap();
let parent_contents = file_cache.get(parent_commit.id()).unwrap();
Ok(get_same_line_map(current_contents, parent_contents))
}
/// For two files, get a map of all lines in common (e.g. line 8 maps to line 9)
fn get_same_line_map(current_contents: &[u8], parent_contents: &[u8]) -> HashMap<usize, usize> {
let mut result_map = HashMap::new();
let inputs = vec![current_contents, parent_contents];
let diff = Diff::by_line(&inputs);
let mut current_line_counter: usize = 0;
let mut parent_line_counter: usize = 0;
for hunk in diff.hunks() {
match hunk {
DiffHunk::Matching(common_string) => {
for _ in common_string.split_inclusive(|b| *b == b'\n') {
result_map.insert(current_line_counter, parent_line_counter);
current_line_counter += 1;
parent_line_counter += 1;
}
}
DiffHunk::Different(outputs) => {
let current_output = outputs[0];
let parent_output = outputs[1];
if !current_output.is_empty() {
for _ in current_output.split_inclusive(|b| *b == b'\n') {
current_line_counter += 1;
}
}
if !parent_output.is_empty() {
for _ in parent_output.split_inclusive(|b| *b == b'\n') {
parent_line_counter += 1;
}
}
}
}
}
result_map
}
fn get_file_contents(
store: &Store,
path: &RepoPath,
tree: &MergedTree,
) -> Result<Option<Vec<u8>>, BackendError> {
let file_value = tree.path_value(path)?;
if file_value.is_absent() {
Ok(None)
} else {
let effective_file_value = materialize_tree_value(store, path, file_value).block_on()?;
match effective_file_value {
MaterializedTreeValue::File { mut reader, id, .. } => {
let mut file_contents = Vec::new();
let _ =
reader
.read_to_end(&mut file_contents)
.map_err(|e| BackendError::ReadFile {
path: path.to_owned(),
id,
source: Box::new(e),
});
Ok(Some(file_contents))
}
MaterializedTreeValue::FileConflict { id, contents, .. } => {
let mut materialized_conflict_buffer = Vec::new();
materialize_merge_result(&contents, &mut materialized_conflict_buffer).map_err(
|io_err| BackendError::ReadFile {
path: path.to_owned(),
source: Box::new(io_err),
id: id.first().clone().unwrap(),
},
)?;
Ok(Some(materialized_conflict_buffer))
}
_ => Ok(None),
}
}
}

View File

@ -28,6 +28,7 @@ extern crate self as jj_lib;
#[macro_use]
pub mod content_hash;
pub mod annotate;
pub mod backend;
pub mod commit;
pub mod commit_builder;