jj/cli/src/commands/mod.rs
2023-08-28 15:58:34 -07:00

3792 lines
136 KiB
Rust

// Copyright 2020 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.
#[cfg(feature = "bench")]
mod bench;
mod branch;
mod debug;
mod git;
mod operation;
use std::collections::{BTreeMap, HashSet};
use std::fmt::Debug;
use std::io::{BufRead, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::{fs, io};
use clap::builder::NonEmptyStringValueParser;
use clap::parser::ValueSource;
use clap::{ArgGroup, Command, CommandFactory, FromArgMatches, Subcommand};
use indexmap::{IndexMap, IndexSet};
use itertools::Itertools;
use jj_lib::backend::{CommitId, ObjectId, TreeValue};
use jj_lib::commit::Commit;
use jj_lib::dag_walk::topo_order_reverse;
use jj_lib::git_backend::GitBackend;
use jj_lib::matchers::EverythingMatcher;
use jj_lib::merge::Merge;
use jj_lib::merged_tree::{MergedTree, MergedTreeBuilder};
use jj_lib::op_store::WorkspaceId;
use jj_lib::repo::{ReadonlyRepo, Repo};
use jj_lib::repo_path::RepoPath;
use jj_lib::revset::{RevsetExpression, RevsetFilterPredicate, RevsetIteratorExt};
use jj_lib::revset_graph::{
ReverseRevsetGraphIterator, RevsetGraphEdgeType, TopoGroupedRevsetGraphIterator,
};
use jj_lib::rewrite::{back_out_commit, merge_commit_trees, rebase_commit, DescendantRebaser};
use jj_lib::settings::UserSettings;
use jj_lib::tree::{merge_trees, Tree};
use jj_lib::working_copy::SnapshotOptions;
use jj_lib::workspace::Workspace;
use jj_lib::{conflicts, file_util, revset};
use maplit::{hashmap, hashset};
use tracing::instrument;
use crate::cli_util::{
self, check_stale_working_copy, get_new_config_file_path, print_checkout_stats,
resolve_multiple_nonempty_revsets, resolve_multiple_nonempty_revsets_default_single,
run_ui_editor, serialize_config_value, short_commit_hash, user_error, user_error_with_hint,
write_config_value_to_file, Args, CommandError, CommandHelper, LogContentFormat, RevisionArg,
WorkspaceCommandHelper,
};
use crate::config::{AnnotatedValue, ConfigSource};
use crate::diff_util::{self, DiffFormat, DiffFormatArgs};
use crate::formatter::{Formatter, PlainTextFormatter};
use crate::graphlog::{get_graphlog, Edge};
use crate::text_util;
use crate::ui::Ui;
#[derive(clap::Parser, Clone, Debug)]
enum Commands {
Abandon(AbandonArgs),
Backout(BackoutArgs),
#[cfg(feature = "bench")]
#[command(subcommand)]
Bench(bench::BenchCommands),
#[command(subcommand)]
Branch(branch::BranchSubcommand),
#[command(alias = "print")]
Cat(CatArgs),
Checkout(CheckoutArgs),
Chmod(ChmodArgs),
Commit(CommitArgs),
#[command(subcommand)]
Config(ConfigSubcommand),
#[command(subcommand)]
Debug(debug::DebugCommands),
Describe(DescribeArgs),
Diff(DiffArgs),
Diffedit(DiffeditArgs),
Duplicate(DuplicateArgs),
Edit(EditArgs),
Files(FilesArgs),
#[command(subcommand)]
Git(git::GitCommands),
Init(InitArgs),
Interdiff(InterdiffArgs),
Log(LogArgs),
/// Merge work from multiple branches
///
/// Unlike most other VCSs, `jj merge` does not implicitly include the
/// working copy revision's parent as one of the parents of the merge;
/// you need to explicitly list all revisions that should become parents
/// of the merge.
///
/// This is the same as `jj new`, except that it requires at least two
/// arguments.
Merge(NewArgs),
Move(MoveArgs),
New(NewArgs),
Obslog(ObslogArgs),
#[command(subcommand)]
#[command(visible_alias = "op")]
Operation(operation::OperationCommands),
Rebase(RebaseArgs),
Resolve(ResolveArgs),
Restore(RestoreArgs),
#[command(hide = true)]
// TODO: Flesh out.
Run(RunArgs),
Show(ShowArgs),
#[command(subcommand)]
Sparse(SparseArgs),
Split(SplitArgs),
Squash(SquashArgs),
Status(StatusArgs),
#[command(subcommand)]
Util(UtilCommands),
/// Undo an operation (shortcut for `jj op undo`)
Undo(operation::OperationUndoArgs),
Unsquash(UnsquashArgs),
Untrack(UntrackArgs),
Version(VersionArgs),
#[command(subcommand)]
Workspace(WorkspaceCommands),
}
/// Display version information
#[derive(clap::Args, Clone, Debug)]
struct VersionArgs {}
/// Create a new repo in the given directory
///
/// If the given directory does not exist, it will be created. If no directory
/// is given, the current directory is used.
#[derive(clap::Args, Clone, Debug)]
#[command(group(ArgGroup::new("backend").args(&["git", "git_repo"])))]
struct InitArgs {
/// The destination directory
#[arg(default_value = ".", value_hint = clap::ValueHint::DirPath)]
destination: String,
/// Use the Git backend, creating a jj repo backed by a Git repo
#[arg(long)]
git: bool,
/// Path to a git repo the jj repo will be backed by
#[arg(long, value_hint = clap::ValueHint::DirPath)]
git_repo: Option<String>,
}
#[derive(clap::Args, Clone, Debug)]
#[command(group = clap::ArgGroup::new("config_level").multiple(false).required(true))]
struct ConfigArgs {
/// Target the user-level config
#[arg(long, group = "config_level")]
user: bool,
/// Target the repo-level config
#[arg(long, group = "config_level")]
repo: bool,
}
impl ConfigArgs {
fn get_source_kind(&self) -> ConfigSource {
if self.user {
ConfigSource::User
} else if self.repo {
ConfigSource::Repo
} else {
// Shouldn't be reachable unless clap ArgGroup is broken.
panic!("No config_level provided");
}
}
}
/// Manage config options
///
/// Operates on jj configuration, which comes from the config file and
/// environment variables.
///
/// For file locations, supported config options, and other details about jj
/// config, see https://github.com/martinvonz/jj/blob/main/docs/config.md.
#[derive(clap::Subcommand, Clone, Debug)]
enum ConfigSubcommand {
#[command(visible_alias("l"))]
List(ConfigListArgs),
#[command(visible_alias("g"))]
Get(ConfigGetArgs),
#[command(visible_alias("s"))]
Set(ConfigSetArgs),
#[command(visible_alias("e"))]
Edit(ConfigEditArgs),
}
/// List variables set in config file, along with their values.
#[derive(clap::Args, Clone, Debug)]
struct ConfigListArgs {
/// An optional name of a specific config option to look up.
#[arg(value_parser = NonEmptyStringValueParser::new())]
pub name: Option<String>,
/// Whether to explicitly include built-in default values in the list.
#[arg(long)]
pub include_defaults: bool,
// TODO(#1047): Support --show-origin using LayeredConfigs.
// TODO(#1047): Support ConfigArgs (--user or --repo).
}
/// Get the value of a given config option.
///
/// Unlike `jj config list`, the result of `jj config get` is printed without
/// extra formatting and therefore is usable in scripting. For example:
///
/// $ jj config list user.name
/// user.name="Martin von Zweigbergk"
/// $ jj config get user.name
/// Martin von Zweigbergk
#[derive(clap::Args, Clone, Debug)]
#[command(verbatim_doc_comment)]
struct ConfigGetArgs {
#[arg(required = true)]
name: String,
}
/// Update config file to set the given option to a given value.
#[derive(clap::Args, Clone, Debug)]
struct ConfigSetArgs {
#[arg(required = true)]
name: String,
#[arg(required = true)]
value: String,
#[clap(flatten)]
config_args: ConfigArgs,
}
/// Start an editor on a jj config file.
#[derive(clap::Args, Clone, Debug)]
struct ConfigEditArgs {
#[clap(flatten)]
pub config_args: ConfigArgs,
}
/// Create a new, empty change and edit it in the working copy
///
/// For more information, see
/// https://github.com/martinvonz/jj/blob/main/docs/working-copy.md.
#[derive(clap::Args, Clone, Debug)]
#[command(visible_aliases = &["co"])]
struct CheckoutArgs {
/// The revision to update to
revision: RevisionArg,
/// Ignored (but lets you pass `-r` for consistency with other commands)
#[arg(short = 'r', hide = true)]
unused_revision: bool,
/// The change description to use
#[arg(long = "message", short, value_name = "MESSAGE")]
message_paragraphs: Vec<String>,
}
/// Stop tracking specified paths in the working copy
#[derive(clap::Args, Clone, Debug)]
struct UntrackArgs {
/// Paths to untrack
#[arg(required = true, value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
/// List files in a revision
#[derive(clap::Args, Clone, Debug)]
struct FilesArgs {
/// The revision to list files in
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
/// Only list files matching these prefixes (instead of all files)
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
/// Print contents of a file in a revision
#[derive(clap::Args, Clone, Debug)]
struct CatArgs {
/// The revision to get the file contents from
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
/// The file to print
#[arg(value_hint = clap::ValueHint::FilePath)]
path: String,
}
/// Show changes in a revision
///
/// With the `-r` option, which is the default, shows the changes compared to
/// the parent revision. If there are several parent revisions (i.e., the given
/// revision is a merge), then they will be merged and the changes from the
/// result to the given revision will be shown.
///
/// With the `--from` and/or `--to` options, shows the difference from/to the
/// given revisions. If either is left out, it defaults to the working-copy
/// commit. For example, `jj diff --from main` shows the changes from "main"
/// (perhaps a branch name) to the working-copy commit.
#[derive(clap::Args, Clone, Debug)]
struct DiffArgs {
/// Show changes in this revision, compared to its parent(s)
#[arg(long, short)]
revision: Option<RevisionArg>,
/// Show changes from this revision
#[arg(long, conflicts_with = "revision")]
from: Option<RevisionArg>,
/// Show changes to this revision
#[arg(long, conflicts_with = "revision")]
to: Option<RevisionArg>,
/// Restrict the diff to these paths
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
#[command(flatten)]
format: DiffFormatArgs,
}
/// Show commit description and changes in a revision
#[derive(clap::Args, Clone, Debug)]
struct ShowArgs {
/// Show changes in this revision, compared to its parent(s)
#[arg(default_value = "@")]
revision: RevisionArg,
/// Ignored (but lets you pass `-r` for consistency with other commands)
#[arg(short = 'r', hide = true)]
unused_revision: bool,
#[command(flatten)]
format: DiffFormatArgs,
}
/// Show high-level repo status
///
/// This includes:
///
/// * The working copy commit and its (first) parent, and a summary of the
/// changes between them
///
/// * Conflicted branches (see https://github.com/martinvonz/jj/blob/main/docs/branches.md)
#[derive(clap::Args, Clone, Debug)]
#[command(visible_alias = "st")]
struct StatusArgs {}
/// Show commit history
#[derive(clap::Args, Clone, Debug)]
struct LogArgs {
/// Which revisions to show. Defaults to the `revsets.log` setting,
/// or `@ | (remote_branches() | tags()).. | ((remote_branches() |
/// tags())..)-` if it is not set.
#[arg(long, short)]
revisions: Vec<RevisionArg>,
/// Show commits modifying the given paths
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
/// Show revisions in the opposite order (older revisions first)
#[arg(long)]
reversed: bool,
/// Limit number of revisions to show
///
/// Applied after revisions are filtered and reordered.
#[arg(long, short)]
limit: Option<usize>,
/// Don't show the graph, show a flat list of revisions
#[arg(long)]
no_graph: bool,
/// Render each revision using the given template
///
/// For the syntax, see https://github.com/martinvonz/jj/blob/main/docs/templates.md
#[arg(long, short = 'T')]
template: Option<String>,
/// Show patch
#[arg(long, short = 'p')]
patch: bool,
#[command(flatten)]
diff_format: DiffFormatArgs,
}
/// Show how a change has evolved
///
/// Show how a change has evolved as it's been updated, rebased, etc.
#[derive(clap::Args, Clone, Debug)]
struct ObslogArgs {
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
/// Limit number of revisions to show
#[arg(long, short)]
limit: Option<usize>,
/// Don't show the graph, show a flat list of revisions
#[arg(long)]
no_graph: bool,
/// Render each revision using the given template
///
/// For the syntax, see https://github.com/martinvonz/jj/blob/main/docs/templates.md
#[arg(long, short = 'T')]
template: Option<String>,
/// Show patch compared to the previous version of this change
///
/// 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 the changes of two commits
///
/// This excludes changes from other commits by temporarily rebasing `--from`
/// onto `--to`'s parents. If you wish to compare the same change across
/// versions, consider `jj obslog -p` instead.
#[derive(clap::Args, Clone, Debug)]
#[command(group(ArgGroup::new("to_diff").args(&["from", "to"]).multiple(true).required(true)))]
struct InterdiffArgs {
/// Show changes from this revision
#[arg(long)]
from: Option<RevisionArg>,
/// Show changes to this revision
#[arg(long)]
to: Option<RevisionArg>,
/// Restrict the diff to these paths
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
#[command(flatten)]
format: DiffFormatArgs,
}
/// Update the change description or other metadata
///
/// Starts an editor to let you edit the description of a change. The editor
/// will be $EDITOR, or `pico` if that's not defined (`Notepad` on Windows).
#[derive(clap::Args, Clone, Debug)]
struct DescribeArgs {
/// The revision whose description to edit
#[arg(default_value = "@")]
revision: RevisionArg,
/// Ignored (but lets you pass `-r` for consistency with other commands)
#[arg(short = 'r', hide = true)]
unused_revision: bool,
/// The change description to use (don't open editor)
#[arg(long = "message", short, value_name = "MESSAGE")]
message_paragraphs: Vec<String>,
/// Read the change description from stdin
#[arg(long)]
stdin: bool,
/// Don't open an editor
///
/// This is mainly useful in combination with e.g. `--reset-author`.
#[arg(long)]
no_edit: bool,
/// Reset the author to the configured user
///
/// This resets the author name, email, and timestamp.
#[arg(long)]
reset_author: bool,
}
/// Update the description and create a new change on top.
#[derive(clap::Args, Clone, Debug)]
#[command(visible_aliases=&["ci"])]
struct CommitArgs {
/// The change description to use (don't open editor)
#[arg(long = "message", short, value_name = "MESSAGE")]
message_paragraphs: Vec<String>,
}
/// Create a new change with the same content as an existing one
#[derive(clap::Args, Clone, Debug)]
struct DuplicateArgs {
/// The revision(s) to duplicate
#[arg(default_value = "@")]
revisions: Vec<RevisionArg>,
/// Ignored (but lets you pass `-r` for consistency with other commands)
#[arg(short = 'r', hide = true)]
unused_revision: bool,
}
/// Abandon a revision
///
/// Abandon a revision, rebasing descendants onto its parent(s). The behavior is
/// similar to `jj restore --changes-in`; the difference is that `jj abandon`
/// gives you a new change, while `jj restore` updates the existing change.
#[derive(clap::Args, Clone, Debug)]
struct AbandonArgs {
/// The revision(s) to abandon
#[arg(default_value = "@")]
revisions: Vec<RevisionArg>,
/// Do not print every abandoned commit on a separate line
#[arg(long, short)]
summary: bool,
/// Ignored (but lets you pass `-r` for consistency with other commands)
#[arg(short = 'r', hide = true)]
unused_revision: bool,
}
/// Edit a commit in the working copy
///
/// Puts the contents of a commit in the working copy for editing. Any changes
/// you make in the working copy will update (amend) the commit.
#[derive(clap::Args, Clone, Debug)]
struct EditArgs {
/// The commit to edit
revision: RevisionArg,
/// Ignored (but lets you pass `-r` for consistency with other commands)
#[arg(short = 'r', hide = true)]
unused_revision: bool,
}
/// Create a new, empty change and edit it in the working copy
///
/// Note that you can create a merge commit by specifying multiple revisions as
/// argument. For example, `jj new main @` will create a new commit with the
/// `main` branch and the working copy as parents.
///
/// For more information, see
/// https://github.com/martinvonz/jj/blob/main/docs/working-copy.md.
#[derive(clap::Args, Clone, Debug)]
#[command(group(ArgGroup::new("order").args(&["insert_after", "insert_before"])))]
struct NewArgs {
/// Parent(s) of the new change
#[arg(default_value = "@")]
revisions: Vec<RevisionArg>,
/// Ignored (but lets you pass `-r` for consistency with other commands)
#[arg(short = 'r', hide = true)]
unused_revision: bool,
/// The change description to use
#[arg(long = "message", short, value_name = "MESSAGE")]
message_paragraphs: Vec<String>,
/// Deprecated. Please prefix the revset with `all:` instead.
#[arg(long, short = 'L', hide = true)]
allow_large_revsets: bool,
/// Insert the new change between the target commit(s) and their children
#[arg(long, short = 'A', visible_alias = "after")]
insert_after: bool,
/// Insert the new change between the target commit(s) and their parents
#[arg(long, short = 'B', visible_alias = "before")]
insert_before: bool,
}
/// Move changes from one revision into another
///
/// Use `--interactive` to move only part of the source revision into the
/// destination. The selected changes (or all the changes in the source revision
/// if not using `--interactive`) will be moved into the destination. The
/// changes will be removed from the source. If that means that the source is
/// now empty compared to its parent, it will be abandoned. Without
/// `--interactive`, the source change will always be empty.
///
/// If the source became empty and both the source and destination had a
/// non-empty description, you will be asked for the combined description. If
/// either was empty, then the other one will be used.
#[derive(clap::Args, Clone, Debug)]
#[command(group(ArgGroup::new("to_move").args(&["from", "to"]).multiple(true).required(true)))]
struct MoveArgs {
/// Move part of this change into the destination
#[arg(long)]
from: Option<RevisionArg>,
/// Move part of the source into this change
#[arg(long)]
to: Option<RevisionArg>,
/// Interactively choose which parts to move
#[arg(long, short)]
interactive: bool,
/// Move only changes to these paths (instead of all paths)
#[arg(conflicts_with = "interactive", value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
/// Move changes from a revision into its parent
///
/// After moving the changes into the parent, the child revision will have the
/// same content state as before. If that means that the change is now empty
/// compared to its parent, it will be abandoned.
/// Without `--interactive`, the child change will always be empty.
///
/// If the source became empty and both the source and destination had a
/// non-empty description, you will be asked for the combined description. If
/// either was empty, then the other one will be used.
#[derive(clap::Args, Clone, Debug)]
#[command(visible_alias = "amend")]
struct SquashArgs {
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
/// The description to use for squashed revision (don't open editor)
#[arg(long = "message", short, value_name = "MESSAGE")]
message_paragraphs: Vec<String>,
/// Interactively choose which parts to squash
#[arg(long, short)]
interactive: bool,
/// Move only changes to these paths (instead of all paths)
#[arg(conflicts_with = "interactive", value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
/// Move changes from a revision's parent into the revision
///
/// After moving the changes out of the parent, the child revision will have the
/// same content state as before. If moving the change out of the parent change
/// made it empty compared to its parent, it will be abandoned. Without
/// `--interactive`, the parent change will always become empty.
///
/// If the source became empty and both the source and destination had a
/// non-empty description, you will be asked for the combined description. If
/// either was empty, then the other one will be used.
#[derive(clap::Args, Clone, Debug)]
#[command(visible_alias = "unamend")]
struct UnsquashArgs {
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
/// Interactively choose which parts to unsquash
// TODO: It doesn't make much sense to run this without -i. We should make that
// the default.
#[arg(long, short)]
interactive: bool,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
enum ChmodMode {
/// Make a path non-executable (alias: normal)
// We use short names for enum values so that errors say that the possible values are `n, x`.
#[value(name = "n", alias("normal"))]
Normal,
/// Make a path executable (alias: executable)
#[value(name = "x", alias("executable"))]
Executable,
}
/// Sets or removes the executable bit for paths in the repo
///
/// Unlike the POSIX `chmod`, `jj chmod` also works on Windows, on conflicted
/// files, and on arbitrary revisions.
#[derive(clap::Args, Clone, Debug)]
struct ChmodArgs {
mode: ChmodMode,
/// The revision to update
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
/// Paths to change the executable bit for
#[arg(required = true, value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
/// Resolve a conflicted file with an external merge tool
///
/// Only conflicts that can be resolved with a 3-way merge are supported. See
/// docs for merge tool configuration instructions.
///
/// Note that conflicts can also be resolved without using this command. You may
/// edit the conflict markers in the conflicted file directly with a text
/// editor.
// TODOs:
// - `jj resolve --editor` to resolve a conflict in the default text editor. Should work for
// conflicts with 3+ adds. Useful to resolve conflicts in a commit other than the current one.
// - A way to help split commits with conflicts that are too complicated (more than two sides)
// into commits with simpler conflicts. In case of a tree with many merges, we could for example
// point to existing commits with simpler conflicts where resolving those conflicts would help
// simplify the present one.
#[derive(clap::Args, Clone, Debug)]
struct ResolveArgs {
#[arg(long, short, default_value = "@")]
revision: String,
/// Instead of resolving one conflict, list all the conflicts
// TODO: Also have a `--summary` option. `--list` currently acts like
// `diff --summary`, but should be more verbose.
#[arg(long, short)]
list: bool,
/// Do not print the list of remaining conflicts (if any) after resolving a
/// conflict
#[arg(long, short, conflicts_with = "list")]
quiet: bool,
/// Restrict to these paths when searching for a conflict to resolve. We
/// will attempt to resolve the first conflict we can find. You can use
/// the `--list` argument to find paths to use here.
// TODO: Find the conflict we can resolve even if it's not the first one.
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
/// Restore paths from another revision
///
/// That means that the paths get the same content in the destination (`--to`)
/// as they had in the source (`--from`). This is typically used for undoing
/// changes to some paths in the working copy (`jj restore <paths>`).
///
/// If only one of `--from` or `--to` is specified, the other one defaults to
/// the working copy.
///
/// When neither `--from` nor `--to` is specified, the command restores into the
/// working copy from its parent(s). `jj restore` without arguments is similar
/// to `jj abandon`, except that it leaves an empty revision with its
/// description and other metadata preserved.
///
/// See `jj diffedit` if you'd like to restore portions of files rather than
/// entire files.
#[derive(clap::Args, Clone, Debug)]
struct RestoreArgs {
/// Restore only these paths (instead of all paths)
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
/// Revision to restore from (source)
#[arg(long)]
from: Option<RevisionArg>,
/// Revision to restore into (destination)
#[arg(long)]
to: Option<RevisionArg>,
/// Undo the changes in a revision as compared to the merge of its parents.
///
/// This undoes the changes that can be seen with `jj diff -r REVISION`. If
/// `REVISION` only has a single parent, this option is equivalent to `jj
/// restore --to REVISION --from REVISION-`.
///
/// The default behavior of `jj restore` is equivalent to `jj restore
/// --changes-in @`.
#[arg(long, short, value_name="REVISION", conflicts_with_all=["to", "from"])]
changes_in: Option<RevisionArg>,
/// Prints an error. DO NOT USE.
///
/// If we followed the pattern of `jj diff` and `jj diffedit`, we would use
/// `--revision` instead of `--changes-in` However, that would make it
/// likely that someone unfamiliar with this pattern would use `-r` when
/// they wanted `--from`. This would make a different revision empty, and
/// the user might not even realize something went wrong.
#[arg(long, short, hide = true)]
revision: Option<RevisionArg>,
}
/// Run a command across a set of revisions.
///
///
/// All recorded state will be persisted in the `.jj` directory, so occasionally
/// a `jj run --clean` is needed to clean up disk space.
///
/// # Example
///
/// # Run pre-commit on your local work
/// $ jj run 'pre-commit.py .github/pre-commit.yaml' -r (main..@) -j 4
///
/// This allows pre-commit integration and other funny stuff.
#[derive(clap::Args, Clone, Debug)]
#[command(verbatim_doc_comment)]
struct RunArgs {
/// The command to run across all selected revisions.
#[arg(long, short, alias = "x")]
command: String,
/// The revisions to change.
#[arg(long, short, default_value = "@")]
revisions: Vec<RevisionArg>,
}
/// Touch up the content changes in a revision with a diff editor
///
/// With the `-r` option, which is the default, starts a diff editor (`meld` by
/// default) on the changes in the revision.
///
/// With the `--from` and/or `--to` options, starts a diff editor comparing the
/// "from" revision to the "to" revision.
///
/// Edit the right side of the diff until it looks the way you want. Once you
/// close the editor, the revision specified with `-r` or `--to` will be
/// updated. Descendants will be rebased on top as usual, which may result in
/// conflicts.
///
/// See `jj restore` if you want to move entire files from one revision to
/// another. See `jj squash -i` or `jj unsquash -i` if you instead want to move
/// changes into or out of the parent revision.
#[derive(clap::Args, Clone, Debug)]
struct DiffeditArgs {
/// The revision to touch up. Defaults to @ if neither --to nor --from are
/// specified.
#[arg(long, short)]
revision: Option<RevisionArg>,
/// Show changes from this revision. Defaults to @ if --to is specified.
#[arg(long, conflicts_with = "revision")]
from: Option<RevisionArg>,
/// Edit changes in this revision. Defaults to @ if --from is specified.
#[arg(long, conflicts_with = "revision")]
to: Option<RevisionArg>,
}
/// Split a revision in two
///
/// Starts a diff editor (`meld` by default) on the changes in the revision.
/// Edit the right side of the diff until it has the content you want in the
/// first revision. Once you close the editor, your edited content will replace
/// the previous revision. The remaining changes will be put in a new revision
/// on top.
///
/// If the change you split had a description, you will be asked to enter a
/// change description for each commit. If the change did not have a
/// description, the second part will not get a description, and you will be
/// asked for a description only for the first part.
#[derive(clap::Args, Clone, Debug)]
struct SplitArgs {
/// The revision to split
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
/// Put these paths in the first commit and don't run the diff editor
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
/// Move revisions to different parent(s)
///
/// There are three different ways of specifying which revisions to rebase:
/// `-b` to rebase a whole branch, `-s` to rebase a revision and its
/// descendants, and `-r` to rebase a single commit. If none of them is
/// specified, it defaults to `-b @`.
///
/// With `-s`, the command rebases the specified revision and its descendants
/// onto the destination. For example, `jj rebase -s M -d O` would transform
/// your history like this (letters followed by an apostrophe are post-rebase
/// versions):
///
/// O N'
/// | |
/// | N M'
/// | | |
/// | M O
/// | | => |
/// | | L | L
/// | |/ | |
/// | K | K
/// |/ |/
/// J J
///
/// With `-b`, the command rebases the whole "branch" containing the specified
/// revision. A "branch" is the set of commits that includes:
///
/// * the specified revision and ancestors that are not also ancestors of the
/// destination
/// * all descendants of those commits
///
/// In other words, `jj rebase -b X -d Y` rebases commits in the revset
/// `(Y..X)::` (which is equivalent to `jj rebase -s 'roots(Y..X)' -d Y` for a
/// single root). For example, either `jj rebase -b L -d O` or `jj rebase -b M
/// -d O` would transform your history like this (because `L` and `M` are on the
/// same "branch", relative to the destination):
///
/// O N'
/// | |
/// | N M'
/// | | |
/// | M | L'
/// | | => |/
/// | | L K'
/// | |/ |
/// | K O
/// |/ |
/// J J
///
/// With `-r`, the command rebases only the specified revision onto the
/// destination. Any "hole" left behind will be filled by rebasing descendants
/// onto the specified revision's parent(s). For example, `jj rebase -r K -d M`
/// would transform your history like this:
///
/// M K'
/// | |
/// | L M
/// | | => |
/// | K | L'
/// |/ |/
/// J J
///
/// Note that you can create a merge commit by repeating the `-d` argument.
/// For example, if you realize that commit L actually depends on commit M in
/// order to work (in addition to its current parent K), you can run `jj rebase
/// -s L -d K -d M`:
///
/// M L'
/// | |\
/// | L M |
/// | | => | |
/// | K | K
/// |/ |/
/// J J
#[derive(clap::Args, Clone, Debug)]
#[command(verbatim_doc_comment)]
#[command(group(ArgGroup::new("to_rebase").args(&["branch", "source", "revision"])))]
struct RebaseArgs {
/// Rebase the whole branch relative to destination's ancestors (can be
/// repeated)
///
/// `jj rebase -b=br -d=dst` is equivalent to `jj rebase '-s=roots(dst..br)'
/// -d=dst`.
///
/// If none of `-b`, `-s`, or `-r` is provided, then the default is `-b @`.
#[arg(long, short)]
branch: Vec<RevisionArg>,
/// Rebase specified revision(s) together their tree of descendants (can be
/// repeated)
///
/// Each specified revision will become a direct child of the destination
/// revision(s), even if some of the source revisions are descendants
/// of others.
///
/// If none of `-b`, `-s`, or `-r` is provided, then the default is `-b @`.
#[arg(long, short)]
source: Vec<RevisionArg>,
/// Rebase only this revision, rebasing descendants onto this revision's
/// parent(s)
///
/// If none of `-b`, `-s`, or `-r` is provided, then the default is `-b @`.
#[arg(long, short)]
revision: Option<RevisionArg>,
/// The revision(s) to rebase onto (can be repeated to create a merge
/// commit)
#[arg(long, short, required = true)]
destination: Vec<RevisionArg>,
/// Deprecated. Please prefix the revset with `all:` instead.
#[arg(long, short = 'L', hide = true)]
allow_large_revsets: bool,
}
/// Apply the reverse of a revision on top of another revision
#[derive(clap::Args, Clone, Debug)]
struct BackoutArgs {
/// The revision to apply the reverse of
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
/// The revision to apply the reverse changes on top of
// TODO: It seems better to default this to `@-`. Maybe the working
// copy should be rebased on top?
#[arg(long, short, default_value = "@")]
destination: Vec<RevisionArg>,
}
/// Commands for working with workspaces
#[derive(Subcommand, Clone, Debug)]
enum WorkspaceCommands {
Add(WorkspaceAddArgs),
Forget(WorkspaceForgetArgs),
List(WorkspaceListArgs),
Root(WorkspaceRootArgs),
UpdateStale(WorkspaceUpdateStaleArgs),
}
/// Add a workspace
#[derive(clap::Args, Clone, Debug)]
struct WorkspaceAddArgs {
/// Where to create the new workspace
destination: String,
/// A name for the workspace
///
/// To override the default, which is the basename of the destination
/// directory.
#[arg(long)]
name: Option<String>,
}
/// Stop tracking a workspace's working-copy commit in the repo
///
/// The workspace will not be touched on disk. It can be deleted from disk
/// before or after running this command.
#[derive(clap::Args, Clone, Debug)]
struct WorkspaceForgetArgs {
/// Name of the workspace to forget (the current workspace by default)
workspace: Option<String>,
}
/// List workspaces
#[derive(clap::Args, Clone, Debug)]
struct WorkspaceListArgs {}
/// Show the current workspace root directory
#[derive(clap::Args, Clone, Debug)]
struct WorkspaceRootArgs {}
/// Update a workspace that has become stale
///
/// For information about stale working copies, see
/// https://github.com/martinvonz/jj/blob/main/docs/working-copy.md.
#[derive(clap::Args, Clone, Debug)]
struct WorkspaceUpdateStaleArgs {}
/// Manage which paths from the working-copy commit are present in the working
/// copy
#[derive(Subcommand, Clone, Debug)]
enum SparseArgs {
List(SparseListArgs),
Set(SparseSetArgs),
}
/// List the patterns that are currently present in the working copy
///
/// By default, a newly cloned or initialized repo will have have a pattern
/// matching all files from the repo root. That pattern is rendered as `.` (a
/// single period).
#[derive(clap::Args, Clone, Debug)]
struct SparseListArgs {}
/// Update the patterns that are present in the working copy
///
/// For example, if all you need is the `README.md` and the `lib/`
/// directory, use `jj sparse set --clear --add README.md --add lib`.
/// If you no longer need the `lib` directory, use `jj sparse set --remove lib`.
#[derive(clap::Args, Clone, Debug)]
struct SparseSetArgs {
/// Patterns to add to the working copy
#[arg(long, value_hint = clap::ValueHint::AnyPath)]
add: Vec<String>,
/// Patterns to remove from the working copy
#[arg(long, conflicts_with = "clear", value_hint = clap::ValueHint::AnyPath)]
remove: Vec<String>,
/// Include no files in the working copy (combine with --add)
#[arg(long)]
clear: bool,
/// Edit patterns with $EDITOR
#[arg(long)]
edit: bool,
/// Include all files in the working copy
#[arg(long, conflicts_with_all = &["add", "remove", "clear"])]
reset: bool,
}
/// Infrequently used commands such as for generating shell completions
#[derive(Subcommand, Clone, Debug)]
enum UtilCommands {
Completion(UtilCompletionArgs),
Mangen(UtilMangenArgs),
ConfigSchema(UtilConfigSchemaArgs),
}
/// Print a command-line-completion script
#[derive(clap::Args, Clone, Debug)]
struct UtilCompletionArgs {
/// Print a completion script for Bash
///
/// Apply it by running this:
///
/// source <(jj util completion)
#[arg(long, verbatim_doc_comment)]
bash: bool,
/// Print a completion script for Fish
///
/// Apply it by running this:
///
/// jj util completion --fish | source
#[arg(long, verbatim_doc_comment)]
fish: bool,
/// Print a completion script for Zsh
///
/// Apply it by running this:
///
/// autoload -U compinit
/// compinit
/// source <(jj util completion --zsh)
/// compdef _jj jj
#[arg(long, verbatim_doc_comment)]
zsh: bool,
}
/// Print a ROFF (manpage)
#[derive(clap::Args, Clone, Debug)]
struct UtilMangenArgs {}
/// Print the JSON schema for the jj TOML config format.
#[derive(clap::Args, Clone, Debug)]
struct UtilConfigSchemaArgs {}
#[instrument(skip_all)]
fn cmd_version(
ui: &mut Ui,
command: &CommandHelper,
_args: &VersionArgs,
) -> Result<(), CommandError> {
ui.write(&command.app().render_version())?;
Ok(())
}
#[instrument(skip_all)]
fn cmd_init(ui: &mut Ui, command: &CommandHelper, args: &InitArgs) -> Result<(), CommandError> {
if command.global_args().repository.is_some() {
return Err(user_error("'--repository' cannot be used with 'init'"));
}
let wc_path = command.cwd().join(&args.destination);
match fs::create_dir(&wc_path) {
Ok(()) => {}
Err(_) if wc_path.is_dir() => {}
Err(e) => return Err(user_error(format!("Failed to create workspace: {e}"))),
}
let wc_path = wc_path
.canonicalize()
.map_err(|e| user_error(format!("Failed to create workspace: {e}")))?; // raced?
if let Some(git_store_str) = &args.git_repo {
let mut git_store_path = command.cwd().join(git_store_str);
git_store_path = git_store_path
.canonicalize()
.map_err(|_| user_error(format!("{} doesn't exist", git_store_path.display())))?;
if !git_store_path.ends_with(".git") {
git_store_path.push(".git");
// Undo if .git doesn't exist - likely a bare repo.
if !git_store_path.exists() {
git_store_path.pop();
}
}
// If the git repo is inside the workspace, use a relative path to it so the
// whole workspace can be moved without breaking.
if let Ok(relative_path) = git_store_path.strip_prefix(&wc_path) {
git_store_path = PathBuf::from("..")
.join("..")
.join("..")
.join(relative_path);
}
let (workspace, repo) =
Workspace::init_external_git(command.settings(), &wc_path, &git_store_path)?;
let git_repo = repo
.store()
.backend_impl()
.downcast_ref::<GitBackend>()
.unwrap()
.git_repo_clone();
let mut workspace_command = command.for_loaded_repo(ui, workspace, repo)?;
workspace_command.snapshot(ui)?;
if workspace_command.working_copy_shared_with_git() {
git::add_to_git_exclude(ui, &git_repo)?;
} else {
let mut tx = workspace_command.start_transaction("import git refs");
jj_lib::git::import_refs(tx.mut_repo(), &git_repo, &command.settings().git_settings())?;
if let Some(git_head_id) = tx.mut_repo().view().git_head().as_normal().cloned() {
let git_head_commit = tx.mut_repo().store().get_commit(&git_head_id)?;
tx.check_out(&git_head_commit)?;
}
if tx.mut_repo().has_changes() {
tx.finish(ui)?;
}
}
} else if args.git {
Workspace::init_internal_git(command.settings(), &wc_path)?;
} else {
if !command.settings().allow_native_backend() {
return Err(user_error_with_hint(
"The native backend is disallowed by default.",
"Did you mean to pass `--git`?
Set `ui.allow-init-native` to allow initializing a repo with the native backend.",
));
}
Workspace::init_local(command.settings(), &wc_path)?;
};
let cwd = command.cwd().canonicalize().unwrap();
let relative_wc_path = file_util::relative_path(&cwd, &wc_path);
writeln!(ui, "Initialized repo in \"{}\"", relative_wc_path.display())?;
if args.git && wc_path.join(".git").exists() {
writeln!(ui.warning(), "Empty repo created.")?;
writeln!(
ui.hint(),
"Hint: To create a repo backed by the existing Git repo, run `jj init --git-repo={}` \
instead.",
relative_wc_path.display()
)?;
}
Ok(())
}
#[instrument(skip_all)]
fn cmd_config(
ui: &mut Ui,
command: &CommandHelper,
subcommand: &ConfigSubcommand,
) -> Result<(), CommandError> {
match subcommand {
ConfigSubcommand::List(sub_args) => cmd_config_list(ui, command, sub_args),
ConfigSubcommand::Get(sub_args) => cmd_config_get(ui, command, sub_args),
ConfigSubcommand::Set(sub_args) => cmd_config_set(ui, command, sub_args),
ConfigSubcommand::Edit(sub_args) => cmd_config_edit(ui, command, sub_args),
}
}
#[instrument(skip_all)]
fn cmd_config_list(
ui: &mut Ui,
command: &CommandHelper,
args: &ConfigListArgs,
) -> Result<(), CommandError> {
ui.request_pager();
let name_path = args
.name
.as_ref()
.map_or(vec![], |name| name.split('.').collect_vec());
let values = command.resolved_config_values(&name_path)?;
let mut wrote_values = false;
for AnnotatedValue {
path,
value,
source,
is_overridden,
} in &values
{
// Remove overridden values.
// TODO(#1047): Allow printing overridden values via `--include-overridden`.
if *is_overridden {
continue;
}
// Skip built-ins if not included.
if !args.include_defaults && *source == ConfigSource::Default {
continue;
}
writeln!(ui, "{}={}", path.join("."), serialize_config_value(value))?;
wrote_values = true;
}
if !wrote_values {
// Note to stderr explaining why output is empty.
if let Some(name) = &args.name {
writeln!(ui.warning(), "No matching config key for {name}")?;
} else {
writeln!(ui.warning(), "No config to list")?;
}
}
Ok(())
}
#[instrument(skip_all)]
fn cmd_config_get(
ui: &mut Ui,
command: &CommandHelper,
args: &ConfigGetArgs,
) -> Result<(), CommandError> {
let value = command
.settings()
.config()
.get_string(&args.name)
.map_err(|err| match err {
config::ConfigError::Type {
origin,
unexpected,
expected,
key,
} => {
let expected = format!("a value convertible to {expected}");
// Copied from `impl fmt::Display for ConfigError`. We can't use
// the `Display` impl directly because `expected` is required to
// be a `'static str`.
let mut buf = String::new();
use std::fmt::Write;
write!(buf, "invalid type: {unexpected}, expected {expected}").unwrap();
if let Some(key) = key {
write!(buf, " for key `{key}`").unwrap();
}
if let Some(origin) = origin {
write!(buf, " in {origin}").unwrap();
}
CommandError::ConfigError(buf.to_string())
}
err => err.into(),
})?;
writeln!(ui, "{value}")?;
Ok(())
}
#[instrument(skip_all)]
fn cmd_config_set(
_ui: &mut Ui,
command: &CommandHelper,
args: &ConfigSetArgs,
) -> Result<(), CommandError> {
let config_path = get_new_config_file_path(&args.config_args.get_source_kind(), command)?;
if config_path.is_dir() {
return Err(user_error(format!(
"Can't set config in path {path} (dirs not supported)",
path = config_path.display()
)));
}
write_config_value_to_file(&args.name, &args.value, &config_path)
}
#[instrument(skip_all)]
fn cmd_config_edit(
_ui: &mut Ui,
command: &CommandHelper,
args: &ConfigEditArgs,
) -> Result<(), CommandError> {
let config_path = get_new_config_file_path(&args.config_args.get_source_kind(), command)?;
run_ui_editor(command.settings(), &config_path)
}
#[instrument(skip_all)]
fn cmd_checkout(
ui: &mut Ui,
command: &CommandHelper,
args: &CheckoutArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let target = workspace_command.resolve_single_rev(&args.revision, ui)?;
let mut tx =
workspace_command.start_transaction(&format!("check out commit {}", target.id().hex()));
let commit_builder = tx
.mut_repo()
.new_commit(
command.settings(),
vec![target.id().clone()],
target.merged_tree_id().clone(),
)
.set_description(cli_util::join_message_paragraphs(&args.message_paragraphs));
let new_commit = commit_builder.write()?;
tx.edit(&new_commit).unwrap();
tx.finish(ui)?;
Ok(())
}
#[instrument(skip_all)]
fn cmd_untrack(
ui: &mut Ui,
command: &CommandHelper,
args: &UntrackArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let store = workspace_command.repo().store().clone();
let matcher = workspace_command.matcher_from_values(&args.paths)?;
let mut tx = workspace_command
.start_transaction("untrack paths")
.into_inner();
let base_ignores = workspace_command.base_ignores();
let (mut locked_working_copy, wc_commit) = workspace_command.start_working_copy_mutation()?;
// Create a new tree without the unwanted files
let mut tree_builder =
MergedTreeBuilder::new(store.clone(), wc_commit.merged_tree_id().clone());
let wc_tree = wc_commit.merged_tree()?;
for (path, _value) in wc_tree.entries_matching(matcher.as_ref()) {
tree_builder.set_or_remove(path, Merge::absent());
}
let new_tree_id = tree_builder.write_tree()?;
let new_tree = store.get_root_tree(&new_tree_id)?;
// Reset the working copy to the new tree
locked_working_copy.reset(&new_tree)?;
// Commit the working copy again so we can inform the user if paths couldn't be
// untracked because they're not ignored.
let wc_tree_id = locked_working_copy.snapshot(SnapshotOptions {
base_ignores,
fsmonitor_kind: command.settings().fsmonitor_kind()?,
progress: None,
max_new_file_size: command.settings().max_new_file_size()?,
})?;
if wc_tree_id != new_tree_id {
let wc_tree = store.get_root_tree(&wc_tree_id)?;
let added_back = wc_tree.entries_matching(matcher.as_ref()).collect_vec();
if !added_back.is_empty() {
locked_working_copy.discard();
let path = &added_back[0].0;
let ui_path = workspace_command.format_file_path(path);
let message = if added_back.len() > 1 {
format!(
"'{}' and {} other files are not ignored.",
ui_path,
added_back.len() - 1
)
} else {
format!("'{ui_path}' is not ignored.")
};
return Err(user_error_with_hint(
message,
"Files that are not ignored will be added back by the next command.
Make sure they're ignored, then try again.",
));
} else {
// This means there were some concurrent changes made in the working copy. We
// don't want to mix those in, so reset the working copy again.
locked_working_copy.reset(&new_tree)?;
}
}
tx.mut_repo()
.rewrite_commit(command.settings(), &wc_commit)
.set_tree_id(new_tree_id)
.write()?;
let num_rebased = tx.mut_repo().rebase_descendants(command.settings())?;
if num_rebased > 0 {
writeln!(ui, "Rebased {num_rebased} descendant commits")?;
}
let repo = tx.commit();
locked_working_copy.finish(repo.op_id().clone())?;
Ok(())
}
#[instrument(skip_all)]
fn cmd_files(ui: &mut Ui, command: &CommandHelper, args: &FilesArgs) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let commit = workspace_command.resolve_single_rev(&args.revision, ui)?;
let tree = commit.merged_tree()?;
let matcher = workspace_command.matcher_from_values(&args.paths)?;
ui.request_pager();
for (name, _value) in tree.entries_matching(matcher.as_ref()) {
writeln!(ui, "{}", &workspace_command.format_file_path(&name))?;
}
Ok(())
}
#[instrument(skip_all)]
fn cmd_cat(ui: &mut Ui, command: &CommandHelper, args: &CatArgs) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let commit = workspace_command.resolve_single_rev(&args.revision, ui)?;
let tree = commit.merged_tree()?;
let path = workspace_command.parse_file_path(&args.path)?;
let repo = workspace_command.repo();
match tree.path_value(&path).into_resolved() {
Ok(None) => {
return Err(user_error("No such path"));
}
Ok(Some(TreeValue::File { id, .. })) => {
let mut contents = repo.store().read_file(&path, &id)?;
ui.request_pager();
std::io::copy(&mut contents, &mut ui.stdout_formatter().as_mut())?;
}
Err(conflict) => {
let mut contents = vec![];
conflicts::materialize(&conflict, repo.store(), &path, &mut contents).unwrap();
ui.request_pager();
ui.stdout_formatter().write_all(&contents)?;
}
_ => {
return Err(user_error("Path exists but is not a file"));
}
}
Ok(())
}
#[instrument(skip_all)]
fn cmd_diff(ui: &mut Ui, command: &CommandHelper, args: &DiffArgs) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let from_tree;
let to_tree;
if args.from.is_some() || args.to.is_some() {
let from = workspace_command.resolve_single_rev(args.from.as_deref().unwrap_or("@"), ui)?;
from_tree = from.merged_tree()?;
let to = workspace_command.resolve_single_rev(args.to.as_deref().unwrap_or("@"), ui)?;
to_tree = to.merged_tree()?;
} else {
let commit =
workspace_command.resolve_single_rev(args.revision.as_deref().unwrap_or("@"), ui)?;
let parents = commit.parents();
from_tree = MergedTree::legacy(merge_commit_trees(
workspace_command.repo().as_ref(),
&parents,
)?);
to_tree = commit.merged_tree()?
}
let matcher = workspace_command.matcher_from_values(&args.paths)?;
let diff_formats = diff_util::diff_formats_for(command.settings(), &args.format)?;
ui.request_pager();
diff_util::show_diff(
ui,
ui.stdout_formatter().as_mut(),
&workspace_command,
&from_tree,
&to_tree,
matcher.as_ref(),
&diff_formats,
)?;
Ok(())
}
#[instrument(skip_all)]
fn cmd_show(ui: &mut Ui, command: &CommandHelper, args: &ShowArgs) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let commit = workspace_command.resolve_single_rev(&args.revision, ui)?;
let template_string = command.settings().config().get_string("templates.show")?;
let template = workspace_command.parse_commit_template(&template_string)?;
let diff_formats = diff_util::diff_formats_for(command.settings(), &args.format)?;
ui.request_pager();
let mut formatter = ui.stdout_formatter();
let formatter = formatter.as_mut();
template.format(&commit, formatter)?;
diff_util::show_patch(
ui,
formatter,
&workspace_command,
&commit,
&EverythingMatcher,
&diff_formats,
)?;
Ok(())
}
#[instrument(skip_all)]
fn cmd_status(
ui: &mut Ui,
command: &CommandHelper,
_args: &StatusArgs,
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let repo = workspace_command.repo();
let maybe_wc_commit = workspace_command
.get_wc_commit_id()
.map(|id| repo.store().get_commit(id))
.transpose()?;
ui.request_pager();
let mut formatter = ui.stdout_formatter();
let formatter = formatter.as_mut();
if let Some(wc_commit) = &maybe_wc_commit {
let parent_tree =
MergedTree::legacy(merge_commit_trees(repo.as_ref(), &wc_commit.parents())?);
let tree = wc_commit.merged_tree()?;
if tree.id() == parent_tree.id() {
formatter.write_str("The working copy is clean\n")?;
} else {
formatter.write_str("Working copy changes:\n")?;
diff_util::show_diff_summary(
formatter,
&workspace_command,
parent_tree.diff(&tree, &EverythingMatcher),
)?;
}
let conflicts = wc_commit.merged_tree()?.conflicts().collect_vec();
if !conflicts.is_empty() {
writeln!(
formatter.labeled("conflict"),
"There are unresolved conflicts at these paths:"
)?;
print_conflicted_paths(&conflicts, formatter, &workspace_command)?
}
formatter.write_str("Working copy : ")?;
workspace_command.write_commit_summary(formatter, wc_commit)?;
formatter.write_str("\n")?;
for parent in wc_commit.parents() {
formatter.write_str("Parent commit: ")?;
workspace_command.write_commit_summary(formatter, &parent)?;
formatter.write_str("\n")?;
}
} else {
formatter.write_str("No working copy\n")?;
}
let mut conflicted_local_branches = vec![];
let mut conflicted_remote_branches = vec![];
for (branch_name, branch_target) in repo.view().branches() {
if branch_target.local_target.has_conflict() {
conflicted_local_branches.push(branch_name.clone());
}
for (remote_name, remote_target) in &branch_target.remote_targets {
if remote_target.has_conflict() {
conflicted_remote_branches.push((branch_name.clone(), remote_name.clone()));
}
}
}
if !conflicted_local_branches.is_empty() {
writeln!(
formatter.labeled("conflict"),
"These branches have conflicts:"
)?;
for branch_name in conflicted_local_branches {
write!(formatter, " ")?;
write!(formatter.labeled("branch"), "{branch_name}")?;
writeln!(formatter)?;
}
writeln!(
formatter,
" Use `jj branch list` to see details. Use `jj branch set <name> -r <rev>` to \
resolve."
)?;
}
if !conflicted_remote_branches.is_empty() {
writeln!(
formatter.labeled("conflict"),
"These remote branches have conflicts:"
)?;
for (branch_name, remote_name) in conflicted_remote_branches {
write!(formatter, " ")?;
write!(formatter.labeled("branch"), "{branch_name}@{remote_name}")?;
writeln!(formatter)?;
}
writeln!(
formatter,
" Use `jj branch list` to see details. Use `jj git fetch` to resolve."
)?;
}
Ok(())
}
#[instrument(skip_all)]
fn cmd_log(ui: &mut Ui, command: &CommandHelper, args: &LogArgs) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let revset_expression = {
let mut expression = if args.revisions.is_empty() {
workspace_command.parse_revset(&command.settings().default_revset(), Some(ui))?
} else {
let expressions: Vec<_> = args
.revisions
.iter()
.map(|revision_str| workspace_command.parse_revset(revision_str, Some(ui)))
.try_collect()?;
RevsetExpression::union_all(&expressions)
};
if !args.paths.is_empty() {
let repo_paths: Vec<_> = args
.paths
.iter()
.map(|path_arg| workspace_command.parse_file_path(path_arg))
.try_collect()?;
expression = expression.intersection(&RevsetExpression::filter(
RevsetFilterPredicate::File(Some(repo_paths)),
));
}
revset::optimize(expression)
};
let repo = workspace_command.repo();
let wc_commit_id = workspace_command.get_wc_commit_id();
let matcher = workspace_command.matcher_from_values(&args.paths)?;
let revset = workspace_command.evaluate_revset(revset_expression)?;
let store = repo.store();
let diff_formats =
diff_util::diff_formats_for_log(command.settings(), &args.diff_format, args.patch)?;
let template_string = match &args.template {
Some(value) => value.to_string(),
None => command.settings().config().get_string("templates.log")?,
};
let template = workspace_command.parse_commit_template(&template_string)?;
let with_content_format = LogContentFormat::new(ui, command.settings())?;
{
ui.request_pager();
let mut formatter = ui.stdout_formatter();
let formatter = formatter.as_mut();
if !args.no_graph {
let mut graph = get_graphlog(command.settings(), formatter.raw());
let default_node_symbol = graph.default_node_symbol().to_owned();
let forward_iter = TopoGroupedRevsetGraphIterator::new(revset.iter_graph());
let iter: Box<dyn Iterator<Item = _>> = if args.reversed {
Box::new(ReverseRevsetGraphIterator::new(forward_iter))
} else {
Box::new(forward_iter)
};
for (commit_id, edges) in iter.take(args.limit.unwrap_or(usize::MAX)) {
let mut graphlog_edges = vec![];
// TODO: Should we update RevsetGraphIterator to yield this flag instead of all
// the missing edges since we don't care about where they point here
// anyway?
let mut has_missing = false;
for edge in edges {
match edge.edge_type {
RevsetGraphEdgeType::Missing => {
has_missing = true;
}
RevsetGraphEdgeType::Direct => graphlog_edges.push(Edge::Present {
direct: true,
target: edge.target,
}),
RevsetGraphEdgeType::Indirect => graphlog_edges.push(Edge::Present {
direct: false,
target: edge.target,
}),
}
}
if has_missing {
graphlog_edges.push(Edge::Missing);
}
let mut buffer = vec![];
let commit = store.get_commit(&commit_id)?;
with_content_format.write_graph_text(
ui.new_formatter(&mut buffer).as_mut(),
|formatter| template.format(&commit, formatter),
|| graph.width(&commit_id, &graphlog_edges),
)?;
if !buffer.ends_with(b"\n") {
buffer.push(b'\n');
}
if !diff_formats.is_empty() {
let mut formatter = ui.new_formatter(&mut buffer);
diff_util::show_patch(
ui,
formatter.as_mut(),
&workspace_command,
&commit,
matcher.as_ref(),
&diff_formats,
)?;
}
let node_symbol = if Some(&commit_id) == wc_commit_id {
"@"
} else {
&default_node_symbol
};
graph.add_node(
&commit_id,
&graphlog_edges,
node_symbol,
&String::from_utf8_lossy(&buffer),
)?;
}
} else {
let iter: Box<dyn Iterator<Item = CommitId>> = if args.reversed {
Box::new(revset.iter().reversed())
} else {
Box::new(revset.iter())
};
for commit_or_error in iter.commits(store).take(args.limit.unwrap_or(usize::MAX)) {
let commit = commit_or_error?;
with_content_format
.write(formatter, |formatter| template.format(&commit, formatter))?;
if !diff_formats.is_empty() {
diff_util::show_patch(
ui,
formatter,
&workspace_command,
&commit,
matcher.as_ref(),
&diff_formats,
)?;
}
}
}
}
// Check to see if the user might have specified a path when they intended
// to specify a revset.
if let ([], [only_path]) = (args.revisions.as_slice(), args.paths.as_slice()) {
if only_path == "." && workspace_command.parse_file_path(only_path)?.is_root() {
// For users of e.g. Mercurial, where `.` indicates the current commit.
writeln!(
ui.warning(),
"warning: The argument {only_path:?} is being interpreted as a path, but this is \
often not useful because all non-empty commits touch '.'. If you meant to show \
the working copy commit, pass -r '@' instead."
)?;
} else if revset.is_empty()
&& revset::parse(only_path, &workspace_command.revset_parse_context()).is_ok()
{
writeln!(
ui.warning(),
"warning: The argument {only_path:?} is being interpreted as a path. To specify a \
revset, pass -r {only_path:?} instead."
)?;
}
}
Ok(())
}
#[instrument(skip_all)]
fn cmd_obslog(ui: &mut Ui, command: &CommandHelper, args: &ObslogArgs) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let start_commit = workspace_command.resolve_single_rev(&args.revision, ui)?;
let wc_commit_id = workspace_command.get_wc_commit_id();
let diff_formats =
diff_util::diff_formats_for_log(command.settings(), &args.diff_format, args.patch)?;
let template_string = match &args.template {
Some(value) => value.to_string(),
None => command.settings().config().get_string("templates.log")?,
};
let template = workspace_command.parse_commit_template(&template_string)?;
let with_content_format = LogContentFormat::new(ui, command.settings())?;
ui.request_pager();
let mut formatter = ui.stdout_formatter();
let formatter = formatter.as_mut();
formatter.push_label("log")?;
let mut commits = topo_order_reverse(
vec![start_commit],
|commit: &Commit| commit.id().clone(),
|commit: &Commit| commit.predecessors(),
);
if let Some(n) = args.limit {
commits.truncate(n);
}
if !args.no_graph {
let mut graph = get_graphlog(command.settings(), formatter.raw());
let default_node_symbol = graph.default_node_symbol().to_owned();
for commit in commits {
let mut edges = vec![];
for predecessor in &commit.predecessors() {
edges.push(Edge::direct(predecessor.id().clone()));
}
let mut buffer = vec![];
with_content_format.write_graph_text(
ui.new_formatter(&mut buffer).as_mut(),
|formatter| template.format(&commit, formatter),
|| graph.width(commit.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_predecessor_patch(
ui,
formatter.as_mut(),
&workspace_command,
&commit,
&diff_formats,
)?;
}
let node_symbol = if Some(commit.id()) == wc_commit_id {
"@"
} else {
&default_node_symbol
};
graph.add_node(
commit.id(),
&edges,
node_symbol,
&String::from_utf8_lossy(&buffer),
)?;
}
} else {
for commit in commits {
with_content_format
.write(formatter, |formatter| template.format(&commit, formatter))?;
if !diff_formats.is_empty() {
show_predecessor_patch(ui, formatter, &workspace_command, &commit, &diff_formats)?;
}
}
}
Ok(())
}
fn show_predecessor_patch(
ui: &Ui,
formatter: &mut dyn Formatter,
workspace_command: &WorkspaceCommandHelper,
commit: &Commit,
diff_formats: &[DiffFormat],
) -> Result<(), CommandError> {
let predecessors = commit.predecessors();
let predecessor = match predecessors.first() {
Some(predecessor) => predecessor,
None => return Ok(()),
};
let predecessor_tree = MergedTree::legacy(rebase_to_dest_parent(
workspace_command,
predecessor,
commit,
)?);
let tree = commit.merged_tree()?;
diff_util::show_diff(
ui,
formatter,
workspace_command,
&predecessor_tree,
&tree,
&EverythingMatcher,
diff_formats,
)
}
#[instrument(skip_all)]
fn cmd_interdiff(
ui: &mut Ui,
command: &CommandHelper,
args: &InterdiffArgs,
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let from = workspace_command.resolve_single_rev(args.from.as_deref().unwrap_or("@"), ui)?;
let to = workspace_command.resolve_single_rev(args.to.as_deref().unwrap_or("@"), ui)?;
let from_tree = MergedTree::legacy(rebase_to_dest_parent(&workspace_command, &from, &to)?);
let to_tree = to.merged_tree()?;
let matcher = workspace_command.matcher_from_values(&args.paths)?;
let diff_formats = diff_util::diff_formats_for(command.settings(), &args.format)?;
ui.request_pager();
diff_util::show_diff(
ui,
ui.stdout_formatter().as_mut(),
&workspace_command,
&from_tree,
&to_tree,
matcher.as_ref(),
&diff_formats,
)
}
fn rebase_to_dest_parent(
workspace_command: &WorkspaceCommandHelper,
source: &Commit,
destination: &Commit,
) -> Result<Tree, CommandError> {
if source.parent_ids() == destination.parent_ids() {
Ok(source.tree())
} else {
let destination_parent_tree =
merge_commit_trees(workspace_command.repo().as_ref(), &destination.parents())?;
let source_parent_tree =
merge_commit_trees(workspace_command.repo().as_ref(), &source.parents())?;
let rebased_tree = merge_trees(
&destination_parent_tree,
&source_parent_tree,
&source.tree(),
)?;
Ok(rebased_tree)
}
}
fn edit_description(
repo: &ReadonlyRepo,
description: &str,
settings: &UserSettings,
) -> Result<String, CommandError> {
let description_file_path = (|| -> Result<_, io::Error> {
let mut file = tempfile::Builder::new()
.prefix("editor-")
.suffix(".jjdescription")
.tempfile_in(repo.repo_path())?;
file.write_all(description.as_bytes())?;
file.write_all(b"\nJJ: Lines starting with \"JJ: \" (like this one) will be removed.\n")?;
let (_, path) = file.keep().map_err(|e| e.error)?;
Ok(path)
})()
.map_err(|e| {
user_error(format!(
r#"Failed to create description file in "{path}": {e}"#,
path = repo.repo_path().display()
))
})?;
run_ui_editor(settings, &description_file_path)?;
let description = fs::read_to_string(&description_file_path).map_err(|e| {
user_error(format!(
r#"Failed to read description file "{path}": {e}"#,
path = description_file_path.display()
))
})?;
// Delete the file only if everything went well.
// TODO: Tell the user the name of the file we left behind.
std::fs::remove_file(description_file_path).ok();
// Normalize line ending, remove leading and trailing blank lines.
let description = description
.lines()
.filter(|line| !line.starts_with("JJ: "))
.join("\n");
Ok(text_util::complete_newline(description.trim_matches('\n')))
}
fn edit_sparse(
workspace_root: &Path,
repo_path: &Path,
sparse: &[RepoPath],
settings: &UserSettings,
) -> Result<Vec<RepoPath>, CommandError> {
let file = (|| -> Result<_, io::Error> {
let mut file = tempfile::Builder::new()
.prefix("editor-")
.suffix(".jjsparse")
.tempfile_in(repo_path)?;
for sparse_path in sparse {
let workspace_relative_sparse_path =
file_util::relative_path(workspace_root, &sparse_path.to_fs_path(workspace_root));
file.write_all(
workspace_relative_sparse_path
.to_str()
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"stored sparse path is not valid utf-8: {}",
workspace_relative_sparse_path.display()
),
)
})?
.as_bytes(),
)?;
file.write_all(b"\n")?;
}
file.seek(SeekFrom::Start(0))?;
Ok(file)
})()
.map_err(|e| {
user_error(format!(
r#"Failed to create sparse patterns file in "{path}": {e}"#,
path = repo_path.display()
))
})?;
let file_path = file.path().to_owned();
run_ui_editor(settings, &file_path)?;
// Read and parse patterns.
io::BufReader::new(file)
.lines()
.filter(|line| {
line.as_ref()
.map(|line| !line.starts_with("JJ: ") && !line.trim().is_empty())
.unwrap_or(true)
})
.map(|line| {
let line = line.map_err(|e| {
user_error(format!(
r#"Failed to read sparse patterns file "{path}": {e}"#,
path = file_path.display()
))
})?;
Ok::<_, CommandError>(RepoPath::parse_fs_path(
workspace_root,
workspace_root,
line.trim(),
)?)
})
.try_collect()
}
#[instrument(skip_all)]
fn cmd_describe(
ui: &mut Ui,
command: &CommandHelper,
args: &DescribeArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let commit = workspace_command.resolve_single_rev(&args.revision, ui)?;
workspace_command.check_rewritable(&commit)?;
let description = if args.stdin {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer).unwrap();
buffer
} else if !args.message_paragraphs.is_empty() {
cli_util::join_message_paragraphs(&args.message_paragraphs)
} else if args.no_edit {
commit.description().to_owned()
} else {
let template =
description_template_for_commit(ui, command.settings(), &workspace_command, &commit)?;
edit_description(workspace_command.repo(), &template, command.settings())?
};
if description == *commit.description() && !args.reset_author {
ui.write("Nothing changed.\n")?;
} else {
let mut tx =
workspace_command.start_transaction(&format!("describe commit {}", commit.id().hex()));
let mut commit_builder = tx
.mut_repo()
.rewrite_commit(command.settings(), &commit)
.set_description(description);
if args.reset_author {
let new_author = commit_builder.committer().clone();
commit_builder = commit_builder.set_author(new_author);
}
commit_builder.write()?;
tx.finish(ui)?;
}
Ok(())
}
#[instrument(skip_all)]
fn cmd_commit(ui: &mut Ui, command: &CommandHelper, args: &CommitArgs) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let commit_id = workspace_command
.get_wc_commit_id()
.ok_or_else(|| user_error("This command requires a working copy"))?;
let commit = workspace_command.repo().store().get_commit(commit_id)?;
let description = if !args.message_paragraphs.is_empty() {
cli_util::join_message_paragraphs(&args.message_paragraphs)
} else {
let template =
description_template_for_commit(ui, command.settings(), &workspace_command, &commit)?;
edit_description(workspace_command.repo(), &template, command.settings())?
};
let mut tx = workspace_command.start_transaction(&format!("commit {}", commit.id().hex()));
let new_commit = tx
.mut_repo()
.rewrite_commit(command.settings(), &commit)
.set_description(description)
.write()?;
let workspace_ids = tx
.mut_repo()
.view()
.workspaces_for_wc_commit_id(commit.id());
if !workspace_ids.is_empty() {
let new_wc_commit = tx
.mut_repo()
.new_commit(
command.settings(),
vec![new_commit.id().clone()],
new_commit.merged_tree_id().clone(),
)
.write()?;
for workspace_id in workspace_ids {
tx.mut_repo().edit(workspace_id, &new_wc_commit).unwrap();
}
}
tx.finish(ui)?;
Ok(())
}
#[instrument(skip_all)]
fn cmd_duplicate(
ui: &mut Ui,
command: &CommandHelper,
args: &DuplicateArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let to_duplicate: IndexSet<Commit> =
resolve_multiple_nonempty_revsets(&args.revisions, &workspace_command, ui)?;
to_duplicate
.iter()
.map(|commit| workspace_command.check_rewritable(commit))
.try_collect()?;
let mut duplicated_old_to_new: IndexMap<Commit, Commit> = IndexMap::new();
let mut tx = workspace_command
.start_transaction(&format!("duplicating {} commit(s)", to_duplicate.len()));
let base_repo = tx.base_repo().clone();
let store = base_repo.store();
let mut_repo = tx.mut_repo();
for original_commit_id in base_repo
.index()
.topo_order(&mut to_duplicate.iter().map(|c| c.id()))
.into_iter()
{
// Topological order ensures that any parents of `original_commit` are
// either not in `to_duplicate` or were already duplicated.
let original_commit = store.get_commit(&original_commit_id).unwrap();
let new_parents = original_commit
.parents()
.iter()
.map(|parent| {
if let Some(duplicated_parent) = duplicated_old_to_new.get(parent) {
duplicated_parent
} else {
parent
}
.id()
.clone()
})
.collect();
let new_commit = mut_repo
.rewrite_commit(command.settings(), &original_commit)
.generate_new_change_id()
.set_parents(new_parents)
.write()?;
duplicated_old_to_new.insert(original_commit, new_commit);
}
for (old, new) in duplicated_old_to_new.iter() {
ui.write(&format!("Duplicated {} as ", short_commit_hash(old.id())))?;
tx.write_commit_summary(ui.stdout_formatter().as_mut(), new)?;
ui.write("\n")?;
}
tx.finish(ui)?;
Ok(())
}
#[instrument(skip_all)]
fn cmd_abandon(
ui: &mut Ui,
command: &CommandHelper,
args: &AbandonArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let to_abandon = resolve_multiple_nonempty_revsets(&args.revisions, &workspace_command, ui)?;
to_abandon
.iter()
.map(|commit| workspace_command.check_rewritable(commit))
.try_collect()?;
let transaction_description = if to_abandon.len() == 1 {
format!("abandon commit {}", to_abandon[0].id().hex())
} else {
format!(
"abandon commit {} and {} more",
to_abandon[0].id().hex(),
to_abandon.len() - 1
)
};
let mut tx = workspace_command.start_transaction(&transaction_description);
for commit in &to_abandon {
tx.mut_repo().record_abandoned_commit(commit.id().clone());
}
let num_rebased = tx.mut_repo().rebase_descendants(command.settings())?;
if to_abandon.len() == 1 {
ui.write("Abandoned commit ")?;
tx.write_commit_summary(ui.stdout_formatter().as_mut(), &to_abandon[0])?;
ui.write("\n")?;
} else if !args.summary {
ui.write("Abandoned the following commits:\n")?;
for commit in to_abandon {
ui.write(" ")?;
tx.write_commit_summary(ui.stdout_formatter().as_mut(), &commit)?;
ui.write("\n")?;
}
} else {
writeln!(ui, "Abandoned {} commits.", &to_abandon.len())?;
}
if num_rebased > 0 {
writeln!(
ui,
"Rebased {num_rebased} descendant commits onto parents of abandoned commits"
)?;
}
tx.finish(ui)?;
Ok(())
}
#[instrument(skip_all)]
fn cmd_edit(ui: &mut Ui, command: &CommandHelper, args: &EditArgs) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let new_commit = workspace_command.resolve_single_rev(&args.revision, ui)?;
workspace_command.check_rewritable(&new_commit)?;
if workspace_command.get_wc_commit_id() == Some(new_commit.id()) {
ui.write("Already editing that commit\n")?;
} else {
let mut tx =
workspace_command.start_transaction(&format!("edit commit {}", new_commit.id().hex()));
tx.edit(&new_commit)?;
tx.finish(ui)?;
}
Ok(())
}
/// Resolves revsets into revisions to rebase onto. These revisions don't have
/// to be rewriteable.
fn resolve_destination_revs(
workspace_command: &WorkspaceCommandHelper,
ui: &mut Ui,
revisions: &[RevisionArg],
) -> Result<IndexSet<Commit>, CommandError> {
let commits =
resolve_multiple_nonempty_revsets_default_single(workspace_command, ui, revisions)?;
let root_commit_id = workspace_command.repo().store().root_commit_id();
if commits.len() >= 2 && commits.iter().any(|c| c.id() == root_commit_id) {
Err(user_error("Cannot merge with root revision"))
} else {
Ok(commits)
}
}
#[instrument(skip_all)]
fn cmd_new(ui: &mut Ui, command: &CommandHelper, args: &NewArgs) -> Result<(), CommandError> {
if args.allow_large_revsets {
return Err(user_error(
"--allow-large-revsets has been deprecated.
Please use `jj new 'all:x|y'` instead of `jj new --allow-large-revsets x y`.",
));
}
let mut workspace_command = command.workspace_helper(ui)?;
assert!(
!args.revisions.is_empty(),
"expected a non-empty list from clap"
);
let target_commits = resolve_destination_revs(&workspace_command, ui, &args.revisions)?
.into_iter()
.collect_vec();
let target_ids = target_commits.iter().map(|c| c.id().clone()).collect_vec();
let mut tx = workspace_command.start_transaction("new empty commit");
let mut num_rebased = 0;
let new_commit;
if args.insert_before {
// Instead of having the new commit as a child of the changes given on the
// command line, add it between the changes' parents and the changes.
// The parents of the new commit will be the parents of the target commits
// which are not descendants of other target commits.
let root_commit = tx.repo().store().root_commit();
if target_ids.contains(root_commit.id()) {
return Err(user_error("Cannot insert a commit before the root commit"));
}
let new_children = RevsetExpression::commits(target_ids.clone());
let new_parents = new_children.parents();
if let Some(commit_id) = new_children
.dag_range_to(&new_parents)
.resolve(tx.repo())?
.evaluate(tx.repo())?
.iter()
.next()
{
return Err(user_error(format!(
"Refusing to create a loop: commit {} would be both an ancestor and a descendant \
of the new commit",
short_commit_hash(&commit_id),
)));
}
let mut new_parents_commits: Vec<Commit> = new_parents
.resolve(tx.repo())?
.evaluate(tx.repo())?
.iter()
.commits(tx.repo().store())
.try_collect()?;
// The git backend does not support creating merge commits involving the root
// commit.
if new_parents_commits.len() > 1 {
new_parents_commits.retain(|c| c != &root_commit);
}
let merged_tree = merge_commit_trees(tx.repo(), &new_parents_commits)?;
let new_parents_commit_id = new_parents_commits.iter().map(|c| c.id().clone()).collect();
new_commit = tx
.mut_repo()
.new_commit(
command.settings(),
new_parents_commit_id,
merged_tree.legacy_id(),
)
.set_description(cli_util::join_message_paragraphs(&args.message_paragraphs))
.write()?;
num_rebased = target_ids.len();
for child_commit in target_commits {
rebase_commit(
command.settings(),
tx.mut_repo(),
&child_commit,
&[new_commit.clone()],
)?;
}
} else {
let merged_tree = merge_commit_trees(tx.repo(), &target_commits)?;
new_commit = tx
.mut_repo()
.new_commit(
command.settings(),
target_ids.clone(),
merged_tree.legacy_id(),
)
.set_description(cli_util::join_message_paragraphs(&args.message_paragraphs))
.write()?;
if args.insert_after {
// Each child of the targets will be rebased: its set of parents will be updated
// so that the targets are replaced by the new commit.
let old_parents = RevsetExpression::commits(target_ids);
// Exclude children that are ancestors of the new commit
let to_rebase = old_parents.children().minus(&old_parents.ancestors());
let commits_to_rebase: Vec<Commit> = to_rebase
.resolve(tx.base_repo().as_ref())?
.evaluate(tx.base_repo().as_ref())?
.iter()
.commits(tx.base_repo().store())
.try_collect()?;
num_rebased = commits_to_rebase.len();
for child_commit in commits_to_rebase {
let commit_parents =
RevsetExpression::commits(child_commit.parent_ids().to_owned());
let new_parents = commit_parents.minus(&old_parents);
let mut new_parent_commits: Vec<Commit> = new_parents
.resolve(tx.base_repo().as_ref())?
.evaluate(tx.base_repo().as_ref())?
.iter()
.commits(tx.base_repo().store())
.try_collect()?;
new_parent_commits.push(new_commit.clone());
rebase_commit(
command.settings(),
tx.mut_repo(),
&child_commit,
&new_parent_commits,
)?;
}
}
}
num_rebased += tx.mut_repo().rebase_descendants(command.settings())?;
if num_rebased > 0 {
writeln!(ui, "Rebased {num_rebased} descendant commits")?;
}
tx.edit(&new_commit).unwrap();
tx.finish(ui)?;
Ok(())
}
fn combine_messages(
repo: &ReadonlyRepo,
source: &Commit,
destination: &Commit,
settings: &UserSettings,
abandon_source: bool,
) -> Result<String, CommandError> {
let description = if abandon_source {
if source.description().is_empty() {
destination.description().to_string()
} else if destination.description().is_empty() {
source.description().to_string()
} else {
let combined = "JJ: Enter a description for the combined commit.\n".to_string()
+ "JJ: Description from the destination commit:\n"
+ destination.description()
+ "\nJJ: Description from the source commit:\n"
+ source.description();
edit_description(repo, &combined, settings)?
}
} else {
destination.description().to_string()
};
Ok(description)
}
#[instrument(skip_all)]
fn cmd_move(ui: &mut Ui, command: &CommandHelper, args: &MoveArgs) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let source = workspace_command.resolve_single_rev(args.from.as_deref().unwrap_or("@"), ui)?;
let mut destination =
workspace_command.resolve_single_rev(args.to.as_deref().unwrap_or("@"), ui)?;
if source.id() == destination.id() {
return Err(user_error("Source and destination cannot be the same."));
}
workspace_command.check_rewritable(&source)?;
workspace_command.check_rewritable(&destination)?;
let matcher = workspace_command.matcher_from_values(&args.paths)?;
let mut tx = workspace_command.start_transaction(&format!(
"move changes from {} to {}",
source.id().hex(),
destination.id().hex()
));
let parent_tree = MergedTree::legacy(merge_commit_trees(tx.repo(), &source.parents())?);
let source_tree = source.merged_tree()?;
let instructions = format!(
"\
You are moving changes from: {}
into commit: {}
The left side of the diff shows the contents of the parent commit. The
right side initially shows the contents of the commit you're moving
changes from.
Adjust the right side until the diff shows the changes you want to move
to the destination. If you don't make any changes, then all the changes
from the source will be moved into the destination.
",
tx.format_commit_summary(&source),
tx.format_commit_summary(&destination)
);
let new_parent_tree_id = tx.select_diff(
ui,
&parent_tree,
&source_tree,
&instructions,
args.interactive,
matcher.as_ref(),
)?;
if args.interactive && new_parent_tree_id == parent_tree.id() {
return Err(user_error("No changes to move"));
}
let new_parent_tree = tx.repo().store().get_root_tree(&new_parent_tree_id)?;
// Apply the reverse of the selected changes onto the source
let new_source_tree = source_tree.merge(&new_parent_tree, &parent_tree)?;
let abandon_source = new_source_tree.id() == parent_tree.id();
if abandon_source {
tx.mut_repo().record_abandoned_commit(source.id().clone());
} else {
tx.mut_repo()
.rewrite_commit(command.settings(), &source)
.set_tree_id(new_source_tree.id().clone())
.write()?;
}
if tx.repo().index().is_ancestor(source.id(), destination.id()) {
// If we're moving changes to a descendant, first rebase descendants onto the
// rewritten source. Otherwise it will likely already have the content
// changes we're moving, so applying them will have no effect and the
// changes will disappear.
let mut rebaser = tx.mut_repo().create_descendant_rebaser(command.settings());
rebaser.rebase_all()?;
let rebased_destination_id = rebaser.rebased().get(destination.id()).unwrap().clone();
destination = tx.mut_repo().store().get_commit(&rebased_destination_id)?;
}
// Apply the selected changes onto the destination
let destination_tree = destination.merged_tree()?;
let new_destination_tree = destination_tree.merge(&parent_tree, &new_parent_tree)?;
let description = combine_messages(
tx.base_repo(),
&source,
&destination,
command.settings(),
abandon_source,
)?;
tx.mut_repo()
.rewrite_commit(command.settings(), &destination)
.set_tree_id(new_destination_tree.id().clone())
.set_description(description)
.write()?;
tx.finish(ui)?;
Ok(())
}
#[instrument(skip_all)]
fn cmd_squash(ui: &mut Ui, command: &CommandHelper, args: &SquashArgs) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let commit = workspace_command.resolve_single_rev(&args.revision, ui)?;
workspace_command.check_rewritable(&commit)?;
let parents = commit.parents();
if parents.len() != 1 {
return Err(user_error("Cannot squash merge commits"));
}
let parent = &parents[0];
workspace_command.check_rewritable(parent)?;
let matcher = workspace_command.matcher_from_values(&args.paths)?;
let mut tx =
workspace_command.start_transaction(&format!("squash commit {}", commit.id().hex()));
let instructions = format!(
"\
You are moving changes from: {}
into its parent: {}
The left side of the diff shows the contents of the parent commit. The
right side initially shows the contents of the commit you're moving
changes from.
Adjust the right side until the diff shows the changes you want to move
to the destination. If you don't make any changes, then all the changes
from the source will be moved into the parent.
",
tx.format_commit_summary(&commit),
tx.format_commit_summary(parent)
);
let parent_tree = parent.merged_tree()?;
let tree = commit.merged_tree()?;
let new_parent_tree_id = tx.select_diff(
ui,
&parent_tree,
&tree,
&instructions,
args.interactive,
matcher.as_ref(),
)?;
if &new_parent_tree_id == parent.merged_tree_id() {
if args.interactive {
return Err(user_error("No changes selected"));
}
if let [only_path] = &args.paths[..] {
let (_, matches) = command.matches().subcommand().unwrap();
if matches.value_source("revision").unwrap() == ValueSource::DefaultValue
&& revset::parse(
only_path,
&tx.base_workspace_helper().revset_parse_context(),
)
.is_ok()
{
writeln!(
ui.warning(),
"warning: The argument {only_path:?} is being interpreted as a path. To \
specify a revset, pass -r {only_path:?} instead."
)?;
}
}
}
// Abandon the child if the parent now has all the content from the child
// (always the case in the non-interactive case).
let abandon_child = &new_parent_tree_id == commit.merged_tree_id();
let description = if !args.message_paragraphs.is_empty() {
cli_util::join_message_paragraphs(&args.message_paragraphs)
} else {
combine_messages(
tx.base_repo(),
&commit,
parent,
command.settings(),
abandon_child,
)?
};
let mut_repo = tx.mut_repo();
let new_parent = mut_repo
.rewrite_commit(command.settings(), parent)
.set_tree_id(new_parent_tree_id)
.set_predecessors(vec![parent.id().clone(), commit.id().clone()])
.set_description(description)
.write()?;
if abandon_child {
mut_repo.record_abandoned_commit(commit.id().clone());
} else {
// Commit the remainder on top of the new parent commit.
mut_repo
.rewrite_commit(command.settings(), &commit)
.set_parents(vec![new_parent.id().clone()])
.write()?;
}
tx.finish(ui)?;
Ok(())
}
#[instrument(skip_all)]
fn cmd_unsquash(
ui: &mut Ui,
command: &CommandHelper,
args: &UnsquashArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let commit = workspace_command.resolve_single_rev(&args.revision, ui)?;
workspace_command.check_rewritable(&commit)?;
let parents = commit.parents();
if parents.len() != 1 {
return Err(user_error("Cannot unsquash merge commits"));
}
let parent = &parents[0];
workspace_command.check_rewritable(parent)?;
let mut tx =
workspace_command.start_transaction(&format!("unsquash commit {}", commit.id().hex()));
let parent_base_tree = MergedTree::legacy(merge_commit_trees(tx.repo(), &parent.parents())?);
let new_parent_tree_id;
if args.interactive {
let instructions = format!(
"\
You are moving changes from: {}
into its child: {}
The diff initially shows the parent commit's changes.
Adjust the right side until it shows the contents you want to keep in
the parent commit. The changes you edited out will be moved into the
child commit. If you don't make any changes, then the operation will be
aborted.
",
tx.format_commit_summary(parent),
tx.format_commit_summary(&commit)
);
let parent_tree = parent.merged_tree()?;
new_parent_tree_id = tx.edit_diff(ui, &parent_base_tree, &parent_tree, &instructions)?;
if new_parent_tree_id == parent_base_tree.id() {
return Err(user_error("No changes selected"));
}
} else {
new_parent_tree_id = parent_base_tree.id().clone();
}
// Abandon the parent if it is now empty (always the case in the non-interactive
// case).
if new_parent_tree_id == parent_base_tree.id() {
tx.mut_repo().record_abandoned_commit(parent.id().clone());
let description =
combine_messages(tx.base_repo(), parent, &commit, command.settings(), true)?;
// Commit the new child on top of the parent's parents.
tx.mut_repo()
.rewrite_commit(command.settings(), &commit)
.set_parents(parent.parent_ids().to_vec())
.set_description(description)
.write()?;
} else {
let new_parent = tx
.mut_repo()
.rewrite_commit(command.settings(), parent)
.set_tree_id(new_parent_tree_id)
.set_predecessors(vec![parent.id().clone(), commit.id().clone()])
.write()?;
// Commit the new child on top of the new parent.
tx.mut_repo()
.rewrite_commit(command.settings(), &commit)
.set_parents(vec![new_parent.id().clone()])
.write()?;
}
tx.finish(ui)?;
Ok(())
}
#[instrument(skip_all)]
fn cmd_chmod(ui: &mut Ui, command: &CommandHelper, args: &ChmodArgs) -> Result<(), CommandError> {
let executable_bit = match args.mode {
ChmodMode::Executable => true,
ChmodMode::Normal => false,
};
let mut workspace_command = command.workspace_helper(ui)?;
let repo_paths: Vec<_> = args
.paths
.iter()
.map(|path| workspace_command.parse_file_path(path))
.try_collect()?;
let commit = workspace_command.resolve_single_rev(&args.revision, ui)?;
workspace_command.check_rewritable(&commit)?;
let mut tx = workspace_command.start_transaction(&format!(
"make paths {} in commit {}",
if executable_bit {
"executable"
} else {
"non-executable"
},
commit.id().hex(),
));
let tree = commit.merged_tree()?;
let store = tree.store();
let mut tree_builder = MergedTreeBuilder::new(store.clone(), commit.merged_tree_id().clone());
for repo_path in repo_paths {
let user_error_with_path = |msg: &str| {
user_error(format!(
"{msg} at '{}'.",
tx.base_workspace_helper().format_file_path(&repo_path)
))
};
let tree_value = tree.path_value(&repo_path);
if tree_value.is_absent() {
return Err(user_error_with_path("No such path"));
}
let all_files = tree_value
.adds()
.iter()
.flatten()
.all(|tree_value| matches!(tree_value, TreeValue::File { .. }));
if !all_files {
let message = if tree_value.is_resolved() {
"Found neither a file nor a conflict"
} else {
"Some of the sides of the conflict are not files"
};
return Err(user_error_with_path(message));
}
let new_tree_value = tree_value.map(|value| match value {
Some(TreeValue::File { id, executable: _ }) => Some(TreeValue::File {
id: id.clone(),
executable: executable_bit,
}),
Some(TreeValue::Conflict(_)) => {
panic!("Conflict sides must not themselves be conflicts")
}
value => value.clone(),
});
tree_builder.set_or_remove(repo_path, new_tree_value);
}
let new_tree_id = tree_builder.write_tree()?;
tx.mut_repo()
.rewrite_commit(command.settings(), &commit)
.set_tree_id(new_tree_id)
.write()?;
tx.finish(ui)
}
#[instrument(skip_all)]
fn cmd_resolve(
ui: &mut Ui,
command: &CommandHelper,
args: &ResolveArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let matcher = workspace_command.matcher_from_values(&args.paths)?;
let commit = workspace_command.resolve_single_rev(&args.revision, ui)?;
let tree = commit.merged_tree()?;
let conflicts = tree
.conflicts()
.filter(|path| matcher.matches(&path.0))
.collect_vec();
if conflicts.is_empty() {
return Err(CommandError::CliError(format!(
"No conflicts found {}",
if args.paths.is_empty() {
"at this revision"
} else {
"at the given path(s)"
}
)));
}
if args.list {
return print_conflicted_paths(
&conflicts,
ui.stdout_formatter().as_mut(),
&workspace_command,
);
};
let (repo_path, _) = conflicts.get(0).unwrap();
workspace_command.check_rewritable(&commit)?;
let mut tx = workspace_command.start_transaction(&format!(
"Resolve conflicts in commit {}",
commit.id().hex()
));
let new_tree_id = tx.run_mergetool(ui, &tree, repo_path)?;
let new_commit = tx
.mut_repo()
.rewrite_commit(command.settings(), &commit)
.set_tree_id(new_tree_id)
.write()?;
tx.finish(ui)?;
if !args.quiet {
let new_tree = new_commit.merged_tree()?;
let new_conflicts = new_tree.conflicts().collect_vec();
if !new_conflicts.is_empty() {
ui.write("After this operation, some files at this revision still have conflicts:\n")?;
print_conflicted_paths(
&new_conflicts,
ui.stdout_formatter().as_mut(),
&workspace_command,
)?;
}
};
Ok(())
}
#[instrument(skip_all)]
fn print_conflicted_paths(
conflicts: &[(RepoPath, Merge<Option<TreeValue>>)],
formatter: &mut dyn Formatter,
workspace_command: &WorkspaceCommandHelper,
) -> Result<(), CommandError> {
let formatted_paths = conflicts
.iter()
.map(|(path, _conflict)| workspace_command.format_file_path(path))
.collect_vec();
let max_path_len = formatted_paths.iter().map(|p| p.len()).max().unwrap_or(0);
let formatted_paths = formatted_paths
.into_iter()
.map(|p| format!("{:width$}", p, width = max_path_len.min(32) + 3));
for ((_, conflict), formatted_path) in std::iter::zip(conflicts.iter(), formatted_paths) {
let sides = conflict.num_sides();
let n_adds = conflict.adds().iter().flatten().count();
let deletions = sides - n_adds;
let mut seen_objects = BTreeMap::new(); // Sort for consistency and easier testing
if deletions > 0 {
seen_objects.insert(
format!(
// Starting with a number sorts this first
"{deletions} deletion{}",
if deletions > 1 { "s" } else { "" }
),
"normal", // Deletions don't interfere with `jj resolve` or diff display
);
}
// TODO: We might decide it's OK for `jj resolve` to ignore special files in the
// `removes` of a conflict (see e.g. https://github.com/martinvonz/jj/pull/978). In
// that case, `conflict.removes` should be removed below.
for term in itertools::chain(conflict.removes().iter(), conflict.adds().iter()).flatten() {
seen_objects.insert(
match term {
TreeValue::File {
executable: false, ..
} => continue,
TreeValue::File {
executable: true, ..
} => "an executable",
TreeValue::Symlink(_) => "a symlink",
TreeValue::Tree(_) => "a directory",
TreeValue::GitSubmodule(_) => "a git submodule",
TreeValue::Conflict(_) => "another conflict (you found a bug!)",
}
.to_string(),
"difficult",
);
}
write!(formatter, "{formatted_path} ",)?;
formatter.with_label("conflict_description", |formatter| {
let print_pair = |formatter: &mut dyn Formatter, (text, label): &(String, &str)| {
formatter.with_label(label, |fmt| fmt.write_str(text))
};
print_pair(
formatter,
&(
format!("{sides}-sided"),
if sides > 2 { "difficult" } else { "normal" },
),
)?;
formatter.write_str(" conflict")?;
if !seen_objects.is_empty() {
formatter.write_str(" including ")?;
let seen_objects = seen_objects.into_iter().collect_vec();
match &seen_objects[..] {
[] => unreachable!(),
[only] => print_pair(formatter, only)?,
[first, middle @ .., last] => {
print_pair(formatter, first)?;
for pair in middle {
formatter.write_str(", ")?;
print_pair(formatter, pair)?;
}
formatter.write_str(" and ")?;
print_pair(formatter, last)?;
}
};
}
Ok(())
})?;
writeln!(formatter)?;
}
Ok(())
}
#[instrument(skip_all)]
fn cmd_restore(
ui: &mut Ui,
command: &CommandHelper,
args: &RestoreArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let (from_tree, to_commit);
if args.revision.is_some() {
return Err(user_error(
"`jj restore` does not have a `--revision`/`-r` option. If you'd like to modify\nthe \
*current* revision, use `--from`. If you'd like to modify a *different* \
revision,\nuse `--to` or `--changes-in`.",
));
}
if args.from.is_some() || args.to.is_some() {
to_commit = workspace_command.resolve_single_rev(args.to.as_deref().unwrap_or("@"), ui)?;
from_tree = workspace_command
.resolve_single_rev(args.from.as_deref().unwrap_or("@"), ui)?
.merged_tree()?;
} else {
to_commit =
workspace_command.resolve_single_rev(args.changes_in.as_deref().unwrap_or("@"), ui)?;
from_tree = MergedTree::legacy(merge_commit_trees(
workspace_command.repo().as_ref(),
&to_commit.parents(),
)?);
}
workspace_command.check_rewritable(&to_commit)?;
let new_tree_id = if args.paths.is_empty() {
from_tree.id().clone()
} else {
let matcher = workspace_command.matcher_from_values(&args.paths)?;
let mut tree_builder = MergedTreeBuilder::new(
workspace_command.repo().store().clone(),
to_commit.merged_tree_id().clone(),
);
let to_tree = to_commit.merged_tree()?;
for (repo_path, before, _after) in from_tree.diff(&to_tree, matcher.as_ref()) {
tree_builder.set_or_remove(repo_path, before);
}
tree_builder.write_tree()?
};
if &new_tree_id == to_commit.merged_tree_id() {
ui.write("Nothing changed.\n")?;
} else {
let mut tx = workspace_command
.start_transaction(&format!("restore into commit {}", to_commit.id().hex()));
let mut_repo = tx.mut_repo();
let new_commit = mut_repo
.rewrite_commit(command.settings(), &to_commit)
.set_tree_id(new_tree_id)
.write()?;
ui.write("Created ")?;
tx.write_commit_summary(ui.stdout_formatter().as_mut(), &new_commit)?;
ui.write("\n")?;
tx.finish(ui)?;
}
Ok(())
}
#[instrument(skip_all)]
fn cmd_diffedit(
ui: &mut Ui,
command: &CommandHelper,
args: &DiffeditArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let (target_commit, base_commits, diff_description);
if args.from.is_some() || args.to.is_some() {
target_commit =
workspace_command.resolve_single_rev(args.to.as_deref().unwrap_or("@"), ui)?;
base_commits =
vec![workspace_command.resolve_single_rev(args.from.as_deref().unwrap_or("@"), ui)?];
diff_description = format!(
"The diff initially shows the commit's changes relative to:\n{}",
workspace_command.format_commit_summary(&base_commits[0])
);
} else {
target_commit =
workspace_command.resolve_single_rev(args.revision.as_deref().unwrap_or("@"), ui)?;
base_commits = target_commit.parents();
diff_description = "The diff initially shows the commit's changes.".to_string();
};
workspace_command.check_rewritable(&target_commit)?;
let mut tx =
workspace_command.start_transaction(&format!("edit commit {}", target_commit.id().hex()));
let instructions = format!(
"\
You are editing changes in: {}
{diff_description}
Adjust the right side until it shows the contents you want. If you
don't make any changes, then the operation will be aborted.",
tx.format_commit_summary(&target_commit),
);
let base_tree = MergedTree::legacy(merge_commit_trees(tx.repo(), base_commits.as_slice())?);
let tree = target_commit.merged_tree()?;
let tree_id = tx.edit_diff(ui, &base_tree, &tree, &instructions)?;
if tree_id == *target_commit.merged_tree_id() {
ui.write("Nothing changed.\n")?;
} else {
let mut_repo = tx.mut_repo();
let new_commit = mut_repo
.rewrite_commit(command.settings(), &target_commit)
.set_tree_id(tree_id)
.write()?;
ui.write("Created ")?;
tx.write_commit_summary(ui.stdout_formatter().as_mut(), &new_commit)?;
ui.write("\n")?;
tx.finish(ui)?;
}
Ok(())
}
fn description_template_for_commit(
ui: &Ui,
settings: &UserSettings,
workspace_command: &WorkspaceCommandHelper,
commit: &Commit,
) -> Result<String, CommandError> {
let mut diff_summary_bytes = Vec::new();
diff_util::show_patch(
ui,
&mut PlainTextFormatter::new(&mut diff_summary_bytes),
workspace_command,
commit,
&EverythingMatcher,
&[DiffFormat::Summary],
)?;
let description = if commit.description().is_empty() {
settings.default_description()
} else {
commit.description().to_owned()
};
if diff_summary_bytes.is_empty() {
Ok(description)
} else {
Ok(description + "\n" + &diff_summary_to_description(&diff_summary_bytes))
}
}
fn description_template_for_cmd_split(
ui: &Ui,
settings: &UserSettings,
workspace_command: &WorkspaceCommandHelper,
intro: &str,
overall_commit_description: &str,
from_tree: &MergedTree,
to_tree: &MergedTree,
) -> Result<String, CommandError> {
let mut diff_summary_bytes = Vec::new();
diff_util::show_diff(
ui,
&mut PlainTextFormatter::new(&mut diff_summary_bytes),
workspace_command,
from_tree,
to_tree,
&EverythingMatcher,
&[DiffFormat::Summary],
)?;
let description = if overall_commit_description.is_empty() {
settings.default_description()
} else {
overall_commit_description.to_owned()
};
Ok(format!("JJ: {intro}\n{description}\n") + &diff_summary_to_description(&diff_summary_bytes))
}
fn diff_summary_to_description(bytes: &[u8]) -> String {
let text = std::str::from_utf8(bytes).expect(
"Summary diffs and repo paths must always be valid UTF8.",
// Double-check this assumption for diffs that include file content.
);
"JJ: This commit contains the following changes:\n".to_owned()
+ &textwrap::indent(text, "JJ: ")
}
#[instrument(skip_all)]
fn cmd_split(ui: &mut Ui, command: &CommandHelper, args: &SplitArgs) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let commit = workspace_command.resolve_single_rev(&args.revision, ui)?;
workspace_command.check_rewritable(&commit)?;
let matcher = workspace_command.matcher_from_values(&args.paths)?;
let mut tx =
workspace_command.start_transaction(&format!("split commit {}", commit.id().hex()));
let end_tree = commit.merged_tree()?;
let base_tree = MergedTree::legacy(merge_commit_trees(tx.repo(), &commit.parents())?);
let interactive = args.paths.is_empty();
let instructions = format!(
"\
You are splitting a commit in two: {}
The diff initially shows the changes in the commit you're splitting.
Adjust the right side until it shows the contents you want for the first
(parent) commit. The remainder will be in the second commit. If you
don't make any changes, then the operation will be aborted.
",
tx.format_commit_summary(&commit)
);
let tree_id = tx.select_diff(
ui,
&base_tree,
&end_tree,
&instructions,
interactive,
matcher.as_ref(),
)?;
if &tree_id == commit.merged_tree_id() && interactive {
ui.write("Nothing changed.\n")?;
return Ok(());
}
let middle_tree = tx.repo().store().get_root_tree(&tree_id)?;
if middle_tree.id() == base_tree.id() {
writeln!(
ui.warning(),
"The given paths do not match any file: {}",
args.paths.join(" ")
)?;
}
let first_template = description_template_for_cmd_split(
ui,
command.settings(),
tx.base_workspace_helper(),
"Enter commit description for the first part (parent).",
commit.description(),
&base_tree,
&middle_tree,
)?;
let first_description = edit_description(tx.base_repo(), &first_template, command.settings())?;
let first_commit = tx
.mut_repo()
.rewrite_commit(command.settings(), &commit)
.set_tree_id(tree_id)
.set_description(first_description)
.write()?;
let second_description = if commit.description().is_empty() {
// If there was no description before, don't ask for one for the second commit.
"".to_string()
} else {
let second_template = description_template_for_cmd_split(
ui,
command.settings(),
tx.base_workspace_helper(),
"Enter commit description for the second part (child).",
commit.description(),
&middle_tree,
&end_tree,
)?;
edit_description(tx.base_repo(), &second_template, command.settings())?
};
let second_commit = tx
.mut_repo()
.rewrite_commit(command.settings(), &commit)
.set_parents(vec![first_commit.id().clone()])
.set_tree(commit.tree_id().clone())
.generate_new_change_id()
.set_description(second_description)
.write()?;
let mut rebaser = DescendantRebaser::new(
command.settings(),
tx.mut_repo(),
hashmap! { commit.id().clone() => hashset!{second_commit.id().clone()} },
hashset! {},
);
rebaser.rebase_all()?;
let num_rebased = rebaser.rebased().len();
if num_rebased > 0 {
writeln!(ui, "Rebased {num_rebased} descendant commits")?;
}
ui.write("First part: ")?;
tx.write_commit_summary(ui.stdout_formatter().as_mut(), &first_commit)?;
ui.write("\nSecond part: ")?;
tx.write_commit_summary(ui.stdout_formatter().as_mut(), &second_commit)?;
ui.write("\n")?;
tx.finish(ui)?;
Ok(())
}
#[instrument(skip_all)]
fn cmd_merge(ui: &mut Ui, command: &CommandHelper, args: &NewArgs) -> Result<(), CommandError> {
if args.revisions.len() < 2 {
return Err(CommandError::CliError(String::from(
"Merge requires at least two revisions",
)));
}
cmd_new(ui, command, args)
}
// TODO: Move to run.rs
fn cmd_run(_ui: &mut Ui, _command: &CommandHelper, _args: &RunArgs) -> Result<(), CommandError> {
Err(user_error("This is a stub, do not use"))
}
#[instrument(skip_all)]
fn cmd_rebase(ui: &mut Ui, command: &CommandHelper, args: &RebaseArgs) -> Result<(), CommandError> {
if args.allow_large_revsets {
return Err(user_error(
"--allow-large-revsets has been deprecated.
Please use `jj rebase -d 'all:x|y'` instead of `jj rebase --allow-large-revsets -d x -d y`.",
));
}
let mut workspace_command = command.workspace_helper(ui)?;
let new_parents = resolve_destination_revs(&workspace_command, ui, &args.destination)?
.into_iter()
.collect_vec();
if let Some(rev_str) = &args.revision {
rebase_revision(
ui,
command.settings(),
&mut workspace_command,
&new_parents,
rev_str,
)?;
} else if !args.source.is_empty() {
let source_commits =
resolve_multiple_nonempty_revsets_default_single(&workspace_command, ui, &args.source)?;
rebase_descendants(
ui,
command.settings(),
&mut workspace_command,
&new_parents,
&source_commits,
)?;
} else {
let branch_commits = if args.branch.is_empty() {
IndexSet::from([workspace_command.resolve_single_rev("@", ui)?])
} else {
resolve_multiple_nonempty_revsets_default_single(&workspace_command, ui, &args.branch)?
};
rebase_branch(
ui,
command.settings(),
&mut workspace_command,
&new_parents,
&branch_commits,
)?;
}
Ok(())
}
fn rebase_branch(
ui: &mut Ui,
settings: &UserSettings,
workspace_command: &mut WorkspaceCommandHelper,
new_parents: &[Commit],
branch_commits: &IndexSet<Commit>,
) -> Result<(), CommandError> {
let parent_ids = new_parents
.iter()
.map(|commit| commit.id().clone())
.collect_vec();
let branch_commit_ids = branch_commits
.iter()
.map(|commit| commit.id().clone())
.collect_vec();
let roots_expression = RevsetExpression::commits(parent_ids)
.range(&RevsetExpression::commits(branch_commit_ids))
.roots();
let root_commits: IndexSet<_> = roots_expression
.resolve(workspace_command.repo().as_ref())
.unwrap()
.evaluate(workspace_command.repo().as_ref())
.unwrap()
.iter()
.commits(workspace_command.repo().store())
.try_collect()?;
rebase_descendants(ui, settings, workspace_command, new_parents, &root_commits)
}
fn rebase_descendants(
ui: &mut Ui,
settings: &UserSettings,
workspace_command: &mut WorkspaceCommandHelper,
new_parents: &[Commit],
old_commits: &IndexSet<Commit>,
) -> Result<(), CommandError> {
for old_commit in old_commits.iter() {
workspace_command.check_rewritable(old_commit)?;
check_rebase_destinations(workspace_command.repo(), new_parents, old_commit)?;
}
let tx_message = if old_commits.len() == 1 {
format!(
"rebase commit {} and descendants",
old_commits.first().unwrap().id().hex()
)
} else {
format!("rebase {} commits and their descendants", old_commits.len())
};
let mut tx = workspace_command.start_transaction(&tx_message);
// `rebase_descendants` takes care of sorting in reverse topological order, so
// no need to do it here.
for old_commit in old_commits {
rebase_commit(settings, tx.mut_repo(), old_commit, new_parents)?;
}
let num_rebased = old_commits.len() + tx.mut_repo().rebase_descendants(settings)?;
writeln!(ui, "Rebased {num_rebased} commits")?;
tx.finish(ui)?;
Ok(())
}
fn rebase_revision(
ui: &mut Ui,
settings: &UserSettings,
workspace_command: &mut WorkspaceCommandHelper,
new_parents: &[Commit],
rev_str: &str,
) -> Result<(), CommandError> {
let old_commit = workspace_command.resolve_single_rev(rev_str, ui)?;
workspace_command.check_rewritable(&old_commit)?;
check_rebase_destinations(workspace_command.repo(), new_parents, &old_commit)?;
let children_expression = RevsetExpression::commit(old_commit.id().clone()).children();
let child_commits: Vec<_> = children_expression
.resolve(workspace_command.repo().as_ref())
.unwrap()
.evaluate(workspace_command.repo().as_ref())
.unwrap()
.iter()
.commits(workspace_command.repo().store())
.try_collect()?;
let mut tx =
workspace_command.start_transaction(&format!("rebase commit {}", old_commit.id().hex()));
rebase_commit(settings, tx.mut_repo(), &old_commit, new_parents)?;
// Manually rebase children because we don't want to rebase them onto the
// rewritten commit. (But we still want to record the commit as rewritten so
// branches and the working copy get updated to the rewritten commit.)
let mut num_rebased_descendants = 0;
for child_commit in &child_commits {
let new_child_parent_ids: Vec<CommitId> = child_commit
.parents()
.iter()
.flat_map(|c| {
if c == &old_commit {
old_commit
.parents()
.iter()
.map(|c| c.id().clone())
.collect()
} else {
[c.id().clone()].to_vec()
}
})
.collect();
// Some of the new parents may be ancestors of others as in
// `test_rebase_single_revision`.
let new_child_parents_expression = RevsetExpression::commits(new_child_parent_ids.clone())
.minus(
&RevsetExpression::commits(new_child_parent_ids.clone())
.parents()
.ancestors(),
);
let new_child_parents: Vec<Commit> = new_child_parents_expression
.resolve(tx.base_repo().as_ref())
.unwrap()
.evaluate(tx.base_repo().as_ref())
.unwrap()
.iter()
.commits(tx.base_repo().store())
.try_collect()?;
rebase_commit(settings, tx.mut_repo(), child_commit, &new_child_parents)?;
num_rebased_descendants += 1;
}
num_rebased_descendants += tx.mut_repo().rebase_descendants(settings)?;
if num_rebased_descendants > 0 {
writeln!(
ui,
"Also rebased {num_rebased_descendants} descendant commits onto parent of rebased \
commit"
)?;
}
tx.finish(ui)?;
Ok(())
}
fn check_rebase_destinations(
repo: &Arc<ReadonlyRepo>,
new_parents: &[Commit],
commit: &Commit,
) -> Result<(), CommandError> {
for parent in new_parents {
if repo.index().is_ancestor(commit.id(), parent.id()) {
return Err(user_error(format!(
"Cannot rebase {} onto descendant {}",
short_commit_hash(commit.id()),
short_commit_hash(parent.id())
)));
}
}
Ok(())
}
#[instrument(skip_all)]
fn cmd_backout(
ui: &mut Ui,
command: &CommandHelper,
args: &BackoutArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let commit_to_back_out = workspace_command.resolve_single_rev(&args.revision, ui)?;
let mut parents = vec![];
for revision_str in &args.destination {
let destination = workspace_command.resolve_single_rev(revision_str, ui)?;
parents.push(destination);
}
let mut tx = workspace_command.start_transaction(&format!(
"back out commit {}",
commit_to_back_out.id().hex()
));
back_out_commit(
command.settings(),
tx.mut_repo(),
&commit_to_back_out,
&parents,
)?;
tx.finish(ui)?;
Ok(())
}
fn make_branch_term(branch_names: &[impl AsRef<str>]) -> String {
match branch_names {
[branch_name] => format!("branch {}", branch_name.as_ref()),
branch_names => {
format!(
"branches {}",
branch_names.iter().map(AsRef::as_ref).join(", ")
)
}
}
}
#[instrument(skip_all)]
fn cmd_util(
ui: &mut Ui,
command: &CommandHelper,
subcommand: &UtilCommands,
) -> Result<(), CommandError> {
match subcommand {
UtilCommands::Completion(completion_matches) => {
let mut app = command.app().clone();
let mut buf = vec![];
let shell = if completion_matches.zsh {
clap_complete::Shell::Zsh
} else if completion_matches.fish {
clap_complete::Shell::Fish
} else {
clap_complete::Shell::Bash
};
clap_complete::generate(shell, &mut app, "jj", &mut buf);
ui.stdout_formatter().write_all(&buf)?;
}
UtilCommands::Mangen(_mangen_matches) => {
let mut buf = vec![];
let man = clap_mangen::Man::new(command.app().clone());
man.render(&mut buf)?;
ui.stdout_formatter().write_all(&buf)?;
}
UtilCommands::ConfigSchema(_config_schema_matches) => {
// TODO(#879): Consider generating entire schema dynamically vs. static file.
let buf = include_bytes!("../config-schema.json");
ui.stdout_formatter().write_all(buf)?;
}
}
Ok(())
}
#[instrument(skip_all)]
fn cmd_workspace(
ui: &mut Ui,
command: &CommandHelper,
subcommand: &WorkspaceCommands,
) -> Result<(), CommandError> {
match subcommand {
WorkspaceCommands::Add(command_matches) => cmd_workspace_add(ui, command, command_matches),
WorkspaceCommands::Forget(command_matches) => {
cmd_workspace_forget(ui, command, command_matches)
}
WorkspaceCommands::List(command_matches) => {
cmd_workspace_list(ui, command, command_matches)
}
WorkspaceCommands::Root(command_matches) => {
cmd_workspace_root(ui, command, command_matches)
}
WorkspaceCommands::UpdateStale(command_matches) => {
cmd_workspace_update_stale(ui, command, command_matches)
}
}
}
#[instrument(skip_all)]
fn cmd_workspace_add(
ui: &mut Ui,
command: &CommandHelper,
args: &WorkspaceAddArgs,
) -> Result<(), CommandError> {
let old_workspace_command = command.workspace_helper(ui)?;
let destination_path = command.cwd().join(&args.destination);
if destination_path.exists() {
return Err(user_error("Workspace already exists"));
} else {
fs::create_dir(&destination_path).unwrap();
}
let name = if let Some(name) = &args.name {
name.to_string()
} else {
destination_path
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string()
};
let workspace_id = WorkspaceId::new(name.clone());
let repo = old_workspace_command.repo();
if repo.view().get_wc_commit_id(&workspace_id).is_some() {
return Err(user_error(format!(
"Workspace named '{name}' already exists"
)));
}
let (new_workspace, repo) = Workspace::init_workspace_with_existing_repo(
command.settings(),
&destination_path,
repo,
workspace_id,
)?;
writeln!(
ui,
"Created workspace in \"{}\"",
file_util::relative_path(old_workspace_command.workspace_root(), &destination_path)
.display()
)?;
let mut new_workspace_command = WorkspaceCommandHelper::new(ui, command, new_workspace, repo)?;
let mut tx = new_workspace_command.start_transaction(&format!(
"Create initial working-copy commit in workspace {}",
&name
));
// Check out a parent of the current workspace's working-copy commit, or the
// root if there is no working-copy commit in the current workspace.
let new_wc_commit = if let Some(old_wc_commit_id) = tx
.base_repo()
.view()
.get_wc_commit_id(old_workspace_command.workspace_id())
{
tx.repo().store().get_commit(old_wc_commit_id)?.parents()[0].clone()
} else {
tx.repo().store().root_commit()
};
tx.check_out(&new_wc_commit)?;
tx.finish(ui)?;
Ok(())
}
#[instrument(skip_all)]
fn cmd_workspace_forget(
ui: &mut Ui,
command: &CommandHelper,
args: &WorkspaceForgetArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let workspace_id = if let Some(workspace_str) = &args.workspace {
WorkspaceId::new(workspace_str.to_string())
} else {
workspace_command.workspace_id().to_owned()
};
if workspace_command
.repo()
.view()
.get_wc_commit_id(&workspace_id)
.is_none()
{
return Err(user_error("No such workspace"));
}
let mut tx =
workspace_command.start_transaction(&format!("forget workspace {}", workspace_id.as_str()));
tx.mut_repo().remove_wc_commit(&workspace_id);
tx.finish(ui)?;
Ok(())
}
#[instrument(skip_all)]
fn cmd_workspace_list(
ui: &mut Ui,
command: &CommandHelper,
_args: &WorkspaceListArgs,
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let repo = workspace_command.repo();
for (workspace_id, wc_commit_id) in repo.view().wc_commit_ids().iter().sorted() {
write!(ui, "{}: ", workspace_id.as_str())?;
let commit = repo.store().get_commit(wc_commit_id)?;
workspace_command.write_commit_summary(ui.stdout_formatter().as_mut(), &commit)?;
writeln!(ui)?;
}
Ok(())
}
#[instrument(skip_all)]
fn cmd_workspace_root(
ui: &mut Ui,
command: &CommandHelper,
_args: &WorkspaceRootArgs,
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let root = workspace_command
.workspace_root()
.to_str()
.ok_or_else(|| user_error("The workspace root is not valid UTF-8"))?;
writeln!(ui, "{root}")?;
Ok(())
}
#[instrument(skip_all)]
fn cmd_workspace_update_stale(
ui: &mut Ui,
command: &CommandHelper,
_args: &WorkspaceUpdateStaleArgs,
) -> Result<(), CommandError> {
// Snapshot the current working copy on top of the last known working-copy
// operation, then merge the concurrent operations. The wc_commit_id of the
// merged repo wouldn't change because the old one wins, but it's probably
// fine if we picked the new wc_commit_id.
let known_wc_commit = {
let mut workspace_command = command.for_stale_working_copy(ui)?;
workspace_command.snapshot(ui)?;
let wc_commit_id = workspace_command.get_wc_commit_id().unwrap();
workspace_command.repo().store().get_commit(wc_commit_id)?
};
let mut workspace_command = command.workspace_helper_no_snapshot(ui)?;
let repo = workspace_command.repo().clone();
let (mut locked_wc, desired_wc_commit) =
workspace_command.unchecked_start_working_copy_mutation()?;
match check_stale_working_copy(&locked_wc, &desired_wc_commit, &repo) {
Ok(_) => {
locked_wc.discard();
ui.write("Nothing to do (the working copy is not stale).\n")?;
}
Err(_) => {
// The same check as start_working_copy_mutation(), but with the stale
// working-copy commit.
if known_wc_commit.merged_tree_id() != locked_wc.old_tree_id() {
return Err(user_error("Concurrent working copy operation. Try again."));
}
let desired_tree = desired_wc_commit.merged_tree()?;
let stats = locked_wc.check_out(&desired_tree).map_err(|err| {
CommandError::InternalError(format!(
"Failed to check out commit {}: {}",
desired_wc_commit.id().hex(),
err
))
})?;
locked_wc.finish(repo.op_id().clone())?;
ui.write("Working copy now at: ")?;
workspace_command
.write_commit_summary(ui.stdout_formatter().as_mut(), &desired_wc_commit)?;
ui.write("\n")?;
print_checkout_stats(ui, stats)?;
}
}
Ok(())
}
#[instrument(skip_all)]
fn cmd_sparse(ui: &mut Ui, command: &CommandHelper, args: &SparseArgs) -> Result<(), CommandError> {
match args {
SparseArgs::List(sub_args) => cmd_sparse_list(ui, command, sub_args),
SparseArgs::Set(sub_args) => cmd_sparse_set(ui, command, sub_args),
}
}
#[instrument(skip_all)]
fn cmd_sparse_list(
ui: &mut Ui,
command: &CommandHelper,
_args: &SparseListArgs,
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
for path in workspace_command.working_copy().sparse_patterns()? {
let ui_path = workspace_command.format_file_path(path);
writeln!(ui, "{ui_path}")?;
}
Ok(())
}
#[instrument(skip_all)]
fn cmd_sparse_set(
ui: &mut Ui,
command: &CommandHelper,
args: &SparseSetArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let paths_to_add: Vec<_> = args
.add
.iter()
.map(|v| workspace_command.parse_file_path(v))
.try_collect()?;
let paths_to_remove: Vec<_> = args
.remove
.iter()
.map(|v| workspace_command.parse_file_path(v))
.try_collect()?;
// Determine inputs of `edit` operation now, since `workspace_command` is
// inaccessible while the working copy is locked.
let edit_inputs = args.edit.then(|| {
(
workspace_command.repo().clone(),
workspace_command.workspace_root().clone(),
)
});
let (mut locked_wc, _wc_commit) = workspace_command.start_working_copy_mutation()?;
let mut new_patterns = HashSet::new();
if args.reset {
new_patterns.insert(RepoPath::root());
} else {
if !args.clear {
new_patterns.extend(locked_wc.sparse_patterns()?.iter().cloned());
for path in paths_to_remove {
new_patterns.remove(&path);
}
}
for path in paths_to_add {
new_patterns.insert(path);
}
}
let mut new_patterns = new_patterns.into_iter().collect_vec();
new_patterns.sort();
if let Some((repo, workspace_root)) = edit_inputs {
new_patterns = edit_sparse(
&workspace_root,
repo.repo_path(),
&new_patterns,
command.settings(),
)?;
new_patterns.sort();
}
let stats = locked_wc.set_sparse_patterns(new_patterns).map_err(|err| {
CommandError::InternalError(format!("Failed to update working copy paths: {err}"))
})?;
let operation_id = locked_wc.old_operation_id().clone();
locked_wc.finish(operation_id)?;
print_checkout_stats(ui, stats)?;
Ok(())
}
pub fn default_app() -> Command {
Commands::augment_subcommands(Args::command())
}
#[instrument(skip_all)]
pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), CommandError> {
let derived_subcommands: Commands =
Commands::from_arg_matches(command_helper.matches()).unwrap();
match &derived_subcommands {
Commands::Version(sub_args) => cmd_version(ui, command_helper, sub_args),
Commands::Init(sub_args) => cmd_init(ui, command_helper, sub_args),
Commands::Config(sub_args) => cmd_config(ui, command_helper, sub_args),
Commands::Checkout(sub_args) => cmd_checkout(ui, command_helper, sub_args),
Commands::Untrack(sub_args) => cmd_untrack(ui, command_helper, sub_args),
Commands::Files(sub_args) => cmd_files(ui, command_helper, sub_args),
Commands::Cat(sub_args) => cmd_cat(ui, command_helper, sub_args),
Commands::Diff(sub_args) => cmd_diff(ui, command_helper, sub_args),
Commands::Show(sub_args) => cmd_show(ui, command_helper, sub_args),
Commands::Status(sub_args) => cmd_status(ui, command_helper, sub_args),
Commands::Log(sub_args) => cmd_log(ui, command_helper, sub_args),
Commands::Interdiff(sub_args) => cmd_interdiff(ui, command_helper, sub_args),
Commands::Obslog(sub_args) => cmd_obslog(ui, command_helper, sub_args),
Commands::Describe(sub_args) => cmd_describe(ui, command_helper, sub_args),
Commands::Commit(sub_args) => cmd_commit(ui, command_helper, sub_args),
Commands::Duplicate(sub_args) => cmd_duplicate(ui, command_helper, sub_args),
Commands::Abandon(sub_args) => cmd_abandon(ui, command_helper, sub_args),
Commands::Edit(sub_args) => cmd_edit(ui, command_helper, sub_args),
Commands::New(sub_args) => cmd_new(ui, command_helper, sub_args),
Commands::Move(sub_args) => cmd_move(ui, command_helper, sub_args),
Commands::Squash(sub_args) => cmd_squash(ui, command_helper, sub_args),
Commands::Unsquash(sub_args) => cmd_unsquash(ui, command_helper, sub_args),
Commands::Restore(sub_args) => cmd_restore(ui, command_helper, sub_args),
Commands::Run(sub_args) => cmd_run(ui, command_helper, sub_args),
Commands::Diffedit(sub_args) => cmd_diffedit(ui, command_helper, sub_args),
Commands::Split(sub_args) => cmd_split(ui, command_helper, sub_args),
Commands::Merge(sub_args) => cmd_merge(ui, command_helper, sub_args),
Commands::Rebase(sub_args) => cmd_rebase(ui, command_helper, sub_args),
Commands::Backout(sub_args) => cmd_backout(ui, command_helper, sub_args),
Commands::Resolve(sub_args) => cmd_resolve(ui, command_helper, sub_args),
Commands::Branch(sub_args) => branch::cmd_branch(ui, command_helper, sub_args),
Commands::Undo(sub_args) => operation::cmd_op_undo(ui, command_helper, sub_args),
Commands::Operation(sub_args) => operation::cmd_operation(ui, command_helper, sub_args),
Commands::Workspace(sub_args) => cmd_workspace(ui, command_helper, sub_args),
Commands::Sparse(sub_args) => cmd_sparse(ui, command_helper, sub_args),
Commands::Chmod(sub_args) => cmd_chmod(ui, command_helper, sub_args),
Commands::Git(sub_args) => git::cmd_git(ui, command_helper, sub_args),
Commands::Util(sub_args) => cmd_util(ui, command_helper, sub_args),
#[cfg(feature = "bench")]
Commands::Bench(sub_args) => bench::cmd_bench(ui, command_helper, sub_args),
Commands::Debug(sub_args) => debug::cmd_debug(ui, command_helper, sub_args),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verify_app() {
default_app().debug_assert();
}
}