From c82584f18d5439d94a7862da67ed0bb4831b4f14 Mon Sep 17 00:00:00 2001 From: Benjamin Tan Date: Thu, 2 May 2024 16:29:31 +0800 Subject: [PATCH] cli: add `jj operation show` and `jj operation diff` commands --- CHANGELOG.md | 3 + cli/src/commands/operation.rs | 642 ++++++++++++++++++++++++++++++- cli/tests/cli-reference@.md.snap | 95 +++++ lib/src/repo.rs | 6 + 4 files changed, 742 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 263390d10c..066feb7ef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New features +* New commands `jj op show` and `jj op diff` that can show the changes made in a + single operation, and compare changes made between two operations, respectively. + ### Fixed bugs ## [0.17.0] - 2024-05-01 diff --git a/cli/src/commands/operation.rs b/cli/src/commands/operation.rs index 782cb85335..5673d43900 100644 --- a/cli/src/commands/operation.rs +++ b/cli/src/commands/operation.rs @@ -12,19 +12,36 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::collections::HashMap; use std::io::Write as _; use std::slice; +use std::sync::Arc; use clap::Subcommand; +use indexmap::IndexMap; use itertools::Itertools as _; +use jj_lib::backend::{BackendResult, ChangeId, CommitId}; +use jj_lib::commit::Commit; +use jj_lib::git::REMOTE_NAME_FOR_LOCAL_GIT_REPO; +use jj_lib::matchers::EverythingMatcher; use jj_lib::object_id::ObjectId; -use jj_lib::op_store::OperationId; -use jj_lib::op_walk; +use jj_lib::op_store::{OpStoreResult, OperationId, RefTarget, RemoteRef, RemoteRefState}; use jj_lib::operation::Operation; -use jj_lib::repo::Repo; +use jj_lib::refs::{diff_named_ref_targets, diff_named_remote_refs}; +use jj_lib::repo::{MutableRepo, ReadonlyRepo, Repo, RepoLoader}; +use jj_lib::revset::RevsetIteratorExt; +use jj_lib::revset_graph::{RevsetGraphEdge, TopoGroupedRevsetGraphIterator}; +use jj_lib::rewrite::{merge_commit_trees, rebase_to_dest_parent}; +use jj_lib::settings::UserSettings; +use jj_lib::{dag_walk, op_walk, revset}; -use crate::cli_util::{format_template, short_operation_hash, CommandHelper, LogContentFormat}; +use crate::cli_util::{ + format_template, short_change_hash, short_operation_hash, CommandHelper, LogContentFormat, + WorkspaceCommandHelper, WorkspaceCommandTransaction, +}; use crate::command_error::{user_error, user_error_with_hint, CommandError}; +use crate::diff_util::{self, DiffFormat, DiffFormatArgs}; +use crate::formatter::Formatter; use crate::graphlog::{get_graphlog, Edge}; use crate::operation_templater::OperationTemplateLanguage; use crate::ui::Ui; @@ -36,7 +53,9 @@ use crate::ui::Ui; #[derive(Subcommand, Clone, Debug)] pub enum OperationCommand { Abandon(OperationAbandonArgs), + Diff(OperationDiffArgs), Log(OperationLogArgs), + Show(OperationShowArgs), Undo(OperationUndoArgs), Restore(OperationRestoreArgs), } @@ -122,6 +141,52 @@ enum UndoWhatToRestore { RemoteTracking, } +/// Show changes to the repository in an operation +#[derive(clap::Args, Clone, Debug)] +pub struct OperationShowArgs { + /// Show repository changes in this operation, compared to its parent(s) + #[arg(default_value = "@")] + operation: String, + /// Don't show the graph, show a flat list of modified changes + #[arg(long)] + no_graph: bool, + /// Show patch of modifications to changes + /// + /// If the previous version has different parents, it will be temporarily + /// rebased to the parents of the new version, so the diff is not + /// contaminated by unrelated changes. + #[arg(long, short = 'p')] + patch: bool, + #[command(flatten)] + diff_format: DiffFormatArgs, +} + +/// Compare changes to the repository between two operations +#[derive(clap::Args, Clone, Debug)] +pub struct OperationDiffArgs { + /// Show repository changes in this operation, compared to its parent + #[arg(long)] + operation: Option, + /// Show repository changes from this operation + #[arg(long, conflicts_with = "operation")] + from: Option, + /// Show repository changes to this operation + #[arg(long, conflicts_with = "operation")] + to: Option, + /// Don't show the graph, show a flat list of modified changes + #[arg(long)] + no_graph: bool, + /// Show patch of modifications to changes + /// + /// If the previous version has different parents, it will be temporarily + /// rebased to the parents of the new version, so the diff is not + /// contaminated by unrelated changes. + #[arg(long, short = 'p')] + patch: bool, + #[command(flatten)] + diff_format: DiffFormatArgs, +} + const DEFAULT_UNDO_WHAT: [UndoWhatToRestore; 2] = [UndoWhatToRestore::Repo, UndoWhatToRestore::RemoteTracking]; @@ -395,6 +460,573 @@ fn cmd_op_abandon( Ok(()) } +fn merge_operations( + repo_loader: &RepoLoader, + settings: &UserSettings, + mut operations: impl ExactSizeIterator>, +) -> Result, CommandError> { + let num_operations = operations.len(); + if num_operations == 0 { + return Ok(None); + } + + let base_op = operations.next().transpose()?.unwrap(); + let final_op = if num_operations > 1 { + let base_repo = repo_loader.load_at(&base_op)?; + let mut tx = base_repo.start_transaction(settings); + for other_op in operations { + let other_op = other_op?; + tx.merge_operation(other_op)?; + tx.mut_repo().rebase_descendants(settings)?; + } + let tx_description = format!("merge {} operations", num_operations); + let merged_repo = tx.write(tx_description).leave_unpublished(); + merged_repo.operation().clone() + } else { + base_op + }; + + Ok(Some(final_op)) +} + +fn cmd_op_show( + ui: &mut Ui, + command: &CommandHelper, + args: &OperationShowArgs, +) -> Result<(), CommandError> { + // TODO: Should we load the repo here? + let workspace = command.load_workspace()?; + let repo_loader = workspace.repo_loader(); + let head_op_str = &command.global_args().at_operation; + let head_ops = if head_op_str == "@" { + // If multiple head ops can't be resolved without merging, let the + // current op be empty. Beware that resolve_op_for_load() will eliminate + // redundant heads whereas get_current_head_ops() won't. + let current_op = op_walk::resolve_op_for_load(repo_loader, head_op_str).ok(); + if let Some(op) = current_op { + vec![op] + } else { + op_walk::get_current_head_ops( + repo_loader.op_store(), + repo_loader.op_heads_store().as_ref(), + )? + } + } else { + vec![op_walk::resolve_op_for_load(repo_loader, head_op_str)?] + }; + let current_op_id = match &*head_ops { + [op] => Some(op.id()), + _ => None, + }; + let op = op_walk::resolve_op_for_load(repo_loader, &args.operation)?; + let parent_op = merge_operations(repo_loader, command.settings(), op.parents())?; + if parent_op.is_none() { + return Err(user_error("Cannot show the root operation")); + } + let parent_op = parent_op.unwrap(); + let diff_formats = + diff_util::diff_formats_for_log(command.settings(), &args.diff_format, args.patch)?; + let with_content_format = LogContentFormat::new(ui, command.settings())?; + + // TODO: Should we make this customizable via clap arg? + let template; + { + let language = OperationTemplateLanguage::new( + repo_loader.op_store().root_operation_id(), + current_op_id, + command.operation_template_extensions(), + ); + let text = command.settings().config().get_string("templates.op_log")?; + template = command + .parse_template( + ui, + &language, + &text, + OperationTemplateLanguage::wrap_operation, + )? + .labeled("op_log"); + } + + let parent_repo = repo_loader.load_at(&parent_op)?; + let repo = repo_loader.load_at(&op)?; + + ui.request_pager(); + template.format(&op, ui.stdout_formatter().as_mut())?; + writeln!(ui.stdout())?; + + show_op_diff( + ui, + command, + &parent_repo, + &repo, + !args.no_graph, + &with_content_format, + &diff_formats, + ) +} + +fn cmd_op_diff( + ui: &mut Ui, + command: &CommandHelper, + args: &OperationDiffArgs, +) -> Result<(), CommandError> { + // TODO: Should we load the repo here? + let workspace = command.load_workspace()?; + let repo_loader = workspace.repo_loader(); + let head_op_str = &command.global_args().at_operation; + let from_op; + let to_op; + if args.from.is_some() || args.to.is_some() { + from_op = + op_walk::resolve_op_for_load(repo_loader, args.from.as_ref().unwrap_or(head_op_str))?; + to_op = op_walk::resolve_op_for_load(repo_loader, args.to.as_ref().unwrap_or(head_op_str))?; + } else { + to_op = op_walk::resolve_op_for_load( + repo_loader, + args.operation.as_ref().unwrap_or(head_op_str), + )?; + let merged_parents_op = merge_operations(repo_loader, command.settings(), to_op.parents())?; + if merged_parents_op.is_none() { + return Err(user_error("Cannot diff operation with no parents")); + } + from_op = merged_parents_op.unwrap(); + } + let diff_formats = + diff_util::diff_formats_for_log(command.settings(), &args.diff_format, args.patch)?; + let with_content_format = LogContentFormat::new(ui, command.settings())?; + + let from_repo = repo_loader.load_at(&from_op)?; + let to_repo = repo_loader.load_at(&to_op)?; + + ui.request_pager(); + writeln!( + ui.stdout(), + "From operation {}: {}", + short_operation_hash(from_op.id()), + from_op.metadata().description, + )?; + writeln!( + ui.stdout(), + " To operation {}: {}", + short_operation_hash(to_op.id()), + to_op.metadata().description, + )?; + writeln!(ui.stdout())?; + + show_op_diff( + ui, + command, + &from_repo, + &to_repo, + !args.no_graph, + &with_content_format, + &diff_formats, + ) +} + +// Computes and shows the differences between two operations, using the given +// `Repo`s for the operations. +fn show_op_diff( + ui: &mut Ui, + command: &CommandHelper, + from_repo: &Arc, + to_repo: &Arc, + show_graph: bool, + with_content_format: &LogContentFormat, + diff_formats: &[DiffFormat], +) -> Result<(), CommandError> { + // TODO: Remove? This other workspace command is used only in to show diffs. + let diff_workspace_command = + command.for_loaded_repo(ui, command.load_workspace()?, to_repo.clone())?; + // Create a new transaction starting from `to_repo`. + let mut workspace_command = + command.for_loaded_repo(ui, command.load_workspace()?, to_repo.clone())?; + let mut tx = workspace_command.start_transaction(); + // Merge index from `from_repo` to `to_repo`, so commits in `from_repo` are + // accessible. + tx.mut_repo().merge_index(from_repo); + + let changes = compute_operation_commits_diff(tx.mut_repo(), from_repo, to_repo)?; + + let commit_id_change_id_map: HashMap = changes + .iter() + .flat_map(|(change_id, modified_change)| { + modified_change + .added_commits + .iter() + .map(|commit| (commit.id().clone(), change_id.clone())) + .chain( + modified_change + .removed_commits + .iter() + .map(|commit| (commit.id().clone(), change_id.clone())), + ) + }) + .collect(); + + let change_parents: HashMap<_, _> = changes + .iter() + .map(|(change_id, modified_change)| { + let parent_change_ids = get_parent_changes(modified_change, &commit_id_change_id_map); + (change_id.clone(), parent_change_ids) + }) + .collect(); + + // Order changes in reverse topological order. + let ordered_changes = dag_walk::topo_order_reverse( + changes.keys().cloned().collect_vec(), + |change_id: &ChangeId| change_id.clone(), + |change_id: &ChangeId| change_parents.get(change_id).unwrap().clone(), + ); + + let graph_iter = TopoGroupedRevsetGraphIterator::new(ordered_changes.iter().map(|change_id| { + let parent_change_ids = change_parents.get(change_id).unwrap(); + ( + change_id.clone(), + parent_change_ids + .iter() + .map(|parent_change_id| RevsetGraphEdge::direct(parent_change_id.clone())) + .collect_vec(), + ) + })); + + let mut formatter = ui.stdout_formatter(); + let formatter = formatter.as_mut(); + + if !ordered_changes.is_empty() { + writeln!(formatter, "Changed commits:")?; + if show_graph { + let mut graph = get_graphlog(command.settings(), formatter.raw()); + for (change_id, edges) in graph_iter { + let modified_change = changes.get(&change_id).unwrap(); + let edges = edges + .iter() + .map(|edge| Edge::Direct(edge.target.clone())) + .collect_vec(); + + let mut buffer = vec![]; + with_content_format.write_graph_text( + ui.new_formatter(&mut buffer).as_mut(), + |formatter| { + write_modified_change_summary(formatter, &tx, &change_id, modified_change) + }, + || graph.width(&change_id, &edges), + )?; + if !buffer.ends_with(b"\n") { + buffer.push(b'\n'); + } + if !diff_formats.is_empty() { + let mut formatter = ui.new_formatter(&mut buffer); + show_change_diff( + ui, + formatter.as_mut(), + &tx, + &diff_workspace_command, + modified_change, + diff_formats, + )?; + } + + // TODO: customize node symbol? + let node_symbol = "○"; + graph.add_node( + &change_id, + &edges, + node_symbol, + &String::from_utf8_lossy(&buffer), + )?; + } + } else { + for (change_id, _) in graph_iter { + let modified_change = changes.get(&change_id).unwrap(); + write_modified_change_summary(formatter, &tx, &change_id, modified_change)?; + if !diff_formats.is_empty() { + show_change_diff( + ui, + formatter, + &tx, + &diff_workspace_command, + modified_change, + diff_formats, + )?; + } + } + } + writeln!(formatter)?; + } + + let changed_local_branches = diff_named_ref_targets( + from_repo.view().local_branches(), + to_repo.view().local_branches(), + ) + .collect_vec(); + if !changed_local_branches.is_empty() { + writeln!(formatter, "Changed local branches:")?; + for (name, (from_target, to_target)) in changed_local_branches { + writeln!(formatter, "{}:", name)?; + write_ref_target_summary(formatter, &tx, "+", to_target)?; + write_ref_target_summary(formatter, &tx, "-", from_target)?; + } + writeln!(formatter)?; + } + + let changed_tags = + diff_named_ref_targets(from_repo.view().tags(), to_repo.view().tags()).collect_vec(); + if !changed_tags.is_empty() { + writeln!(formatter, "Changed tags:")?; + for (name, (from_target, to_target)) in changed_tags { + writeln!(formatter, "{}:", name)?; + write_ref_target_summary(formatter, &tx, "+", to_target)?; + write_ref_target_summary(formatter, &tx, "-", from_target)?; + } + writeln!(formatter)?; + } + + let changed_remote_branches = diff_named_remote_refs( + from_repo.view().all_remote_branches(), + to_repo.view().all_remote_branches(), + ) + .collect_vec(); + if !changed_remote_branches.is_empty() { + writeln!(formatter, "Changed remote branches:")?; + let format_remote_ref_prefix = |prefix: &str, remote_ref: &RemoteRef| { + format!( + "{} ({})", + prefix, + match remote_ref.state { + RemoteRefState::New => "untracked", + RemoteRefState::Tracking => "tracked", + } + ) + }; + for ((name, remote_name), (from_ref, to_ref)) in changed_remote_branches { + // Skip updates to local git repo, since they should typically be covered in + // local branches. + if remote_name == REMOTE_NAME_FOR_LOCAL_GIT_REPO { + continue; + } + writeln!(formatter, "{}@{}:", name, remote_name)?; + write_ref_target_summary( + formatter, + &tx, + &format_remote_ref_prefix("+", to_ref), + &to_ref.target, + )?; + write_ref_target_summary( + formatter, + &tx, + &format_remote_ref_prefix("-", from_ref), + &from_ref.target, + )?; + } + } + + Ok(()) +} + +// Writes a summary for the given `ModifiedChange`. +fn write_modified_change_summary( + formatter: &mut dyn Formatter, + tx: &WorkspaceCommandTransaction, + change_id: &ChangeId, + modified_change: &ModifiedChange, +) -> Result<(), std::io::Error> { + writeln!( + formatter, + "Modified change {}", + short_change_hash(change_id) + )?; + for commit in modified_change.added_commits.iter() { + write!(formatter, "+")?; + tx.write_commit_summary(formatter, commit)?; + writeln!(formatter)?; + } + for commit in modified_change.removed_commits.iter() { + write!(formatter, "-")?; + tx.write_commit_summary(formatter, commit)?; + writeln!(formatter)?; + } + Ok(()) +} + +// Writes a summary for the given `RefTarget`. +fn write_ref_target_summary( + formatter: &mut dyn Formatter, + tx: &WorkspaceCommandTransaction, + prefix: &str, + ref_target: &RefTarget, +) -> Result<(), CommandError> { + if ref_target.is_absent() { + writeln!(formatter, "{} (absent)", prefix)?; + } else if ref_target.has_conflict() { + for commit_id in ref_target.added_ids() { + write!(formatter, "{} (added) ", prefix)?; + let commit = tx.repo().store().get_commit(commit_id)?; + tx.write_commit_summary(formatter, &commit)?; + writeln!(formatter)?; + } + for commit_id in ref_target.removed_ids() { + write!(formatter, "{} (removed) ", prefix)?; + let commit = tx.repo().store().get_commit(commit_id)?; + tx.write_commit_summary(formatter, &commit)?; + writeln!(formatter)?; + } + } else { + write!(formatter, "{} ", prefix)?; + let commit_id = ref_target.as_normal().unwrap(); + let commit = tx.repo().store().get_commit(commit_id)?; + tx.write_commit_summary(formatter, &commit)?; + writeln!(formatter)?; + } + Ok(()) +} + +// Returns the change IDs of the parents of the given `modified_change`, which +// are the parents of all newly added commits for the change, or the parents of +// all removed commits if there are no added commits. +fn get_parent_changes( + modified_change: &ModifiedChange, + commit_id_change_id_map: &HashMap, +) -> Vec { + // TODO: how should we handle multiple added or removed commits? + // This logic is probably slightly iffy. + if !modified_change.added_commits.is_empty() { + modified_change + .added_commits + .iter() + .flat_map(|commit| commit.parent_ids()) + .filter_map(|parent_id| commit_id_change_id_map.get(parent_id).cloned()) + .unique() + .collect_vec() + } else { + modified_change + .removed_commits + .iter() + .flat_map(|commit| commit.parent_ids()) + .filter_map(|parent_id| commit_id_change_id_map.get(parent_id).cloned()) + .unique() + .collect_vec() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct ModifiedChange { + added_commits: Vec, + removed_commits: Vec, +} + +// Compute the changes in commits between two operations, returned as a +// `HashMap` from `ChangeId` to a `ModifiedChange` struct containing the added +// and removed commits for the change ID. +fn compute_operation_commits_diff( + repo: &MutableRepo, + from_repo: &ReadonlyRepo, + to_repo: &ReadonlyRepo, +) -> BackendResult> { + let mut changes: IndexMap = IndexMap::new(); + + let from_heads = from_repo.view().heads().iter().cloned().collect_vec(); + let to_heads = to_repo.view().heads().iter().cloned().collect_vec(); + + // Find newly added commits in `to_repo` which were not present in + // `from_repo`. + for commit in revset::walk_revs(repo, &to_heads, &from_heads) + .unwrap() + .iter() + .commits(repo.store()) + { + let commit = commit?; + let modified_change = changes + .entry(commit.change_id().clone()) + .or_insert_with(|| ModifiedChange { + added_commits: vec![], + removed_commits: vec![], + }); + modified_change.added_commits.push(commit); + } + + // Find commits which were hidden in `to_repo`. + for commit in revset::walk_revs(repo, &from_heads, &to_heads) + .unwrap() + .iter() + .commits(repo.store()) + { + let commit = commit?; + let modified_change = changes + .entry(commit.change_id().clone()) + .or_insert_with(|| ModifiedChange { + added_commits: vec![], + removed_commits: vec![], + }); + modified_change.removed_commits.push(commit); + } + + Ok(changes) +} + +// Displays the diffs of a modified change. The output differs based on the +// commits added and removed for the change. +// If there is a single added and removed commit, the diff is shown between the +// removed commit and the added commit rebased onto the removed commit's +// parents. If there is only a single added or single removed commit, the diff +// is shown of that commit's contents. +fn show_change_diff( + ui: &Ui, + formatter: &mut dyn Formatter, + tx: &WorkspaceCommandTransaction, + workspace_command: &WorkspaceCommandHelper, + modified_change: &ModifiedChange, + diff_formats: &[DiffFormat], +) -> Result<(), CommandError> { + // TODO: how should we handle multiple added or removed commits? + // Alternatively, use `predecessors`? + if modified_change.added_commits.len() == 1 && modified_change.removed_commits.len() == 1 { + let commit = &modified_change.added_commits[0]; + let predecessor = &modified_change.removed_commits[0]; + let predecessor_tree = rebase_to_dest_parent(tx.repo(), predecessor, commit)?; + let tree = commit.tree()?; + diff_util::show_diff( + ui, + formatter, + workspace_command, + &predecessor_tree, + &tree, + &EverythingMatcher, + diff_formats, + )?; + } + // TODO: Should we even show a diff for added or removed commits? + else if modified_change.added_commits.len() == 1 { + let commit = &modified_change.added_commits[0]; + let parent_tree = merge_commit_trees(tx.repo(), &commit.parents())?; + let tree = commit.tree()?; + diff_util::show_diff( + ui, + formatter, + workspace_command, + &parent_tree, + &tree, + &EverythingMatcher, + diff_formats, + )?; + } else if modified_change.removed_commits.len() == 1 { + let commit = &modified_change.removed_commits[0]; + let parent_tree = merge_commit_trees(workspace_command.repo().as_ref(), &commit.parents())?; + let tree = commit.tree()?; + diff_util::show_diff( + ui, + formatter, + workspace_command, + &parent_tree, + &tree, + &EverythingMatcher, + diff_formats, + )?; + } + + Ok(()) +} + pub fn cmd_operation( ui: &mut Ui, command: &CommandHelper, @@ -402,7 +1034,9 @@ pub fn cmd_operation( ) -> Result<(), CommandError> { match subcommand { OperationCommand::Abandon(args) => cmd_op_abandon(ui, command, args), + OperationCommand::Diff(args) => cmd_op_diff(ui, command, args), OperationCommand::Log(args) => cmd_op_log(ui, command, args), + OperationCommand::Show(args) => cmd_op_show(ui, command, args), OperationCommand::Restore(args) => cmd_op_restore(ui, command, args), OperationCommand::Undo(args) => cmd_op_undo(ui, command, args), } diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index a542c8d236..5b273ba7be 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -58,7 +58,9 @@ This document contains the help content for the `jj` command-line program. * [`jj obslog`↴](#jj-obslog) * [`jj operation`↴](#jj-operation) * [`jj operation abandon`↴](#jj-operation-abandon) +* [`jj operation diff`↴](#jj-operation-diff) * [`jj operation log`↴](#jj-operation-log) +* [`jj operation show`↴](#jj-operation-show) * [`jj operation undo`↴](#jj-operation-undo) * [`jj operation restore`↴](#jj-operation-restore) * [`jj parallelize`↴](#jj-parallelize) @@ -1261,7 +1263,9 @@ For information about the operation log, see https://github.com/martinvonz/jj/bl ###### **Subcommands:** * `abandon` — Abandon operation history +* `diff` — Compare changes to the repository between two operations * `log` — Show the operation log +* `show` — Show changes to the repository in an operation * `undo` — Create a new operation that undoes an earlier operation * `restore` — Create a new operation that restores the repo to an earlier state @@ -1285,6 +1289,50 @@ The abandoned operations, commits, and other unreachable objects can later be ga +## `jj operation diff` + +Compare changes to the repository between two operations + +**Usage:** `jj operation diff [OPTIONS]` + +###### **Options:** + +* `--operation ` — Show repository changes in this operation, compared to its parent +* `--from ` — Show repository changes from this operation +* `--to ` — Show repository changes to this operation +* `--no-graph` — Don't show the graph, show a flat list of modified changes + + Possible values: `true`, `false` + +* `-p`, `--patch` — Show patch of modifications to changes + + Possible values: `true`, `false` + +* `-s`, `--summary` — For each path, show only whether it was modified, added, or deleted + + Possible values: `true`, `false` + +* `--stat` — Show a histogram of the changes + + Possible values: `true`, `false` + +* `--types` — For each path, show only its type before and after + + Possible values: `true`, `false` + +* `--git` — Show a Git-format diff + + Possible values: `true`, `false` + +* `--color-words` — Show a word-level diff with changes indicated only by color + + Possible values: `true`, `false` + +* `--tool ` — Generate diff by external command +* `--context ` — Number of lines of context to show + + + ## `jj operation log` Show the operation log @@ -1302,6 +1350,53 @@ Show the operation log +## `jj operation show` + +Show changes to the repository in an operation + +**Usage:** `jj operation show [OPTIONS] [OPERATION]` + +###### **Arguments:** + +* `` — Show repository changes in this operation, compared to its parent(s) + + Default value: `@` + +###### **Options:** + +* `--no-graph` — Don't show the graph, show a flat list of modified changes + + Possible values: `true`, `false` + +* `-p`, `--patch` — Show patch of modifications to changes + + Possible values: `true`, `false` + +* `-s`, `--summary` — For each path, show only whether it was modified, added, or deleted + + Possible values: `true`, `false` + +* `--stat` — Show a histogram of the changes + + Possible values: `true`, `false` + +* `--types` — For each path, show only its type before and after + + Possible values: `true`, `false` + +* `--git` — Show a Git-format diff + + Possible values: `true`, `false` + +* `--color-words` — Show a word-level diff with changes indicated only by color + + Possible values: `true`, `false` + +* `--tool ` — Generate diff by external command +* `--context ` — Number of lines of context to show + + + ## `jj operation undo` Create a new operation that undoes an earlier operation diff --git a/lib/src/repo.rs b/lib/src/repo.rs index 6e01b528b8..0556c03c62 100644 --- a/lib/src/repo.rs +++ b/lib/src/repo.rs @@ -1564,6 +1564,12 @@ impl MutableRepo { self.view.mark_dirty(); } + pub fn merge_index(&mut self, other_repo: &ReadonlyRepo) { + self.index.merge_in(other_repo.readonly_index()); + self.view.ensure_clean(|v| self.enforce_view_invariants(v)); + self.view.mark_dirty(); + } + fn merge_view(&mut self, base: &View, other: &View) { // Merge working-copy commits. If there's a conflict, we keep the self side. for (workspace_id, base_wc_commit) in base.wc_commit_ids() {