From 684840df62901b0ab3b7806d506a40fd987b9e22 Mon Sep 17 00:00:00 2001 From: Alec Snyder Date: Mon, 22 Jul 2024 22:16:10 +0300 Subject: [PATCH] Add command jj annotate 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 --- CHANGELOG.md | 5 + cli/src/commands/annotate.rs | 85 +++++++ cli/src/commands/mod.rs | 3 + cli/src/config/templates.toml | 6 + cli/tests/cli-reference@.md.snap | 16 ++ cli/tests/runner.rs | 1 + cli/tests/test_annotate_command.rs | 114 +++++++++ lib/src/annotate.rs | 373 +++++++++++++++++++++++++++++ lib/src/lib.rs | 1 + 9 files changed, 604 insertions(+) create mode 100644 cli/src/commands/annotate.rs create mode 100644 cli/tests/test_annotate_command.rs create mode 100644 lib/src/annotate.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e1444c566b..af69cfbba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `. + 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. diff --git a/cli/src/commands/annotate.rs b/cli/src/commands/annotate.rs new file mode 100644 index 0000000000..375e43f4c6 --- /dev/null +++ b/cli/src/commands/annotate.rs @@ -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(()) +} diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 4498ed398c..6fd0bcfb3c 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. mod abandon; +mod annotate; mod backout; #[cfg(feature = "bench")] mod bench; @@ -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)] @@ -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), diff --git a/cli/src/config/templates.toml b/cli/src/config/templates.toml index ecc199a0ca..fcceb42aac 100644 --- a/cli/src/config/templates.toml +++ b/cli/src/config/templates.toml @@ -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)), diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index b06a2f5e0f..c271f45b79 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -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) @@ -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 @@ -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 ` + +###### **Arguments:** + +* `` — the file to annotate + + + ## `jj backout` Apply the reverse of a revision on top of another revision diff --git a/cli/tests/runner.rs b/cli/tests/runner.rs index 116667adae..5f234146d6 100644 --- a/cli/tests/runner.rs +++ b/cli/tests/runner.rs @@ -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; diff --git a/cli/tests/test_annotate_command.rs b/cli/tests/test_annotate_command.rs new file mode 100644 index 0000000000..ddbfab959c --- /dev/null +++ b/cli/tests/test_annotate_command.rs @@ -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 + "###); +} diff --git a/lib/src/annotate.rs b/lib/src/annotate.rs new file mode 100644 index 0000000000..d39455e021 --- /dev/null +++ b/lib/src/annotate.rs @@ -0,0 +1,373 @@ +// 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 + +use std::collections::HashMap; +use std::io::BufRead; + +use thiserror::Error; + +use crate::backend::{BackendError, BackendResult, CommitId, FileId, TreeValue}; +use crate::commit::Commit; +use crate::diff::{Diff, DiffHunk}; +use crate::merged_tree::MergedTree; +use crate::object_id::ObjectId; +use crate::repo::{ReadonlyRepo, Repo}; +use crate::repo_path::RepoPath; +use crate::store::Store; + +/// Various errors that can arise from annotation +#[derive(Debug, Error)] +pub enum AnnotateError { + /// the requested file path was not found + #[error("Unable to locate file: {0}")] + FileNotFound(String), + /// the file type is incorrect. Usually a directory was given but a regular + /// file is required + #[error("File {0} must be a regular file, not a directory")] + UnsupportedFileType(String), + /// the file is in a conflicted state and can therefore not be annotated + /// properly + #[error("File {0} is conflicted at commit: {0}")] + Conflicted(String, String), + /// pass-through of uncaught backend errors + #[error(transparent)] + BackendError(#[from] BackendError), +} + +/// 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, String)>, +} + +/// A note on implementation: +/// This structure represents the results along the way. +/// We first start at the original commit, for each commit, we compare the file +/// to the version in each parent. We only look at lines in common. For each +/// line in common, we add it to local_line_map according to how the lines match +/// up. If, we discover a line that is not in common with any parent commit, we +/// know that the current commit originated that line and we add it to +/// original_line_map. +/// We then proceed to walk through the graph, until we've found commits for +/// each line (local_line_map is empty when this happens) +struct PartialResults { + /// A mapping from line_number in the original file to most recent commit + /// that changed it. + original_line_map: HashMap, + /// CommitId -> (line_number in CommitId -> line_number in the original). + /// This is a map for a given commit_id, returns a mapping of line numbers + /// in the file version at commit_id to the original version. + /// For example, Commit 123 contains a map {(1, 1), (2, 3)} which means line + /// 1 at 123 goes to the original line 1 and line 2 at 123 goes to line 3 at + /// the original + local_line_map: HashMap>, +} + +impl PartialResults { + fn new(starting_commit_id: &CommitId, num_lines: usize) -> Self { + let mut starting_map = HashMap::new(); + for i in 0..num_lines { + starting_map.insert(i, i); + } + let mut results = PartialResults { + original_line_map: HashMap::new(), + local_line_map: HashMap::new(), + }; + results + .local_line_map + .insert(starting_commit_id.clone(), starting_map); + results + } + + /// Take a line mapping from an old commit and move it to a new commit. + /// For example, if we figure out that line 2 in commit A maps to line 7 in + /// the original, and line 3 in commit B maps to line 2 in commit A, we + /// update the mapping so line 3 maps to line 7 in the original. + fn forward_to_new_commit( + &mut self, + old_commit_id: &CommitId, + old_local_line_number: usize, + new_commit_id: &CommitId, + new_local_line_number: usize, + ) { + let old_map = self.local_line_map.get_mut(old_commit_id); + if old_map.is_none() { + return; + } + let old_map = old_map.unwrap(); + let removed_original_line_number = old_map.remove(&old_local_line_number); + if removed_original_line_number.is_none() { + return; + } + let removed_original_line_number = removed_original_line_number.unwrap(); + if self.local_line_map.contains_key(new_commit_id) { + self.local_line_map + .get_mut(new_commit_id) + .unwrap() + .insert(new_local_line_number, removed_original_line_number); + } else { + let mut new_map = HashMap::new(); + new_map.insert(new_local_line_number, removed_original_line_number); + self.local_line_map.insert(new_commit_id.clone(), new_map); + } + } + + /// Used for two commits with the same file contents. We wholesale move all + /// mappings from the old commit to the new commit. + fn swap_full_commit_id(&mut self, old_commit_id: &CommitId, new_commit_id: &CommitId) { + let old_commit_map = self.local_line_map.remove(old_commit_id); + if old_commit_map.is_none() { + return; + } + let old_commit_map = old_commit_map.unwrap(); + self.local_line_map + .insert(new_commit_id.clone(), old_commit_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 drain_remaining_for_commit_id(&mut self, commit_id: &CommitId) { + let remaining_lines = self.local_line_map.remove(commit_id); + if remaining_lines.is_none() { + return; + } + let remaining_lines = remaining_lines.unwrap(); + for (_, original_line_number) in remaining_lines { + self.original_line_map + .insert(original_line_number, commit_id.clone()); + } + } + + fn get_next_commit(&self) -> Option { + return self.local_line_map.keys().next().cloned(); + } + + fn convert_to_results(self, original_contents: &[u8]) -> AnnotateResults { + let original_content_lines: Vec = + original_contents.lines().map(|s| s.unwrap()).collect(); + let mut result_lines = Vec::new(); + for (idx, line) in original_content_lines.into_iter().enumerate() { + result_lines.push((self.original_line_map.get(&idx).unwrap().clone(), line)); + } + AnnotateResults { + file_annotations: result_lines, + } + } +} + +/// Get line by line annotations for a specific file path in the repo. +pub fn get_annotation_for_file( + file_path: &RepoPath, + repo: &ReadonlyRepo, + starting_commit_id: &CommitId, +) -> Result { + let store = repo.store(); + + let current_commit = store.get_commit(starting_commit_id)?; + let current_tree = current_commit.tree()?; + let original_file_id = get_file_id(current_commit.id(), ¤t_tree, file_path)?; + if original_file_id.is_none() { + return Err(AnnotateError::FileNotFound( + file_path.as_internal_file_string().to_string(), + )); + } + let original_contents = get_file_contents(repo.store(), file_path, &original_file_id.unwrap())?; + let num_lines = original_contents.split_inclusive(|b| *b == b'\n').count(); + let mut partial_results = PartialResults::new(starting_commit_id, num_lines); + + process_commits(&mut partial_results, repo, current_commit.id(), file_path)?; + + Ok(partial_results.convert_to_results(&original_contents)) +} + +/// Starting at the starting commit, compute changes at that commit, updating +/// the mappings. So long as there are mappings left in local_line_map, we +/// continue. Once local_line_map is empty, we've found sources for each line +/// and exit. +fn process_commits( + results: &mut PartialResults, + repo: &ReadonlyRepo, + starting_commit_id: &CommitId, + file_name: &RepoPath, +) -> Result<(), AnnotateError> { + let mut current_commit = repo.store().get_commit(starting_commit_id)?; + loop { + process_commit(results, ¤t_commit, repo, file_name)?; + let next_commit_id = results.get_next_commit(); + match next_commit_id { + None => break, + Some(next_id) => { + current_commit = repo.store().get_commit(&next_id)?; + } + } + } + Ok(()) +} + +/// 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. +fn process_commit( + results: &mut PartialResults, + commit: &Commit, + repo: &ReadonlyRepo, + file_name: &RepoPath, +) -> Result<(), AnnotateError> { + let current_tree = repo.store().get_commit(commit.id())?.tree()?; + let current_file_id = get_file_id(commit.id(), ¤t_tree, file_name)?.unwrap(); + + for parent in commit.parents() { + let parent_commit = parent?; + let parent_tree = parent_commit.tree()?; + let parent_file_id = get_file_id(parent_commit.id(), &parent_tree, file_name)?; + + if let Some(pfid) = parent_file_id { + process_file_ids( + results, + repo.store(), + file_name, + ¤t_file_id, + commit.id(), + &pfid, + parent_commit.id(), + )?; + } + } + results.drain_remaining_for_commit_id(commit.id()); + + Ok(()) +} + +/// For two versions of the same file, for all the lines in common, overwrite +/// the new mapping in the results for the new commit. Meaning, 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_file_ids( + results: &mut PartialResults, + store: &Store, + file_name: &RepoPath, + current_file_id: &FileId, + current_commit_id: &CommitId, + parent_file_id: &FileId, + parent_commit_id: &CommitId, +) -> BackendResult<()> { + if current_file_id == parent_file_id { + results.swap_full_commit_id(current_commit_id, parent_commit_id); + return Ok(()); + } + let current_contents = get_file_contents(store, file_name, current_file_id)?; + let parent_contents = get_file_contents(store, file_name, parent_file_id)?; + + let same_lines = get_same_line_map(¤t_contents, &parent_contents); + for (current_line_no, parent_line_no) in same_lines { + results.forward_to_new_commit( + current_commit_id, + current_line_no, + parent_commit_id, + parent_line_no, + ); + } + Ok(()) +} + +/// 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 { + 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.lines() { + 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.lines() { + current_line_counter += 1; + } + } + if !parent_output.is_empty() { + for _ in parent_output.lines() { + parent_line_counter += 1; + } + } + } + } + } + + result_map +} + +fn get_file_id( + commit_id: &CommitId, + tree: &MergedTree, + file_name: &RepoPath, +) -> Result, AnnotateError> { + let file_value = tree.path_value(file_name)?; + if file_value.is_absent() { + return Ok(None); + } + if !file_value.is_resolved() { + return Err(AnnotateError::Conflicted( + file_name.to_internal_dir_string(), + commit_id.hex(), + )); + } + + let file_object = file_value.first().as_ref().unwrap(); + match file_object { + TreeValue::File { id, .. } => Ok(Some(id.clone())), + _ => Err(AnnotateError::UnsupportedFileType( + file_name.to_internal_dir_string(), + )), + } +} + +fn get_file_contents(store: &Store, path: &RepoPath, file_id: &FileId) -> BackendResult> { + let mut reader = store.read_file(path, file_id)?; + let mut contents: Vec = Vec::new(); + let err = reader.read_to_end(&mut contents); + if let Err(e) = err { + return Err(BackendError::ReadFile { + path: path.to_owned(), + id: file_id.to_owned(), + source: Box::new(e), + }); + } + Ok(contents) +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 149956ad2b..52855ff9bf 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -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;