From 95cb24c3e9dfc3402a7d64eae1e2f364d0fa5d1d Mon Sep 17 00:00:00 2001 From: Martin von Zweigbergk Date: Fri, 21 Jun 2024 13:20:20 -0700 Subject: [PATCH] cli: split up `commands/operation.rs` into one file per command --- cli/src/commands/mod.rs | 4 +- cli/src/commands/operation.rs | 424 -------------------------- cli/src/commands/operation/abandon.rs | 133 ++++++++ cli/src/commands/operation/log.rs | 154 ++++++++++ cli/src/commands/operation/mod.rs | 92 ++++++ cli/src/commands/operation/restore.rs | 59 ++++ cli/src/commands/operation/undo.rs | 71 +++++ 7 files changed, 511 insertions(+), 426 deletions(-) delete mode 100644 cli/src/commands/operation.rs create mode 100644 cli/src/commands/operation/abandon.rs create mode 100644 cli/src/commands/operation/log.rs create mode 100644 cli/src/commands/operation/mod.rs create mode 100644 cli/src/commands/operation/restore.rs create mode 100644 cli/src/commands/operation/undo.rs diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 17be67dde4..744268d37f 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -146,7 +146,7 @@ enum Command { #[command(subcommand)] Util(util::UtilCommand), /// Undo an operation (shortcut for `jj op undo`) - Undo(operation::OperationUndoArgs), + Undo(operation::undo::OperationUndoArgs), Unsquash(unsquash::UnsquashArgs), Untrack(untrack::UntrackArgs), Version(version::VersionArgs), @@ -212,7 +212,7 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co Command::Squash(args) => squash::cmd_squash(ui, command_helper, args), Command::Status(args) => status::cmd_status(ui, command_helper, args), Command::Tag(args) => tag::cmd_tag(ui, command_helper, args), - Command::Undo(args) => operation::cmd_op_undo(ui, command_helper, args), + Command::Undo(args) => operation::undo::cmd_op_undo(ui, command_helper, args), Command::Unsquash(args) => unsquash::cmd_unsquash(ui, command_helper, args), Command::Untrack(args) => untrack::cmd_untrack(ui, command_helper, args), Command::Util(args) => util::cmd_util(ui, command_helper, args), diff --git a/cli/src/commands/operation.rs b/cli/src/commands/operation.rs deleted file mode 100644 index 86cd44dc95..0000000000 --- a/cli/src/commands/operation.rs +++ /dev/null @@ -1,424 +0,0 @@ -// Copyright 2020-2023 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::io::Write as _; -use std::slice; - -use clap::Subcommand; -use itertools::Itertools as _; -use jj_lib::object_id::ObjectId; -use jj_lib::op_store::OperationId; -use jj_lib::op_walk; -use jj_lib::operation::Operation; -use jj_lib::repo::Repo; - -use crate::cli_util::{format_template, short_operation_hash, CommandHelper, LogContentFormat}; -use crate::command_error::{user_error, user_error_with_hint, CommandError}; -use crate::graphlog::{get_graphlog, Edge}; -use crate::operation_templater::OperationTemplateLanguage; -use crate::ui::Ui; - -/// Commands for working with the operation log -/// -/// For information about the operation log, see -/// https://github.com/martinvonz/jj/blob/main/docs/operation-log.md. -#[derive(Subcommand, Clone, Debug)] -pub enum OperationCommand { - Abandon(OperationAbandonArgs), - Log(OperationLogArgs), - Undo(OperationUndoArgs), - Restore(OperationRestoreArgs), -} - -/// Show the operation log -#[derive(clap::Args, Clone, Debug)] -pub struct OperationLogArgs { - /// Limit number of operations to show - #[arg(long, short = 'n')] - limit: Option, - // TODO: Delete `-l` alias in jj 0.25+ - #[arg( - short = 'l', - hide = true, - conflicts_with = "limit", - value_name = "LIMIT" - )] - deprecated_limit: Option, - /// Don't show the graph, show a flat list of operations - #[arg(long)] - no_graph: bool, - /// Render each operation using the given template - /// - /// For the syntax, see https://github.com/martinvonz/jj/blob/main/docs/templates.md - #[arg(long, short = 'T')] - template: Option, -} - -/// Create a new operation that restores the repo to an earlier state -/// -/// This restores the repo to the state at the specified operation, effectively -/// undoing all later operations. It does so by creating a new operation. -#[derive(clap::Args, Clone, Debug)] -pub struct OperationRestoreArgs { - /// The operation to restore to - /// - /// Use `jj op log` to find an operation to restore to. Use e.g. `jj - /// --at-op= log` before restoring to an operation to see the - /// state of the repo at that operation. - operation: String, - - /// What portions of the local state to restore (can be repeated) - /// - /// This option is EXPERIMENTAL. - #[arg(long, value_enum, default_values_t = DEFAULT_UNDO_WHAT)] - what: Vec, -} - -/// Create a new operation that undoes an earlier operation -/// -/// This undoes an individual operation by applying the inverse of the -/// operation. -#[derive(clap::Args, Clone, Debug)] -pub struct OperationUndoArgs { - /// The operation to undo - /// - /// Use `jj op log` to find an operation to undo. - #[arg(default_value = "@")] - operation: String, - - /// What portions of the local state to restore (can be repeated) - /// - /// This option is EXPERIMENTAL. - #[arg(long, value_enum, default_values_t = DEFAULT_UNDO_WHAT)] - what: Vec, -} - -/// Abandon operation history -/// -/// To discard old operation history, use `jj op abandon ..`. It -/// will abandon the specified operation and all its ancestors. The descendants -/// will be reparented onto the root operation. -/// -/// To discard recent operations, use `jj op restore ` followed -/// by `jj op abandon ..@-`. -/// -/// The abandoned operations, commits, and other unreachable objects can later -/// be garbage collected by using `jj util gc` command. -#[derive(clap::Args, Clone, Debug)] -pub struct OperationAbandonArgs { - /// The operation or operation range to abandon - operation: String, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] -enum UndoWhatToRestore { - /// The jj repo state and local branches - Repo, - /// The remote-tracking branches. Do not restore these if you'd like to push - /// after the undo - RemoteTracking, -} - -const DEFAULT_UNDO_WHAT: [UndoWhatToRestore; 2] = - [UndoWhatToRestore::Repo, UndoWhatToRestore::RemoteTracking]; - -fn cmd_op_log( - ui: &mut Ui, - command: &CommandHelper, - args: &OperationLogArgs, -) -> Result<(), CommandError> { - // Don't load the repo so that the operation history can be inspected even - // with a corrupted repo state. For example, you can find the first bad - // operation id to be abandoned. - 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 with_content_format = LogContentFormat::new(ui, command.settings())?; - - let template; - let op_node_template; - { - let language = OperationTemplateLanguage::new( - repo_loader.op_store().root_operation_id(), - current_op_id, - command.operation_template_extensions(), - ); - let text = match &args.template { - Some(value) => value.to_owned(), - None => command.settings().config().get_string("templates.op_log")?, - }; - template = command - .parse_template( - ui, - &language, - &text, - OperationTemplateLanguage::wrap_operation, - )? - .labeled("op_log"); - op_node_template = command - .parse_template( - ui, - &language, - &command.settings().op_node_template(), - OperationTemplateLanguage::wrap_operation, - )? - .labeled("node"); - } - - ui.request_pager(); - let mut formatter = ui.stdout_formatter(); - let formatter = formatter.as_mut(); - if args.deprecated_limit.is_some() { - writeln!( - ui.warning_default(), - "The -l shorthand is deprecated, use -n instead." - )?; - } - let limit = args.limit.or(args.deprecated_limit).unwrap_or(usize::MAX); - let iter = op_walk::walk_ancestors(&head_ops).take(limit); - if !args.no_graph { - let mut graph = get_graphlog(command.settings(), formatter.raw()); - for op in iter { - let op = op?; - let mut edges = vec![]; - for id in op.parent_ids() { - edges.push(Edge::Direct(id.clone())); - } - let mut buffer = vec![]; - with_content_format.write_graph_text( - ui.new_formatter(&mut buffer).as_mut(), - |formatter| template.format(&op, formatter), - || graph.width(op.id(), &edges), - )?; - if !buffer.ends_with(b"\n") { - buffer.push(b'\n'); - } - let node_symbol = format_template(ui, &op, &op_node_template); - graph.add_node( - op.id(), - &edges, - &node_symbol, - &String::from_utf8_lossy(&buffer), - )?; - } - } else { - for op in iter { - let op = op?; - with_content_format.write(formatter, |formatter| template.format(&op, formatter))?; - } - } - - Ok(()) -} - -/// Restore only the portions of the view specified by the `what` argument -fn view_with_desired_portions_restored( - view_being_restored: &jj_lib::op_store::View, - current_view: &jj_lib::op_store::View, - what: &[UndoWhatToRestore], -) -> jj_lib::op_store::View { - let repo_source = if what.contains(&UndoWhatToRestore::Repo) { - view_being_restored - } else { - current_view - }; - let remote_source = if what.contains(&UndoWhatToRestore::RemoteTracking) { - view_being_restored - } else { - current_view - }; - jj_lib::op_store::View { - head_ids: repo_source.head_ids.clone(), - local_branches: repo_source.local_branches.clone(), - tags: repo_source.tags.clone(), - remote_views: remote_source.remote_views.clone(), - git_refs: current_view.git_refs.clone(), - git_head: current_view.git_head.clone(), - wc_commit_ids: repo_source.wc_commit_ids.clone(), - } -} - -pub fn cmd_op_undo( - ui: &mut Ui, - command: &CommandHelper, - args: &OperationUndoArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let bad_op = workspace_command.resolve_single_op(&args.operation)?; - let mut parent_ops = bad_op.parents(); - let Some(parent_op) = parent_ops.next().transpose()? else { - return Err(user_error("Cannot undo repo initialization")); - }; - if parent_ops.next().is_some() { - return Err(user_error("Cannot undo a merge operation")); - } - - let mut tx = workspace_command.start_transaction(); - let repo_loader = tx.base_repo().loader(); - let bad_repo = repo_loader.load_at(&bad_op)?; - let parent_repo = repo_loader.load_at(&parent_op)?; - tx.mut_repo().merge(&bad_repo, &parent_repo); - let new_view = view_with_desired_portions_restored( - tx.repo().view().store_view(), - tx.base_repo().view().store_view(), - &args.what, - ); - tx.mut_repo().set_view(new_view); - tx.finish(ui, format!("undo operation {}", bad_op.id().hex()))?; - - Ok(()) -} - -fn cmd_op_restore( - ui: &mut Ui, - command: &CommandHelper, - args: &OperationRestoreArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let target_op = workspace_command.resolve_single_op(&args.operation)?; - let mut tx = workspace_command.start_transaction(); - let new_view = view_with_desired_portions_restored( - target_op.view()?.store_view(), - tx.base_repo().view().store_view(), - &args.what, - ); - tx.mut_repo().set_view(new_view); - tx.finish(ui, format!("restore to operation {}", target_op.id().hex()))?; - - Ok(()) -} - -fn cmd_op_abandon( - ui: &mut Ui, - command: &CommandHelper, - args: &OperationAbandonArgs, -) -> Result<(), CommandError> { - // Don't load the repo so that this command can be used to recover from - // corrupted repo state. - let mut workspace = command.load_workspace()?; - let repo_loader = workspace.repo_loader(); - let op_store = repo_loader.op_store(); - // It doesn't make sense to create concurrent operations that will be merged - // with the current head. - let head_op_str = &command.global_args().at_operation; - if head_op_str != "@" { - return Err(user_error("--at-op is not respected")); - } - let current_head_op = op_walk::resolve_op_for_load(repo_loader, head_op_str)?; - let resolve_op = |op_str| op_walk::resolve_op_at(op_store, ¤t_head_op, op_str); - let (abandon_root_op, abandon_head_op) = - if let Some((root_op_str, head_op_str)) = args.operation.split_once("..") { - let root_op = if root_op_str.is_empty() { - let id = op_store.root_operation_id(); - let data = op_store.read_operation(id)?; - Operation::new(op_store.clone(), id.clone(), data) - } else { - resolve_op(root_op_str)? - }; - let head_op = if head_op_str.is_empty() { - current_head_op.clone() - } else { - resolve_op(head_op_str)? - }; - (root_op, head_op) - } else { - let op = resolve_op(&args.operation)?; - let parent_ops: Vec<_> = op.parents().try_collect()?; - let parent_op = match parent_ops.len() { - 0 => return Err(user_error("Cannot abandon the root operation")), - 1 => parent_ops.into_iter().next().unwrap(), - _ => return Err(user_error("Cannot abandon a merge operation")), - }; - (parent_op, op) - }; - - if abandon_head_op == current_head_op { - return Err(user_error_with_hint( - "Cannot abandon the current operation", - "Run `jj undo` to revert the current operation, then use `jj op abandon`", - )); - } - - // Reparent descendants, count the number of abandoned operations. - let stats = op_walk::reparent_range( - op_store.as_ref(), - slice::from_ref(&abandon_head_op), - slice::from_ref(¤t_head_op), - &abandon_root_op, - )?; - let [new_head_id]: [OperationId; 1] = stats.new_head_ids.try_into().unwrap(); - if current_head_op.id() == &new_head_id { - writeln!(ui.status(), "Nothing changed.")?; - return Ok(()); - } - writeln!( - ui.status(), - "Abandoned {} operations and reparented {} descendant operations.", - stats.unreachable_count, - stats.rewritten_count, - )?; - repo_loader - .op_heads_store() - .update_op_heads(slice::from_ref(current_head_op.id()), &new_head_id); - // Remap the operation id of the current workspace. If there were any - // concurrent operations, user will need to re-abandon their ancestors. - if !command.global_args().ignore_working_copy { - let mut locked_ws = workspace.start_working_copy_mutation()?; - let old_op_id = locked_ws.locked_wc().old_operation_id(); - if old_op_id != current_head_op.id() { - writeln!( - ui.warning_default(), - "The working copy operation {} is not updated because it differs from the repo {}.", - short_operation_hash(old_op_id), - short_operation_hash(current_head_op.id()), - )?; - } else { - locked_ws.finish(new_head_id)? - } - } - Ok(()) -} - -pub fn cmd_operation( - ui: &mut Ui, - command: &CommandHelper, - subcommand: &OperationCommand, -) -> Result<(), CommandError> { - match subcommand { - OperationCommand::Abandon(args) => cmd_op_abandon(ui, command, args), - OperationCommand::Log(args) => cmd_op_log(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/src/commands/operation/abandon.rs b/cli/src/commands/operation/abandon.rs new file mode 100644 index 0000000000..3d252c45e7 --- /dev/null +++ b/cli/src/commands/operation/abandon.rs @@ -0,0 +1,133 @@ +// Copyright 2020-2023 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::io::Write as _; +use std::slice; + +use itertools::Itertools as _; +use jj_lib::op_store::OperationId; +use jj_lib::op_walk; +use jj_lib::operation::Operation; + +use crate::cli_util::{short_operation_hash, CommandHelper}; +use crate::command_error::{user_error, user_error_with_hint, CommandError}; +use crate::ui::Ui; + +/// Abandon operation history +/// +/// To discard old operation history, use `jj op abandon ..`. It +/// will abandon the specified operation and all its ancestors. The descendants +/// will be reparented onto the root operation. +/// +/// To discard recent operations, use `jj op restore ` followed +/// by `jj op abandon ..@-`. +/// +/// The abandoned operations, commits, and other unreachable objects can later +/// be garbage collected by using `jj util gc` command. +#[derive(clap::Args, Clone, Debug)] +pub struct OperationAbandonArgs { + /// The operation or operation range to abandon + operation: String, +} + +pub fn cmd_op_abandon( + ui: &mut Ui, + command: &CommandHelper, + args: &OperationAbandonArgs, +) -> Result<(), CommandError> { + // Don't load the repo so that this command can be used to recover from + // corrupted repo state. + let mut workspace = command.load_workspace()?; + let repo_loader = workspace.repo_loader(); + let op_store = repo_loader.op_store(); + // It doesn't make sense to create concurrent operations that will be merged + // with the current head. + let head_op_str = &command.global_args().at_operation; + if head_op_str != "@" { + return Err(user_error("--at-op is not respected")); + } + let current_head_op = op_walk::resolve_op_for_load(repo_loader, head_op_str)?; + let resolve_op = |op_str| op_walk::resolve_op_at(op_store, ¤t_head_op, op_str); + let (abandon_root_op, abandon_head_op) = + if let Some((root_op_str, head_op_str)) = args.operation.split_once("..") { + let root_op = if root_op_str.is_empty() { + let id = op_store.root_operation_id(); + let data = op_store.read_operation(id)?; + Operation::new(op_store.clone(), id.clone(), data) + } else { + resolve_op(root_op_str)? + }; + let head_op = if head_op_str.is_empty() { + current_head_op.clone() + } else { + resolve_op(head_op_str)? + }; + (root_op, head_op) + } else { + let op = resolve_op(&args.operation)?; + let parent_ops: Vec<_> = op.parents().try_collect()?; + let parent_op = match parent_ops.len() { + 0 => return Err(user_error("Cannot abandon the root operation")), + 1 => parent_ops.into_iter().next().unwrap(), + _ => return Err(user_error("Cannot abandon a merge operation")), + }; + (parent_op, op) + }; + + if abandon_head_op == current_head_op { + return Err(user_error_with_hint( + "Cannot abandon the current operation", + "Run `jj undo` to revert the current operation, then use `jj op abandon`", + )); + } + + // Reparent descendants, count the number of abandoned operations. + let stats = op_walk::reparent_range( + op_store.as_ref(), + slice::from_ref(&abandon_head_op), + slice::from_ref(¤t_head_op), + &abandon_root_op, + )?; + let [new_head_id]: [OperationId; 1] = stats.new_head_ids.try_into().unwrap(); + if current_head_op.id() == &new_head_id { + writeln!(ui.status(), "Nothing changed.")?; + return Ok(()); + } + writeln!( + ui.status(), + "Abandoned {} operations and reparented {} descendant operations.", + stats.unreachable_count, + stats.rewritten_count, + )?; + repo_loader + .op_heads_store() + .update_op_heads(slice::from_ref(current_head_op.id()), &new_head_id); + // Remap the operation id of the current workspace. If there were any + // concurrent operations, user will need to re-abandon their ancestors. + if !command.global_args().ignore_working_copy { + let mut locked_ws = workspace.start_working_copy_mutation()?; + let old_op_id = locked_ws.locked_wc().old_operation_id(); + if old_op_id != current_head_op.id() { + writeln!( + ui.warning_default(), + "The working copy operation {} is not updated because it differs from the repo {}.", + short_operation_hash(old_op_id), + short_operation_hash(current_head_op.id()), + )?; + } else { + locked_ws.finish(new_head_id)? + } + } + Ok(()) +} diff --git a/cli/src/commands/operation/log.rs b/cli/src/commands/operation/log.rs new file mode 100644 index 0000000000..d0bc505ff5 --- /dev/null +++ b/cli/src/commands/operation/log.rs @@ -0,0 +1,154 @@ +// Copyright 2020-2023 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::op_walk; + +use crate::cli_util::{format_template, CommandHelper, LogContentFormat}; +use crate::command_error::CommandError; +use crate::graphlog::{get_graphlog, Edge}; +use crate::operation_templater::OperationTemplateLanguage; +use crate::ui::Ui; + +/// Show the operation log +#[derive(clap::Args, Clone, Debug)] +pub struct OperationLogArgs { + /// Limit number of operations to show + #[arg(long, short = 'n')] + limit: Option, + // TODO: Delete `-l` alias in jj 0.25+ + #[arg( + short = 'l', + hide = true, + conflicts_with = "limit", + value_name = "LIMIT" + )] + deprecated_limit: Option, + /// Don't show the graph, show a flat list of operations + #[arg(long)] + no_graph: bool, + /// Render each operation using the given template + /// + /// For the syntax, see https://github.com/martinvonz/jj/blob/main/docs/templates.md + #[arg(long, short = 'T')] + template: Option, +} + +pub fn cmd_op_log( + ui: &mut Ui, + command: &CommandHelper, + args: &OperationLogArgs, +) -> Result<(), CommandError> { + // Don't load the repo so that the operation history can be inspected even + // with a corrupted repo state. For example, you can find the first bad + // operation id to be abandoned. + 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 with_content_format = LogContentFormat::new(ui, command.settings())?; + + let template; + let op_node_template; + { + let language = OperationTemplateLanguage::new( + repo_loader.op_store().root_operation_id(), + current_op_id, + command.operation_template_extensions(), + ); + let text = match &args.template { + Some(value) => value.to_owned(), + None => command.settings().config().get_string("templates.op_log")?, + }; + template = command + .parse_template( + ui, + &language, + &text, + OperationTemplateLanguage::wrap_operation, + )? + .labeled("op_log"); + op_node_template = command + .parse_template( + ui, + &language, + &command.settings().op_node_template(), + OperationTemplateLanguage::wrap_operation, + )? + .labeled("node"); + } + + ui.request_pager(); + let mut formatter = ui.stdout_formatter(); + let formatter = formatter.as_mut(); + if args.deprecated_limit.is_some() { + writeln!( + ui.warning_default(), + "The -l shorthand is deprecated, use -n instead." + )?; + } + let limit = args.limit.or(args.deprecated_limit).unwrap_or(usize::MAX); + let iter = op_walk::walk_ancestors(&head_ops).take(limit); + if !args.no_graph { + let mut graph = get_graphlog(command.settings(), formatter.raw()); + for op in iter { + let op = op?; + let mut edges = vec![]; + for id in op.parent_ids() { + edges.push(Edge::Direct(id.clone())); + } + let mut buffer = vec![]; + with_content_format.write_graph_text( + ui.new_formatter(&mut buffer).as_mut(), + |formatter| template.format(&op, formatter), + || graph.width(op.id(), &edges), + )?; + if !buffer.ends_with(b"\n") { + buffer.push(b'\n'); + } + let node_symbol = format_template(ui, &op, &op_node_template); + graph.add_node( + op.id(), + &edges, + &node_symbol, + &String::from_utf8_lossy(&buffer), + )?; + } + } else { + for op in iter { + let op = op?; + with_content_format.write(formatter, |formatter| template.format(&op, formatter))?; + } + } + + Ok(()) +} diff --git a/cli/src/commands/operation/mod.rs b/cli/src/commands/operation/mod.rs new file mode 100644 index 0000000000..1567f4ed7d --- /dev/null +++ b/cli/src/commands/operation/mod.rs @@ -0,0 +1,92 @@ +// Copyright 2020-2023 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. + +mod abandon; +mod log; +mod restore; +pub mod undo; + +use abandon::{cmd_op_abandon, OperationAbandonArgs}; +use clap::Subcommand; +use log::{cmd_op_log, OperationLogArgs}; +use restore::{cmd_op_restore, OperationRestoreArgs}; +use undo::{cmd_op_undo, OperationUndoArgs}; + +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Commands for working with the operation log +/// +/// For information about the operation log, see +/// https://github.com/martinvonz/jj/blob/main/docs/operation-log.md. +#[derive(Subcommand, Clone, Debug)] +pub enum OperationCommand { + Abandon(OperationAbandonArgs), + Log(OperationLogArgs), + Undo(OperationUndoArgs), + Restore(OperationRestoreArgs), +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] +pub enum UndoWhatToRestore { + /// The jj repo state and local branches + Repo, + /// The remote-tracking branches. Do not restore these if you'd like to push + /// after the undo + RemoteTracking, +} + +pub const DEFAULT_UNDO_WHAT: [UndoWhatToRestore; 2] = + [UndoWhatToRestore::Repo, UndoWhatToRestore::RemoteTracking]; + +/// Restore only the portions of the view specified by the `what` argument +fn view_with_desired_portions_restored( + view_being_restored: &jj_lib::op_store::View, + current_view: &jj_lib::op_store::View, + what: &[UndoWhatToRestore], +) -> jj_lib::op_store::View { + let repo_source = if what.contains(&UndoWhatToRestore::Repo) { + view_being_restored + } else { + current_view + }; + let remote_source = if what.contains(&UndoWhatToRestore::RemoteTracking) { + view_being_restored + } else { + current_view + }; + jj_lib::op_store::View { + head_ids: repo_source.head_ids.clone(), + local_branches: repo_source.local_branches.clone(), + tags: repo_source.tags.clone(), + remote_views: remote_source.remote_views.clone(), + git_refs: current_view.git_refs.clone(), + git_head: current_view.git_head.clone(), + wc_commit_ids: repo_source.wc_commit_ids.clone(), + } +} + +pub fn cmd_operation( + ui: &mut Ui, + command: &CommandHelper, + subcommand: &OperationCommand, +) -> Result<(), CommandError> { + match subcommand { + OperationCommand::Abandon(args) => cmd_op_abandon(ui, command, args), + OperationCommand::Log(args) => cmd_op_log(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/src/commands/operation/restore.rs b/cli/src/commands/operation/restore.rs new file mode 100644 index 0000000000..a9c9dc2695 --- /dev/null +++ b/cli/src/commands/operation/restore.rs @@ -0,0 +1,59 @@ +// Copyright 2020-2023 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::object_id::ObjectId; + +use super::{view_with_desired_portions_restored, UndoWhatToRestore, DEFAULT_UNDO_WHAT}; +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Create a new operation that restores the repo to an earlier state +/// +/// This restores the repo to the state at the specified operation, effectively +/// undoing all later operations. It does so by creating a new operation. +#[derive(clap::Args, Clone, Debug)] +pub struct OperationRestoreArgs { + /// The operation to restore to + /// + /// Use `jj op log` to find an operation to restore to. Use e.g. `jj + /// --at-op= log` before restoring to an operation to see the + /// state of the repo at that operation. + operation: String, + + /// What portions of the local state to restore (can be repeated) + /// + /// This option is EXPERIMENTAL. + #[arg(long, value_enum, default_values_t = DEFAULT_UNDO_WHAT)] + what: Vec, +} + +pub fn cmd_op_restore( + ui: &mut Ui, + command: &CommandHelper, + args: &OperationRestoreArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let target_op = workspace_command.resolve_single_op(&args.operation)?; + let mut tx = workspace_command.start_transaction(); + let new_view = view_with_desired_portions_restored( + target_op.view()?.store_view(), + tx.base_repo().view().store_view(), + &args.what, + ); + tx.mut_repo().set_view(new_view); + tx.finish(ui, format!("restore to operation {}", target_op.id().hex()))?; + + Ok(()) +} diff --git a/cli/src/commands/operation/undo.rs b/cli/src/commands/operation/undo.rs new file mode 100644 index 0000000000..2468ce62f2 --- /dev/null +++ b/cli/src/commands/operation/undo.rs @@ -0,0 +1,71 @@ +// Copyright 2020-2023 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::object_id::ObjectId; +use jj_lib::repo::Repo; + +use super::{view_with_desired_portions_restored, UndoWhatToRestore, DEFAULT_UNDO_WHAT}; +use crate::cli_util::CommandHelper; +use crate::command_error::{user_error, CommandError}; +use crate::ui::Ui; + +/// Create a new operation that undoes an earlier operation +/// +/// This undoes an individual operation by applying the inverse of the +/// operation. +#[derive(clap::Args, Clone, Debug)] +pub struct OperationUndoArgs { + /// The operation to undo + /// + /// Use `jj op log` to find an operation to undo. + #[arg(default_value = "@")] + operation: String, + + /// What portions of the local state to restore (can be repeated) + /// + /// This option is EXPERIMENTAL. + #[arg(long, value_enum, default_values_t = DEFAULT_UNDO_WHAT)] + what: Vec, +} + +pub fn cmd_op_undo( + ui: &mut Ui, + command: &CommandHelper, + args: &OperationUndoArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let bad_op = workspace_command.resolve_single_op(&args.operation)?; + let mut parent_ops = bad_op.parents(); + let Some(parent_op) = parent_ops.next().transpose()? else { + return Err(user_error("Cannot undo repo initialization")); + }; + if parent_ops.next().is_some() { + return Err(user_error("Cannot undo a merge operation")); + } + + let mut tx = workspace_command.start_transaction(); + let repo_loader = tx.base_repo().loader(); + let bad_repo = repo_loader.load_at(&bad_op)?; + let parent_repo = repo_loader.load_at(&parent_op)?; + tx.mut_repo().merge(&bad_repo, &parent_repo); + let new_view = view_with_desired_portions_restored( + tx.repo().view().store_view(), + tx.base_repo().view().store_view(), + &args.what, + ); + tx.mut_repo().set_view(new_view); + tx.finish(ui, format!("undo operation {}", bad_op.id().hex()))?; + + Ok(()) +}