#[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::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(NewArgs),
Move(MoveArgs),
New(NewArgs),
Next(NextArgs),
Obslog(ObslogArgs),
#[command(subcommand)]
#[command(visible_alias = "op")]
Operation(operation::OperationCommands),
Prev(PrevArgs),
Rebase(RebaseArgs),
Resolve(ResolveArgs),
Restore(RestoreArgs),
#[command(hide = true)]
Run(RunArgs),
Show(ShowArgs),
#[command(subcommand)]
Sparse(SparseArgs),
Split(SplitArgs),
Squash(SquashArgs),
Status(StatusArgs),
#[command(subcommand)]
Util(UtilCommands),
Undo(operation::OperationUndoArgs),
Unsquash(UnsquashArgs),
Untrack(UntrackArgs),
Version(VersionArgs),
#[command(subcommand)]
Workspace(WorkspaceCommands),
}
#[derive(clap::Args, Clone, Debug)]
struct VersionArgs {}
#[derive(clap::Args, Clone, Debug)]
#[command(group(ArgGroup::new("backend").args(&["git", "git_repo"])))]
struct InitArgs {
#[arg(default_value = ".", value_hint = clap::ValueHint::DirPath)]
destination: String,
#[arg(long)]
git: bool,
#[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 {
#[arg(long, group = "config_level")]
user: bool,
#[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 {
panic!("No config_level provided");
}
}
}
#[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),
}
#[derive(clap::Args, Clone, Debug)]
struct ConfigListArgs {
#[arg(value_parser = NonEmptyStringValueParser::new())]
pub name: Option<String>,
#[arg(long)]
pub include_defaults: bool,
}
#[derive(clap::Args, Clone, Debug)]
#[command(verbatim_doc_comment)]
struct ConfigGetArgs {
#[arg(required = true)]
name: String,
}
#[derive(clap::Args, Clone, Debug)]
struct ConfigSetArgs {
#[arg(required = true)]
name: String,
#[arg(required = true)]
value: String,
#[clap(flatten)]
config_args: ConfigArgs,
}
#[derive(clap::Args, Clone, Debug)]
struct ConfigEditArgs {
#[clap(flatten)]
pub config_args: ConfigArgs,
}
#[derive(clap::Args, Clone, Debug)]
#[command(visible_aliases = &["co"])]
struct CheckoutArgs {
revision: RevisionArg,
#[arg(short = 'r', hide = true)]
unused_revision: bool,
#[arg(long = "message", short, value_name = "MESSAGE")]
message_paragraphs: Vec<String>,
}
#[derive(clap::Args, Clone, Debug)]
struct UntrackArgs {
#[arg(required = true, value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
#[derive(clap::Args, Clone, Debug)]
struct FilesArgs {
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
#[derive(clap::Args, Clone, Debug)]
struct CatArgs {
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
#[arg(value_hint = clap::ValueHint::FilePath)]
path: String,
}
#[derive(clap::Args, Clone, Debug)]
struct DiffArgs {
#[arg(long, short)]
revision: Option<RevisionArg>,
#[arg(long, conflicts_with = "revision")]
from: Option<RevisionArg>,
#[arg(long, conflicts_with = "revision")]
to: Option<RevisionArg>,
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
#[command(flatten)]
format: DiffFormatArgs,
}
#[derive(clap::Args, Clone, Debug)]
struct ShowArgs {
#[arg(default_value = "@")]
revision: RevisionArg,
#[arg(short = 'r', hide = true)]
unused_revision: bool,
#[command(flatten)]
format: DiffFormatArgs,
}
#[derive(clap::Args, Clone, Debug)]
#[command(visible_alias = "st")]
struct StatusArgs {}
#[derive(clap::Args, Clone, Debug)]
struct LogArgs {
#[arg(long, short)]
revisions: Vec<RevisionArg>,
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
#[arg(long)]
reversed: bool,
#[arg(long, short)]
limit: Option<usize>,
#[arg(long)]
no_graph: bool,
#[arg(long, short = 'T')]
template: Option<String>,
#[arg(long, short = 'p')]
patch: bool,
#[command(flatten)]
diff_format: DiffFormatArgs,
}
#[derive(clap::Args, Clone, Debug)]
struct ObslogArgs {
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
#[arg(long, short)]
limit: Option<usize>,
#[arg(long)]
no_graph: bool,
#[arg(long, short = 'T')]
template: Option<String>,
#[arg(long, short = 'p')]
patch: bool,
#[command(flatten)]
diff_format: DiffFormatArgs,
}
#[derive(clap::Args, Clone, Debug)]
#[command(group(ArgGroup::new("to_diff").args(&["from", "to"]).multiple(true).required(true)))]
struct InterdiffArgs {
#[arg(long)]
from: Option<RevisionArg>,
#[arg(long)]
to: Option<RevisionArg>,
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
#[command(flatten)]
format: DiffFormatArgs,
}
#[derive(clap::Args, Clone, Debug)]
struct DescribeArgs {
#[arg(default_value = "@")]
revision: RevisionArg,
#[arg(short = 'r', hide = true)]
unused_revision: bool,
#[arg(long = "message", short, value_name = "MESSAGE")]
message_paragraphs: Vec<String>,
#[arg(long)]
stdin: bool,
#[arg(long)]
no_edit: bool,
#[arg(long)]
reset_author: bool,
}
#[derive(clap::Args, Clone, Debug)]
#[command(visible_aliases=&["ci"])]
struct CommitArgs {
#[arg(long = "message", short, value_name = "MESSAGE")]
message_paragraphs: Vec<String>,
}
#[derive(clap::Args, Clone, Debug)]
struct DuplicateArgs {
#[arg(default_value = "@")]
revisions: Vec<RevisionArg>,
#[arg(short = 'r', hide = true)]
unused_revision: bool,
}
#[derive(clap::Args, Clone, Debug)]
struct AbandonArgs {
#[arg(default_value = "@")]
revisions: Vec<RevisionArg>,
#[arg(long, short)]
summary: bool,
#[arg(short = 'r', hide = true)]
unused_revision: bool,
}
#[derive(clap::Args, Clone, Debug)]
struct EditArgs {
revision: RevisionArg,
#[arg(short = 'r', hide = true)]
unused_revision: bool,
}
#[derive(clap::Args, Clone, Debug)]
#[command(group(ArgGroup::new("order").args(&["insert_after", "insert_before"])))]
struct NewArgs {
#[arg(default_value = "@")]
revisions: Vec<RevisionArg>,
#[arg(short = 'r', hide = true)]
unused_revision: bool,
#[arg(long = "message", short, value_name = "MESSAGE")]
message_paragraphs: Vec<String>,
#[arg(long, short = 'L', hide = true)]
allow_large_revsets: bool,
#[arg(long, short = 'A', visible_alias = "after")]
insert_after: bool,
#[arg(long, short = 'B', visible_alias = "before")]
insert_before: bool,
}
#[derive(clap::Args, Clone, Debug)]
#[command(verbatim_doc_comment)]
struct NextArgs {
#[arg(default_value = "1")]
amount: u64,
#[arg(long)]
edit: bool,
}
#[derive(clap::Args, Clone, Debug)]
#[command(verbatim_doc_comment)]
struct PrevArgs {
#[arg(default_value = "1")]
amount: u64,
#[arg(long)]
edit: bool,
}
#[derive(clap::Args, Clone, Debug)]
#[command(group(ArgGroup::new("to_move").args(&["from", "to"]).multiple(true).required(true)))]
struct MoveArgs {
#[arg(long)]
from: Option<RevisionArg>,
#[arg(long)]
to: Option<RevisionArg>,
#[arg(long, short)]
interactive: bool,
#[arg(conflicts_with = "interactive", value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
#[derive(clap::Args, Clone, Debug)]
#[command(visible_alias = "amend")]
struct SquashArgs {
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
#[arg(long = "message", short, value_name = "MESSAGE")]
message_paragraphs: Vec<String>,
#[arg(long, short)]
interactive: bool,
#[arg(conflicts_with = "interactive", value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
#[derive(clap::Args, Clone, Debug)]
#[command(visible_alias = "unamend")]
struct UnsquashArgs {
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
#[arg(long, short)]
interactive: bool,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
enum ChmodMode {
#[value(name = "n", alias("normal"))]
Normal,
#[value(name = "x", alias("executable"))]
Executable,
}
#[derive(clap::Args, Clone, Debug)]
struct ChmodArgs {
mode: ChmodMode,
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
#[arg(required = true, value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
#[derive(clap::Args, Clone, Debug)]
struct ResolveArgs {
#[arg(long, short, default_value = "@")]
revision: String,
#[arg(long, short)]
list: bool,
#[arg(long, short, conflicts_with = "list")]
quiet: bool,
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
#[derive(clap::Args, Clone, Debug)]
struct RestoreArgs {
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
#[arg(long)]
from: Option<RevisionArg>,
#[arg(long)]
to: Option<RevisionArg>,
#[arg(long, short, value_name="REVISION", conflicts_with_all=["to", "from"])]
changes_in: Option<RevisionArg>,
#[arg(long, short, hide = true)]
revision: Option<RevisionArg>,
}
#[derive(clap::Args, Clone, Debug)]
#[command(verbatim_doc_comment)]
struct RunArgs {
#[arg(long, short, alias = "x")]
command: String,
#[arg(long, short, default_value = "@")]
revisions: Vec<RevisionArg>,
}
#[derive(clap::Args, Clone, Debug)]
struct DiffeditArgs {
#[arg(long, short)]
revision: Option<RevisionArg>,
#[arg(long, conflicts_with = "revision")]
from: Option<RevisionArg>,
#[arg(long, conflicts_with = "revision")]
to: Option<RevisionArg>,
}
#[derive(clap::Args, Clone, Debug)]
struct SplitArgs {
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
#[derive(clap::Args, Clone, Debug)]
#[command(verbatim_doc_comment)]
#[command(group(ArgGroup::new("to_rebase").args(&["branch", "source", "revision"])))]
struct RebaseArgs {
#[arg(long, short)]
branch: Vec<RevisionArg>,
#[arg(long, short)]
source: Vec<RevisionArg>,
#[arg(long, short)]
revision: Option<RevisionArg>,
#[arg(long, short, required = true)]
destination: Vec<RevisionArg>,
#[arg(long, short = 'L', hide = true)]
allow_large_revsets: bool,
}
#[derive(clap::Args, Clone, Debug)]
struct BackoutArgs {
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
#[arg(long, short, default_value = "@")]
destination: Vec<RevisionArg>,
}
#[derive(Subcommand, Clone, Debug)]
enum WorkspaceCommands {
Add(WorkspaceAddArgs),
Forget(WorkspaceForgetArgs),
List(WorkspaceListArgs),
Root(WorkspaceRootArgs),
UpdateStale(WorkspaceUpdateStaleArgs),
}
#[derive(clap::Args, Clone, Debug)]
struct WorkspaceAddArgs {
destination: String,
#[arg(long)]
name: Option<String>,
}
#[derive(clap::Args, Clone, Debug)]
struct WorkspaceForgetArgs {
workspace: Option<String>,
}
#[derive(clap::Args, Clone, Debug)]
struct WorkspaceListArgs {}
#[derive(clap::Args, Clone, Debug)]
struct WorkspaceRootArgs {}
#[derive(clap::Args, Clone, Debug)]
struct WorkspaceUpdateStaleArgs {}
#[derive(Subcommand, Clone, Debug)]
enum SparseArgs {
List(SparseListArgs),
Set(SparseSetArgs),
}
#[derive(clap::Args, Clone, Debug)]
struct SparseListArgs {}
#[derive(clap::Args, Clone, Debug)]
struct SparseSetArgs {
#[arg(long, value_hint = clap::ValueHint::AnyPath)]
add: Vec<String>,
#[arg(long, conflicts_with = "clear", value_hint = clap::ValueHint::AnyPath)]
remove: Vec<String>,
#[arg(long)]
clear: bool,
#[arg(long)]
edit: bool,
#[arg(long, conflicts_with_all = &["add", "remove", "clear"])]
reset: bool,
}
#[derive(Subcommand, Clone, Debug)]
enum UtilCommands {
Completion(UtilCompletionArgs),
Mangen(UtilMangenArgs),
ConfigSchema(UtilConfigSchemaArgs),
}
#[derive(clap::Args, Clone, Debug)]
struct UtilCompletionArgs {
#[arg(long, verbatim_doc_comment)]
bash: bool,
#[arg(long, verbatim_doc_comment)]
fish: bool,
#[arg(long, verbatim_doc_comment)]
zsh: bool,
}
#[derive(clap::Args, Clone, Debug)]
struct UtilMangenArgs {}
#[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}")))?; 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");
if !git_store_path.exists() {
git_store_path.pop();
}
}
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_some_refs(
tx.mut_repo(),
&git_repo,
&command.settings().git_settings(),
|ref_name| !jj_lib::git::is_reserved_git_remote_ref(ref_name),
)?;
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
{
if *is_overridden {
continue;
}
if !args.include_defaults && *source == ConfigSource::Default {
continue;
}
writeln!(ui, "{}={}", path.join("."), serialize_config_value(value))?;
wrote_values = true;
}
if !wrote_values {
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}");
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.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()?;
let mut tree_builder = MergedTreeBuilder::new(wc_commit.tree_id().clone());
let wc_tree = wc_commit.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(&store)?;
let new_tree = store.get_root_tree(&new_tree_id)?;
locked_working_copy.reset(&new_tree)?;
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() {
drop(locked_working_copy);
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 {
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.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.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.tree()?;
let to = workspace_command.resolve_single_rev(args.to.as_deref().unwrap_or("@"), ui)?;
to_tree = to.tree()?;
} else {
let commit =
workspace_command.resolve_single_rev(args.revision.as_deref().unwrap_or("@"), ui)?;
let parents = commit.parents();
from_tree = merge_commit_trees(workspace_command.repo().as_ref(), &parents)?;
to_tree = commit.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 = merge_commit_trees(repo.as_ref(), &wc_commit.parents())?;
let tree = wc_commit.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.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![];
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,
)?;
}
}
}
}
if let ([], [only_path]) = (args.revisions.as_slice(), args.paths.as_slice()) {
if only_path == "." && workspace_command.parse_file_path(only_path)?.is_root() {
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 = rebase_to_dest_parent(workspace_command, predecessor, commit)?;
let tree = commit.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 = rebase_to_dest_parent(&workspace_command, &from, &to)?;
let to_tree = to.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<MergedTree, 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 source_tree = source.tree()?;
let rebased_tree = destination_parent_tree.merge(&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()
))
})?;
std::fs::remove_file(description_file_path).ok();
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)?;
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.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()
{
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.base_workspace_helper()
.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.base_workspace_helper()
.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(())
}
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 {
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()?;
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.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.id())
.set_description(cli_util::join_message_paragraphs(&args.message_paragraphs))
.write()?;
if args.insert_after {
let old_parents = RevsetExpression::commits(target_ids);
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 cmd_next(ui: &mut Ui, command: &CommandHelper, args: &NextArgs) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let edit = args.edit;
let amount = args.amount;
let current_wc_id = workspace_command
.get_wc_commit_id()
.ok_or_else(|| user_error("This command requires a working copy"))?;
let current_wc = workspace_command.repo().store().get_commit(current_wc_id)?;
let current_short = short_commit_hash(current_wc.id());
let start_id = if edit {
current_wc_id
} else {
match current_wc.parent_ids() {
[parent_id] => parent_id,
_ => return Err(user_error("Cannot run `jj next` on a merge commit")),
}
};
let descendant_expression = RevsetExpression::commit(start_id.clone()).descendants_at(amount);
let target_expression = if edit {
descendant_expression
} else {
descendant_expression.minus(&RevsetExpression::commit(current_wc_id.clone()).descendants())
};
let targets: Vec<Commit> = target_expression
.resolve(workspace_command.repo().as_ref())?
.evaluate(workspace_command.repo().as_ref())?
.iter()
.commits(workspace_command.repo().store())
.take(2)
.try_collect()?;
let target = match targets.as_slice() {
[target] => target,
[] => {
return Err(user_error(format!(
"No descendant found {amount} commit{} forward",
if amount > 1 { "s" } else { "" }
)));
}
_ => {
return Err(user_error("Ambiguous target commit"));
}
};
let target_short = short_commit_hash(target.id());
if edit {
workspace_command.check_rewritable(target)?;
let mut tx = workspace_command
.start_transaction(&format!("next: {current_short} -> editing {target_short}"));
tx.edit(target)?;
tx.finish(ui)?;
return Ok(());
}
let mut tx =
workspace_command.start_transaction(&format!("next: {current_short} -> {target_short}"));
tx.check_out(target)?;
tx.finish(ui)?;
Ok(())
}
fn cmd_prev(ui: &mut Ui, command: &CommandHelper, args: &PrevArgs) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let edit = args.edit;
let amount = args.amount;
let current_wc_id = workspace_command
.get_wc_commit_id()
.ok_or_else(|| user_error("This command requires a working copy"))?;
let current_wc = workspace_command.repo().store().get_commit(current_wc_id)?;
let current_short = short_commit_hash(current_wc.id());
let start_id = if edit {
current_wc_id
} else {
match current_wc.parent_ids() {
[parent_id] => parent_id,
_ => return Err(user_error("Cannot run `jj prev` on a merge commit")),
}
};
let ancestor_expression = RevsetExpression::commit(start_id.clone()).ancestors_at(amount);
let target_revset = if edit {
ancestor_expression
} else {
ancestor_expression.minus(&RevsetExpression::commit(current_wc_id.clone()))
};
let targets: Vec<_> = target_revset
.resolve(workspace_command.repo().as_ref())?
.evaluate(workspace_command.repo().as_ref())?
.iter()
.commits(workspace_command.repo().store())
.take(2)
.try_collect()?;
let target = match targets.as_slice() {
[target] => target,
[] => {
return Err(user_error(format!(
"No ancestor found {amount} commit{} back",
if amount > 1 { "s" } else { "" }
)))
}
_ => return Err(user_error("Ambiguous target commit")),
};
let target_short = short_commit_hash(target.id());
if edit {
workspace_command.check_rewritable(target)?;
let mut tx = workspace_command
.start_transaction(&format!("prev: {current_short} -> editing {target_short}"));
tx.edit(target)?;
tx.finish(ui)?;
return Ok(());
}
let mut tx =
workspace_command.start_transaction(&format!("prev: {current_short} -> {target_short}"));
tx.check_out(target)?;
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 = merge_commit_trees(tx.repo(), &source.parents())?;
let source_tree = source.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)?;
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()) {
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)?;
}
let destination_tree = destination.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.tree()?;
let tree = commit.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.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."
)?;
}
}
}
let abandon_child = &new_parent_tree_id == commit.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 {
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 = 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.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();
}
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)?;
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()?;
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.tree()?;
let store = tree.store();
let mut tree_builder = MergedTreeBuilder::new(commit.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(store)?;
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.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.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(); if deletions > 0 {
seen_objects.insert(
format!(
"{deletions} deletion{}",
if deletions > 1 { "s" } else { "" }
),
"normal", );
}
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)?
.tree()?;
} else {
to_commit =
workspace_command.resolve_single_rev(args.changes_in.as_deref().unwrap_or("@"), ui)?;
from_tree = 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(to_commit.tree_id().clone());
let to_tree = to_commit.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(workspace_command.repo().store())?
};
if &new_tree_id == to_commit.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 = merge_commit_trees(tx.repo(), base_commits.as_slice())?;
let tree = target_commit.tree()?;
let tree_id = tx.edit_diff(ui, &base_tree, &tree, &instructions)?;
if tree_id == *target_commit.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.",
);
"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.tree()?;
let base_tree = 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.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() {
"".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_id(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)
}
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);
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)?;
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();
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) => {
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
));
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> {
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(_) => {
ui.write("Nothing to do (the working copy is not stale).\n")?;
}
Err(_) => {
if known_wc_commit.tree_id() != locked_wc.old_tree_id() {
return Err(user_error("Concurrent working copy operation. Try again."));
}
let desired_tree = desired_wc_commit.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()?;
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::Next(sub_args) => cmd_next(ui, command_helper, sub_args),
Commands::Prev(sub_args) => cmd_prev(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();
}
}