use std::borrow::Cow;
use std::cell::OnceCell;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::env;
use std::ffi::OsString;
use std::fmt;
use std::fmt::Debug;
use std::io;
use std::io::Write as _;
use std::mem;
use std::path::Path;
use std::path::PathBuf;
use std::pin::Pin;
use std::rc::Rc;
use std::str::FromStr;
use std::sync::Arc;
use std::sync::LazyLock;
use std::time::SystemTime;
use bstr::ByteVec as _;
use chrono::TimeZone as _;
use clap::ArgAction;
use clap::ArgMatches;
use clap::Command;
use clap::FromArgMatches as _;
use clap::builder::MapValueParser;
use clap::builder::NonEmptyStringValueParser;
use clap::builder::TypedValueParser as _;
use clap::builder::ValueParserFactory;
use clap::error::ContextKind;
use clap::error::ContextValue;
use clap_complete::ArgValueCandidates;
use clap_complete::ArgValueCompleter;
use futures::TryStreamExt as _;
use futures::future::try_join_all;
use indexmap::IndexMap;
use indexmap::IndexSet;
use indoc::indoc;
use indoc::writedoc;
use itertools::Itertools as _;
use jj_lib::backend::BackendResult;
use jj_lib::backend::ChangeId;
use jj_lib::backend::CommitId;
use jj_lib::backend::TreeValue;
use jj_lib::commit::Commit;
use jj_lib::config::ConfigGetError;
use jj_lib::config::ConfigGetResultExt as _;
use jj_lib::config::ConfigLayer;
use jj_lib::config::ConfigMigrationRule;
use jj_lib::config::ConfigNamePathBuf;
use jj_lib::config::ConfigSource;
use jj_lib::config::ConfigValue;
use jj_lib::config::StackedConfig;
use jj_lib::conflicts::ConflictMarkerStyle;
use jj_lib::fileset;
use jj_lib::fileset::FilesetAliasesMap;
use jj_lib::fileset::FilesetDiagnostics;
use jj_lib::fileset::FilesetExpression;
use jj_lib::fileset::FilesetParseContext;
use jj_lib::gitignore::GitIgnoreError;
use jj_lib::gitignore::GitIgnoreFile;
use jj_lib::id_prefix::IdPrefixContext;
use jj_lib::lock::FileLock;
use jj_lib::matchers::Matcher;
use jj_lib::matchers::NothingMatcher;
use jj_lib::merge::Diff;
use jj_lib::merge::MergedTreeValue;
use jj_lib::merged_tree::MergedTree;
use jj_lib::object_id::ObjectId as _;
use jj_lib::op_heads_store;
use jj_lib::op_store::OpStoreError;
use jj_lib::op_store::OperationId;
use jj_lib::op_store::RefTarget;
use jj_lib::op_walk;
use jj_lib::op_walk::OpsetEvaluationError;
use jj_lib::operation::Operation;
use jj_lib::ref_name::RefName;
use jj_lib::ref_name::RefNameBuf;
use jj_lib::ref_name::RemoteName;
use jj_lib::ref_name::RemoteRefSymbol;
use jj_lib::ref_name::WorkspaceName;
use jj_lib::ref_name::WorkspaceNameBuf;
use jj_lib::repo::CheckOutCommitError;
use jj_lib::repo::EditCommitError;
use jj_lib::repo::MutableRepo;
use jj_lib::repo::ReadonlyRepo;
use jj_lib::repo::Repo;
use jj_lib::repo::RepoLoader;
use jj_lib::repo::StoreFactories;
use jj_lib::repo::StoreLoadError;
use jj_lib::repo::merge_factories_map;
use jj_lib::repo_path::RepoPath;
use jj_lib::repo_path::RepoPathBuf;
use jj_lib::repo_path::RepoPathUiConverter;
use jj_lib::repo_path::UiPathParseError;
use jj_lib::revset;
use jj_lib::revset::ResolvedRevsetExpression;
use jj_lib::revset::RevsetAliasesMap;
use jj_lib::revset::RevsetDiagnostics;
use jj_lib::revset::RevsetExpression;
use jj_lib::revset::RevsetExtensions;
use jj_lib::revset::RevsetFilterPredicate;
use jj_lib::revset::RevsetFunction;
use jj_lib::revset::RevsetParseContext;
use jj_lib::revset::RevsetStreamExt as _;
use jj_lib::revset::RevsetWorkspaceContext;
use jj_lib::revset::SymbolResolverExtension;
use jj_lib::revset::UserRevsetExpression;
use jj_lib::rewrite::restore_tree;
use jj_lib::settings::HumanByteSize;
use jj_lib::settings::UserSettings;
use jj_lib::store::Store;
use jj_lib::str_util::StringExpression;
use jj_lib::str_util::StringMatcher;
use jj_lib::str_util::StringPattern;
use jj_lib::transaction::Transaction;
use jj_lib::transaction::TransactionCommitError;
use jj_lib::working_copy;
use jj_lib::working_copy::CheckoutStats;
use jj_lib::working_copy::LockedWorkingCopy;
use jj_lib::working_copy::SnapshotOptions;
use jj_lib::working_copy::SnapshotStats;
use jj_lib::working_copy::UntrackedReason;
use jj_lib::working_copy::WorkingCopy;
use jj_lib::working_copy::WorkingCopyFactory;
use jj_lib::working_copy::WorkingCopyFreshness;
use jj_lib::workspace::DefaultWorkspaceLoaderFactory;
use jj_lib::workspace::LockedWorkspace;
use jj_lib::workspace::WorkingCopyFactories;
use jj_lib::workspace::Workspace;
use jj_lib::workspace::WorkspaceLoadError;
use jj_lib::workspace::WorkspaceLoader;
use jj_lib::workspace::WorkspaceLoaderFactory;
use jj_lib::workspace::default_working_copy_factories;
use jj_lib::workspace::get_working_copy_factory;
use pollster::FutureExt as _;
use tracing::instrument;
use tracing_chrome::ChromeLayerBuilder;
use tracing_subscriber::prelude::*;
use crate::command_error::CommandError;
use crate::command_error::cli_error;
use crate::command_error::config_error_with_message;
use crate::command_error::handle_command_result;
use crate::command_error::internal_error;
use crate::command_error::internal_error_with_message;
use crate::command_error::print_error_sources;
use crate::command_error::print_parse_diagnostics;
use crate::command_error::user_error;
use crate::command_error::user_error_with_message;
use crate::commit_templater::CommitTemplateLanguage;
use crate::commit_templater::CommitTemplateLanguageExtension;
use crate::complete;
use crate::config::ConfigArgKind;
use crate::config::ConfigEnv;
use crate::config::RawConfig;
use crate::config::config_from_environment;
use crate::config::load_aliases_map;
use crate::config::parse_config_args;
use crate::description_util::TextEditor;
use crate::diff_util;
use crate::diff_util::DiffFormat;
use crate::diff_util::DiffFormatArgs;
use crate::diff_util::DiffRenderer;
use crate::formatter::FormatRecorder;
use crate::formatter::Formatter;
use crate::formatter::FormatterExt as _;
use crate::merge_tools::DiffEditor;
use crate::merge_tools::MergeEditor;
use crate::merge_tools::MergeToolConfigError;
use crate::operation_templater::OperationTemplateLanguage;
use crate::operation_templater::OperationTemplateLanguageExtension;
use crate::revset_util;
use crate::revset_util::RevsetExpressionEvaluator;
use crate::revset_util::parse_union_name_patterns;
use crate::template_builder;
use crate::template_builder::TemplateLanguage;
use crate::template_parser::TemplateAliasesMap;
use crate::template_parser::TemplateDiagnostics;
use crate::templater::TemplateRenderer;
use crate::templater::WrapTemplateProperty;
use crate::text_util;
use crate::ui::ColorChoice;
use crate::ui::Ui;
const SHORT_CHANGE_ID_TEMPLATE_TEXT: &str = "format_short_change_id_with_change_offset(self)";
#[derive(Clone)]
struct ChromeTracingFlushGuard {
_inner: Option<Rc<tracing_chrome::FlushGuard>>,
}
impl Debug for ChromeTracingFlushGuard {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let Self { _inner } = self;
f.debug_struct("ChromeTracingFlushGuard")
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
pub struct TracingSubscription {
reload_log_filter: tracing_subscriber::reload::Handle<
tracing_subscriber::EnvFilter,
tracing_subscriber::Registry,
>,
_chrome_tracing_flush_guard: ChromeTracingFlushGuard,
}
impl TracingSubscription {
const ENV_VAR_NAME: &str = "JJ_LOG";
pub fn init() -> Self {
let filter = tracing_subscriber::EnvFilter::builder()
.with_default_directive(tracing::metadata::LevelFilter::ERROR.into())
.with_env_var(Self::ENV_VAR_NAME)
.from_env_lossy();
let (filter, reload_log_filter) = tracing_subscriber::reload::Layer::new(filter);
let (chrome_tracing_layer, chrome_tracing_flush_guard) = match std::env::var("JJ_TRACE") {
Ok(filename) => {
let filename = if filename.is_empty() {
format!(
"jj-trace-{}.json",
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs(),
)
} else {
filename
};
let include_args = std::env::var("JJ_TRACE_INCLUDE_ARGS").is_ok();
let (layer, guard) = ChromeLayerBuilder::new()
.file(filename)
.include_args(include_args)
.build();
(
Some(layer),
ChromeTracingFlushGuard {
_inner: Some(Rc::new(guard)),
},
)
}
Err(_) => (None, ChromeTracingFlushGuard { _inner: None }),
};
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::Layer::default()
.with_writer(std::io::stderr)
.with_filter(filter),
)
.with(chrome_tracing_layer)
.init();
Self {
reload_log_filter,
_chrome_tracing_flush_guard: chrome_tracing_flush_guard,
}
}
pub fn enable_debug_logging(&self) -> Result<(), CommandError> {
self.reload_log_filter
.modify(|filter| {
*filter = tracing_subscriber::EnvFilter::builder()
.with_default_directive(tracing::metadata::LevelFilter::INFO.into())
.with_env_var(Self::ENV_VAR_NAME)
.from_env_lossy()
.add_directive("jj_lib=debug".parse().unwrap())
.add_directive("jj_cli=debug".parse().unwrap());
})
.map_err(|err| internal_error_with_message("failed to enable debug logging", err))?;
tracing::info!("debug logging enabled");
Ok(())
}
}
#[derive(Clone)]
pub struct CommandHelper {
data: Rc<CommandHelperData>,
}
struct CommandHelperData {
app: Command,
cwd: PathBuf,
string_args: Vec<String>,
matches: ArgMatches,
global_args: GlobalArgs,
config_env: ConfigEnv,
config_migrations: Vec<ConfigMigrationRule>,
raw_config: RawConfig,
settings: UserSettings,
revset_extensions: Arc<RevsetExtensions>,
commit_template_extensions: Vec<Arc<dyn CommitTemplateLanguageExtension>>,
operation_template_extensions: Vec<Arc<dyn OperationTemplateLanguageExtension>>,
maybe_workspace_loader: Result<Box<dyn WorkspaceLoader>, CommandError>,
store_factories: StoreFactories,
working_copy_factories: WorkingCopyFactories,
workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
}
impl CommandHelper {
pub fn app(&self) -> &Command {
&self.data.app
}
pub fn cwd(&self) -> &Path {
&self.data.cwd
}
pub fn string_args(&self) -> &Vec<String> {
&self.data.string_args
}
pub fn matches(&self) -> &ArgMatches {
&self.data.matches
}
pub fn global_args(&self) -> &GlobalArgs {
&self.data.global_args
}
pub fn config_env(&self) -> &ConfigEnv {
&self.data.config_env
}
pub fn raw_config(&self) -> &RawConfig {
&self.data.raw_config
}
pub fn settings(&self) -> &UserSettings {
&self.data.settings
}
pub fn settings_for_new_workspace(
&self,
ui: &Ui,
workspace_root: &Path,
) -> Result<(UserSettings, ConfigEnv), CommandError> {
let mut config_env = self.data.config_env.clone();
let mut raw_config = self.data.raw_config.clone();
let repo_path = workspace_root.join(".jj").join("repo");
config_env.reset_repo_path(&repo_path);
config_env.reload_repo_config(ui, &mut raw_config)?;
config_env.reset_workspace_path(workspace_root);
config_env.reload_workspace_config(ui, &mut raw_config)?;
let mut config = config_env.resolve_config(&raw_config)?;
jj_lib::config::migrate(&mut config, &self.data.config_migrations)?;
Ok((self.data.settings.with_new_config(config)?, config_env))
}
pub fn text_editor(&self) -> Result<TextEditor, ConfigGetError> {
TextEditor::from_settings(self.settings())
}
pub fn revset_extensions(&self) -> &Arc<RevsetExtensions> {
&self.data.revset_extensions
}
pub fn parse_template<'a, C, L>(
&self,
ui: &Ui,
language: &L,
template_text: &str,
) -> Result<TemplateRenderer<'a, C>, CommandError>
where
C: Clone + 'a,
L: TemplateLanguage<'a> + ?Sized,
L::Property: WrapTemplateProperty<'a, C>,
{
let mut diagnostics = TemplateDiagnostics::new();
let aliases = load_template_aliases(ui, self.settings().config())?;
let template =
template_builder::parse(language, &mut diagnostics, template_text, &aliases)?;
print_parse_diagnostics(ui, "In template expression", &diagnostics)?;
Ok(template)
}
pub fn should_commit_transaction(&self) -> bool {
!self.global_args().no_integrate_operation
}
async fn maybe_commit_transaction(
&self,
tx: Transaction,
description: impl Into<String>,
) -> Result<Arc<ReadonlyRepo>, TransactionCommitError> {
let unpublished_op = tx.write(description).await?;
if self.should_commit_transaction() {
unpublished_op.publish().await
} else {
Ok(unpublished_op.leave_unpublished())
}
}
pub fn workspace_loader(&self) -> Result<&dyn WorkspaceLoader, CommandError> {
self.data
.maybe_workspace_loader
.as_deref()
.map_err(Clone::clone)
}
fn new_workspace_loader_at(
&self,
workspace_root: &Path,
) -> Result<Box<dyn WorkspaceLoader>, CommandError> {
self.data
.workspace_loader_factory
.create(workspace_root)
.map_err(|err| map_workspace_load_error(err, None))
}
#[instrument(skip(self, ui))]
pub fn workspace_helper(&self, ui: &Ui) -> Result<WorkspaceCommandHelper, CommandError> {
let (workspace_command, stats) = self.workspace_helper_with_stats(ui)?;
print_snapshot_stats(ui, &stats, workspace_command.env().path_converter())?;
Ok(workspace_command)
}
#[instrument(skip(self, ui))]
pub fn workspace_helper_with_stats(
&self,
ui: &Ui,
) -> Result<(WorkspaceCommandHelper, SnapshotStats), CommandError> {
let mut workspace_command = self.workspace_helper_no_snapshot(ui)?;
let (workspace_command, stats) = match workspace_command.maybe_snapshot_impl(ui).block_on()
{
Ok(stats) => (workspace_command, stats),
Err(SnapshotWorkingCopyError::Command(err)) => return Err(err),
Err(SnapshotWorkingCopyError::StaleWorkingCopy(err)) => {
let auto_update_stale = self.settings().get_bool("snapshot.auto-update-stale")?;
if !auto_update_stale {
return Err(err);
}
self.recover_stale_working_copy(ui).block_on()?
}
};
Ok((workspace_command, stats))
}
#[instrument(skip(self, ui))]
pub fn workspace_helper_no_snapshot(
&self,
ui: &Ui,
) -> Result<WorkspaceCommandHelper, CommandError> {
let workspace = self.load_workspace()?;
let op_head =
self.resolve_operation(ui, workspace.repo_loader(), workspace.workspace_name())?;
let repo = workspace.repo_loader().load_at(&op_head).block_on()?;
let mut env = self.workspace_environment(ui, &workspace)?;
if let Err(err) =
revset_util::try_resolve_trunk_alias(repo.as_ref(), &env.revset_parse_context())
{
let fallback = "root()";
writeln!(
ui.warning_default(),
"Failed to resolve `revset-aliases.trunk()`: {err}"
)?;
writeln!(
ui.warning_no_heading(),
"The `trunk()` alias is temporarily set to `{fallback}`."
)?;
writeln!(
ui.hint_default(),
"Use `jj config edit --repo` to adjust the `trunk()` alias."
)?;
env.revset_aliases_map
.insert("trunk()", fallback)
.expect("valid syntax");
env.reload_revset_expressions(ui)?;
}
WorkspaceCommandHelper::new(ui, workspace, repo, env, self.is_at_head_operation())
}
pub fn get_working_copy_factory(&self) -> Result<&dyn WorkingCopyFactory, CommandError> {
let loader = self.workspace_loader()?;
let factory: Result<_, WorkspaceLoadError> =
get_working_copy_factory(loader, &self.data.working_copy_factories)
.map_err(|e| e.into());
let factory = factory.map_err(|err| {
map_workspace_load_error(err, self.data.global_args.repository.as_deref())
})?;
Ok(factory)
}
#[instrument(skip_all)]
pub fn load_workspace(&self) -> Result<Workspace, CommandError> {
let loader = self.workspace_loader()?;
loader
.load(
&self.data.settings,
&self.data.store_factories,
&self.data.working_copy_factories,
)
.map_err(|err| {
map_workspace_load_error(err, self.data.global_args.repository.as_deref())
})
}
#[instrument(skip(self, settings))]
pub fn load_workspace_at(
&self,
workspace_root: &Path,
settings: &UserSettings,
) -> Result<Workspace, CommandError> {
let loader = self.new_workspace_loader_at(workspace_root)?;
loader
.load(
settings,
&self.data.store_factories,
&self.data.working_copy_factories,
)
.map_err(|err| map_workspace_load_error(err, None))
}
pub async fn recover_stale_working_copy(
&self,
ui: &Ui,
) -> Result<(WorkspaceCommandHelper, SnapshotStats), CommandError> {
let workspace = self.load_workspace()?;
let op_id = workspace.working_copy().operation_id();
match workspace.repo_loader().load_operation(op_id).await {
Ok(op) => {
let repo = workspace.repo_loader().load_at(&op).await?;
let mut workspace_command = self.for_workable_repo(ui, workspace, repo)?;
workspace_command.check_working_copy_writable()?;
let stale_stats = workspace_command
.snapshot_working_copy(ui)
.await
.map_err(|err| err.into_command_error())?;
let wc_commit_id = workspace_command.get_wc_commit_id().unwrap();
let repo = workspace_command.repo().clone();
let stale_wc_commit = repo.store().get_commit_async(wc_commit_id).await?;
let mut workspace_command = self.workspace_helper_no_snapshot(ui)?;
let repo = workspace_command.repo().clone();
let (mut locked_ws, desired_wc_commit) = workspace_command
.unchecked_start_working_copy_mutation()
.await?;
match WorkingCopyFreshness::check_stale(
locked_ws.locked_wc(),
&desired_wc_commit,
&repo,
)
.await?
{
WorkingCopyFreshness::Fresh | WorkingCopyFreshness::Updated(_) => {
drop(locked_ws);
writeln!(
ui.status(),
"Attempted recovery, but the working copy is not stale"
)?;
}
WorkingCopyFreshness::WorkingCopyStale
| WorkingCopyFreshness::SiblingOperation => {
let stats = update_stale_working_copy(
locked_ws,
repo.op_id().clone(),
&stale_wc_commit,
&desired_wc_commit,
)
.await?;
workspace_command.print_updated_working_copy_stats(
ui,
Some(&stale_wc_commit),
&desired_wc_commit,
&stats,
)?;
writeln!(
ui.status(),
"Updated working copy to fresh commit {}",
short_commit_hash(desired_wc_commit.id())
)?;
}
}
let fresh_stats = workspace_command
.maybe_snapshot_impl(ui)
.await
.map_err(|err| err.into_command_error())?;
let merged_stats = {
let SnapshotStats {
mut untracked_paths,
} = stale_stats;
untracked_paths.extend(fresh_stats.untracked_paths);
SnapshotStats { untracked_paths }
};
Ok((workspace_command, merged_stats))
}
Err(e @ OpStoreError::ObjectNotFound { .. }) => {
writeln!(
ui.status(),
"Failed to read working copy's current operation; attempting recovery. Error \
message from read attempt: {e}"
)?;
let mut workspace_command = self.workspace_helper_no_snapshot(ui)?;
let stats = workspace_command
.create_and_check_out_recovery_commit(ui)
.await?;
Ok((workspace_command, stats))
}
Err(e) => Err(e.into()),
}
}
pub fn workspace_environment(
&self,
ui: &Ui,
workspace: &Workspace,
) -> Result<WorkspaceCommandEnvironment, CommandError> {
WorkspaceCommandEnvironment::new(ui, self, workspace)
}
pub fn is_working_copy_writable(&self) -> bool {
self.is_at_head_operation() && !self.data.global_args.ignore_working_copy
}
pub fn is_at_head_operation(&self) -> bool {
matches!(
self.data.global_args.at_operation.as_deref(),
None | Some("@")
)
}
#[instrument(skip_all)]
pub fn resolve_operation(
&self,
ui: &Ui,
repo_loader: &RepoLoader,
workspace_name: &WorkspaceName,
) -> Result<Operation, CommandError> {
if let Some(op_str) = &self.data.global_args.at_operation {
Ok(op_walk::resolve_op_for_load(repo_loader, op_str).block_on()?)
} else {
op_heads_store::resolve_op_heads(
repo_loader.op_heads_store().as_ref(),
repo_loader.op_store(),
async |op_heads| {
writeln!(
ui.status(),
"Concurrent modification detected, resolving automatically.",
)?;
let base_repo = repo_loader.load_at(&op_heads[0]).block_on()?;
let mut tx =
start_repo_transaction(&base_repo, workspace_name, &self.data.string_args);
for other_op_head in op_heads.into_iter().skip(1) {
tx.merge_operation(other_op_head).await?;
let num_rebased = tx.repo_mut().rebase_descendants().await?;
if num_rebased > 0 {
writeln!(
ui.status(),
"Rebased {num_rebased} descendant commits onto commits rewritten \
by other operation"
)?;
}
}
Ok(tx
.write("reconcile divergent operations")
.await?
.leave_unpublished()
.operation()
.clone())
},
)
.block_on()
}
}
#[instrument(skip_all)]
pub fn for_workable_repo(
&self,
ui: &Ui,
workspace: Workspace,
repo: Arc<ReadonlyRepo>,
) -> Result<WorkspaceCommandHelper, CommandError> {
let env = self.workspace_environment(ui, &workspace)?;
let loaded_at_head = true;
WorkspaceCommandHelper::new(ui, workspace, repo, env, loaded_at_head)
}
}
struct ReadonlyUserRepo {
repo: Arc<ReadonlyRepo>,
id_prefix_context: OnceCell<IdPrefixContext>,
}
impl ReadonlyUserRepo {
fn new(repo: Arc<ReadonlyRepo>) -> Self {
Self {
repo,
id_prefix_context: OnceCell::new(),
}
}
}
pub struct AdvanceableBookmark {
name: RefNameBuf,
old_commit_id: CommitId,
}
fn load_advance_bookmarks_matcher(
ui: &Ui,
settings: &UserSettings,
) -> Result<Option<StringMatcher>, CommandError> {
let get_setting = |setting_key: &str| -> Result<Vec<String>, _> {
let name = ConfigNamePathBuf::from_iter(["experimental-advance-branches", setting_key]);
settings.get(&name)
};
let enabled_names = get_setting("enabled-branches")?;
let disabled_names = get_setting("disabled-branches")?;
let enabled_expr = parse_union_name_patterns(ui, &enabled_names)?;
let disabled_expr = parse_union_name_patterns(ui, &disabled_names)?;
if enabled_names.is_empty() {
Ok(None)
} else {
let expr = enabled_expr.intersection(disabled_expr.negated());
Ok(Some(expr.to_matcher()))
}
}
pub struct WorkspaceCommandEnvironment {
command: CommandHelper,
settings: UserSettings,
fileset_aliases_map: FilesetAliasesMap,
revset_aliases_map: RevsetAliasesMap,
template_aliases_map: TemplateAliasesMap,
default_ignored_remote: Option<&'static RemoteName>,
revsets_use_glob_by_default: bool,
path_converter: RepoPathUiConverter,
workspace_name: WorkspaceNameBuf,
immutable_heads_expression: Arc<UserRevsetExpression>,
short_prefixes_expression: Option<Arc<UserRevsetExpression>>,
conflict_marker_style: ConflictMarkerStyle,
}
impl WorkspaceCommandEnvironment {
#[instrument(skip_all)]
fn new(ui: &Ui, command: &CommandHelper, workspace: &Workspace) -> Result<Self, CommandError> {
let settings = workspace.settings();
let fileset_aliases_map = load_fileset_aliases(ui, settings.config())?;
let revset_aliases_map = load_revset_aliases(ui, settings.config())?;
let template_aliases_map = load_template_aliases(ui, settings.config())?;
let default_ignored_remote = default_ignored_remote_name(workspace.repo_loader().store());
let path_converter = RepoPathUiConverter::Fs {
cwd: command.cwd().to_owned(),
base: workspace.workspace_root().to_owned(),
};
let mut env = Self {
command: command.clone(),
settings: settings.clone(),
fileset_aliases_map,
revset_aliases_map,
template_aliases_map,
default_ignored_remote,
revsets_use_glob_by_default: settings.get("ui.revsets-use-glob-by-default")?,
path_converter,
workspace_name: workspace.workspace_name().to_owned(),
immutable_heads_expression: RevsetExpression::root(),
short_prefixes_expression: None,
conflict_marker_style: settings.get("ui.conflict-marker-style")?,
};
env.reload_revset_expressions(ui)?;
Ok(env)
}
pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
&self.path_converter
}
pub fn workspace_name(&self) -> &WorkspaceName {
&self.workspace_name
}
pub(crate) fn fileset_parse_context(&self) -> FilesetParseContext<'_> {
FilesetParseContext {
aliases_map: &self.fileset_aliases_map,
path_converter: &self.path_converter,
}
}
pub(crate) fn fileset_parse_context_for_config(&self) -> FilesetParseContext<'_> {
static ROOT_PATH_CONVERTER: LazyLock<RepoPathUiConverter> =
LazyLock::new(|| RepoPathUiConverter::Fs {
cwd: PathBuf::new(),
base: PathBuf::new(),
});
FilesetParseContext {
aliases_map: &self.fileset_aliases_map,
path_converter: &ROOT_PATH_CONVERTER,
}
}
pub(crate) fn revset_parse_context(&self) -> RevsetParseContext<'_> {
let workspace_context = RevsetWorkspaceContext {
path_converter: &self.path_converter,
workspace_name: &self.workspace_name,
};
let now = if let Some(timestamp) = self.settings.commit_timestamp() {
chrono::Local
.timestamp_millis_opt(timestamp.timestamp.0)
.unwrap()
} else {
chrono::Local::now()
};
RevsetParseContext {
aliases_map: &self.revset_aliases_map,
local_variables: HashMap::new(),
user_email: self.settings.user_email(),
date_pattern_context: now.into(),
default_ignored_remote: self.default_ignored_remote,
fileset_aliases_map: &self.fileset_aliases_map,
use_glob_by_default: self.revsets_use_glob_by_default,
extensions: self.command.revset_extensions(),
workspace: Some(workspace_context),
}
}
pub fn new_id_prefix_context(&self) -> IdPrefixContext {
let context = IdPrefixContext::new(self.command.revset_extensions().clone());
match &self.short_prefixes_expression {
None => context,
Some(expression) => context.disambiguate_within(expression.clone()),
}
}
fn reload_revset_expressions(&mut self, ui: &Ui) -> Result<(), CommandError> {
self.immutable_heads_expression = self.load_immutable_heads_expression(ui)?;
self.short_prefixes_expression = self.load_short_prefixes_expression(ui)?;
Ok(())
}
pub fn immutable_expression(&self) -> Arc<UserRevsetExpression> {
self.immutable_heads_expression.ancestors()
}
pub fn immutable_heads_expression(&self) -> &Arc<UserRevsetExpression> {
&self.immutable_heads_expression
}
pub fn conflict_marker_style(&self) -> ConflictMarkerStyle {
self.conflict_marker_style
}
fn load_immutable_heads_expression(
&self,
ui: &Ui,
) -> Result<Arc<UserRevsetExpression>, CommandError> {
let mut diagnostics = RevsetDiagnostics::new();
let expression = revset_util::parse_immutable_heads_expression(
&mut diagnostics,
&self.revset_parse_context(),
)
.map_err(|e| config_error_with_message("Invalid `revset-aliases.immutable_heads()`", e))?;
print_parse_diagnostics(ui, "In `revset-aliases.immutable_heads()`", &diagnostics)?;
Ok(expression)
}
fn load_short_prefixes_expression(
&self,
ui: &Ui,
) -> Result<Option<Arc<UserRevsetExpression>>, CommandError> {
let revset_string = self
.settings
.get_string("revsets.short-prefixes")
.optional()?
.map_or_else(|| self.settings.get_string("revsets.log"), Ok)?;
if revset_string.is_empty() {
Ok(None)
} else {
let mut diagnostics = RevsetDiagnostics::new();
let expression = revset::parse(
&mut diagnostics,
&revset_string,
&self.revset_parse_context(),
)
.map_err(|err| config_error_with_message("Invalid `revsets.short-prefixes`", err))?;
print_parse_diagnostics(ui, "In `revsets.short-prefixes`", &diagnostics)?;
Ok(Some(expression))
}
}
async fn find_immutable_commit(
&self,
repo: &dyn Repo,
to_rewrite_expr: &Arc<ResolvedRevsetExpression>,
) -> Result<Option<CommitId>, CommandError> {
let immutable_expression = if self.command.global_args().ignore_immutable {
UserRevsetExpression::root()
} else {
self.immutable_expression()
};
let id_prefix_context = IdPrefixContext::new(self.command.revset_extensions().clone());
let immutable_expr = RevsetExpressionEvaluator::new(
repo,
self.command.revset_extensions().clone(),
&id_prefix_context,
immutable_expression,
)
.resolve()
.map_err(|e| config_error_with_message("Invalid `revset-aliases.immutable_heads()`", e))?;
let mut commit_id_iter = immutable_expr
.intersection(to_rewrite_expr)
.evaluate(repo)?
.stream();
Ok(commit_id_iter.try_next().await?)
}
pub fn template_aliases_map(&self) -> &TemplateAliasesMap {
&self.template_aliases_map
}
pub fn parse_template<'a, C, L>(
&self,
ui: &Ui,
language: &L,
template_text: &str,
) -> Result<TemplateRenderer<'a, C>, CommandError>
where
C: Clone + 'a,
L: TemplateLanguage<'a> + ?Sized,
L::Property: WrapTemplateProperty<'a, C>,
{
let mut diagnostics = TemplateDiagnostics::new();
let template = template_builder::parse(
language,
&mut diagnostics,
template_text,
&self.template_aliases_map,
)?;
print_parse_diagnostics(ui, "In template expression", &diagnostics)?;
Ok(template)
}
pub fn commit_template_language<'a>(
&'a self,
repo: &'a dyn Repo,
id_prefix_context: &'a IdPrefixContext,
) -> CommitTemplateLanguage<'a> {
CommitTemplateLanguage::new(
repo,
&self.path_converter,
&self.workspace_name,
self.revset_parse_context(),
id_prefix_context,
self.immutable_expression(),
self.conflict_marker_style,
&self.command.data.commit_template_extensions,
)
}
pub fn operation_template_extensions(&self) -> &[Arc<dyn OperationTemplateLanguageExtension>] {
&self.command.data.operation_template_extensions
}
}
pub struct GitImportExportLock {
_lock: Option<FileLock>,
}
pub struct WorkspaceCommandHelper {
workspace: Workspace,
user_repo: ReadonlyUserRepo,
env: WorkspaceCommandEnvironment,
commit_summary_template_text: String,
op_summary_template_text: String,
may_snapshot_working_copy: bool,
may_update_working_copy: bool,
working_copy_shared_with_git: bool,
}
enum SnapshotWorkingCopyError {
Command(CommandError),
StaleWorkingCopy(CommandError),
}
impl SnapshotWorkingCopyError {
fn into_command_error(self) -> CommandError {
match self {
Self::Command(err) => err,
Self::StaleWorkingCopy(err) => err,
}
}
}
fn snapshot_command_error<E>(err: E) -> SnapshotWorkingCopyError
where
E: Into<CommandError>,
{
SnapshotWorkingCopyError::Command(err.into())
}
impl WorkspaceCommandHelper {
#[instrument(skip_all)]
fn new(
ui: &Ui,
workspace: Workspace,
repo: Arc<ReadonlyRepo>,
env: WorkspaceCommandEnvironment,
loaded_at_head: bool,
) -> Result<Self, CommandError> {
let settings = workspace.settings();
let commit_summary_template_text = settings.get_string("templates.commit_summary")?;
let op_summary_template_text = settings.get_string("templates.op_summary")?;
let may_snapshot_working_copy =
loaded_at_head && !env.command.global_args().ignore_working_copy;
let may_update_working_copy =
may_snapshot_working_copy && env.command.should_commit_transaction();
let working_copy_shared_with_git =
crate::git_util::is_colocated_git_workspace(&workspace, &repo);
let helper = Self {
workspace,
user_repo: ReadonlyUserRepo::new(repo),
env,
commit_summary_template_text,
op_summary_template_text,
may_snapshot_working_copy,
may_update_working_copy,
working_copy_shared_with_git,
};
helper.parse_operation_template(ui, &helper.op_summary_template_text)?;
helper.parse_commit_template(ui, &helper.commit_summary_template_text)?;
helper.parse_commit_template(ui, SHORT_CHANGE_ID_TEMPLATE_TEXT)?;
Ok(helper)
}
pub fn settings(&self) -> &UserSettings {
self.workspace.settings()
}
pub fn check_working_copy_writable(&self) -> Result<(), CommandError> {
if self.may_update_working_copy {
Ok(())
} else {
let hint = if self.env.command.global_args().ignore_working_copy {
"Don't use --ignore-working-copy."
} else if self.env.command.global_args().no_integrate_operation {
"Don't use --no-integrate-operation."
} else {
"Don't use --at-op."
};
Err(user_error("This command must be able to update the working copy.").hinted(hint))
}
}
fn lock_git_import_export(&self) -> Result<GitImportExportLock, CommandError> {
let lock = if self.working_copy_shared_with_git {
let lock_path = self.workspace.repo_path().join("git_import_export.lock");
Some(FileLock::lock(lock_path.clone()).map_err(|err| {
user_error_with_message("Failed to take lock for Git import/export", err)
})?)
} else {
None
};
Ok(GitImportExportLock { _lock: lock })
}
#[instrument(skip_all)]
async fn maybe_snapshot_impl(
&mut self,
ui: &Ui,
) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
if !self.may_snapshot_working_copy {
return Ok(SnapshotStats::default());
}
#[cfg_attr(not(feature = "git"), allow(unused_variables))]
let git_import_export_lock = self
.lock_git_import_export()
.map_err(snapshot_command_error)?;
if self.working_copy_shared_with_git {
let repo = self.repo().clone();
let op_heads_store = repo.loader().op_heads_store();
let op_heads = op_heads_store
.get_op_heads()
.await
.map_err(snapshot_command_error)?;
if std::slice::from_ref(repo.op_id()) != op_heads {
let op = self
.env
.command
.resolve_operation(ui, repo.loader(), self.workspace_name())
.map_err(snapshot_command_error)?;
let current_repo = repo
.loader()
.load_at(&op)
.await
.map_err(snapshot_command_error)?;
self.user_repo = ReadonlyUserRepo::new(current_repo);
}
}
#[cfg(feature = "git")]
if self.working_copy_shared_with_git {
self.import_git_head(ui, &git_import_export_lock)
.await
.map_err(snapshot_command_error)?;
}
let stats = self.snapshot_working_copy(ui).await?;
#[cfg(feature = "git")]
if self.working_copy_shared_with_git {
self.import_git_refs(ui, &git_import_export_lock)
.await
.map_err(snapshot_command_error)?;
}
Ok(stats)
}
#[instrument(skip_all)]
pub async fn maybe_snapshot(&mut self, ui: &Ui) -> Result<bool, CommandError> {
let op_id_before = self.repo().op_id().clone();
let stats = self
.maybe_snapshot_impl(ui)
.await
.map_err(|err| err.into_command_error())?;
print_snapshot_stats(ui, &stats, self.env().path_converter())?;
let op_id_after = self.repo().op_id();
Ok(op_id_before != *op_id_after)
}
#[cfg(feature = "git")]
#[instrument(skip_all)]
async fn import_git_head(
&mut self,
ui: &Ui,
git_import_export_lock: &GitImportExportLock,
) -> Result<(), CommandError> {
assert!(self.may_snapshot_working_copy);
let mut tx = self.start_transaction();
jj_lib::git::import_head(tx.repo_mut()).await?;
if !tx.repo().has_changes() {
return Ok(());
}
let mut tx = tx.into_inner();
let old_git_head = self.repo().view().git_head().clone();
let new_git_head = tx.repo().view().git_head().clone();
if let Some(new_git_head_id) = new_git_head.as_normal() {
let workspace_name = self.workspace_name().to_owned();
let new_git_head_commit = tx.repo().store().get_commit_async(new_git_head_id).await?;
let wc_commit = tx
.repo_mut()
.check_out(workspace_name, &new_git_head_commit)
.await?;
let mut locked_ws = self.workspace.start_working_copy_mutation().await?;
locked_ws.locked_wc().reset(&wc_commit).await?;
tx.repo_mut().rebase_descendants().await?;
self.user_repo = ReadonlyUserRepo::new(
self.env
.command
.maybe_commit_transaction(tx, "import git head")
.await?,
);
if self.env.command.should_commit_transaction() {
locked_ws
.finish(self.user_repo.repo.op_id().clone())
.await?;
}
if old_git_head.is_present() {
writeln!(
ui.status(),
"Reset the working copy parent to the new Git HEAD."
)?;
} else {
}
} else {
self.finish_transaction(ui, tx, "import git head", git_import_export_lock)
.await?;
}
Ok(())
}
#[cfg(feature = "git")]
#[instrument(skip_all)]
async fn import_git_refs(
&mut self,
ui: &Ui,
git_import_export_lock: &GitImportExportLock,
) -> Result<(), CommandError> {
use jj_lib::git;
let git_settings = git::GitSettings::from_settings(self.settings())?;
let remote_settings = self.settings().remote_settings()?;
let import_options =
crate::git_util::load_git_import_options(ui, &git_settings, &remote_settings)?;
let mut tx = self.start_transaction();
let stats = git::import_refs(tx.repo_mut(), &import_options).await?;
crate::git_util::print_git_import_stats_summary(ui, &stats)?;
if !tx.repo().has_changes() {
return Ok(());
}
let mut tx = tx.into_inner();
let num_rebased = tx.repo_mut().rebase_descendants().await?;
if num_rebased > 0 {
writeln!(
ui.status(),
"Rebased {num_rebased} descendant commits off of commits rewritten from git"
)?;
}
self.finish_transaction(ui, tx, "import git refs", git_import_export_lock)
.await?;
writeln!(
ui.status(),
"Done importing changes from the underlying Git repo."
)?;
Ok(())
}
pub fn repo(&self) -> &Arc<ReadonlyRepo> {
&self.user_repo.repo
}
pub fn repo_path(&self) -> &Path {
self.workspace.repo_path()
}
pub fn workspace(&self) -> &Workspace {
&self.workspace
}
pub fn working_copy(&self) -> &dyn WorkingCopy {
self.workspace.working_copy()
}
pub fn env(&self) -> &WorkspaceCommandEnvironment {
&self.env
}
pub async fn unchecked_start_working_copy_mutation(
&mut self,
) -> Result<(LockedWorkspace<'_>, Commit), CommandError> {
self.check_working_copy_writable()?;
let wc_commit = if let Some(wc_commit_id) = self.get_wc_commit_id() {
self.repo().store().get_commit_async(wc_commit_id).await?
} else {
return Err(user_error("Nothing checked out in this workspace"));
};
let locked_ws = self.workspace.start_working_copy_mutation().await?;
Ok((locked_ws, wc_commit))
}
pub async fn start_working_copy_mutation(
&mut self,
) -> Result<(LockedWorkspace<'_>, Commit), CommandError> {
let (mut locked_ws, wc_commit) = self.unchecked_start_working_copy_mutation().await?;
if wc_commit.tree().tree_ids_and_labels()
!= locked_ws.locked_wc().old_tree().tree_ids_and_labels()
{
return Err(user_error("Concurrent working copy operation. Try again."));
}
Ok((locked_ws, wc_commit))
}
async fn create_and_check_out_recovery_commit(
&mut self,
ui: &Ui,
) -> Result<SnapshotStats, CommandError> {
self.check_working_copy_writable()?;
let workspace_name = self.workspace_name().to_owned();
let mut locked_ws = self.workspace.start_working_copy_mutation().await?;
let (repo, new_commit) = working_copy::create_and_check_out_recovery_commit(
locked_ws.locked_wc(),
&self.user_repo.repo,
workspace_name,
"RECOVERY COMMIT FROM `jj workspace update-stale`
This commit contains changes that were written to the working copy by an
operation that was subsequently lost (or was at least unavailable when you ran
`jj workspace update-stale`). Because the operation was lost, we don't know
what the parent commits are supposed to be. That means that the diff compared
to the current parents may contain changes from multiple commits.
",
)
.await?;
writeln!(
ui.status(),
"Created and checked out recovery commit {}",
short_commit_hash(new_commit.id())
)?;
locked_ws.finish(repo.op_id().clone()).await?;
self.user_repo = ReadonlyUserRepo::new(repo);
self.maybe_snapshot_impl(ui)
.await
.map_err(|err| err.into_command_error())
}
pub fn workspace_root(&self) -> &Path {
self.workspace.workspace_root()
}
pub fn workspace_name(&self) -> &WorkspaceName {
self.workspace.workspace_name()
}
pub fn get_wc_commit_id(&self) -> Option<&CommitId> {
self.repo().view().get_wc_commit_id(self.workspace_name())
}
pub fn working_copy_shared_with_git(&self) -> bool {
self.working_copy_shared_with_git
}
pub fn format_file_path(&self, file: &RepoPath) -> String {
self.path_converter().format_file_path(file)
}
pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
self.path_converter().parse_file_path(input)
}
pub fn parse_file_patterns(
&self,
ui: &Ui,
values: &[String],
) -> Result<FilesetExpression, CommandError> {
if values.is_empty() {
Ok(FilesetExpression::all())
} else {
self.parse_union_filesets(ui, values)
}
}
pub fn parse_union_filesets(
&self,
ui: &Ui,
file_args: &[String], ) -> Result<FilesetExpression, CommandError> {
let mut diagnostics = FilesetDiagnostics::new();
let context = self.env.fileset_parse_context();
let expressions: Vec<_> = file_args
.iter()
.map(|arg| fileset::parse_maybe_bare(&mut diagnostics, arg, &context))
.try_collect()?;
print_parse_diagnostics(ui, "In fileset expression", &diagnostics)?;
Ok(FilesetExpression::union_all(expressions))
}
pub fn auto_tracking_matcher(&self, ui: &Ui) -> Result<Box<dyn Matcher>, CommandError> {
let mut diagnostics = FilesetDiagnostics::new();
let pattern = self.settings().get_string("snapshot.auto-track")?;
let context = self.env.fileset_parse_context_for_config();
let expression = fileset::parse(&mut diagnostics, &pattern, &context)?;
print_parse_diagnostics(ui, "In `snapshot.auto-track`", &diagnostics)?;
Ok(expression.to_matcher())
}
pub fn snapshot_options_with_start_tracking_matcher<'a>(
&self,
start_tracking_matcher: &'a dyn Matcher,
) -> Result<SnapshotOptions<'a>, CommandError> {
let base_ignores = self.base_ignores()?;
let HumanByteSize(mut max_new_file_size) = self
.settings()
.get_value_with("snapshot.max-new-file-size", TryInto::try_into)?;
if max_new_file_size == 0 {
max_new_file_size = u64::MAX;
}
Ok(SnapshotOptions {
base_ignores,
progress: None,
start_tracking_matcher,
force_tracking_matcher: &NothingMatcher,
max_new_file_size,
})
}
pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
self.env.path_converter()
}
#[cfg(not(feature = "git"))]
pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
Ok(GitIgnoreFile::empty())
}
#[cfg(feature = "git")]
#[instrument(skip_all)]
pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
let get_excludes_file_path = |config: &gix::config::File| -> Option<PathBuf> {
if let Some(value) = config.string("core.excludesFile") {
let path = str::from_utf8(&value)
.ok()
.map(jj_lib::file_util::expand_home_path)?;
Some(self.workspace_root().join(path))
} else {
xdg_config_home().map(|x| x.join("git").join("ignore"))
}
};
fn xdg_config_home() -> Option<PathBuf> {
if let Ok(x) = std::env::var("XDG_CONFIG_HOME")
&& !x.is_empty()
{
return Some(PathBuf::from(x));
}
etcetera::home_dir().ok().map(|home| home.join(".config"))
}
let mut git_ignores = GitIgnoreFile::empty();
if let Ok(git_backend) = jj_lib::git::get_git_backend(self.repo().store()) {
let git_repo = git_backend.git_repo();
if let Some(excludes_file_path) = get_excludes_file_path(&git_repo.config_snapshot()) {
git_ignores = git_ignores.chain_with_file(RepoPath::root(), excludes_file_path)?;
}
git_ignores = git_ignores.chain_with_file(
RepoPath::root(),
git_backend.git_repo_path().join("info").join("exclude"),
)?;
} else if let Ok(git_config) = gix::config::File::from_globals()
&& let Some(excludes_file_path) = get_excludes_file_path(&git_config)
{
git_ignores = git_ignores.chain_with_file(RepoPath::root(), excludes_file_path)?;
}
Ok(git_ignores)
}
pub fn diff_renderer(&self, formats: Vec<DiffFormat>) -> DiffRenderer<'_> {
DiffRenderer::new(
self.repo().as_ref(),
self.path_converter(),
self.env.conflict_marker_style(),
formats,
)
}
pub fn diff_renderer_for(
&self,
args: &DiffFormatArgs,
) -> Result<DiffRenderer<'_>, CommandError> {
let formats = diff_util::diff_formats_for(self.settings(), args)?;
Ok(self.diff_renderer(formats))
}
pub fn diff_renderer_for_log(
&self,
args: &DiffFormatArgs,
patch: bool,
) -> Result<Option<DiffRenderer<'_>>, CommandError> {
let formats = diff_util::diff_formats_for_log(self.settings(), args, patch)?;
Ok((!formats.is_empty()).then(|| self.diff_renderer(formats)))
}
pub fn diff_editor(
&self,
ui: &Ui,
tool_name: Option<&str>,
) -> Result<DiffEditor, CommandError> {
let base_ignores = self.base_ignores()?;
let conflict_marker_style = self.env.conflict_marker_style();
if let Some(name) = tool_name {
Ok(DiffEditor::with_name(
name,
self.settings(),
base_ignores,
conflict_marker_style,
)?)
} else {
Ok(DiffEditor::from_settings(
ui,
self.settings(),
base_ignores,
conflict_marker_style,
)?)
}
}
pub fn diff_selector(
&self,
ui: &Ui,
tool_name: Option<&str>,
force_interactive: bool,
) -> Result<DiffSelector, CommandError> {
if tool_name.is_some() || force_interactive {
Ok(DiffSelector::Interactive(self.diff_editor(ui, tool_name)?))
} else {
Ok(DiffSelector::NonInteractive)
}
}
pub fn merge_editor(
&self,
ui: &Ui,
tool_name: Option<&str>,
) -> Result<MergeEditor, MergeToolConfigError> {
let conflict_marker_style = self.env.conflict_marker_style();
if let Some(name) = tool_name {
MergeEditor::with_name(
name,
self.settings(),
self.path_converter().clone(),
conflict_marker_style,
)
} else {
MergeEditor::from_settings(
ui,
self.settings(),
self.path_converter().clone(),
conflict_marker_style,
)
}
}
pub fn text_editor(&self) -> Result<TextEditor, ConfigGetError> {
TextEditor::from_settings(self.settings())
}
pub fn resolve_single_op(&self, op_str: &str) -> Result<Operation, OpsetEvaluationError> {
op_walk::resolve_op_with_repo(self.repo(), op_str).block_on()
}
pub async fn resolve_single_rev(
&self,
ui: &Ui,
revision_arg: &RevisionArg,
) -> Result<Commit, CommandError> {
let expression = self.parse_revset(ui, revision_arg)?;
revset_util::evaluate_revset_to_single_commit(revision_arg.as_ref(), &expression, || {
self.commit_summary_template()
})
.await
}
pub async fn resolve_revsets_ordered(
&self,
ui: &Ui,
revision_args: &[RevisionArg],
) -> Result<IndexSet<CommitId>, CommandError> {
let mut all_commits = IndexSet::new();
for revision_arg in revision_args {
let expression = self.parse_revset(ui, revision_arg)?;
let mut stream = expression.evaluate_to_commit_ids()?;
while let Some(commit_id) = stream.try_next().await? {
all_commits.insert(commit_id);
}
}
Ok(all_commits)
}
pub async fn resolve_some_revsets(
&self,
ui: &Ui,
revision_args: &[RevisionArg],
) -> Result<IndexSet<CommitId>, CommandError> {
let all_commits = self.resolve_revsets_ordered(ui, revision_args).await?;
if all_commits.is_empty() {
Err(user_error("Empty revision set"))
} else {
Ok(all_commits)
}
}
pub fn parse_revset(
&self,
ui: &Ui,
revision_arg: &RevisionArg,
) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
let mut diagnostics = RevsetDiagnostics::new();
let context = self.env.revset_parse_context();
let expression = revset::parse(&mut diagnostics, revision_arg.as_ref(), &context)?;
print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
Ok(self.attach_revset_evaluator(expression))
}
pub fn parse_union_revsets(
&self,
ui: &Ui,
revision_args: &[RevisionArg],
) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
let mut diagnostics = RevsetDiagnostics::new();
let context = self.env.revset_parse_context();
let expressions: Vec<_> = revision_args
.iter()
.map(|arg| revset::parse(&mut diagnostics, arg.as_ref(), &context))
.try_collect()?;
print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
let expression = RevsetExpression::union_all(&expressions);
Ok(self.attach_revset_evaluator(expression))
}
pub fn attach_revset_evaluator(
&self,
expression: Arc<UserRevsetExpression>,
) -> RevsetExpressionEvaluator<'_> {
RevsetExpressionEvaluator::new(
self.repo().as_ref(),
self.env.command.revset_extensions().clone(),
self.id_prefix_context(),
expression,
)
}
pub fn id_prefix_context(&self) -> &IdPrefixContext {
self.user_repo
.id_prefix_context
.get_or_init(|| self.env.new_id_prefix_context())
}
pub fn parse_template<'a, C, L>(
&self,
ui: &Ui,
language: &L,
template_text: &str,
) -> Result<TemplateRenderer<'a, C>, CommandError>
where
C: Clone + 'a,
L: TemplateLanguage<'a> + ?Sized,
L::Property: WrapTemplateProperty<'a, C>,
{
self.env.parse_template(ui, language, template_text)
}
fn reparse_valid_template<'a, C, L>(
&self,
language: &L,
template_text: &str,
) -> TemplateRenderer<'a, C>
where
C: Clone + 'a,
L: TemplateLanguage<'a> + ?Sized,
L::Property: WrapTemplateProperty<'a, C>,
{
template_builder::parse(
language,
&mut TemplateDiagnostics::new(),
template_text,
&self.env.template_aliases_map,
)
.expect("parse error should be confined by WorkspaceCommandHelper::new()")
}
pub fn parse_commit_template(
&self,
ui: &Ui,
template_text: &str,
) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
let language = self.commit_template_language();
self.parse_template(ui, &language, template_text)
}
pub fn parse_operation_template(
&self,
ui: &Ui,
template_text: &str,
) -> Result<TemplateRenderer<'_, Operation>, CommandError> {
let language = self.operation_template_language();
self.parse_template(ui, &language, template_text)
}
pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
self.env
.commit_template_language(self.repo().as_ref(), self.id_prefix_context())
}
pub fn operation_template_language(&self) -> OperationTemplateLanguage {
OperationTemplateLanguage::new(
self.workspace.repo_loader(),
Some(self.repo().op_id()),
self.env.operation_template_extensions(),
)
}
pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
let language = self.commit_template_language();
self.reparse_valid_template(&language, &self.commit_summary_template_text)
.labeled(["commit"])
}
pub fn operation_summary_template(&self) -> TemplateRenderer<'_, Operation> {
let language = self.operation_template_language();
self.reparse_valid_template(&language, &self.op_summary_template_text)
.labeled(["operation"])
}
pub fn short_change_id_template(&self) -> TemplateRenderer<'_, Commit> {
let language = self.commit_template_language();
self.reparse_valid_template(&language, SHORT_CHANGE_ID_TEMPLATE_TEXT)
.labeled(["commit"])
}
pub fn format_commit_summary(&self, commit: &Commit) -> String {
let output = self.commit_summary_template().format_plain_text(commit);
output.into_string_lossy()
}
#[instrument(skip_all)]
pub fn write_commit_summary(
&self,
formatter: &mut dyn Formatter,
commit: &Commit,
) -> std::io::Result<()> {
self.commit_summary_template().format(commit, formatter)
}
pub async fn check_rewritable<'a>(
&self,
commits: impl IntoIterator<Item = &'a CommitId>,
) -> Result<(), CommandError> {
let commit_ids = commits.into_iter().cloned().collect_vec();
let to_rewrite_expr = RevsetExpression::commits(commit_ids);
self.check_rewritable_expr(&to_rewrite_expr).await
}
pub async fn check_rewritable_expr(
&self,
to_rewrite_expr: &Arc<ResolvedRevsetExpression>,
) -> Result<(), CommandError> {
let repo = self.repo().as_ref();
let Some(commit_id) = self
.env
.find_immutable_commit(repo, to_rewrite_expr)
.await?
else {
return Ok(());
};
let error = if &commit_id == repo.store().root_commit_id() {
user_error(format!("The root commit {commit_id:.12} is immutable"))
} else {
let mut error = user_error(format!("Commit {commit_id:.12} is immutable"));
let commit = repo.store().get_commit_async(&commit_id).await?;
error.add_formatted_hint_with(|formatter| {
write!(formatter, "Could not modify commit: ")?;
self.write_commit_summary(formatter, &commit)?;
Ok(())
});
error.add_hint("Immutable commits are used to protect shared history.");
error.add_hint(indoc::indoc! {"
For more information, see:
- https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits
- `jj help -k config`, \"Set of immutable commits\""});
let id_prefix_context =
IdPrefixContext::new(self.env.command.revset_extensions().clone());
let (lower_bound, upper_bound) = RevsetExpressionEvaluator::new(
repo,
self.env.command.revset_extensions().clone(),
&id_prefix_context,
self.env.immutable_expression(),
)
.resolve()?
.intersection(&to_rewrite_expr.descendants())
.evaluate(repo)?
.count_estimate()?;
let exact = upper_bound == Some(lower_bound);
let or_more = if exact { "" } else { " or more" };
error.add_hint(format!(
"This operation would rewrite {lower_bound}{or_more} immutable commits."
));
error
};
Err(error)
}
#[instrument(skip_all)]
async fn snapshot_working_copy(
&mut self,
ui: &Ui,
) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
let workspace_name = self.workspace_name().to_owned();
let repo = self.repo().clone();
let auto_tracking_matcher = self
.auto_tracking_matcher(ui)
.map_err(snapshot_command_error)?;
let options = self
.snapshot_options_with_start_tracking_matcher(&auto_tracking_matcher)
.map_err(snapshot_command_error)?;
let mut locked_ws = self
.workspace
.start_working_copy_mutation()
.await
.map_err(snapshot_command_error)?;
let Some((repo, wc_commit)) =
handle_stale_working_copy(locked_ws.locked_wc(), repo, &workspace_name).await?
else {
return Ok(SnapshotStats::default());
};
self.user_repo = ReadonlyUserRepo::new(repo);
let (new_tree, stats) = {
let mut options = options;
let progress = crate::progress::snapshot_progress(ui);
options.progress = progress.as_ref().map(|x| x as _);
locked_ws
.locked_wc()
.snapshot(&options)
.await
.map_err(snapshot_command_error)?
};
if new_tree.tree_ids_and_labels() != wc_commit.tree().tree_ids_and_labels() {
let mut tx = start_repo_transaction(
&self.user_repo.repo,
&workspace_name,
self.env.command.string_args(),
);
tx.set_is_snapshot(true);
let mut_repo = tx.repo_mut();
let commit = mut_repo
.rewrite_commit(&wc_commit)
.set_tree(new_tree.clone())
.write()
.await
.map_err(snapshot_command_error)?;
mut_repo
.set_wc_commit(workspace_name, commit.id().clone())
.map_err(snapshot_command_error)?;
let num_rebased = mut_repo
.rebase_descendants()
.await
.map_err(snapshot_command_error)?;
if num_rebased > 0 {
writeln!(
ui.status(),
"Rebased {num_rebased} descendant commits onto updated working copy"
)
.map_err(snapshot_command_error)?;
}
#[cfg(feature = "git")]
if self.working_copy_shared_with_git && self.env.command.should_commit_transaction() {
let old_tree = wc_commit.tree();
let new_tree = commit.tree();
export_working_copy_changes_to_git(ui, mut_repo, &old_tree, &new_tree)
.await
.map_err(snapshot_command_error)?;
}
let repo = self
.env
.command
.maybe_commit_transaction(tx, "snapshot working copy")
.await
.map_err(snapshot_command_error)?;
self.user_repo = ReadonlyUserRepo::new(repo);
}
#[cfg(feature = "git")]
if self.working_copy_shared_with_git
&& let Ok(resolved_tree) = new_tree
.trees()
.await
.map_err(snapshot_command_error)?
.into_resolved()
&& resolved_tree
.entries_non_recursive()
.any(|entry| entry.name().as_internal_str().starts_with(".jjconflict"))
{
writeln!(
ui.warning_default(),
"The working copy contains '.jjconflict' files. These files are used by `jj` \
internally and should not be present in the working copy."
)
.map_err(snapshot_command_error)?;
writeln!(
ui.hint_default(),
"You may have used a regular `git` command to check out a conflicted commit."
)
.map_err(snapshot_command_error)?;
writeln!(
ui.hint_default(),
"You can use `jj abandon` to discard the working copy changes."
)
.map_err(snapshot_command_error)?;
}
if self.env.command.should_commit_transaction() {
locked_ws
.finish(self.user_repo.repo.op_id().clone())
.await
.map_err(snapshot_command_error)?;
}
Ok(stats)
}
async fn update_working_copy(
&mut self,
ui: &Ui,
maybe_old_commit: Option<&Commit>,
new_commit: &Commit,
) -> Result<(), CommandError> {
assert!(self.may_update_working_copy);
let stats = update_working_copy(
&self.user_repo.repo,
&mut self.workspace,
maybe_old_commit,
new_commit,
)
.await?;
self.print_updated_working_copy_stats(ui, maybe_old_commit, new_commit, &stats)
}
fn print_updated_working_copy_stats(
&self,
ui: &Ui,
maybe_old_commit: Option<&Commit>,
new_commit: &Commit,
stats: &CheckoutStats,
) -> Result<(), CommandError> {
if Some(new_commit) != maybe_old_commit
&& let Some(mut formatter) = ui.status_formatter()
{
let template = self.commit_summary_template();
write!(formatter, "Working copy (@) now at: ")?;
template.format(new_commit, formatter.as_mut())?;
writeln!(formatter)?;
for parent in new_commit.parents().block_on()? {
write!(formatter, "Parent commit (@-) : ")?;
template.format(&parent, formatter.as_mut())?;
writeln!(formatter)?;
}
}
print_checkout_stats(ui, stats, new_commit)?;
if Some(new_commit) != maybe_old_commit
&& let Some(mut formatter) = ui.status_formatter()
&& new_commit.has_conflict()
{
let conflicts = new_commit.tree().conflicts().collect_vec();
writeln!(
formatter.labeled("warning").with_heading("Warning: "),
"There are unresolved conflicts at these paths:"
)?;
print_conflicted_paths(conflicts, formatter.as_mut(), self)?;
}
Ok(())
}
pub fn start_transaction(&mut self) -> WorkspaceCommandTransaction<'_> {
let tx = start_repo_transaction(
self.repo(),
self.workspace_name(),
self.env.command.string_args(),
);
let id_prefix_context = mem::take(&mut self.user_repo.id_prefix_context);
WorkspaceCommandTransaction {
helper: self,
tx,
id_prefix_context,
}
}
async fn finish_transaction(
&mut self,
ui: &Ui,
mut tx: Transaction,
description: impl Into<String>,
_git_import_export_lock: &GitImportExportLock,
) -> Result<(), CommandError> {
let num_rebased = tx.repo_mut().rebase_descendants().await?;
if num_rebased > 0 {
writeln!(ui.status(), "Rebased {num_rebased} descendant commits")?;
}
for (name, wc_commit_id) in &tx.repo().view().wc_commit_ids().clone() {
let wc_expr = RevsetExpression::commit(wc_commit_id.clone());
let is_immutable = match self.env.find_immutable_commit(tx.repo(), &wc_expr).await {
Ok(commit_id) => commit_id.is_some(),
Err(CommandError { error, .. }) => {
writeln!(
ui.warning_default(),
"Failed to check mutability of the new working-copy revision."
)?;
print_error_sources(ui, Some(&error))?;
break;
}
};
if is_immutable {
let wc_commit = tx.repo().store().get_commit_async(wc_commit_id).await?;
tx.repo_mut().check_out(name.clone(), &wc_commit).await?;
writeln!(
ui.warning_default(),
"The working-copy commit in workspace '{name}' became immutable, so a new \
commit has been created on top of it.",
name = name.as_symbol()
)?;
}
}
if let Err(err) =
revset_util::try_resolve_trunk_alias(tx.repo(), &self.env.revset_parse_context())
{
if tx.repo().view().wc_commit_ids().is_empty() {
writeln!(
ui.warning_default(),
"Failed to resolve `revset-aliases.trunk()`: {err}"
)?;
}
writeln!(
ui.hint_default(),
"Use `jj config edit --repo` to adjust the `trunk()` alias."
)?;
}
let old_repo = tx.base_repo().clone();
let maybe_old_wc_commit = old_repo
.view()
.get_wc_commit_id(self.workspace_name())
.map(|commit_id| tx.base_repo().store().get_commit(commit_id))
.transpose()?;
let maybe_new_wc_commit = tx
.repo()
.view()
.get_wc_commit_id(self.workspace_name())
.map(|commit_id| tx.repo().store().get_commit(commit_id))
.transpose()?;
#[cfg(feature = "git")]
if self.working_copy_shared_with_git && self.env.command.should_commit_transaction() {
use std::error::Error as _;
if let Some(wc_commit) = &maybe_new_wc_commit {
match jj_lib::git::reset_head(tx.repo_mut(), wc_commit).await {
Ok(()) => {}
Err(err @ jj_lib::git::GitResetHeadError::UpdateHeadRef(_)) => {
writeln!(ui.warning_default(), "{err}")?;
print_error_sources(ui, err.source())?;
}
Err(err) => return Err(err.into()),
}
}
let stats = jj_lib::git::export_refs(tx.repo_mut())?;
crate::git_util::print_git_export_stats(ui, &stats)?;
}
self.user_repo = ReadonlyUserRepo::new(
self.env
.command
.maybe_commit_transaction(tx, description)
.await?,
);
if self.may_update_working_copy {
if let Some(new_commit) = &maybe_new_wc_commit {
self.update_working_copy(ui, maybe_old_wc_commit.as_ref(), new_commit)
.await?;
} else {
}
}
self.report_repo_changes(ui, &old_repo).await?;
if !self.env.command.should_commit_transaction() {
writeln!(
ui.status(),
"Operation left uncommitted because --no-integrate-operation was requested: {}",
short_operation_hash(self.repo().op_id())
)?;
}
let settings = self.settings();
let missing_user_name = settings.user_name().is_empty();
let missing_user_mail = settings.user_email().is_empty();
if missing_user_name || missing_user_mail {
let not_configured_msg = match (missing_user_name, missing_user_mail) {
(true, true) => "Name and email not configured.",
(true, false) => "Name not configured.",
(false, true) => "Email not configured.",
_ => unreachable!(),
};
writeln!(
ui.warning_default(),
"{not_configured_msg} Until configured, your commits will be created with the \
empty identity, and can't be pushed to remotes."
)?;
writeln!(ui.hint_default(), "To configure, run:")?;
if missing_user_name {
writeln!(
ui.hint_no_heading(),
r#" jj config set --user user.name "Some One""#
)?;
}
if missing_user_mail {
writeln!(
ui.hint_no_heading(),
r#" jj config set --user user.email "someone@example.com""#
)?;
}
}
Ok(())
}
async fn report_repo_changes(
&self,
ui: &Ui,
old_repo: &Arc<ReadonlyRepo>,
) -> Result<(), CommandError> {
let Some(mut fmt) = ui.status_formatter() else {
return Ok(());
};
let old_view = old_repo.view();
let new_repo = self.repo().as_ref();
let new_view = new_repo.view();
let workspace_name = self.workspace_name();
if old_view.wc_commit_ids().contains_key(workspace_name)
&& !new_view.wc_commit_ids().contains_key(workspace_name)
{
writeln!(
fmt.labeled("warning").with_heading("Warning: "),
"The current workspace '{}' no longer exists after this operation. The working \
copy was left untouched.",
workspace_name.as_symbol(),
)?;
writeln!(
fmt.labeled("hint").with_heading("Hint: "),
"Restore to an operation that contains the workspace (e.g. `jj undo` or `jj \
redo`).",
)?;
}
let old_heads = RevsetExpression::commits(old_view.heads().iter().cloned().collect());
let new_heads = RevsetExpression::commits(new_view.heads().iter().cloned().collect());
let conflicts = RevsetExpression::filter(RevsetFilterPredicate::HasConflict)
.filtered(RevsetFilterPredicate::File(FilesetExpression::all()));
let removed_conflicts_expr = new_heads.range(&old_heads).intersection(&conflicts);
let added_conflicts_expr = old_heads.range(&new_heads).intersection(&conflicts);
let get_commits =
async |expr: Arc<ResolvedRevsetExpression>| -> Result<Vec<Commit>, CommandError> {
let commits = expr
.evaluate(new_repo)?
.stream()
.commits(new_repo.store())
.try_collect()
.await?;
Ok(commits)
};
let removed_conflict_commits = get_commits(removed_conflicts_expr).await?;
let added_conflict_commits = get_commits(added_conflicts_expr).await?;
fn commits_by_change_id(commits: &[Commit]) -> IndexMap<&ChangeId, Vec<&Commit>> {
let mut result: IndexMap<&ChangeId, Vec<&Commit>> = IndexMap::new();
for commit in commits {
result.entry(commit.change_id()).or_default().push(commit);
}
result
}
let removed_conflicts_by_change_id = commits_by_change_id(&removed_conflict_commits);
let added_conflicts_by_change_id = commits_by_change_id(&added_conflict_commits);
let mut resolved_conflicts_by_change_id = removed_conflicts_by_change_id.clone();
resolved_conflicts_by_change_id
.retain(|change_id, _commits| !added_conflicts_by_change_id.contains_key(change_id));
let mut new_conflicts_by_change_id = added_conflicts_by_change_id.clone();
new_conflicts_by_change_id
.retain(|change_id, _commits| !removed_conflicts_by_change_id.contains_key(change_id));
if !resolved_conflicts_by_change_id.is_empty() {
let num_resolved: usize = resolved_conflicts_by_change_id
.values()
.map(|commits| commits.len())
.sum();
writeln!(
fmt,
"Existing conflicts were resolved or abandoned from {num_resolved} commits."
)?;
}
if !new_conflicts_by_change_id.is_empty() {
let num_conflicted: usize = new_conflicts_by_change_id
.values()
.map(|commits| commits.len())
.sum();
writeln!(fmt, "New conflicts appeared in {num_conflicted} commits:")?;
print_updated_commits(
fmt.as_mut(),
&self.commit_summary_template(),
new_conflicts_by_change_id.values().flatten().copied(),
)?;
}
if !(added_conflict_commits.is_empty()
|| resolved_conflicts_by_change_id.is_empty() && new_conflicts_by_change_id.is_empty())
{
if new_conflicts_by_change_id.is_empty() {
writeln!(
fmt,
"There are still unresolved conflicts in rebased descendants.",
)?;
}
self.report_repo_conflicts(
fmt.as_mut(),
new_repo,
added_conflict_commits
.iter()
.map(|commit| commit.id().clone())
.collect(),
)
.await?;
}
Ok(())
}
pub async fn report_repo_conflicts(
&self,
fmt: &mut dyn Formatter,
repo: &ReadonlyRepo,
conflicted_commits: Vec<CommitId>,
) -> Result<(), CommandError> {
if !self.settings().get_bool("hints.resolving-conflicts")? || conflicted_commits.is_empty()
{
return Ok(());
}
let only_one_conflicted_commit = conflicted_commits.len() == 1;
let root_conflicts_revset = RevsetExpression::commits(conflicted_commits)
.roots()
.evaluate(repo)?;
let root_conflict_commits: Vec<_> = root_conflicts_revset
.stream()
.commits(repo.store())
.try_collect()
.await?;
let instruction = if only_one_conflicted_commit {
indoc! {"
To resolve the conflicts, start by creating a commit on top of
the conflicted commit:
"}
} else if root_conflict_commits.len() == 1 {
indoc! {"
To resolve the conflicts, start by creating a commit on top of
the first conflicted commit:
"}
} else {
indoc! {"
To resolve the conflicts, start by creating a commit on top of
one of the first conflicted commits:
"}
};
write!(fmt.labeled("hint").with_heading("Hint: "), "{instruction}")?;
let format_short_change_id = self.short_change_id_template();
{
let mut fmt = fmt.labeled("hint");
for commit in &root_conflict_commits {
write!(fmt, " jj new ")?;
format_short_change_id.format(commit, *fmt)?;
writeln!(fmt)?;
}
}
writedoc!(
fmt.labeled("hint"),
"
Then use `jj resolve`, or edit the conflict markers in the file directly.
Once the conflicts are resolved, you can inspect the result with `jj diff`.
Then run `jj squash` to move the resolution into the conflicted commit.
",
)?;
Ok(())
}
pub fn get_advanceable_bookmarks<'a>(
&self,
ui: &Ui,
from: impl IntoIterator<Item = &'a CommitId>,
) -> Result<Vec<AdvanceableBookmark>, CommandError> {
let Some(ab_matcher) = load_advance_bookmarks_matcher(ui, self.settings())? else {
return Ok(Vec::new());
};
let mut advanceable_bookmarks = Vec::new();
for from_commit in from {
for (name, _) in self.repo().view().local_bookmarks_for_commit(from_commit) {
if ab_matcher.is_match(name.as_str()) {
advanceable_bookmarks.push(AdvanceableBookmark {
name: name.to_owned(),
old_commit_id: from_commit.clone(),
});
}
}
}
Ok(advanceable_bookmarks)
}
}
#[cfg(feature = "git")]
pub async fn export_working_copy_changes_to_git(
ui: &Ui,
mut_repo: &mut MutableRepo,
old_tree: &MergedTree,
new_tree: &MergedTree,
) -> Result<(), CommandError> {
let repo = mut_repo.base_repo().as_ref();
jj_lib::git::update_intent_to_add(repo, old_tree, new_tree).await?;
let stats = jj_lib::git::export_refs(mut_repo)?;
crate::git_util::print_git_export_stats(ui, &stats)?;
Ok(())
}
#[cfg(not(feature = "git"))]
pub async fn export_working_copy_changes_to_git(
_ui: &Ui,
_mut_repo: &mut MutableRepo,
_old_tree: &MergedTree,
_new_tree: &MergedTree,
) -> Result<(), CommandError> {
Ok(())
}
#[must_use]
pub struct WorkspaceCommandTransaction<'a> {
helper: &'a mut WorkspaceCommandHelper,
tx: Transaction,
id_prefix_context: OnceCell<IdPrefixContext>,
}
impl WorkspaceCommandTransaction<'_> {
pub fn base_workspace_helper(&self) -> &WorkspaceCommandHelper {
self.helper
}
pub fn settings(&self) -> &UserSettings {
self.helper.settings()
}
pub fn base_repo(&self) -> &Arc<ReadonlyRepo> {
self.tx.base_repo()
}
pub fn repo(&self) -> &MutableRepo {
self.tx.repo()
}
pub fn repo_mut(&mut self) -> &mut MutableRepo {
self.id_prefix_context.take(); self.tx.repo_mut()
}
pub fn check_out(&mut self, commit: &Commit) -> Result<Commit, CheckOutCommitError> {
let name = self.helper.workspace_name().to_owned();
self.id_prefix_context.take(); self.tx.repo_mut().check_out(name, commit).block_on()
}
pub fn edit(&mut self, commit: &Commit) -> Result<(), EditCommitError> {
let name = self.helper.workspace_name().to_owned();
self.id_prefix_context.take(); self.tx.repo_mut().edit(name, commit).block_on()
}
pub fn format_commit_summary(&self, commit: &Commit) -> String {
let output = self.commit_summary_template().format_plain_text(commit);
output.into_string_lossy()
}
pub fn write_commit_summary(
&self,
formatter: &mut dyn Formatter,
commit: &Commit,
) -> std::io::Result<()> {
self.commit_summary_template().format(commit, formatter)
}
pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
let language = self.commit_template_language();
self.helper
.reparse_valid_template(&language, &self.helper.commit_summary_template_text)
.labeled(["commit"])
}
pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
let id_prefix_context = self
.id_prefix_context
.get_or_init(|| self.helper.env.new_id_prefix_context());
self.helper
.env
.commit_template_language(self.tx.repo(), id_prefix_context)
}
pub fn parse_commit_template(
&self,
ui: &Ui,
template_text: &str,
) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
let language = self.commit_template_language();
self.helper.env.parse_template(ui, &language, template_text)
}
pub async fn finish(self, ui: &Ui, description: impl Into<String>) -> Result<(), CommandError> {
if !self.tx.repo().has_changes() {
writeln!(ui.status(), "Nothing changed.")?;
return Ok(());
}
let git_import_export_lock = self.helper.lock_git_import_export()?;
self.helper
.finish_transaction(ui, self.tx, description, &git_import_export_lock)
.await
}
pub fn into_inner(self) -> Transaction {
self.tx
}
pub fn advance_bookmarks(
&mut self,
bookmarks: Vec<AdvanceableBookmark>,
move_to: &CommitId,
) -> Result<(), CommandError> {
for bookmark in bookmarks {
self.repo_mut().merge_local_bookmark(
&bookmark.name,
&RefTarget::normal(bookmark.old_commit_id),
&RefTarget::normal(move_to.clone()),
)?;
}
Ok(())
}
}
pub fn find_workspace_dir(cwd: &Path) -> &Path {
cwd.ancestors()
.find(|path| path.join(".jj").is_dir())
.unwrap_or(cwd)
}
fn map_workspace_load_error(err: WorkspaceLoadError, user_wc_path: Option<&str>) -> CommandError {
match err {
WorkspaceLoadError::NoWorkspaceHere(wc_path) => {
let short_wc_path = user_wc_path.map_or(wc_path.as_ref(), Path::new);
let message = format!(r#"There is no jj repo in "{}""#, short_wc_path.display());
let git_dir = wc_path.join(".git");
if git_dir.is_dir() {
user_error(message).hinted(
"It looks like this is a git repo. You can create a jj repo backed by it by \
running this:
jj git init",
)
} else {
user_error(message)
}
}
WorkspaceLoadError::RepoDoesNotExist(repo_dir) => user_error(format!(
"The repository directory at {} is missing. Was it moved?",
repo_dir.display(),
)),
WorkspaceLoadError::StoreLoadError(err @ StoreLoadError::UnsupportedType { .. }) => {
internal_error_with_message(
"This version of the jj binary doesn't support this type of repo",
err,
)
}
WorkspaceLoadError::StoreLoadError(
err @ (StoreLoadError::ReadError { .. } | StoreLoadError::Backend(_)),
) => internal_error_with_message("The repository appears broken or inaccessible", err),
WorkspaceLoadError::StoreLoadError(StoreLoadError::Signing(err)) => user_error(err),
WorkspaceLoadError::WorkingCopyState(err) => internal_error(err),
WorkspaceLoadError::DecodeRepoPath(_) | WorkspaceLoadError::Path(_) => user_error(err),
}
}
pub fn start_repo_transaction(
repo: &Arc<ReadonlyRepo>,
workspace_name: &WorkspaceName,
string_args: &[String],
) -> Transaction {
let mut tx = repo.start_transaction();
tx.set_workspace_name(workspace_name);
let shell_escape = |arg: &String| {
if arg.as_bytes().iter().all(|b| {
matches!(b,
b'A'..=b'Z'
| b'a'..=b'z'
| b'0'..=b'9'
| b','
| b'-'
| b'.'
| b'/'
| b':'
| b'@'
| b'_'
)
}) {
arg.clone()
} else {
format!("'{}'", arg.replace('\'', "\\'"))
}
};
let mut quoted_strings = vec!["jj".to_string()];
quoted_strings.extend(string_args.iter().skip(1).map(shell_escape));
tx.set_attribute("args".to_string(), quoted_strings.join(" "));
tx
}
async fn handle_stale_working_copy(
locked_wc: &mut dyn LockedWorkingCopy,
repo: Arc<ReadonlyRepo>,
workspace_name: &WorkspaceName,
) -> Result<Option<(Arc<ReadonlyRepo>, Commit)>, SnapshotWorkingCopyError> {
let get_wc_commit = |repo: &ReadonlyRepo| -> Result<Option<_>, _> {
repo.view()
.get_wc_commit_id(workspace_name)
.map(|id| repo.store().get_commit(id))
.transpose()
.map_err(snapshot_command_error)
};
let Some(wc_commit) = get_wc_commit(&repo)? else {
return Ok(None);
};
let old_op_id = locked_wc.old_operation_id().clone();
match WorkingCopyFreshness::check_stale(locked_wc, &wc_commit, &repo).await {
Ok(WorkingCopyFreshness::Fresh) => Ok(Some((repo, wc_commit))),
Ok(WorkingCopyFreshness::Updated(wc_operation)) => {
let repo = repo
.reload_at(&wc_operation)
.await
.map_err(snapshot_command_error)?;
if let Some(wc_commit) = get_wc_commit(&repo)? {
Ok(Some((repo, wc_commit)))
} else {
Ok(None)
}
}
Ok(WorkingCopyFreshness::WorkingCopyStale) => {
Err(SnapshotWorkingCopyError::StaleWorkingCopy(
user_error(format!(
"The working copy is stale (not updated since operation {}).",
short_operation_hash(&old_op_id)
))
.hinted(
"Run `jj workspace update-stale` to update it.
See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy \
for more information.",
),
))
}
Ok(WorkingCopyFreshness::SiblingOperation) => {
Err(SnapshotWorkingCopyError::StaleWorkingCopy(
internal_error(format!(
"The repo was loaded at operation {}, which seems to be a sibling of the \
working copy's operation {}",
short_operation_hash(repo.op_id()),
short_operation_hash(&old_op_id)
))
.hinted(format!(
"Run `jj op integrate {}` to add the working copy's operation to the \
operation log.",
short_operation_hash(&old_op_id)
)),
))
}
Err(OpStoreError::ObjectNotFound { .. }) => {
Err(SnapshotWorkingCopyError::StaleWorkingCopy(
user_error("Could not read working copy's operation.").hinted(
"Run `jj workspace update-stale` to recover.
See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy \
for more information.",
),
))
}
Err(e) => Err(snapshot_command_error(e)),
}
}
async fn update_stale_working_copy(
mut locked_ws: LockedWorkspace<'_>,
op_id: OperationId,
stale_commit: &Commit,
new_commit: &Commit,
) -> Result<CheckoutStats, CommandError> {
if stale_commit.tree().tree_ids_and_labels()
!= locked_ws.locked_wc().old_tree().tree_ids_and_labels()
{
return Err(user_error("Concurrent working copy operation. Try again."));
}
let stats = locked_ws
.locked_wc()
.check_out(new_commit)
.await
.map_err(|err| {
internal_error_with_message(
format!("Failed to check out commit {}", new_commit.id().hex()),
err,
)
})?;
locked_ws.finish(op_id).await?;
Ok(stats)
}
pub fn print_updated_commits<'a>(
formatter: &mut dyn Formatter,
template: &TemplateRenderer<Commit>,
commits: impl IntoIterator<Item = &'a Commit>,
) -> io::Result<()> {
let mut commits = commits.into_iter().fuse();
for commit in commits.by_ref().take(10) {
write!(formatter, " ")?;
template.format(commit, formatter)?;
writeln!(formatter)?;
}
if commits.next().is_some() {
writeln!(formatter, " ...")?;
}
Ok(())
}
#[instrument(skip_all)]
pub fn print_conflicted_paths(
conflicts: Vec<(RepoPathBuf, BackendResult<MergedTreeValue>)>,
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, formatted_paths) {
let conflict = conflict?.simplify();
let sides = conflict.num_sides();
let n_adds = conflict.adds().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(), conflict.adds()).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",
}
.to_string(),
"difficult",
);
}
write!(formatter, "{formatted_path} ")?;
{
let mut formatter = formatter.labeled("conflict_description");
let print_pair = |formatter: &mut dyn Formatter, (text, label): &(String, &str)| {
write!(formatter.labeled(label), "{text}")
};
print_pair(
*formatter,
&(
format!("{sides}-sided"),
if sides > 2 { "difficult" } else { "normal" },
),
)?;
write!(formatter, " conflict")?;
if !seen_objects.is_empty() {
write!(formatter, " 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 {
write!(formatter, ", ")?;
print_pair(*formatter, pair)?;
}
write!(formatter, " and ")?;
print_pair(*formatter, last)?;
}
}
}
}
writeln!(formatter)?;
}
Ok(())
}
fn build_untracked_reason_message(reason: &UntrackedReason) -> Option<String> {
match reason {
UntrackedReason::FileTooLarge { size, max_size } => {
let size_approx = HumanByteSize(*size);
let max_size_approx = HumanByteSize(*max_size);
Some(format!(
"{size_approx} ({size} bytes); the maximum size allowed is {max_size_approx} \
({max_size} bytes)",
))
}
UntrackedReason::FileNotAutoTracked => None,
}
}
pub fn print_untracked_files(
ui: &Ui,
untracked_paths: &BTreeMap<RepoPathBuf, UntrackedReason>,
path_converter: &RepoPathUiConverter,
) -> io::Result<()> {
let mut untracked_paths = untracked_paths
.iter()
.filter_map(|(path, reason)| build_untracked_reason_message(reason).map(|m| (path, m)))
.peekable();
if untracked_paths.peek().is_some() {
writeln!(ui.warning_default(), "Refused to snapshot some files:")?;
let mut formatter = ui.stderr_formatter();
for (path, message) in untracked_paths {
let ui_path = path_converter.format_file_path(path);
writeln!(formatter, " {ui_path}: {message}")?;
}
}
Ok(())
}
pub fn print_snapshot_stats(
ui: &Ui,
stats: &SnapshotStats,
path_converter: &RepoPathUiConverter,
) -> io::Result<()> {
print_untracked_files(ui, &stats.untracked_paths, path_converter)?;
let large_files_sizes = stats
.untracked_paths
.values()
.filter_map(|reason| match reason {
UntrackedReason::FileTooLarge { size, .. } => Some(size),
UntrackedReason::FileNotAutoTracked => None,
});
if let Some(size) = large_files_sizes.max() {
print_large_file_hint(ui, *size, None)?;
}
Ok(())
}
pub fn print_large_file_hint(
ui: &Ui,
max_size: u64,
large_files: Option<&[String]>,
) -> io::Result<()> {
let (command, extra) = large_files
.map(|files| {
let files_list = files
.iter()
.map(|s| shlex::try_quote(s).unwrap_or(s.into()))
.join(" ");
let command = format!("file track {files_list}");
let extra = format!(
r"
* Run `jj file track --include-ignored {files_list}`
This will track the file(s) regardless of size."
);
(command, extra)
})
.unwrap_or(("status".to_string(), String::new()));
writedoc!(
ui.hint_default(),
r"
This is to prevent large files from being added by accident. To fix this:
* Add the file(s) to `.gitignore`
* Run `jj config set --repo snapshot.max-new-file-size {max_size}`
This will increase the maximum file size allowed for new files, in this repository only.
* Run `jj --config snapshot.max-new-file-size={max_size} {command}`
This will increase the maximum file size allowed for new files, for this command only.{extra}
"
)?;
Ok(())
}
pub fn print_checkout_stats(
ui: &Ui,
stats: &CheckoutStats,
new_commit: &Commit,
) -> Result<(), std::io::Error> {
if stats.added_files > 0 || stats.updated_files > 0 || stats.removed_files > 0 {
writeln!(
ui.status(),
"Added {} files, modified {} files, removed {} files",
stats.added_files,
stats.updated_files,
stats.removed_files
)?;
}
if stats.skipped_files != 0 {
writeln!(
ui.warning_default(),
"{} of those updates were skipped because there were conflicting changes in the \
working copy.",
stats.skipped_files
)?;
writeln!(
ui.hint_default(),
"Inspect the changes compared to the intended target with `jj diff --from {}`.
Discard the conflicting changes with `jj restore --from {}`.",
short_commit_hash(new_commit.id()),
short_commit_hash(new_commit.id())
)?;
}
Ok(())
}
pub fn print_unmatched_explicit_paths<'a>(
ui: &Ui,
workspace_command: &WorkspaceCommandHelper,
expression: &FilesetExpression,
trees: impl IntoIterator<Item = &'a MergedTree>,
) -> io::Result<()> {
let mut explicit_paths = expression.explicit_paths().collect_vec();
for tree in trees {
explicit_paths.retain(|&path| tree.path_value(path).block_on().unwrap().is_absent());
}
if !explicit_paths.is_empty() {
let ui_paths = explicit_paths
.iter()
.map(|&path| workspace_command.format_file_path(path))
.join(", ");
writeln!(
ui.warning_default(),
"No matching entries for paths: {ui_paths}"
)?;
}
Ok(())
}
pub async fn update_working_copy(
repo: &Arc<ReadonlyRepo>,
workspace: &mut Workspace,
old_commit: Option<&Commit>,
new_commit: &Commit,
) -> Result<CheckoutStats, CommandError> {
let old_tree = old_commit.map(|commit| commit.tree());
let stats = workspace
.check_out(repo.op_id().clone(), old_tree.as_ref(), new_commit)
.await
.map_err(|err| {
internal_error_with_message(
format!("Failed to check out commit {}", new_commit.id().hex()),
err,
)
})?;
Ok(stats)
}
#[cfg_attr(not(feature = "git"), expect(unused_variables))]
pub fn default_ignored_remote_name(store: &Store) -> Option<&'static RemoteName> {
#[cfg(feature = "git")]
{
use jj_lib::git;
if git::get_git_backend(store).is_ok() {
return Some(git::REMOTE_NAME_FOR_LOCAL_GIT_REPO);
}
}
None
}
pub fn has_tracked_remote_bookmarks(repo: &dyn Repo, bookmark: &RefName) -> bool {
let remote_matcher = match default_ignored_remote_name(repo.store()) {
Some(remote) => StringExpression::exact(remote).negated().to_matcher(),
None => StringMatcher::all(),
};
repo.view()
.remote_bookmarks_matching(&StringMatcher::exact(bookmark), &remote_matcher)
.any(|(_, remote_ref)| remote_ref.is_tracked())
}
pub fn has_tracked_remote_tags(repo: &dyn Repo, tag: &RefName) -> bool {
let remote_matcher = match default_ignored_remote_name(repo.store()) {
Some(remote) => StringExpression::exact(remote).negated().to_matcher(),
None => StringMatcher::all(),
};
repo.view()
.remote_tags_matching(&StringMatcher::exact(tag), &remote_matcher)
.any(|(_, remote_ref)| remote_ref.is_tracked())
}
pub fn load_fileset_aliases(
ui: &Ui,
config: &StackedConfig,
) -> Result<FilesetAliasesMap, CommandError> {
let table_name = ConfigNamePathBuf::from_iter(["fileset-aliases"]);
load_aliases_map(ui, config, &table_name)
}
pub fn load_revset_aliases(
ui: &Ui,
config: &StackedConfig,
) -> Result<RevsetAliasesMap, CommandError> {
let table_name = ConfigNamePathBuf::from_iter(["revset-aliases"]);
let aliases_map = load_aliases_map(ui, config, &table_name)?;
revset_util::warn_user_redefined_builtin(ui, config, &table_name)?;
Ok(aliases_map)
}
pub fn load_template_aliases(
ui: &Ui,
config: &StackedConfig,
) -> Result<TemplateAliasesMap, CommandError> {
let table_name = ConfigNamePathBuf::from_iter(["template-aliases"]);
load_aliases_map(ui, config, &table_name)
}
#[derive(Clone, Debug)]
pub struct LogContentFormat {
width: usize,
word_wrap: bool,
}
impl LogContentFormat {
pub fn new(ui: &Ui, settings: &UserSettings) -> Result<Self, ConfigGetError> {
Ok(Self {
width: ui.term_width(),
word_wrap: settings.get_bool("ui.log-word-wrap")?,
})
}
#[must_use]
pub fn sub_width(&self, width: usize) -> Self {
Self {
width: self.width.saturating_sub(width),
word_wrap: self.word_wrap,
}
}
pub fn width(&self) -> usize {
self.width
}
pub async fn write<E: From<io::Error>>(
&self,
formatter: &mut dyn Formatter,
content_fn: impl AsyncFnOnce(&mut dyn Formatter) -> Result<(), E>,
) -> Result<(), E> {
if self.word_wrap {
let mut recorder = FormatRecorder::new(formatter.maybe_color());
content_fn(&mut recorder).await?;
text_util::write_wrapped(formatter, &recorder, self.width)?;
} else {
content_fn(formatter).await?;
}
Ok(())
}
}
pub fn short_commit_hash(commit_id: &CommitId) -> String {
format!("{commit_id:.12}")
}
pub fn short_change_hash(change_id: &ChangeId) -> String {
format!("{change_id:.12}")
}
pub fn short_operation_hash(operation_id: &OperationId) -> String {
format!("{operation_id:.12}")
}
#[derive(Clone, Debug)]
pub enum DiffSelector {
NonInteractive,
Interactive(DiffEditor),
}
impl DiffSelector {
pub fn is_interactive(&self) -> bool {
matches!(self, Self::Interactive(_))
}
pub async fn select(
&self,
ui: &Ui,
trees: Diff<&MergedTree>,
tree_labels: Diff<String>,
matcher: &dyn Matcher,
format_instructions: impl FnOnce() -> String,
) -> Result<MergedTree, CommandError> {
let selected_tree = restore_tree(
trees.after,
trees.before,
tree_labels.after,
tree_labels.before,
matcher,
)
.await?;
match self {
Self::NonInteractive => Ok(selected_tree),
Self::Interactive(editor) => {
if selected_tree.tree_ids() == trees.before.tree_ids() {
writeln!(ui.warning_default(), "Empty diff - won't run diff editor.")?;
Ok(selected_tree)
} else {
Ok(editor
.edit(
Diff::new(trees.before, &selected_tree),
matcher,
format_instructions,
)
.await?)
}
}
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct RemoteBookmarkNamePattern {
pub bookmark: StringPattern,
pub remote: StringPattern,
}
impl FromStr for RemoteBookmarkNamePattern {
type Err = String;
fn from_str(src: &str) -> Result<Self, Self::Err> {
let (maybe_kind, pat) = src
.split_once(':')
.map_or((None, src), |(kind, pat)| (Some(kind), pat));
let to_pattern = |pat: &str| {
if let Some(kind) = maybe_kind {
StringPattern::from_str_kind(pat, kind).map_err(|err| err.to_string())
} else {
StringPattern::glob(pat).map_err(|err| err.to_string())
}
};
let (bookmark, remote) = pat.rsplit_once('@').ok_or_else(|| {
"remote bookmark must be specified in bookmark@remote form".to_owned()
})?;
Ok(Self {
bookmark: to_pattern(bookmark)?,
remote: to_pattern(remote)?,
})
}
}
impl RemoteBookmarkNamePattern {
pub fn as_exact(&self) -> Option<RemoteRefSymbol<'_>> {
let bookmark = RefName::new(self.bookmark.as_exact()?);
let remote = RemoteName::new(self.remote.as_exact()?);
Some(bookmark.to_remote_symbol(remote))
}
}
impl fmt::Display for RemoteBookmarkNamePattern {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let Self { bookmark, remote } = self;
write!(f, "{bookmark}@{remote}")
}
}
pub async fn compute_commit_location(
ui: &Ui,
workspace_command: &WorkspaceCommandHelper,
destination: Option<&[RevisionArg]>,
insert_after: Option<&[RevisionArg]>,
insert_before: Option<&[RevisionArg]>,
commit_type: &str,
) -> Result<(Vec<CommitId>, Vec<CommitId>), CommandError> {
let resolve_revisions =
async |revisions: Option<&[RevisionArg]>| -> Result<Option<Vec<CommitId>>, CommandError> {
if let Some(revisions) = revisions {
Ok(Some(
workspace_command
.resolve_revsets_ordered(ui, revisions)
.await?
.into_iter()
.collect_vec(),
))
} else {
Ok(None)
}
};
let destination_commit_ids = resolve_revisions(destination).await?;
let after_commit_ids = resolve_revisions(insert_after).await?;
let before_commit_ids = resolve_revisions(insert_before).await?;
let (new_parent_ids, new_child_ids) =
match (destination_commit_ids, after_commit_ids, before_commit_ids) {
(Some(destination_commit_ids), None, None) => (destination_commit_ids, vec![]),
(None, Some(after_commit_ids), Some(before_commit_ids)) => {
(after_commit_ids, before_commit_ids)
}
(None, Some(after_commit_ids), None) => {
let new_child_ids = RevsetExpression::commits(after_commit_ids.clone())
.children()
.evaluate(workspace_command.repo().as_ref())?
.stream()
.try_collect()
.await?;
(after_commit_ids, new_child_ids)
}
(None, None, Some(before_commit_ids)) => {
let before_commits = try_join_all(
before_commit_ids
.iter()
.map(|id| workspace_command.repo().store().get_commit_async(id)),
)
.await?;
let new_parent_ids = before_commits
.iter()
.flat_map(|commit| commit.parent_ids())
.unique()
.cloned()
.collect_vec();
(new_parent_ids, before_commit_ids)
}
(Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
panic!("destination cannot be used with insert_after/insert_before")
}
(None, None, None) => {
panic!("expected at least one of destination or insert_after/insert_before")
}
};
if !new_child_ids.is_empty() {
workspace_command
.check_rewritable(new_child_ids.iter())
.await?;
ensure_no_commit_loop(
workspace_command.repo().as_ref(),
&RevsetExpression::commits(new_child_ids.clone()),
&RevsetExpression::commits(new_parent_ids.clone()),
commit_type,
)
.await?;
}
if new_parent_ids.is_empty() {
return Err(user_error("No revisions found to use as parent"));
}
Ok((new_parent_ids, new_child_ids))
}
async fn ensure_no_commit_loop(
repo: &ReadonlyRepo,
children_expression: &Arc<ResolvedRevsetExpression>,
parents_expression: &Arc<ResolvedRevsetExpression>,
commit_type: &str,
) -> Result<(), CommandError> {
if let Some(commit_id) = children_expression
.dag_range_to(parents_expression)
.evaluate(repo)?
.stream()
.try_next()
.await?
{
return Err(user_error(format!(
"Refusing to create a loop: commit {} would be both an ancestor and a descendant of \
the {commit_type}",
short_commit_hash(&commit_id),
)));
}
Ok(())
}
#[derive(clap::Parser, Clone, Debug)]
#[command(name = "jj")]
pub struct Args {
#[command(flatten)]
pub global_args: GlobalArgs,
}
#[derive(clap::Args, Clone, Debug)]
#[command(next_help_heading = "Global Options")]
pub struct GlobalArgs {
#[arg(long, short = 'R', global = true, value_hint = clap::ValueHint::DirPath)]
pub repository: Option<String>,
#[arg(long, global = true)]
pub ignore_working_copy: bool,
#[arg(long, global = true)]
pub no_integrate_operation: bool,
#[arg(long, global = true)]
pub ignore_immutable: bool,
#[arg(long, visible_alias = "at-op", global = true)]
#[arg(add = ArgValueCandidates::new(complete::operations))]
pub at_operation: Option<String>,
#[arg(long, global = true)]
pub debug: bool,
#[command(flatten)]
pub early_args: EarlyArgs,
}
#[derive(clap::Args, Clone, Debug)]
pub struct EarlyArgs {
#[arg(long, value_name = "WHEN", global = true)]
pub color: Option<ColorChoice>,
#[arg(long, global = true, action = ArgAction::SetTrue)]
pub quiet: Option<bool>,
#[arg(long, global = true, action = ArgAction::SetTrue)]
pub no_pager: Option<bool>,
#[arg(long, value_name = "NAME=VALUE", global = true)]
#[arg(add = ArgValueCompleter::new(complete::leaf_config_key_value))]
pub config: Vec<String>,
#[arg(long, value_name = "PATH", global = true, value_hint = clap::ValueHint::FilePath)]
pub config_file: Vec<String>,
}
impl EarlyArgs {
pub(crate) fn merged_config_args(&self, matches: &ArgMatches) -> Vec<(ConfigArgKind, &str)> {
merge_args_with(
matches,
&[("config", &self.config), ("config_file", &self.config_file)],
|id, value| match id {
"config" => (ConfigArgKind::Item, value.as_ref()),
"config_file" => (ConfigArgKind::File, value.as_ref()),
_ => unreachable!("unexpected id {id:?}"),
},
)
}
fn has_config_args(&self) -> bool {
!self.config.is_empty() || !self.config_file.is_empty()
}
}
#[derive(Clone, Debug)]
pub struct RevisionArg(Cow<'static, str>);
impl RevisionArg {
pub const AT: Self = Self(Cow::Borrowed("@"));
}
impl From<String> for RevisionArg {
fn from(s: String) -> Self {
Self(s.into())
}
}
impl AsRef<str> for RevisionArg {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for RevisionArg {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl ValueParserFactory for RevisionArg {
type Parser = MapValueParser<NonEmptyStringValueParser, fn(String) -> Self>;
fn value_parser() -> Self::Parser {
NonEmptyStringValueParser::new().map(Self::from)
}
}
pub fn merge_args_with<'k, 'v, T, U>(
matches: &ArgMatches,
id_values: &[(&'k str, &'v [T])],
mut convert: impl FnMut(&'k str, &'v T) -> U,
) -> Vec<U> {
let mut pos_values: Vec<(usize, U)> = Vec::new();
for (id, values) in id_values {
pos_values.extend(itertools::zip_eq(
matches.indices_of(id).into_iter().flatten(),
values.iter().map(|v| convert(id, v)),
));
}
pos_values.sort_unstable_by_key(|&(pos, _)| pos);
pos_values.into_iter().map(|(_, value)| value).collect()
}
fn resolve_default_command(
ui: &Ui,
config: &StackedConfig,
app: &Command,
mut string_args: Vec<String>,
) -> Result<Vec<String>, CommandError> {
const PRIORITY_FLAGS: &[&str] = &["--help", "-h", "--version", "-V"];
let has_priority_flag = string_args
.iter()
.any(|arg| PRIORITY_FLAGS.contains(&arg.as_str()));
if has_priority_flag {
return Ok(string_args);
}
let app_clone = app
.clone()
.allow_external_subcommands(true)
.ignore_errors(true);
let matches = app_clone.try_get_matches_from(&string_args).ok();
if let Some(matches) = matches
&& matches.subcommand_name().is_none()
{
let default_command = if let Ok(string) = config.get::<String>("ui.default-command") {
if string.contains(' ') {
let elements: ConfigValue = string.split_whitespace().collect();
writeln!(
ui.warning_default(),
"To include flags/arguments in `ui.default-command`, use an array instead of \
a string: `ui.default-command = {elements}`"
)?;
}
vec![string]
} else if let Some(array) = config.get::<Vec<String>>("ui.default-command").optional()? {
array
} else {
writeln!(
ui.hint_default(),
"Use `jj -h` for a list of available commands."
)?;
writeln!(
ui.hint_no_heading(),
"Run `jj config set --user ui.default-command log` to disable this message."
)?;
vec!["log".to_string()]
};
string_args.splice(1..1, default_command);
}
Ok(string_args)
}
fn resolve_aliases(
ui: &Ui,
config: &StackedConfig,
app: &Command,
mut string_args: Vec<String>,
) -> Result<Vec<String>, CommandError> {
let defined_aliases: HashSet<_> = config.table_keys("aliases").collect();
let mut resolved_aliases = HashSet::new();
let mut real_commands = HashSet::new();
for command in app.get_subcommands() {
real_commands.insert(command.get_name());
for alias in command.get_all_aliases() {
real_commands.insert(alias);
}
}
for alias in defined_aliases.intersection(&real_commands).sorted() {
writeln!(
ui.warning_default(),
"Cannot define an alias that overrides the built-in command '{alias}'"
)?;
}
loop {
let app_clone = app.clone().allow_external_subcommands(true);
let matches = app_clone.try_get_matches_from(&string_args).ok();
if let Some((command_name, submatches)) = matches.as_ref().and_then(|m| m.subcommand())
&& !real_commands.contains(command_name)
{
let alias_name = command_name.to_string();
let alias_args = submatches
.get_many::<OsString>("")
.unwrap_or_default()
.map(|arg| arg.to_str().unwrap().to_string())
.collect_vec();
if resolved_aliases.contains(&*alias_name) {
return Err(user_error(format!(
"Recursive alias definition involving `{alias_name}`"
)));
}
if let Some(&alias_name) = defined_aliases.get(&*alias_name) {
let alias_definition: Vec<String> = config.get(["aliases", alias_name])?;
assert!(string_args.ends_with(&alias_args));
string_args.truncate(string_args.len() - 1 - alias_args.len());
string_args.extend(alias_definition);
string_args.extend_from_slice(&alias_args);
resolved_aliases.insert(alias_name);
continue;
} else {
return Ok(string_args);
}
}
return Ok(string_args);
}
}
fn parse_early_args(
app: &Command,
args: &[String],
) -> Result<(EarlyArgs, Vec<ConfigLayer>), CommandError> {
let early_matches = app
.clone()
.disable_version_flag(true)
.disable_help_flag(true)
.arg(
clap::Arg::new("help")
.short('h')
.long("help")
.global(true)
.action(ArgAction::Count),
)
.ignore_errors(true)
.try_get_matches_from(args)?;
let args = EarlyArgs::from_arg_matches(&early_matches).unwrap();
let mut config_layers = parse_config_args(&args.merged_config_args(&early_matches))?;
let mut layer = ConfigLayer::empty(ConfigSource::CommandArg);
if let Some(choice) = args.color {
layer.set_value("ui.color", choice.to_string()).unwrap();
}
if args.quiet.unwrap_or_default() {
layer.set_value("ui.quiet", true).unwrap();
}
if args.no_pager.unwrap_or_default() {
layer.set_value("ui.paginate", "never").unwrap();
}
if !layer.is_empty() {
config_layers.push(layer);
}
Ok((args, config_layers))
}
fn handle_shell_completion(
ui: &Ui,
app: &Command,
config: &StackedConfig,
cwd: &Path,
) -> Result<(), CommandError> {
let mut orig_args = env::args_os();
let mut args = vec![];
args.extend(orig_args.by_ref().take(2));
if orig_args.len() > 0 {
let complete_index: Option<usize> = env::var("_CLAP_COMPLETE_INDEX")
.ok()
.and_then(|s| s.parse().ok());
let resolved_aliases = if let Some(index) = complete_index {
let pad_len = usize::saturating_sub(index + 1, orig_args.len());
let padded_args = orig_args
.by_ref()
.chain(std::iter::repeat_n(OsString::new(), pad_len));
let mut expanded_args =
expand_args_for_completion(ui, app, padded_args.take(index + 1), config)?;
unsafe {
env::set_var(
"_CLAP_COMPLETE_INDEX",
(expanded_args.len() - 1).to_string(),
);
}
let split_off_padding = expanded_args.split_off(expanded_args.len() - pad_len);
assert!(
split_off_padding.iter().all(|s| s.is_empty()),
"split-off padding should only consist of empty strings but was \
{split_off_padding:?}",
);
expanded_args.extend(to_string_args(orig_args)?);
expanded_args
} else {
expand_args_for_completion(ui, app, orig_args, config)?
};
args.extend(resolved_aliases.into_iter().map(OsString::from));
}
let ran_completion = clap_complete::CompleteEnv::with_factory(|| {
let mut app = app.clone();
hide_short_subcommand_aliases(&mut app);
app.allow_external_subcommands(true)
})
.try_complete(args.iter(), Some(cwd))?;
assert!(
ran_completion,
"This function should not be called without the COMPLETE variable set."
);
Ok(())
}
fn hide_short_subcommand_aliases(cmd: &mut Command) {
for cmd in cmd.get_subcommands_mut() {
hide_short_subcommand_aliases(cmd);
}
let (short_aliases, new_visible_aliases) = cmd
.get_visible_aliases()
.map(|name| name.to_owned())
.partition::<Vec<_>, _>(|name| cmd.get_name().starts_with(name));
if short_aliases.is_empty() {
return;
}
*cmd = mem::take(cmd)
.visible_alias(None)
.visible_aliases(new_visible_aliases)
.aliases(short_aliases);
}
pub fn expand_args(
ui: &Ui,
app: &Command,
args_os: impl IntoIterator<Item = OsString>,
config: &StackedConfig,
) -> Result<Vec<String>, CommandError> {
let string_args = to_string_args(args_os)?;
let string_args = resolve_default_command(ui, config, app, string_args)?;
resolve_aliases(ui, config, app, string_args)
}
fn expand_args_for_completion(
ui: &Ui,
app: &Command,
args_os: impl IntoIterator<Item = OsString>,
config: &StackedConfig,
) -> Result<Vec<String>, CommandError> {
let string_args = to_string_args(args_os)?;
let mut string_args = resolve_default_command(ui, config, app, string_args)?;
let cursor_arg = string_args.pop();
let mut resolved_args = resolve_aliases(ui, config, app, string_args)?;
resolved_args.extend(cursor_arg);
Ok(resolved_args)
}
fn to_string_args(
args_os: impl IntoIterator<Item = OsString>,
) -> Result<Vec<String>, CommandError> {
args_os
.into_iter()
.map(|arg_os| {
arg_os
.into_string()
.map_err(|_| cli_error("Non-UTF-8 argument"))
})
.collect()
}
fn parse_args(app: &Command, string_args: &[String]) -> Result<(ArgMatches, Args), clap::Error> {
let matches = app
.clone()
.arg_required_else_help(true)
.subcommand_required(true)
.try_get_matches_from(string_args)?;
let args = Args::from_arg_matches(&matches).unwrap();
Ok((matches, args))
}
fn command_name(mut matches: &ArgMatches) -> String {
let mut command = String::new();
while let Some((subcommand, new_matches)) = matches.subcommand() {
if !command.is_empty() {
command.push(' ');
}
command.push_str(subcommand);
matches = new_matches;
}
command
}
pub fn format_template<C: Clone>(ui: &Ui, arg: &C, template: &TemplateRenderer<C>) -> String {
let mut output = vec![];
template
.format(arg, ui.new_formatter(&mut output).as_mut())
.expect("write() to vec backed formatter should never fail");
output.into_string_lossy()
}
type BoxedCliDispatchFuture<'a> = Pin<Box<dyn Future<Output = Result<(), CommandError>> + 'a>>;
pub type BoxedAsyncCliDispatch<'a> = Box<dyn AsyncCliDispatch + 'a>;
type BoxedAsyncCliDispatchHook<'a> = Box<dyn AsyncCliDispatchHook + 'a>;
pub trait AsyncCliDispatch {
fn call<'a>(
self: Box<Self>,
ui: &'a mut Ui,
command_helper: &'a CommandHelper,
) -> BoxedCliDispatchFuture<'a>
where
Self: 'a;
}
trait AsyncCliDispatchHook {
fn call<'a>(
self: Box<Self>,
ui: &'a mut Ui,
command_helper: &'a CommandHelper,
old_dispatch: BoxedAsyncCliDispatch<'a>,
) -> BoxedCliDispatchFuture<'a>
where
Self: 'a;
}
struct AsyncCliDispatchFn<F>(F);
impl<F> AsyncCliDispatch for AsyncCliDispatchFn<F>
where
F: AsyncFnOnce(&mut Ui, &CommandHelper) -> Result<(), CommandError>,
{
fn call<'a>(
self: Box<Self>,
ui: &'a mut Ui,
command_helper: &'a CommandHelper,
) -> BoxedCliDispatchFuture<'a>
where
Self: 'a,
{
Box::pin((self.0)(ui, command_helper))
}
}
struct AsyncCliDispatchHookFn<F>(F);
impl<F> AsyncCliDispatchHook for AsyncCliDispatchHookFn<F>
where
F: AsyncFnOnce(&mut Ui, &CommandHelper, BoxedAsyncCliDispatch<'_>) -> Result<(), CommandError>,
{
fn call<'a>(
self: Box<Self>,
ui: &'a mut Ui,
command_helper: &'a CommandHelper,
old_dispatch: BoxedAsyncCliDispatch<'a>,
) -> BoxedCliDispatchFuture<'a>
where
Self: 'a,
{
Box::pin((self.0)(ui, command_helper, old_dispatch))
}
}
#[must_use]
pub struct CliRunner<'a> {
tracing_subscription: TracingSubscription,
app: Command,
config_layers: Vec<ConfigLayer>,
config_migrations: Vec<ConfigMigrationRule>,
store_factories: StoreFactories,
working_copy_factories: WorkingCopyFactories,
workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
revset_extensions: RevsetExtensions,
commit_template_extensions: Vec<Arc<dyn CommitTemplateLanguageExtension>>,
operation_template_extensions: Vec<Arc<dyn OperationTemplateLanguageExtension>>,
dispatch: BoxedAsyncCliDispatch<'a>,
dispatch_hooks: Vec<BoxedAsyncCliDispatchHook<'a>>,
process_global_args_fns: Vec<ProcessGlobalArgsFn<'a>>,
}
type ProcessGlobalArgsFn<'a> =
Box<dyn FnOnce(&mut Ui, &ArgMatches) -> Result<(), CommandError> + 'a>;
impl<'a> CliRunner<'a> {
pub fn init() -> Self {
let tracing_subscription = TracingSubscription::init();
crate::cleanup_guard::init();
Self {
tracing_subscription,
app: crate::commands::default_app(),
config_layers: crate::config::default_config_layers(),
config_migrations: crate::config::default_config_migrations(),
store_factories: StoreFactories::default(),
working_copy_factories: default_working_copy_factories(),
workspace_loader_factory: Box::new(DefaultWorkspaceLoaderFactory),
revset_extensions: Default::default(),
commit_template_extensions: vec![],
operation_template_extensions: vec![],
dispatch: Box::new(AsyncCliDispatchFn(crate::commands::run_command)),
dispatch_hooks: vec![],
process_global_args_fns: vec![],
}
}
pub fn name(mut self, name: &str) -> Self {
self.app = self.app.name(name.to_string());
self
}
pub fn about(mut self, about: &str) -> Self {
self.app = self.app.about(about.to_string());
self
}
pub fn version(mut self, version: &str) -> Self {
self.app = self.app.version(version.to_string());
self
}
pub fn add_extra_config(mut self, layer: ConfigLayer) -> Self {
assert_eq!(layer.source, ConfigSource::Default);
self.config_layers.push(layer);
self
}
pub fn add_extra_config_migration(mut self, rule: ConfigMigrationRule) -> Self {
self.config_migrations.push(rule);
self
}
pub fn add_store_factories(mut self, store_factories: StoreFactories) -> Self {
self.store_factories.merge(store_factories);
self
}
pub fn add_working_copy_factories(
mut self,
working_copy_factories: WorkingCopyFactories,
) -> Self {
merge_factories_map(&mut self.working_copy_factories, working_copy_factories);
self
}
pub fn set_workspace_loader_factory(
mut self,
workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
) -> Self {
self.workspace_loader_factory = workspace_loader_factory;
self
}
pub fn add_symbol_resolver_extension(
mut self,
symbol_resolver: Box<dyn SymbolResolverExtension>,
) -> Self {
self.revset_extensions.add_symbol_resolver(symbol_resolver);
self
}
pub fn add_revset_function_extension(
mut self,
name: &'static str,
func: RevsetFunction,
) -> Self {
self.revset_extensions.add_custom_function(name, func);
self
}
pub fn add_commit_template_extension(
mut self,
commit_template_extension: Box<dyn CommitTemplateLanguageExtension>,
) -> Self {
self.commit_template_extensions
.push(commit_template_extension.into());
self
}
pub fn add_operation_template_extension(
mut self,
operation_template_extension: Box<dyn OperationTemplateLanguageExtension>,
) -> Self {
self.operation_template_extensions
.push(operation_template_extension.into());
self
}
pub fn add_dispatch_hook<F>(mut self, dispatch_hook_fn: F) -> Self
where
F: AsyncFnOnce(&mut Ui, &CommandHelper, BoxedAsyncCliDispatch) -> Result<(), CommandError>
+ 'a,
{
self.dispatch_hooks
.push(Box::new(AsyncCliDispatchHookFn(dispatch_hook_fn)));
self
}
pub fn add_subcommand<C, F>(mut self, custom_dispatch_fn: F) -> Self
where
C: clap::Subcommand,
F: AsyncFnOnce(&mut Ui, &CommandHelper, C) -> Result<(), CommandError> + 'a,
{
let old_dispatch = self.dispatch;
let new_dispatch_fn =
async move |ui: &mut Ui, command_helper: &CommandHelper| match C::from_arg_matches(
command_helper.matches(),
) {
Ok(command) => custom_dispatch_fn(ui, command_helper, command).await,
Err(_) => old_dispatch.call(ui, command_helper).await,
};
self.app = C::augment_subcommands(self.app);
self.dispatch = Box::new(AsyncCliDispatchFn(new_dispatch_fn));
self
}
pub fn add_global_args<A, F>(mut self, process_before: F) -> Self
where
A: clap::Args,
F: FnOnce(&mut Ui, A) -> Result<(), CommandError> + 'a,
{
let process_global_args_fn = move |ui: &mut Ui, matches: &ArgMatches| {
let custom_args = A::from_arg_matches(matches).unwrap();
process_before(ui, custom_args)
};
self.app = A::augment_args(self.app);
self.process_global_args_fns
.push(Box::new(process_global_args_fn));
self
}
#[instrument(skip_all)]
async fn run_internal(
self,
ui: &mut Ui,
mut raw_config: RawConfig,
) -> Result<(), CommandError> {
let cwd = env::current_dir()
.and_then(dunce::canonicalize)
.map_err(|_| {
user_error("Could not determine current directory").hinted(
"Did you update to a commit where the directory doesn't exist or can't be \
accessed?",
)
})?;
let mut config_env = ConfigEnv::from_environment();
let mut last_config_migration_descriptions = Vec::new();
let mut migrate_config = |config: &mut StackedConfig| -> Result<(), CommandError> {
last_config_migration_descriptions =
jj_lib::config::migrate(config, &self.config_migrations)?;
Ok(())
};
let maybe_cwd_workspace_loader = self
.workspace_loader_factory
.create(find_workspace_dir(&cwd))
.map_err(|err| map_workspace_load_error(err, Some(".")));
config_env.reload_user_config(&mut raw_config)?;
if let Ok(loader) = &maybe_cwd_workspace_loader {
config_env.reset_repo_path(loader.repo_path());
config_env.reload_repo_config(ui, &mut raw_config)?;
config_env.reset_workspace_path(loader.workspace_root());
config_env.reload_workspace_config(ui, &mut raw_config)?;
}
let mut config = config_env.resolve_config(&raw_config)?;
migrate_config(&mut config)?;
ui.reset(&config)?;
if env::var_os("COMPLETE").is_some_and(|v| !v.is_empty() && v != "0") {
return handle_shell_completion(&Ui::null(), &self.app, &config, &cwd);
}
let string_args = expand_args(ui, &self.app, env::args_os(), &config)?;
let (args, config_layers) = parse_early_args(&self.app, &string_args)?;
if !config_layers.is_empty() {
raw_config.as_mut().extend_layers(config_layers);
config = config_env.resolve_config(&raw_config)?;
migrate_config(&mut config)?;
ui.reset(&config)?;
}
if args.has_config_args() {
warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
}
let (matches, args) = parse_args(&self.app, &string_args)
.map_err(|err| map_clap_cli_error(err, ui, &config))?;
if args.global_args.debug {
self.tracing_subscription.enable_debug_logging()?;
}
for process_global_args_fn in self.process_global_args_fns {
process_global_args_fn(ui, &matches)?;
}
config_env.set_command_name(command_name(&matches));
let maybe_workspace_loader = if let Some(path) = &args.global_args.repository {
let abs_path = cwd.join(path);
let abs_path = dunce::canonicalize(&abs_path).unwrap_or(abs_path);
let loader = self
.workspace_loader_factory
.create(&abs_path)
.map_err(|err| map_workspace_load_error(err, Some(path)))?;
config_env.reset_repo_path(loader.repo_path());
config_env.reload_repo_config(ui, &mut raw_config)?;
config_env.reset_workspace_path(loader.workspace_root());
config_env.reload_workspace_config(ui, &mut raw_config)?;
Ok(loader)
} else {
maybe_cwd_workspace_loader
};
config = config_env.resolve_config(&raw_config)?;
migrate_config(&mut config)?;
ui.reset(&config)?;
for (source, desc) in &last_config_migration_descriptions {
let source_str = match source {
ConfigSource::Default => "default-provided",
ConfigSource::EnvBase | ConfigSource::EnvOverrides => "environment-provided",
ConfigSource::User => "user-level",
ConfigSource::Repo => "repo-level",
ConfigSource::Workspace => "workspace-level",
ConfigSource::CommandArg => "CLI-provided",
};
writeln!(
ui.warning_default(),
"Deprecated {source_str} config: {desc}"
)?;
}
if args.global_args.repository.is_some() {
warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
}
let settings = UserSettings::from_config(config)?;
let command_helper_data = CommandHelperData {
app: self.app,
cwd,
string_args,
matches,
global_args: args.global_args,
config_env,
config_migrations: self.config_migrations,
raw_config,
settings,
revset_extensions: self.revset_extensions.into(),
commit_template_extensions: self.commit_template_extensions,
operation_template_extensions: self.operation_template_extensions,
maybe_workspace_loader,
store_factories: self.store_factories,
working_copy_factories: self.working_copy_factories,
workspace_loader_factory: self.workspace_loader_factory,
};
let command_helper = CommandHelper {
data: Rc::new(command_helper_data),
};
let dispatch =
self.dispatch_hooks
.into_iter()
.fold(self.dispatch, |old_dispatch, dispatch_hook| {
let f = async move |ui: &mut Ui, command_helper: &CommandHelper| {
dispatch_hook.call(ui, command_helper, old_dispatch).await
};
Box::new(AsyncCliDispatchFn(f))
});
dispatch.call(ui, &command_helper).await
}
#[must_use]
#[instrument(skip(self))]
pub fn run(mut self) -> u8 {
crossterm::style::force_color_output(true);
let config = config_from_environment(self.config_layers.drain(..));
let mut ui = Ui::with_config(config.as_ref())
.expect("default config should be valid, env vars are stringly typed");
let result = self.run_internal(&mut ui, config).block_on();
let exit_code = handle_command_result(&mut ui, result);
ui.finalize_pager();
exit_code
}
}
fn map_clap_cli_error(err: clap::Error, ui: &Ui, config: &StackedConfig) -> CommandError {
if let Some(ContextValue::String(cmd)) = err.get(ContextKind::InvalidSubcommand) {
let remove_useless_error_context = |mut err: clap::Error| {
err.remove(ContextKind::SuggestedSubcommand);
err.remove(ContextKind::Suggested); err.remove(ContextKind::Usage); err
};
match cmd.as_str() {
"clone" | "init" => {
let cmd = cmd.clone();
return CommandError::from(remove_useless_error_context(err))
.hinted(format!(
"You probably want `jj git {cmd}`. See also `jj help git`."
))
.hinted(format!(
r#"You can configure `aliases.{cmd} = ["git", "{cmd}"]` if you want `jj {cmd}` to work and always use the Git backend."#
));
}
"amend" => {
return CommandError::from(remove_useless_error_context(err))
.hinted(
r#"You probably want `jj squash`. You can configure `aliases.amend = ["squash"]` if you want `jj amend` to work."#);
}
_ => {}
}
}
if let (Some(ContextValue::String(arg)), Some(ContextValue::String(value))) = (
err.get(ContextKind::InvalidArg),
err.get(ContextKind::InvalidValue),
) && arg.as_str() == "--template <TEMPLATE>"
&& value.is_empty()
{
if let Ok(template_aliases) = load_template_aliases(ui, config) {
return CommandError::from(err).hinted(format_template_aliases_hint(&template_aliases));
}
}
CommandError::from(err)
}
fn format_template_aliases_hint(template_aliases: &TemplateAliasesMap) -> String {
let mut hint = String::from("The following template aliases are defined:\n");
hint.push_str(
&template_aliases
.symbol_names()
.sorted_unstable()
.map(|name| format!("- {name}"))
.join("\n"),
);
hint
}
fn warn_if_args_mismatch(
ui: &Ui,
app: &Command,
config: &StackedConfig,
expected_args: &[String],
) -> Result<(), CommandError> {
let new_string_args = expand_args(ui, app, env::args_os(), config).ok();
if new_string_args.as_deref() != Some(expected_args) {
writeln!(
ui.warning_default(),
"Command aliases cannot be loaded from -R/--repository path or --config/--config-file \
arguments."
)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use clap::CommandFactory as _;
use super::*;
#[derive(clap::Parser, Clone, Debug)]
pub struct TestArgs {
#[arg(long)]
pub foo: Vec<u32>,
#[arg(long)]
pub bar: Vec<u32>,
#[arg(long)]
pub baz: bool,
}
#[test]
fn test_merge_args_with() {
let command = TestArgs::command();
let parse = |args: &[&str]| -> Vec<(&'static str, u32)> {
let matches = command.clone().try_get_matches_from(args).unwrap();
let args = TestArgs::from_arg_matches(&matches).unwrap();
merge_args_with(
&matches,
&[("foo", &args.foo), ("bar", &args.bar)],
|id, value| (id, *value),
)
};
assert_eq!(parse(&["jj"]), vec![]);
assert_eq!(parse(&["jj", "--foo=1"]), vec![("foo", 1)]);
assert_eq!(
parse(&["jj", "--foo=1", "--bar=2"]),
vec![("foo", 1), ("bar", 2)]
);
assert_eq!(
parse(&["jj", "--foo=1", "--baz", "--bar=2", "--foo", "3"]),
vec![("foo", 1), ("bar", 2), ("foo", 3)]
);
}
}