jj/cli/src/cli_util.rs

3438 lines
127 KiB
Rust

// Copyright 2022 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::borrow::Cow;
use std::cell::OnceCell;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::env;
use std::env::ArgsOs;
use std::env::VarError;
use std::ffi::OsString;
use std::fmt;
use std::fmt::Debug;
use std::fs;
use std::io;
use std::io::Write as _;
use std::mem;
use std::path::Path;
use std::path::PathBuf;
use std::process::ExitCode;
use std::rc::Rc;
use std::str;
use std::str::FromStr;
use std::sync::Arc;
use std::time::SystemTime;
use bstr::ByteVec as _;
use chrono::TimeZone;
use clap::builder::MapValueParser;
use clap::builder::NonEmptyStringValueParser;
use clap::builder::TypedValueParser;
use clap::builder::ValueParserFactory;
use clap::error::ContextKind;
use clap::error::ContextValue;
use clap::ArgAction;
use clap::ArgMatches;
use clap::Command;
use clap::FromArgMatches;
use indexmap::IndexMap;
use indexmap::IndexSet;
use itertools::Itertools;
use jj_lib::backend::ChangeId;
use jj_lib::backend::CommitId;
use jj_lib::backend::MergedTreeId;
use jj_lib::backend::TreeValue;
use jj_lib::commit::Commit;
use jj_lib::dag_walk;
use jj_lib::file_util;
use jj_lib::fileset;
use jj_lib::fileset::FilesetDiagnostics;
use jj_lib::fileset::FilesetExpression;
use jj_lib::git;
use jj_lib::git_backend::GitBackend;
use jj_lib::gitignore::GitIgnoreError;
use jj_lib::gitignore::GitIgnoreFile;
use jj_lib::hex_util::to_reverse_hex;
use jj_lib::id_prefix::IdPrefixContext;
use jj_lib::matchers::Matcher;
use jj_lib::merge::MergedTreeValue;
use jj_lib::merged_tree::MergedTree;
use jj_lib::object_id::ObjectId;
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_store::WorkspaceId;
use jj_lib::op_walk;
use jj_lib::op_walk::OpsetEvaluationError;
use jj_lib::operation::Operation;
use jj_lib::repo::merge_factories_map;
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_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::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::RevsetIteratorExt;
use jj_lib::revset::RevsetModifier;
use jj_lib::revset::RevsetParseContext;
use jj_lib::revset::RevsetWorkspaceContext;
use jj_lib::revset::SymbolResolverExtension;
use jj_lib::rewrite::restore_tree;
use jj_lib::settings::ConfigResultExt as _;
use jj_lib::settings::UserSettings;
use jj_lib::signing::SignInitError;
use jj_lib::str_util::StringPattern;
use jj_lib::transaction::Transaction;
use jj_lib::view::View;
use jj_lib::working_copy::CheckoutStats;
use jj_lib::working_copy::LockedWorkingCopy;
use jj_lib::working_copy::SnapshotOptions;
use jj_lib::working_copy::WorkingCopy;
use jj_lib::working_copy::WorkingCopyFactory;
use jj_lib::workspace::default_working_copy_factories;
use jj_lib::workspace::get_working_copy_factory;
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 tracing::instrument;
use tracing_chrome::ChromeLayerBuilder;
use tracing_subscriber::prelude::*;
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_parse_diagnostics;
use crate::command_error::user_error;
use crate::command_error::user_error_with_hint;
use crate::command_error::user_error_with_message;
use crate::command_error::CommandError;
use crate::commit_templater::CommitTemplateLanguage;
use crate::commit_templater::CommitTemplateLanguageExtension;
use crate::config::new_config_path;
use crate::config::AnnotatedValue;
use crate::config::CommandNameAndArgs;
use crate::config::ConfigNamePathBuf;
use crate::config::ConfigSource;
use crate::config::LayeredConfigs;
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::PlainTextFormatter;
use crate::git_util::is_colocated_git_workspace;
use crate::git_util::print_failed_git_export;
use crate::git_util::print_git_import_stats;
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::template_builder;
use crate::template_builder::TemplateLanguage;
use crate::template_parser::TemplateAliasesMap;
use crate::template_parser::TemplateDiagnostics;
use crate::templater::PropertyPlaceholder;
use crate::templater::TemplateRenderer;
use crate::text_util;
use crate::ui::ColorChoice;
use crate::ui::Ui;
const SHORT_CHANGE_ID_TEMPLATE_TEXT: &str = "format_short_change_id(self.change_id())";
#[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()
}
}
/// Handle to initialize or change tracing subscription.
#[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 {
/// Initializes tracing with the default configuration. This should be
/// called as early as possible.
pub fn init() -> Self {
let filter = tracing_subscriber::EnvFilter::builder()
.with_default_directive(tracing::metadata::LevelFilter::ERROR.into())
.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();
TracingSubscription {
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::DEBUG.into())
.from_env_lossy();
})
.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,
settings: UserSettings,
layered_configs: LayeredConfigs,
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,
}
impl CommandHelper {
pub fn app(&self) -> &Command {
&self.data.app
}
/// Canonical form of the current working directory path.
///
/// A loaded `Workspace::workspace_root()` also returns a canonical path, so
/// relative paths can be easily computed from these paths.
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 settings(&self) -> &UserSettings {
&self.data.settings
}
pub fn resolved_config_values(
&self,
prefix: &ConfigNamePathBuf,
) -> Result<Vec<AnnotatedValue>, crate::config::ConfigError> {
self.data.layered_configs.resolved_config_values(prefix)
}
pub fn revset_extensions(&self) -> &Arc<RevsetExtensions> {
&self.data.revset_extensions
}
/// Loads template aliases from the configs.
///
/// For most commands that depend on a loaded repo, you should use
/// `WorkspaceCommandHelper::template_aliases_map()` instead.
fn load_template_aliases(&self, ui: &Ui) -> Result<TemplateAliasesMap, CommandError> {
load_template_aliases(ui, &self.data.layered_configs)
}
/// Parses template of the given language into evaluation tree.
///
/// This function also loads template aliases from the settings. Use
/// `WorkspaceCommandHelper::parse_template()` if you've already
/// instantiated the workspace helper.
pub fn parse_template<'a, C: Clone + 'a, L: TemplateLanguage<'a> + ?Sized>(
&self,
ui: &Ui,
language: &L,
template_text: &str,
wrap_self: impl Fn(PropertyPlaceholder<C>) -> L::Property,
) -> Result<TemplateRenderer<'a, C>, CommandError> {
let mut diagnostics = TemplateDiagnostics::new();
let aliases = self.load_template_aliases(ui)?;
let template = template_builder::parse(
language,
&mut diagnostics,
template_text,
&aliases,
wrap_self,
)?;
print_parse_diagnostics(ui, "In template expression", &diagnostics)?;
Ok(template)
}
pub fn workspace_loader(&self) -> Result<&dyn WorkspaceLoader, CommandError> {
self.data
.maybe_workspace_loader
.as_deref()
.map_err(Clone::clone)
}
/// Loads workspace and repo, then snapshots the working copy if allowed.
#[instrument(skip(self, ui))]
pub fn workspace_helper(&self, ui: &Ui) -> Result<WorkspaceCommandHelper, CommandError> {
let mut workspace_command = self.workspace_helper_no_snapshot(ui)?;
workspace_command.maybe_snapshot(ui)?;
Ok(workspace_command)
}
/// Loads workspace and repo, but never snapshots the working copy. Most
/// commands should use `workspace_helper()` instead.
#[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())?;
let repo = workspace.repo_loader().load_at(&op_head)?;
let env = self.workspace_environment(ui, &workspace)?;
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()?;
// We convert StoreLoadError -> WorkspaceLoadError -> CommandError
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())
})
}
/// Loads command environment for the given `workspace`.
pub fn workspace_environment(
&self,
ui: &Ui,
workspace: &Workspace,
) -> Result<WorkspaceCommandEnvironment, CommandError> {
WorkspaceCommandEnvironment::new(ui, self, workspace)
}
/// Returns true if the working copy to be loaded is writable, and therefore
/// should usually be snapshotted.
pub fn is_working_copy_writable(&self) -> bool {
self.is_at_head_operation() && !self.data.global_args.ignore_working_copy
}
/// Returns true if the current operation is considered to be the head.
pub fn is_at_head_operation(&self) -> bool {
// TODO: should we accept --at-op=<head_id> as the head op? or should we
// make --at-op=@ imply --ignore-workign-copy (i.e. not at the head.)
matches!(
self.data.global_args.at_operation.as_deref(),
None | Some("@")
)
}
/// Resolves the current operation from the command-line argument.
///
/// If no `--at-operation` is specified, the head operations will be
/// loaded. If there are multiple heads, they'll be merged.
#[instrument(skip_all)]
pub fn resolve_operation(
&self,
ui: &Ui,
repo_loader: &RepoLoader,
) -> 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)?)
} else {
op_heads_store::resolve_op_heads(
repo_loader.op_heads_store().as_ref(),
repo_loader.op_store(),
|op_heads| {
writeln!(
ui.status(),
"Concurrent modification detected, resolving automatically.",
)?;
let base_repo = repo_loader.load_at(&op_heads[0])?;
// TODO: It may be helpful to print each operation we're merging here
let mut tx = start_repo_transaction(
&base_repo,
&self.data.settings,
&self.data.string_args,
);
for other_op_head in op_heads.into_iter().skip(1) {
tx.merge_operation(other_op_head)?;
let num_rebased = tx.repo_mut().rebase_descendants(&self.data.settings)?;
if num_rebased > 0 {
writeln!(
ui.status(),
"Rebased {num_rebased} descendant commits onto commits rewritten \
by other operation"
)?;
}
}
Ok(tx
.write("reconcile divergent operations")
.leave_unpublished()
.operation()
.clone())
},
)
}
}
/// Creates helper for the repo whose view is supposed to be in sync with
/// the working copy. If `--ignore-working-copy` is not specified, the
/// returned helper will attempt to update the working copy.
#[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)
}
}
/// A ReadonlyRepo along with user-config-dependent derived data. The derived
/// data is lazily loaded.
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 fn git_backend(&self) -> Option<&GitBackend> {
self.repo.store().backend_impl().downcast_ref()
}
}
/// A advanceable bookmark to satisfy the "advance-bookmarks" feature.
///
/// This is a helper for `WorkspaceCommandTransaction`. It provides a
/// type-safe way to separate the work of checking whether a bookmark
/// can be advanced and actually advancing it. Advancing the bookmark
/// never fails, but can't be done until the new `CommitId` is
/// available. Splitting the work in this way also allows us to
/// identify eligible bookmarks without actually moving them and
/// return config errors to the user early.
pub struct AdvanceableBookmark {
name: String,
old_commit_id: CommitId,
}
/// Helper for parsing and evaluating settings for the advance-bookmarks
/// feature. Settings are configured in the jj config.toml as lists of
/// [`StringPattern`]s for enabled and disabled bookmarks. Example:
/// ```toml
/// [experimental-advance-branches]
/// # Enable the feature for all branches except "main".
/// enabled-branches = ["glob:*"]
/// disabled-branches = ["main"]
/// ```
struct AdvanceBookmarksSettings {
enabled_bookmarks: Vec<StringPattern>,
disabled_bookmarks: Vec<StringPattern>,
}
impl AdvanceBookmarksSettings {
fn from_config(config: &config::Config) -> Result<Self, CommandError> {
let get_setting = |setting_key| {
let setting = format!("experimental-advance-branches.{setting_key}");
match config.get::<Vec<String>>(&setting).optional()? {
Some(patterns) => patterns
.into_iter()
.map(|s| {
StringPattern::parse(&s).map_err(|e| {
config_error_with_message(
format!("Error parsing '{s}' for {setting}"),
e,
)
})
})
.collect(),
None => Ok(Vec::new()),
}
};
Ok(Self {
enabled_bookmarks: get_setting("enabled-branches")?,
disabled_bookmarks: get_setting("disabled-branches")?,
})
}
/// Returns true if the advance-bookmarks feature is enabled for
/// `bookmark_name`.
fn bookmark_is_eligible(&self, bookmark_name: &str) -> bool {
if self
.disabled_bookmarks
.iter()
.any(|d| d.matches(bookmark_name))
{
return false;
}
self.enabled_bookmarks
.iter()
.any(|e| e.matches(bookmark_name))
}
/// Returns true if the config includes at least one "enabled-branches"
/// pattern.
fn feature_enabled(&self) -> bool {
!self.enabled_bookmarks.is_empty()
}
}
/// Metadata and configuration loaded for a specific workspace.
pub struct WorkspaceCommandEnvironment {
command: CommandHelper,
revset_aliases_map: RevsetAliasesMap,
template_aliases_map: TemplateAliasesMap,
path_converter: RepoPathUiConverter,
workspace_id: WorkspaceId,
immutable_heads_expression: Rc<RevsetExpression>,
short_prefixes_expression: Option<Rc<RevsetExpression>>,
}
impl WorkspaceCommandEnvironment {
#[instrument(skip_all)]
fn new(ui: &Ui, command: &CommandHelper, workspace: &Workspace) -> Result<Self, CommandError> {
let revset_aliases_map =
revset_util::load_revset_aliases(ui, &command.data.layered_configs)?;
let template_aliases_map = command.load_template_aliases(ui)?;
let path_converter = RepoPathUiConverter::Fs {
cwd: command.cwd().to_owned(),
base: workspace.workspace_root().to_owned(),
};
let mut env = Self {
command: command.clone(),
revset_aliases_map,
template_aliases_map,
path_converter,
workspace_id: workspace.workspace_id().to_owned(),
immutable_heads_expression: RevsetExpression::root(),
short_prefixes_expression: None,
};
env.immutable_heads_expression = env.load_immutable_heads_expression(ui)?;
env.short_prefixes_expression = env.load_short_prefixes_expression(ui)?;
Ok(env)
}
pub fn settings(&self) -> &UserSettings {
self.command.settings()
}
pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
&self.path_converter
}
pub fn workspace_id(&self) -> &WorkspaceId {
&self.workspace_id
}
pub(crate) fn revset_parse_context(&self) -> RevsetParseContext {
let workspace_context = RevsetWorkspaceContext {
path_converter: &self.path_converter,
workspace_id: &self.workspace_id,
};
let now = if let Some(timestamp) = self.settings().commit_timestamp() {
chrono::Local
.timestamp_millis_opt(timestamp.timestamp.0)
.unwrap()
} else {
chrono::Local::now()
};
RevsetParseContext::new(
&self.revset_aliases_map,
self.settings().user_email(),
now.into(),
self.command.revset_extensions(),
Some(workspace_context),
)
}
/// Creates fresh new context which manages cache of short commit/change ID
/// prefixes. New context should be created per repo view (or operation.)
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()),
}
}
/// User-configured expression defining the immutable set.
pub fn immutable_expression(&self) -> Rc<RevsetExpression> {
// Negated ancestors expression `~::(<heads> | root())` is slightly
// easier to optimize than negated union `~(::<heads> | root())`.
self.immutable_heads_expression.ancestors()
}
/// User-configured expression defining the heads of the immutable set.
pub fn immutable_heads_expression(&self) -> &Rc<RevsetExpression> {
&self.immutable_heads_expression
}
fn load_immutable_heads_expression(
&self,
ui: &Ui,
) -> Result<Rc<RevsetExpression>, 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<Rc<RevsetExpression>>, CommandError> {
let revset_string = self
.settings()
.config()
.get_string("revsets.short-prefixes")
.unwrap_or_else(|_| self.settings().default_revset());
if revset_string.is_empty() {
Ok(None)
} else {
let mut diagnostics = RevsetDiagnostics::new();
let (expression, modifier) = revset::parse_with_modifier(
&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)?;
let (None | Some(RevsetModifier::All)) = modifier;
Ok(Some(revset::optimize(expression)))
}
}
fn check_repo_rewritable<'a>(
&self,
repo: &dyn Repo,
commits: impl IntoIterator<Item = &'a CommitId>,
) -> Result<(), CommandError> {
if self.command.global_args().ignore_immutable {
let root_id = repo.store().root_commit_id();
return if commits.into_iter().contains(root_id) {
Err(user_error(format!(
"The root commit {} is immutable",
short_commit_hash(root_id),
)))
} else {
Ok(())
};
}
// Not using self.id_prefix_context() because the disambiguation data
// must not be calculated and cached against arbitrary repo. It's also
// unlikely that the immutable expression contains short hashes.
let id_prefix_context = IdPrefixContext::new(self.command.revset_extensions().clone());
let to_rewrite_revset =
RevsetExpression::commits(commits.into_iter().cloned().collect_vec());
let mut expression = RevsetExpressionEvaluator::new(
repo,
self.command.revset_extensions().clone(),
&id_prefix_context,
self.immutable_expression(),
);
expression.intersect_with(&to_rewrite_revset);
let mut commit_id_iter = expression.evaluate_to_commit_ids().map_err(|e| {
config_error_with_message("Invalid `revset-aliases.immutable_heads()`", e)
})?;
if let Some(commit_id) = commit_id_iter.next() {
let error = if &commit_id == repo.store().root_commit_id() {
user_error(format!(
"The root commit {} is immutable",
short_commit_hash(&commit_id),
))
} else {
user_error_with_hint(
format!("Commit {} is immutable", short_commit_hash(&commit_id)),
"Pass `--ignore-immutable` or configure the set of immutable commits via \
`revset-aliases.immutable_heads()`.",
)
};
return Err(error);
}
Ok(())
}
/// Parses template of the given language into evaluation tree.
///
/// `wrap_self` specifies the type of the top-level property, which should
/// be one of the `L::wrap_*()` functions.
pub fn parse_template<'a, C: Clone + 'a, L: TemplateLanguage<'a> + ?Sized>(
&self,
ui: &Ui,
language: &L,
template_text: &str,
wrap_self: impl Fn(PropertyPlaceholder<C>) -> L::Property,
) -> Result<TemplateRenderer<'a, C>, CommandError> {
let mut diagnostics = TemplateDiagnostics::new();
let template = template_builder::parse(
language,
&mut diagnostics,
template_text,
&self.template_aliases_map,
wrap_self,
)?;
print_parse_diagnostics(ui, "In template expression", &diagnostics)?;
Ok(template)
}
/// Creates commit template language environment for this workspace and the
/// given `repo`.
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_id,
self.revset_parse_context(),
id_prefix_context,
self.immutable_expression(),
&self.command.data.commit_template_extensions,
)
}
pub fn operation_template_extensions(&self) -> &[Arc<dyn OperationTemplateLanguageExtension>] {
&self.command.data.operation_template_extensions
}
}
/// Provides utilities for writing a command that works on a [`Workspace`]
/// (which most commands do).
pub struct WorkspaceCommandHelper {
workspace: Workspace,
user_repo: ReadonlyUserRepo,
env: WorkspaceCommandEnvironment,
// TODO: Parsed template can be cached if it doesn't capture 'repo lifetime
commit_summary_template_text: String,
op_summary_template_text: String,
may_update_working_copy: bool,
working_copy_shared_with_git: bool,
}
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 = env.settings();
let commit_summary_template_text =
settings.config().get_string("templates.commit_summary")?;
let op_summary_template_text = settings.config().get_string("templates.op_summary")?;
let may_update_working_copy =
loaded_at_head && !env.command.global_args().ignore_working_copy;
let working_copy_shared_with_git = 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_update_working_copy,
working_copy_shared_with_git,
};
// Parse commit_summary template early to report error before starting
// mutable operation.
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.env.settings()
}
pub fn git_backend(&self) -> Option<&GitBackend> {
self.user_repo.git_backend()
}
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 {
"Don't use --at-op."
};
Err(user_error_with_hint(
"This command must be able to update the working copy.",
hint,
))
}
}
/// Snapshot the working copy if allowed, and import Git refs if the working
/// copy is collocated with Git.
#[instrument(skip_all)]
pub fn maybe_snapshot(&mut self, ui: &Ui) -> Result<(), CommandError> {
if self.may_update_working_copy {
if self.working_copy_shared_with_git {
self.import_git_head(ui)?;
}
// Because the Git refs (except HEAD) aren't imported yet, the ref
// pointing to the new working-copy commit might not be exported.
// In that situation, the ref would be conflicted anyway, so export
// failure is okay.
self.snapshot_working_copy(ui)?;
// import_git_refs() can rebase the working-copy commit.
if self.working_copy_shared_with_git {
self.import_git_refs(ui)?;
}
}
Ok(())
}
/// Imports new HEAD from the colocated Git repo.
///
/// If the Git HEAD has changed, this function checks out the new Git HEAD.
/// The old working-copy commit will be abandoned if it's discardable. The
/// working-copy state will be reset to point to the new Git HEAD. The
/// working-copy contents won't be updated.
#[instrument(skip_all)]
fn import_git_head(&mut self, ui: &Ui) -> Result<(), CommandError> {
assert!(self.may_update_working_copy);
let command = self.env.command.clone();
let mut tx = self.start_transaction();
git::import_head(tx.repo_mut())?;
if !tx.repo().has_changes() {
return Ok(());
}
// TODO: There are various ways to get duplicated working-copy
// commits. Some of them could be mitigated by checking the working-copy
// operation id after acquiring the lock, but that isn't enough.
//
// - moved HEAD was observed by multiple jj processes, and new working-copy
// commits are created concurrently.
// - new HEAD was exported by jj, but the operation isn't committed yet.
// - new HEAD was exported by jj, but the new working-copy commit isn't checked
// out yet.
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_id = self.workspace_id().to_owned();
let new_git_head_commit = tx.repo().store().get_commit(new_git_head_id)?;
tx.repo_mut()
.check_out(workspace_id, command.settings(), &new_git_head_commit)?;
let mut locked_ws = self.workspace.start_working_copy_mutation()?;
// The working copy was presumably updated by the git command that updated
// HEAD, so we just need to reset our working copy
// state to it without updating working copy files.
locked_ws.locked_wc().reset(&new_git_head_commit)?;
tx.repo_mut().rebase_descendants(command.settings())?;
self.user_repo = ReadonlyUserRepo::new(tx.commit("import git head"));
locked_ws.finish(self.user_repo.repo.op_id().clone())?;
if old_git_head.is_present() {
writeln!(
ui.status(),
"Reset the working copy parent to the new Git HEAD."
)?;
} else {
// Don't print verbose message on initial checkout.
}
} else {
// Unlikely, but the HEAD ref got deleted by git?
self.finish_transaction(ui, tx, "import git head")?;
}
Ok(())
}
/// Imports branches and tags from the underlying Git repo, abandons old
/// bookmarks.
///
/// If the working-copy branch is rebased, and if update is allowed, the
/// new working-copy commit will be checked out.
///
/// This function does not import the Git HEAD, but the HEAD may be reset to
/// the working copy parent if the repository is colocated.
#[instrument(skip_all)]
fn import_git_refs(&mut self, ui: &Ui) -> Result<(), CommandError> {
let git_settings = self.settings().git_settings();
let mut tx = self.start_transaction();
// Automated import shouldn't fail because of reserved remote name.
let stats = git::import_some_refs(tx.repo_mut(), &git_settings, |ref_name| {
!git::is_reserved_git_remote_ref(ref_name)
})?;
if !tx.repo().has_changes() {
return Ok(());
}
print_git_import_stats(ui, tx.repo(), &stats, false)?;
let mut tx = tx.into_inner();
// Rebase here to show slightly different status message.
let num_rebased = tx.repo_mut().rebase_descendants(self.settings())?;
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")?;
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 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(wc_commit_id)?
} else {
return Err(user_error("Nothing checked out in this workspace"));
};
let locked_ws = self.workspace.start_working_copy_mutation()?;
Ok((locked_ws, wc_commit))
}
pub fn start_working_copy_mutation(
&mut self,
) -> Result<(LockedWorkspace, Commit), CommandError> {
let (mut locked_ws, wc_commit) = self.unchecked_start_working_copy_mutation()?;
if wc_commit.tree_id() != locked_ws.locked_wc().old_tree_id() {
return Err(user_error("Concurrent working copy operation. Try again."));
}
Ok((locked_ws, wc_commit))
}
pub fn workspace_root(&self) -> &Path {
self.workspace.workspace_root()
}
pub fn workspace_id(&self) -> &WorkspaceId {
self.workspace.workspace_id()
}
pub fn get_wc_commit_id(&self) -> Option<&CommitId> {
self.repo().view().get_wc_commit_id(self.workspace_id())
}
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)
}
/// Parses a path relative to cwd into a RepoPath, which is relative to the
/// workspace root.
pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
self.path_converter().parse_file_path(input)
}
/// Parses the given strings as file patterns.
pub fn parse_file_patterns(
&self,
ui: &Ui,
values: &[String],
) -> Result<FilesetExpression, CommandError> {
// TODO: This function might be superseded by parse_union_filesets(),
// but it would be weird if parse_union_*() had a special case for the
// empty arguments.
if values.is_empty() {
Ok(FilesetExpression::all())
} else if self.settings().config().get_bool("ui.allow-filesets")? {
self.parse_union_filesets(ui, values)
} else {
let expressions = values
.iter()
.map(|v| self.parse_file_path(v))
.map_ok(FilesetExpression::prefix_path)
.try_collect()?;
Ok(FilesetExpression::union_all(expressions))
}
}
/// Parses the given fileset expressions and concatenates them all.
pub fn parse_union_filesets(
&self,
ui: &Ui,
file_args: &[String], // TODO: introduce FileArg newtype?
) -> Result<FilesetExpression, CommandError> {
let mut diagnostics = FilesetDiagnostics::new();
let expressions: Vec<_> = file_args
.iter()
.map(|arg| fileset::parse_maybe_bare(&mut diagnostics, arg, self.path_converter()))
.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().config().get_string("snapshot.auto-track")?;
let expression = fileset::parse(
&mut diagnostics,
&pattern,
&RepoPathUiConverter::Fs {
cwd: "".into(),
base: "".into(),
},
)?;
print_parse_diagnostics(ui, "In `snapshot.auto-track`", &diagnostics)?;
Ok(expression.to_matcher())
}
pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
self.env.path_converter()
}
#[instrument(skip_all)]
pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
let get_excludes_file_path = |config: &gix::config::File| -> Option<PathBuf> {
// TODO: maybe use path() and interpolate(), which can process non-utf-8
// path on Unix.
if let Some(value) = config.string("core.excludesFile") {
let path = str::from_utf8(&value)
.ok()
.map(file_util::expand_home_path)?;
// The configured path is usually absolute, but if it's relative,
// the "git" command would read the file at the work-tree directory.
Some(self.workspace_root().join(path))
} else {
xdg_config_home().ok().map(|x| x.join("git").join("ignore"))
}
};
fn xdg_config_home() -> Result<PathBuf, VarError> {
if let Ok(x) = std::env::var("XDG_CONFIG_HOME") {
if !x.is_empty() {
return Ok(PathBuf::from(x));
}
}
std::env::var("HOME").map(|x| Path::new(&x).join(".config"))
}
let mut git_ignores = GitIgnoreFile::empty();
if let Some(git_backend) = self.git_backend() {
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("", excludes_file_path)?;
}
git_ignores = git_ignores
.chain_with_file("", git_backend.git_repo_path().join("info").join("exclude"))?;
} else if let Ok(git_config) = gix::config::File::from_globals() {
if let Some(excludes_file_path) = get_excludes_file_path(&git_config) {
git_ignores = git_ignores.chain_with_file("", excludes_file_path)?;
}
}
Ok(git_ignores)
}
/// Creates textual diff renderer of the specified `formats`.
pub fn diff_renderer(&self, formats: Vec<DiffFormat>) -> DiffRenderer<'_> {
DiffRenderer::new(self.repo().as_ref(), self.path_converter(), formats)
}
/// Loads textual diff renderer from the settings and command arguments.
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))
}
/// Loads textual diff renderer from the settings and log-like command
/// arguments. Returns `Ok(None)` if there are no command arguments that
/// enable patch output.
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)))
}
/// Loads diff editor from the settings.
///
/// If the `tool_name` isn't specified, the default editor will be returned.
pub fn diff_editor(
&self,
ui: &Ui,
tool_name: Option<&str>,
) -> Result<DiffEditor, CommandError> {
let base_ignores = self.base_ignores()?;
if let Some(name) = tool_name {
Ok(DiffEditor::with_name(name, self.settings(), base_ignores)?)
} else {
Ok(DiffEditor::from_settings(
ui,
self.settings(),
base_ignores,
)?)
}
}
/// Conditionally loads diff editor from the settings.
///
/// If the `tool_name` is specified, interactive session is implied.
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)
}
}
/// Loads 3-way merge editor from the settings.
///
/// If the `tool_name` isn't specified, the default editor will be returned.
pub fn merge_editor(
&self,
ui: &Ui,
tool_name: Option<&str>,
) -> Result<MergeEditor, MergeToolConfigError> {
if let Some(name) = tool_name {
MergeEditor::with_name(name, self.settings())
} else {
MergeEditor::from_settings(ui, self.settings())
}
}
pub fn resolve_single_op(&self, op_str: &str) -> Result<Operation, OpsetEvaluationError> {
op_walk::resolve_op_with_repo(self.repo(), op_str)
}
/// Resolve a revset to a single revision. Return an error if the revset is
/// empty or has multiple revisions.
pub fn resolve_single_rev(
&self,
ui: &Ui,
revision_arg: &RevisionArg,
) -> Result<Commit, CommandError> {
let expression = self.parse_revset(ui, revision_arg)?;
let should_hint_about_all_prefix = false;
revset_util::evaluate_revset_to_single_commit(
revision_arg.as_ref(),
&expression,
|| self.commit_summary_template(),
should_hint_about_all_prefix,
)
}
/// Evaluates revset expressions to non-empty set of commits. The returned
/// set preserves the order of the input expressions.
///
/// If an input expression is prefixed with `all:`, it may be evaluated to
/// any number of revisions (including 0.)
pub fn resolve_some_revsets_default_single(
&self,
ui: &Ui,
revision_args: &[RevisionArg],
) -> Result<IndexSet<Commit>, CommandError> {
let mut all_commits = IndexSet::new();
for revision_arg in revision_args {
let (expression, modifier) = self.parse_revset_with_modifier(ui, revision_arg)?;
let all = match modifier {
Some(RevsetModifier::All) => true,
None => self
.settings()
.config()
.get_bool("ui.always-allow-large-revsets")?,
};
if all {
for commit in expression.evaluate_to_commits()? {
all_commits.insert(commit?);
}
} else {
let should_hint_about_all_prefix = true;
let commit = revset_util::evaluate_revset_to_single_commit(
revision_arg.as_ref(),
&expression,
|| self.commit_summary_template(),
should_hint_about_all_prefix,
)?;
let commit_hash = short_commit_hash(commit.id());
if !all_commits.insert(commit) {
return Err(user_error(format!(
r#"More than one revset resolved to revision {commit_hash}"#,
)));
}
}
}
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 (expression, modifier) = self.parse_revset_with_modifier(ui, revision_arg)?;
// Whether the caller accepts multiple revisions or not, "all:" should
// be valid. For example, "all:@" is a valid single-rev expression.
let (None | Some(RevsetModifier::All)) = modifier;
Ok(expression)
}
fn parse_revset_with_modifier(
&self,
ui: &Ui,
revision_arg: &RevisionArg,
) -> Result<(RevsetExpressionEvaluator<'_>, Option<RevsetModifier>), CommandError> {
let mut diagnostics = RevsetDiagnostics::new();
let context = self.revset_parse_context();
let (expression, modifier) =
revset::parse_with_modifier(&mut diagnostics, revision_arg.as_ref(), &context)?;
print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
Ok((self.attach_revset_evaluator(expression), modifier))
}
/// Parses the given revset expressions and concatenates them all.
pub fn parse_union_revsets(
&self,
ui: &Ui,
revision_args: &[RevisionArg],
) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
let mut diagnostics = RevsetDiagnostics::new();
let context = self.revset_parse_context();
let expressions: Vec<_> = revision_args
.iter()
.map(|arg| revset::parse_with_modifier(&mut diagnostics, arg.as_ref(), &context))
.map_ok(|(expression, None | Some(RevsetModifier::All))| expression)
.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: Rc<RevsetExpression>,
) -> RevsetExpressionEvaluator<'_> {
RevsetExpressionEvaluator::new(
self.repo().as_ref(),
self.env.command.revset_extensions().clone(),
self.id_prefix_context(),
expression,
)
}
pub(crate) fn revset_parse_context(&self) -> RevsetParseContext {
self.env.revset_parse_context()
}
pub fn id_prefix_context(&self) -> &IdPrefixContext {
self.user_repo
.id_prefix_context
.get_or_init(|| self.env.new_id_prefix_context())
}
pub fn template_aliases_map(&self) -> &TemplateAliasesMap {
&self.env.template_aliases_map
}
/// Parses template of the given language into evaluation tree.
///
/// `wrap_self` specifies the type of the top-level property, which should
/// be one of the `L::wrap_*()` functions.
pub fn parse_template<'a, C: Clone + 'a, L: TemplateLanguage<'a> + ?Sized>(
&self,
ui: &Ui,
language: &L,
template_text: &str,
wrap_self: impl Fn(PropertyPlaceholder<C>) -> L::Property,
) -> Result<TemplateRenderer<'a, C>, CommandError> {
self.env
.parse_template(ui, language, template_text, wrap_self)
}
/// Parses template that is validated by `Self::new()`.
fn reparse_valid_template<'a, C: Clone + 'a, L: TemplateLanguage<'a> + ?Sized>(
&self,
language: &L,
template_text: &str,
wrap_self: impl Fn(PropertyPlaceholder<C>) -> L::Property,
) -> TemplateRenderer<'a, C> {
template_builder::parse(
language,
&mut TemplateDiagnostics::new(),
template_text,
&self.env.template_aliases_map,
wrap_self,
)
.expect("parse error should be confined by WorkspaceCommandHelper::new()")
}
/// Parses commit template into evaluation tree.
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,
CommitTemplateLanguage::wrap_commit,
)
}
/// Parses commit template into evaluation tree.
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,
OperationTemplateLanguage::wrap_operation,
)
}
/// Creates commit template language environment for this workspace.
pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
self.env
.commit_template_language(self.repo().as_ref(), self.id_prefix_context())
}
/// Creates operation template language environment for this workspace.
pub fn operation_template_language(&self) -> OperationTemplateLanguage {
OperationTemplateLanguage::new(
self.repo().op_store().root_operation_id(),
Some(self.repo().op_id()),
self.env.operation_template_extensions(),
)
}
/// Template for one-line summary of a commit.
pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
let language = self.commit_template_language();
self.reparse_valid_template(
&language,
&self.commit_summary_template_text,
CommitTemplateLanguage::wrap_commit,
)
}
/// Template for one-line summary of an operation.
pub fn operation_summary_template(&self) -> TemplateRenderer<'_, Operation> {
let language = self.operation_template_language();
self.reparse_valid_template(
&language,
&self.op_summary_template_text,
OperationTemplateLanguage::wrap_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,
CommitTemplateLanguage::wrap_commit,
)
}
/// Returns one-line summary of the given `commit`.
///
/// Use `write_commit_summary()` to get colorized output. Use
/// `commit_summary_template()` if you have many commits to process.
pub fn format_commit_summary(&self, commit: &Commit) -> String {
let mut output = Vec::new();
self.write_commit_summary(&mut PlainTextFormatter::new(&mut output), commit)
.expect("write() to PlainTextFormatter should never fail");
// Template output is usually UTF-8, but it can contain file content.
output.into_string_lossy()
}
/// Writes one-line summary of the given `commit`.
///
/// Use `commit_summary_template()` if you have many commits to process.
#[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 fn check_rewritable<'a>(
&self,
commits: impl IntoIterator<Item = &'a CommitId>,
) -> Result<(), CommandError> {
self.env
.check_repo_rewritable(self.repo().as_ref(), commits)
}
#[instrument(skip_all)]
fn snapshot_working_copy(&mut self, ui: &Ui) -> Result<(), CommandError> {
let workspace_id = self.workspace_id().to_owned();
let get_wc_commit = |repo: &ReadonlyRepo| -> Result<Option<_>, _> {
repo.view()
.get_wc_commit_id(&workspace_id)
.map(|id| repo.store().get_commit(id))
.transpose()
};
let repo = self.repo().clone();
let Some(wc_commit) = get_wc_commit(&repo)? else {
// If the workspace has been deleted, it's unclear what to do, so we just skip
// committing the working copy.
return Ok(());
};
let base_ignores = self.base_ignores()?;
let auto_tracking_matcher = self.auto_tracking_matcher(ui)?;
// Compare working-copy tree and operation with repo's, and reload as needed.
let fsmonitor_settings = self.settings().fsmonitor_settings()?;
let max_new_file_size = self.settings().max_new_file_size()?;
let command = self.env.command.clone();
let mut locked_ws = self.workspace.start_working_copy_mutation()?;
let old_op_id = locked_ws.locked_wc().old_operation_id().clone();
let (repo, wc_commit) =
match check_stale_working_copy(locked_ws.locked_wc(), &wc_commit, &repo) {
Ok(WorkingCopyFreshness::Fresh) => (repo, wc_commit),
Ok(WorkingCopyFreshness::Updated(wc_operation)) => {
let repo = repo.reload_at(&wc_operation)?;
let wc_commit = if let Some(wc_commit) = get_wc_commit(&repo)? {
wc_commit
} else {
return Ok(()); // The workspace has been deleted (see
// above)
};
(repo, wc_commit)
}
Ok(WorkingCopyFreshness::WorkingCopyStale) => {
return Err(user_error_with_hint(
format!(
"The working copy is stale (not updated since operation {}).",
short_operation_hash(&old_op_id)
),
"Run `jj workspace update-stale` to update it.
See https://martinvonz.github.io/jj/latest/working-copy/#stale-working-copy \
for more information.",
));
}
Ok(WorkingCopyFreshness::SiblingOperation) => {
return Err(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)
)));
}
Err(OpStoreError::ObjectNotFound { .. }) => {
return Err(user_error_with_hint(
"Could not read working copy's operation.",
"Run `jj workspace update-stale` to recover.
See https://martinvonz.github.io/jj/latest/working-copy/#stale-working-copy \
for more information.",
));
}
Err(e) => return Err(e.into()),
};
self.user_repo = ReadonlyUserRepo::new(repo);
let progress = crate::progress::snapshot_progress(ui);
let new_tree_id = locked_ws.locked_wc().snapshot(&SnapshotOptions {
base_ignores,
fsmonitor_settings,
progress: progress.as_ref().map(|x| x as _),
start_tracking_matcher: &auto_tracking_matcher,
max_new_file_size,
})?;
drop(progress);
if new_tree_id != *wc_commit.tree_id() {
let mut tx = start_repo_transaction(
&self.user_repo.repo,
command.settings(),
command.string_args(),
);
tx.set_is_snapshot(true);
let mut_repo = tx.repo_mut();
let commit = mut_repo
.rewrite_commit(command.settings(), &wc_commit)
.set_tree_id(new_tree_id)
.write()?;
mut_repo.set_wc_commit(workspace_id, commit.id().clone())?;
// Rebase descendants
let num_rebased = mut_repo.rebase_descendants(command.settings())?;
if num_rebased > 0 {
writeln!(
ui.status(),
"Rebased {num_rebased} descendant commits onto updated working copy"
)?;
}
if self.working_copy_shared_with_git {
let refs = git::export_refs(mut_repo)?;
print_failed_git_export(ui, &refs)?;
}
self.user_repo = ReadonlyUserRepo::new(tx.commit("snapshot working copy"));
}
locked_ws.finish(self.user_repo.repo.op_id().clone())?;
Ok(())
}
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,
)?;
if Some(new_commit) != maybe_old_commit {
if let Some(mut formatter) = ui.status_formatter() {
let template = self.commit_summary_template();
write!(formatter, "Working copy now at: ")?;
formatter.with_label("working_copy", |fmt| template.format(new_commit, fmt))?;
writeln!(formatter)?;
for parent in new_commit.parents() {
let parent = parent?;
// "Working copy now at: "
write!(formatter, "Parent commit : ")?;
template.format(&parent, formatter.as_mut())?;
writeln!(formatter)?;
}
}
}
if let Some(stats) = stats {
print_checkout_stats(ui, stats, new_commit)?;
}
if Some(new_commit) != maybe_old_commit {
if let Some(mut formatter) = ui.status_formatter() {
let conflicts = new_commit.tree()?.conflicts().collect_vec();
if !conflicts.is_empty() {
writeln!(formatter, "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.settings(), 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,
}
}
fn finish_transaction(
&mut self,
ui: &Ui,
mut tx: Transaction,
description: impl Into<String>,
) -> Result<(), CommandError> {
if !tx.repo().has_changes() {
writeln!(ui.status(), "Nothing changed.")?;
return Ok(());
}
let num_rebased = tx.repo_mut().rebase_descendants(self.settings())?;
if num_rebased > 0 {
writeln!(ui.status(), "Rebased {num_rebased} descendant commits")?;
}
for (workspace_id, wc_commit_id) in tx.repo().view().wc_commit_ids().clone().iter().sorted()
//sorting otherwise non deterministic order (bad for tests)
{
if self
.env
.check_repo_rewritable(tx.repo(), [wc_commit_id])
.is_err()
{
let wc_commit = tx.repo().store().get_commit(wc_commit_id)?;
tx.repo_mut()
.check_out(workspace_id.clone(), self.settings(), &wc_commit)?;
writeln!(
ui.warning_default(),
"The working-copy commit in workspace '{}' became immutable, so a new commit \
has been created on top of it.",
workspace_id.as_str()
)?;
}
}
let old_repo = tx.base_repo().clone();
let maybe_old_wc_commit = old_repo
.view()
.get_wc_commit_id(self.workspace_id())
.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_id())
.map(|commit_id| tx.repo().store().get_commit(commit_id))
.transpose()?;
if self.working_copy_shared_with_git {
let git_repo = self.git_backend().unwrap().open_git_repo()?;
if let Some(wc_commit) = &maybe_new_wc_commit {
git::reset_head(tx.repo_mut(), &git_repo, wc_commit)?;
}
let refs = git::export_refs(tx.repo_mut())?;
print_failed_git_export(ui, &refs)?;
}
self.user_repo = ReadonlyUserRepo::new(tx.commit(description));
// Update working copy before reporting repo changes, so that
// potential errors while reporting changes (broken pipe, etc)
// don't leave the working copy in a stale state.
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)?;
} else {
// It seems the workspace was deleted, so we shouldn't try to
// update it.
}
}
self.report_repo_changes(ui, &old_repo)?;
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 mut writer = ui.warning_default();
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!(),
};
write!(writer, "{not_configured_msg} ")?;
writeln!(
writer,
"Until configured, your commits will be created with the empty identity, and \
can't be pushed to remotes. To configure, run:",
)?;
if missing_user_name {
writeln!(writer, r#" jj config set --user user.name "Some One""#)?;
}
if missing_user_mail {
writeln!(
writer,
r#" jj config set --user user.email "someone@example.com""#
)?;
}
}
Ok(())
}
/// Inform the user about important changes to the repo since the previous
/// operation (when `old_repo` was loaded).
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 old_heads = RevsetExpression::commits(old_view.heads().iter().cloned().collect());
let new_heads = RevsetExpression::commits(new_view.heads().iter().cloned().collect());
// Filter the revsets by conflicts instead of reading all commits and doing the
// filtering here. That way, we can afford to evaluate the revset even if there
// are millions of commits added to the repo, assuming the revset engine can
// efficiently skip non-conflicting commits. Filter out empty commits mostly so
// `jj new <conflicted commit>` doesn't result in a message about new conflicts.
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 = |expr: Rc<RevsetExpression>| -> Result<Vec<Commit>, CommandError> {
let commits = expr
.evaluate_programmatic(new_repo)?
.iter()
.commits(new_repo.store())
.try_collect()?;
Ok(commits)
};
let removed_conflict_commits = get_commits(removed_conflicts_expr)?;
let added_conflict_commits = get_commits(added_conflicts_expr)?;
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));
// TODO: Also report new divergence and maybe resolved divergence
let template = self.commit_summary_template();
if !resolved_conflicts_by_change_id.is_empty() {
writeln!(
fmt,
"Existing conflicts were resolved or abandoned from these commits:"
)?;
for (_, old_commits) in &resolved_conflicts_by_change_id {
// TODO: Report which ones were resolved and which ones were abandoned. However,
// that involves resolving the change_id among the visible commits in the new
// repo, which isn't currently supported by Google's revset engine.
for commit in old_commits {
write!(fmt, " ")?;
template.format(commit, fmt.as_mut())?;
writeln!(fmt)?;
}
}
}
if !new_conflicts_by_change_id.is_empty() {
writeln!(fmt, "New conflicts appeared in these commits:")?;
for (_, new_commits) in &new_conflicts_by_change_id {
for commit in new_commits {
write!(fmt, " ")?;
template.format(commit, fmt.as_mut())?;
writeln!(fmt)?;
}
}
}
// Hint that the user might want to `jj new` to the first conflict commit to
// resolve conflicts. Only show the hints if there were any new or resolved
// conflicts, and only if there are still some conflicts.
if !(added_conflict_commits.is_empty()
|| resolved_conflicts_by_change_id.is_empty() && new_conflicts_by_change_id.is_empty())
{
// If the user just resolved some conflict and squashed them in, there won't be
// any new conflicts. Clarify to them that there are still some other conflicts
// to resolve. (We don't mention conflicts in commits that weren't affected by
// the operation, however.)
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(),
)?;
}
Ok(())
}
pub fn report_repo_conflicts(
&self,
fmt: &mut dyn Formatter,
repo: &ReadonlyRepo,
conflicted_commits: Vec<CommitId>,
) -> Result<(), CommandError> {
let only_one_conflicted_commit = conflicted_commits.len() == 1;
let root_conflicts_revset = RevsetExpression::commits(conflicted_commits)
.roots()
.evaluate_programmatic(repo)?;
let root_conflict_commits: Vec<_> = root_conflicts_revset
.iter()
.commits(repo.store())
.try_collect()?;
if !root_conflict_commits.is_empty() {
fmt.push_label("hint")?;
if only_one_conflicted_commit {
writeln!(fmt, "To resolve the conflicts, start by updating to it:",)?;
} else if root_conflict_commits.len() == 1 {
writeln!(
fmt,
"To resolve the conflicts, start by updating to the first one:",
)?;
} else {
writeln!(
fmt,
"To resolve the conflicts, start by updating to one of the first ones:",
)?;
}
let format_short_change_id = self.short_change_id_template();
for commit in root_conflict_commits {
write!(fmt, " jj new ")?;
format_short_change_id.format(&commit, fmt)?;
writeln!(fmt)?;
}
writeln!(
fmt,
r#"Then use `jj resolve`, or edit the conflict markers in the file directly.
Once the conflicts are resolved, you may want to inspect the result with `jj diff`.
Then run `jj squash` to move the resolution into the conflicted commit."#,
)?;
fmt.pop_label()?;
}
Ok(())
}
/// Identifies bookmarks which are eligible to be moved automatically
/// during `jj commit` and `jj new`. Whether a bookmark is eligible is
/// determined by its target and the user and repo config for
/// "advance-bookmarks".
///
/// Returns a Vec of bookmarks in `repo` that point to any of the `from`
/// commits and that are eligible to advance. The `from` commits are
/// typically the parents of the target commit of `jj commit` or `jj new`.
///
/// Bookmarks are not moved until
/// `WorkspaceCommandTransaction::advance_bookmarks()` is called with the
/// `AdvanceableBookmark`s returned by this function.
///
/// Returns an empty `std::Vec` if no bookmarks are eligible to advance.
pub fn get_advanceable_bookmarks<'a>(
&self,
from: impl IntoIterator<Item = &'a CommitId>,
) -> Result<Vec<AdvanceableBookmark>, CommandError> {
let ab_settings = AdvanceBookmarksSettings::from_config(self.settings().config())?;
if !ab_settings.feature_enabled() {
// Return early if we know that there's no work to do.
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_settings.bookmark_is_eligible(name) {
advanceable_bookmarks.push(AdvanceableBookmark {
name: name.to_owned(),
old_commit_id: from_commit.clone(),
});
}
}
}
Ok(advanceable_bookmarks)
}
}
/// An ongoing [`Transaction`] tied to a particular workspace.
///
/// `WorkspaceCommandTransaction`s are created with
/// [`WorkspaceCommandHelper::start_transaction`] and committed with
/// [`WorkspaceCommandTransaction::finish`]. The inner `Transaction` can also be
/// extracted using [`WorkspaceCommandTransaction::into_inner`] in situations
/// where finer-grained control over the `Transaction` is necessary.
#[must_use]
pub struct WorkspaceCommandTransaction<'a> {
helper: &'a mut WorkspaceCommandHelper,
tx: Transaction,
/// Cache of index built against the current MutableRepo state.
id_prefix_context: OnceCell<IdPrefixContext>,
}
impl WorkspaceCommandTransaction<'_> {
/// Workspace helper that may use the base repo.
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(); // invalidate
self.tx.repo_mut()
}
pub fn check_out(&mut self, commit: &Commit) -> Result<Commit, CheckOutCommitError> {
let workspace_id = self.helper.workspace_id().to_owned();
let settings = self.helper.settings();
self.id_prefix_context.take(); // invalidate
self.tx.repo_mut().check_out(workspace_id, settings, commit)
}
pub fn edit(&mut self, commit: &Commit) -> Result<(), EditCommitError> {
let workspace_id = self.helper.workspace_id().to_owned();
self.id_prefix_context.take(); // invalidate
self.tx.repo_mut().edit(workspace_id, commit)
}
pub fn format_commit_summary(&self, commit: &Commit) -> String {
let mut output = Vec::new();
self.write_commit_summary(&mut PlainTextFormatter::new(&mut output), commit)
.expect("write() to PlainTextFormatter should never fail");
// Template output is usually UTF-8, but it can contain file content.
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)
}
/// Template for one-line summary of a commit within transaction.
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,
CommitTemplateLanguage::wrap_commit,
)
}
/// Creates commit template language environment capturing the current
/// transaction state.
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)
}
/// Parses commit template with the current transaction state.
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,
CommitTemplateLanguage::wrap_commit,
)
}
pub fn finish(self, ui: &Ui, description: impl Into<String>) -> Result<(), CommandError> {
self.helper.finish_transaction(ui, self.tx, description)
}
/// Returns the wrapped [`Transaction`] for circumstances where
/// finer-grained control is needed. The caller becomes responsible for
/// finishing the `Transaction`, including rebasing descendants and updating
/// the working copy, if applicable.
pub fn into_inner(self) -> Transaction {
self.tx
}
/// Moves each bookmark in `bookmarks` from an old commit it's associated
/// with (configured by `get_advanceable_bookmarks`) to the `move_to`
/// commit. If the bookmark is conflicted before the update, it will
/// remain conflicted after the update, but the conflict will involve
/// the `move_to` commit instead of the old commit.
pub fn advance_bookmarks(&mut self, bookmarks: Vec<AdvanceableBookmark>, move_to: &CommitId) {
for bookmark in bookmarks {
// This removes the old commit ID from the bookmark's RefTarget and
// replaces it with the `move_to` ID.
self.repo_mut().merge_local_bookmark(
&bookmark.name,
&RefTarget::normal(bookmark.old_commit_id),
&RefTarget::normal(move_to.clone()),
);
}
}
}
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, workspace_path: Option<&str>) -> CommandError {
match err {
WorkspaceLoadError::NoWorkspaceHere(wc_path) => {
// Prefer user-specified workspace_path_str instead of absolute wc_path.
let workspace_path_str = workspace_path.unwrap_or(".");
let message = format!(r#"There is no jj repo in "{workspace_path_str}""#);
let git_dir = wc_path.join(".git");
if git_dir.is_dir() {
user_error_with_hint(
message,
"It looks like this is a git repo. You can create a jj repo backed by it by \
running this:
jj git init --colocate",
)
} 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 @ SignInitError::UnknownBackend(_),
)) => user_error(err),
WorkspaceLoadError::StoreLoadError(err) => internal_error(err),
WorkspaceLoadError::WorkingCopyState(err) => internal_error(err),
WorkspaceLoadError::NonUnicodePath | WorkspaceLoadError::Path(_) => user_error(err),
}
}
pub fn start_repo_transaction(
repo: &Arc<ReadonlyRepo>,
settings: &UserSettings,
string_args: &[String],
) -> Transaction {
let mut tx = repo.start_transaction(settings);
// TODO: Either do better shell-escaping here or store the values in some list
// type (which we currently don't have).
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_tag("args".to_string(), quoted_strings.join(" "));
tx
}
/// Whether the working copy is stale or not.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WorkingCopyFreshness {
/// The working copy isn't stale, and no need to reload the repo.
Fresh,
/// The working copy was updated since we loaded the repo. The repo must be
/// reloaded at the working copy's operation.
Updated(Box<Operation>),
/// The working copy is behind the latest operation.
WorkingCopyStale,
/// The working copy is a sibling of the latest operation.
SiblingOperation,
}
#[instrument(skip_all)]
pub fn check_stale_working_copy(
locked_wc: &dyn LockedWorkingCopy,
wc_commit: &Commit,
repo: &ReadonlyRepo,
) -> Result<WorkingCopyFreshness, OpStoreError> {
// Check if the working copy's tree matches the repo's view
let wc_tree_id = locked_wc.old_tree_id();
if wc_commit.tree_id() == wc_tree_id {
// The working copy isn't stale, and no need to reload the repo.
Ok(WorkingCopyFreshness::Fresh)
} else {
let wc_operation_data = repo
.op_store()
.read_operation(locked_wc.old_operation_id())?;
let wc_operation = Operation::new(
repo.op_store().clone(),
locked_wc.old_operation_id().clone(),
wc_operation_data,
);
let repo_operation = repo.operation();
let ancestor_op = dag_walk::closest_common_node_ok(
[Ok(wc_operation.clone())],
[Ok(repo_operation.clone())],
|op: &Operation| op.id().clone(),
|op: &Operation| op.parents().collect_vec(),
)?
.expect("unrelated operations");
if ancestor_op.id() == repo_operation.id() {
// The working copy was updated since we loaded the repo. The repo must be
// reloaded at the working copy's operation.
Ok(WorkingCopyFreshness::Updated(Box::new(wc_operation)))
} else if ancestor_op.id() == wc_operation.id() {
// The working copy was not updated when some repo operation committed,
// meaning that it's stale compared to the repo view.
Ok(WorkingCopyFreshness::WorkingCopyStale)
} else {
Ok(WorkingCopyFreshness::SiblingOperation)
}
}
}
#[instrument(skip_all)]
pub fn print_conflicted_paths(
conflicts: &[(RepoPathBuf, 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.iter(), formatted_paths) {
let conflict = conflict.clone().simplify();
let sides = conflict.num_sides();
let n_adds = conflict.adds().flatten().count();
let deletions = sides - n_adds;
let mut seen_objects = BTreeMap::new(); // Sort for consistency and easier testing
if deletions > 0 {
seen_objects.insert(
format!(
// Starting with a number sorts this first
"{deletions} deletion{}",
if deletions > 1 { "s" } else { "" }
),
"normal", // Deletions don't interfere with `jj resolve` or diff display
);
}
// TODO: We might decide it's OK for `jj resolve` to ignore special files in the
// `removes` of a conflict (see e.g. https://github.com/martinvonz/jj/pull/978). In
// that case, `conflict.removes` should be removed below.
for term in itertools::chain(conflict.removes(), 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",
TreeValue::Conflict(_) => "another conflict (you found a bug!)",
}
.to_string(),
"difficult",
);
}
write!(formatter, "{formatted_path} ")?;
formatter.with_label("conflict_description", |formatter| {
let print_pair = |formatter: &mut dyn Formatter, (text, label): &(String, &str)| {
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)?;
}
};
}
io::Result::Ok(())
})?;
writeln!(formatter)?;
}
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(())
}
/// Prints warning about explicit paths that don't match any of the tree
/// entries.
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 {
// TODO: propagate errors
explicit_paths.retain(|&path| tree.path_value(path).unwrap().is_absent());
if explicit_paths.is_empty() {
return Ok(());
}
}
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 fn print_trackable_remote_bookmarks(ui: &Ui, view: &View) -> io::Result<()> {
let remote_bookmark_names = view
.bookmarks()
.filter(|(_, bookmark_target)| bookmark_target.local_target.is_present())
.flat_map(|(name, bookmark_target)| {
bookmark_target
.remote_refs
.into_iter()
.filter(|&(_, remote_ref)| !remote_ref.is_tracking())
.map(move |(remote, _)| format!("{name}@{remote}"))
})
.collect_vec();
if remote_bookmark_names.is_empty() {
return Ok(());
}
if let Some(mut formatter) = ui.status_formatter() {
writeln!(
formatter.labeled("hint").with_heading("Hint: "),
"The following remote bookmarks aren't associated with the existing local bookmarks:"
)?;
for full_name in &remote_bookmark_names {
write!(formatter, " ")?;
writeln!(formatter.labeled("bookmark"), "{full_name}")?;
}
writeln!(
formatter.labeled("hint").with_heading("Hint: "),
"Run `jj bookmark track {names}` to keep local bookmarks updated on future pulls.",
names = remote_bookmark_names.join(" "),
)?;
}
Ok(())
}
pub fn update_working_copy(
repo: &Arc<ReadonlyRepo>,
workspace: &mut Workspace,
old_commit: Option<&Commit>,
new_commit: &Commit,
) -> Result<Option<CheckoutStats>, CommandError> {
let old_tree_id = old_commit.map(|commit| commit.tree_id().clone());
let stats = if Some(new_commit.tree_id()) != old_tree_id.as_ref() {
// TODO: CheckoutError::ConcurrentCheckout should probably just result in a
// warning for most commands (but be an error for the checkout command)
let stats = workspace
.check_out(repo.op_id().clone(), old_tree_id.as_ref(), new_commit)
.map_err(|err| {
internal_error_with_message(
format!("Failed to check out commit {}", new_commit.id().hex()),
err,
)
})?;
Some(stats)
} else {
// Record new operation id which represents the latest working-copy state
let locked_ws = workspace.start_working_copy_mutation()?;
locked_ws.finish(repo.op_id().clone())?;
None
};
Ok(stats)
}
fn load_template_aliases(
ui: &Ui,
layered_configs: &LayeredConfigs,
) -> Result<TemplateAliasesMap, CommandError> {
const TABLE_KEY: &str = "template-aliases";
let mut aliases_map = TemplateAliasesMap::new();
// Load from all config layers in order. 'f(x)' in default layer should be
// overridden by 'f(a)' in user.
for (_, config) in layered_configs.sources() {
let table = if let Some(table) = config.get_table(TABLE_KEY).optional()? {
table
} else {
continue;
};
for (decl, value) in table.into_iter().sorted_by(|a, b| a.0.cmp(&b.0)) {
let r = value
.into_string()
.map_err(|e| e.to_string())
.and_then(|v| aliases_map.insert(&decl, v).map_err(|e| e.to_string()));
if let Err(s) = r {
writeln!(
ui.warning_default(),
r#"Failed to load "{TABLE_KEY}.{decl}": {s}"#
)?;
}
}
}
Ok(aliases_map)
}
/// Helper to reformat content of log-like commands.
#[derive(Clone, Debug)]
pub struct LogContentFormat {
width: usize,
word_wrap: bool,
}
impl LogContentFormat {
/// Creates new formatting helper for the terminal.
pub fn new(ui: &Ui, settings: &UserSettings) -> Result<Self, config::ConfigError> {
Ok(LogContentFormat {
width: ui.term_width(),
word_wrap: settings.config().get_bool("ui.log-word-wrap")?,
})
}
/// Subtracts the given `width` and returns new formatting helper.
#[must_use]
pub fn sub_width(&self, width: usize) -> Self {
LogContentFormat {
width: self.width.saturating_sub(width),
word_wrap: self.word_wrap,
}
}
/// Current width available to content.
pub fn width(&self) -> usize {
self.width
}
/// Writes content which will optionally be wrapped at the current width.
pub fn write<E: From<io::Error>>(
&self,
formatter: &mut dyn Formatter,
content_fn: impl FnOnce(&mut dyn Formatter) -> Result<(), E>,
) -> Result<(), E> {
if self.word_wrap {
let mut recorder = FormatRecorder::new();
content_fn(&mut recorder)?;
text_util::write_wrapped(formatter, &recorder, self.width)?;
} else {
content_fn(formatter)?;
}
Ok(())
}
}
pub fn get_new_config_file_path(
config_source: &ConfigSource,
command: &CommandHelper,
) -> Result<PathBuf, CommandError> {
let edit_path = match config_source {
// TODO(#531): Special-case for editors that can't handle viewing directories?
ConfigSource::User => {
new_config_path()?.ok_or_else(|| user_error("No repo config path found to edit"))?
}
ConfigSource::Repo => command.workspace_loader()?.repo_path().join("config.toml"),
_ => {
return Err(user_error(format!(
"Can't get path for config source {config_source:?}"
)));
}
};
Ok(edit_path)
}
pub fn run_ui_editor(settings: &UserSettings, edit_path: &Path) -> Result<(), CommandError> {
// Work around UNC paths not being well supported on Windows (no-op for
// non-Windows): https://github.com/martinvonz/jj/issues/3986
let edit_path = dunce::simplified(edit_path);
let editor: CommandNameAndArgs = settings
.config()
.get("ui.editor")
.map_err(|err| config_error_with_message("Invalid `ui.editor`", err))?;
let mut cmd = editor.to_command();
cmd.arg(edit_path);
tracing::info!(?cmd, "running editor");
let exit_status = cmd.status().map_err(|err| {
user_error_with_message(
format!(
// The executable couldn't be found or run; command-line arguments are not relevant
"Failed to run editor '{name}'",
name = editor.split_name(),
),
err,
)
})?;
if !exit_status.success() {
return Err(user_error(format!(
"Editor '{editor}' exited with an error"
)));
}
Ok(())
}
pub fn edit_temp_file(
error_name: &str,
tempfile_suffix: &str,
dir: &Path,
content: &str,
settings: &UserSettings,
) -> Result<String, CommandError> {
let path = (|| -> Result<_, io::Error> {
let mut file = tempfile::Builder::new()
.prefix("editor-")
.suffix(tempfile_suffix)
.tempfile_in(dir)?;
file.write_all(content.as_bytes())?;
let (_, path) = file.keep().map_err(|e| e.error)?;
Ok(path)
})()
.map_err(|e| {
user_error_with_message(
format!(
r#"Failed to create {} file in "{}""#,
error_name,
dir.display(),
),
e,
)
})?;
run_ui_editor(settings, &path)?;
let edited = fs::read_to_string(&path).map_err(|e| {
user_error_with_message(
format!(r#"Failed to read {} file "{}""#, error_name, path.display()),
e,
)
})?;
// Delete the file only if everything went well.
// TODO: Tell the user the name of the file we left behind.
std::fs::remove_file(path).ok();
Ok(edited)
}
pub fn short_commit_hash(commit_id: &CommitId) -> String {
commit_id.hex()[0..12].to_string()
}
pub fn short_change_hash(change_id: &ChangeId) -> String {
// TODO: We could avoid the unwrap() and make this more efficient by converting
// straight from binary.
to_reverse_hex(&change_id.hex()[0..12]).unwrap()
}
pub fn short_operation_hash(operation_id: &OperationId) -> String {
operation_id.hex()[0..12].to_string()
}
/// Wrapper around a `DiffEditor` to conditionally start interactive session.
#[derive(Clone, Debug)]
pub enum DiffSelector {
NonInteractive,
Interactive(DiffEditor),
}
impl DiffSelector {
pub fn is_interactive(&self) -> bool {
matches!(self, DiffSelector::Interactive(_))
}
/// Restores diffs from the `right_tree` to the `left_tree` by using an
/// interactive editor if enabled.
pub fn select(
&self,
left_tree: &MergedTree,
right_tree: &MergedTree,
matcher: &dyn Matcher,
format_instructions: impl FnOnce() -> String,
) -> Result<MergedTreeId, CommandError> {
match self {
DiffSelector::NonInteractive => Ok(restore_tree(right_tree, left_tree, matcher)?),
DiffSelector::Interactive(editor) => {
Ok(editor.edit(left_tree, right_tree, matcher, format_instructions)?)
}
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RemoteBookmarkName {
pub bookmark: String,
pub remote: String,
}
impl fmt::Display for RemoteBookmarkName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let RemoteBookmarkName { bookmark, remote } = self;
write!(f, "{bookmark}@{remote}")
}
}
#[derive(Clone, Debug)]
pub struct RemoteBookmarkNamePattern {
pub bookmark: StringPattern,
pub remote: StringPattern,
}
impl FromStr for RemoteBookmarkNamePattern {
type Err = String;
fn from_str(src: &str) -> Result<Self, Self::Err> {
// The kind prefix applies to both bookmark and remote fragments. It's
// weird that unanchored patterns like substring:bookmark@remote is split
// into two, but I can't think of a better syntax.
// TODO: should we disable substring pattern? what if we added regex?
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 {
Ok(StringPattern::exact(pat))
}
};
// TODO: maybe reuse revset parser to handle bookmark/remote name containing @
let (bookmark, remote) = pat.rsplit_once('@').ok_or_else(|| {
"remote bookmark must be specified in bookmark@remote form".to_owned()
})?;
Ok(RemoteBookmarkNamePattern {
bookmark: to_pattern(bookmark)?,
remote: to_pattern(remote)?,
})
}
}
impl RemoteBookmarkNamePattern {
pub fn is_exact(&self) -> bool {
self.bookmark.is_exact() && self.remote.is_exact()
}
}
impl fmt::Display for RemoteBookmarkNamePattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let RemoteBookmarkNamePattern { bookmark, remote } = self;
write!(f, "{bookmark}@{remote}")
}
}
/// Jujutsu (An experimental VCS)
///
/// To get started, see the tutorial at https://martinvonz.github.io/jj/latest/tutorial/.
#[allow(rustdoc::bare_urls)]
#[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 {
/// Path to repository to operate on
///
/// By default, Jujutsu searches for the closest .jj/ directory in an
/// ancestor of the current working directory.
#[arg(long, short = 'R', global = true, value_hint = clap::ValueHint::DirPath)]
pub repository: Option<String>,
/// Don't snapshot the working copy, and don't update it
///
/// By default, Jujutsu snapshots the working copy at the beginning of every
/// command. The working copy is also updated at the end of the command,
/// if the command modified the working-copy commit (`@`). If you want
/// to avoid snapshotting the working copy and instead see a possibly
/// stale working copy commit, you can use `--ignore-working-copy`.
/// This may be useful e.g. in a command prompt, especially if you have
/// another process that commits the working copy.
///
/// Loading the repository at a specific operation with `--at-operation`
/// implies `--ignore-working-copy`.
#[arg(long, global = true)]
pub ignore_working_copy: bool,
/// Allow rewriting immutable commits
///
/// By default, Jujutsu prevents rewriting commits in the configured set of
/// immutable commits. This option disables that check and lets you rewrite
/// any commit but the root commit.
///
/// This option only affects the check. It does not affect the
/// `immutable_heads()` revset or the `immutable` template keyword.
#[arg(long, global = true)]
pub ignore_immutable: bool,
/// Operation to load the repo at
///
/// Operation to load the repo at. By default, Jujutsu loads the repo at the
/// most recent operation, or at the merge of the divergent operations if
/// any.
///
/// You can use `--at-op=<operation ID>` to see what the repo looked like at
/// an earlier operation. For example `jj --at-op=<operation ID> st` will
/// show you what `jj st` would have shown you when the given operation had
/// just finished. `--at-op=@` is pretty much the same as the default except
/// that divergent operations will never be merged.
///
/// Use `jj op log` to find the operation ID you want. Any unambiguous
/// prefix of the operation ID is enough.
///
/// When loading the repo at an earlier operation, the working copy will be
/// ignored, as if `--ignore-working-copy` had been specified.
///
/// It is possible to run mutating commands when loading the repo at an
/// earlier operation. Doing that is equivalent to having run concurrent
/// commands starting at the earlier operation. There's rarely a reason to
/// do that, but it is possible.
#[arg(long, visible_alias = "at-op", global = true)]
pub at_operation: Option<String>,
/// Enable debug logging
#[arg(long, global = true)]
pub debug: bool,
#[command(flatten)]
pub early_args: EarlyArgs,
}
#[derive(clap::Args, Clone, Debug)]
pub struct EarlyArgs {
/// When to colorize output (always, never, debug, auto)
#[arg(long, value_name = "WHEN", global = true)]
pub color: Option<ColorChoice>,
/// Silence non-primary command output
///
/// For example, `jj file list ` will still list files, but it won't tell
/// you if the working copy was snapshotted or if descendants were rebased.
///
/// Warnings and errors will still be printed.
#[arg(long, global = true, action = ArgAction::SetTrue)]
// Parsing with ignore_errors will crash if this is bool, so use
// Option<bool>.
pub quiet: Option<bool>,
/// Disable the pager
#[arg(long, value_name = "WHEN", global = true, action = ArgAction::SetTrue)]
// Parsing with ignore_errors will crash if this is bool, so use
// Option<bool>.
pub no_pager: Option<bool>,
/// Additional configuration options (can be repeated)
// TODO: Introduce a `--config` option with simpler syntax for simple
// cases, designed so that `--config ui.color=auto` works
#[arg(long, value_name = "TOML", global = true)]
pub config_toml: Vec<String>,
}
/// Wrapper around revset expression argument.
///
/// An empty string is rejected early by the CLI value parser, but it's still
/// allowed to construct an empty `RevisionArg` from a config value for
/// example. An empty expression will be rejected by the revset parser.
#[derive(Clone, Debug)]
pub struct RevisionArg(Cow<'static, str>);
impl RevisionArg {
/// The working-copy symbol, which is the default of the most commands.
pub const AT: Self = RevisionArg(Cow::Borrowed("@"));
}
impl From<String> for RevisionArg {
fn from(s: String) -> Self {
RevisionArg(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) -> RevisionArg>;
fn value_parser() -> Self::Parser {
NonEmptyStringValueParser::new().map(RevisionArg::from)
}
}
fn get_string_or_array(
config: &config::Config,
key: &str,
) -> Result<Vec<String>, config::ConfigError> {
config
.get(key)
.map(|string| vec![string])
.or_else(|_| config.get::<Vec<String>>(key))
}
fn resolve_default_command(
ui: &Ui,
config: &config::Config,
app: &Command,
mut string_args: Vec<String>,
) -> Result<Vec<String>, CommandError> {
const PRIORITY_FLAGS: &[&str] = &["help", "--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 {
if matches.subcommand_name().is_none() {
let args = get_string_or_array(config, "ui.default-command").optional()?;
if args.is_none() {
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."
)?;
}
let default_command = args.unwrap_or_else(|| vec!["log".to_string()]);
// Insert the default command directly after the path to the binary.
string_args.splice(1..1, default_command);
}
}
Ok(string_args)
}
fn resolve_aliases(
ui: &Ui,
config: &config::Config,
app: &Command,
mut string_args: Vec<String>,
) -> Result<Vec<String>, CommandError> {
let mut aliases_map = config.get_table("aliases")?;
if let Ok(alias_map) = config.get_table("alias") {
for (alias, definition) in alias_map {
if aliases_map.insert(alias.clone(), definition).is_some() {
return Err(user_error_with_hint(
format!(r#"Alias "{alias}" is defined in both [aliases] and [alias]"#),
"[aliases] is the preferred section for aliases. Please remove the alias from \
[alias].",
));
}
}
}
let mut resolved_aliases = HashSet::new();
let mut real_commands = HashSet::new();
for command in app.get_subcommands() {
real_commands.insert(command.get_name().to_string());
for alias in command.get_all_aliases() {
real_commands.insert(alias.to_string());
}
}
for alias in aliases_map.keys() {
if real_commands.contains(alias) {
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()) {
if !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!(
r#"Recursive alias definition involving "{alias_name}""#
)));
}
if let Some(value) = aliases_map.remove(&alias_name) {
if let Ok(alias_definition) = value.try_deserialize::<Vec<String>>() {
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.clone());
continue;
} else {
return Err(user_error(format!(
r#"Alias definition for "{alias_name}" must be a string list"#
)));
}
} else {
// Not a real command and not an alias, so return what we've resolved so far
return Ok(string_args);
}
}
}
// No more alias commands, or hit unknown option
return Ok(string_args);
}
}
/// Parse args that must be interpreted early, e.g. before printing help.
fn handle_early_args(
ui: &mut Ui,
app: &Command,
args: &[String],
layered_configs: &mut LayeredConfigs,
) -> Result<(), CommandError> {
// ignore_errors() bypasses errors like missing subcommand
let early_matches = app
.clone()
.disable_version_flag(true)
.disable_help_flag(true)
.disable_help_subcommand(true)
.ignore_errors(true)
.try_get_matches_from(args)?;
let mut args: EarlyArgs = EarlyArgs::from_arg_matches(&early_matches).unwrap();
if let Some(choice) = args.color {
args.config_toml.push(format!(r#"ui.color="{choice}""#));
}
if args.quiet.unwrap_or_default() {
args.config_toml.push(r#"ui.quiet=true"#.to_string());
}
if args.no_pager.unwrap_or_default() {
args.config_toml.push(r#"ui.paginate="never""#.to_owned());
}
if !args.config_toml.is_empty() {
layered_configs.parse_config_args(&args.config_toml)?;
ui.reset(&layered_configs.merge())?;
}
Ok(())
}
pub fn expand_args(
ui: &Ui,
app: &Command,
args_os: ArgsOs,
config: &config::Config,
) -> Result<Vec<String>, CommandError> {
let mut string_args: Vec<String> = vec![];
for arg_os in args_os {
if let Some(string_arg) = arg_os.to_str() {
string_args.push(string_arg.to_owned());
} else {
return Err(cli_error("Non-utf8 argument"));
}
}
let string_args = resolve_default_command(ui, config, app, string_args)?;
resolve_aliases(ui, config, app, string_args)
}
pub fn parse_args(
ui: &mut Ui,
app: &Command,
tracing_subscription: &TracingSubscription,
string_args: &[String],
layered_configs: &mut LayeredConfigs,
) -> Result<(ArgMatches, Args), CommandError> {
handle_early_args(ui, app, string_args, layered_configs)?;
let matches = app
.clone()
.arg_required_else_help(true)
.subcommand_required(true)
.try_get_matches_from(string_args)?;
let args: Args = Args::from_arg_matches(&matches).unwrap();
if args.global_args.debug {
// TODO: set up debug logging as early as possible
tracing_subscription.enable_debug_logging()?;
}
Ok((matches, args))
}
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");
// Template output is usually UTF-8, but it can contain file content.
output.into_string_lossy()
}
/// CLI command builder and runner.
#[must_use]
pub struct CliRunner {
tracing_subscription: TracingSubscription,
app: Command,
extra_configs: Vec<config::Config>,
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_fn: CliDispatchFn,
start_hook_fns: Vec<CliDispatchFn>,
process_global_args_fns: Vec<ProcessGlobalArgsFn>,
}
type CliDispatchFn = Box<dyn FnOnce(&mut Ui, &CommandHelper) -> Result<(), CommandError>>;
type ProcessGlobalArgsFn = Box<dyn FnOnce(&mut Ui, &ArgMatches) -> Result<(), CommandError>>;
impl CliRunner {
/// Initializes CLI environment and returns a builder. This should be called
/// as early as possible.
pub fn init() -> Self {
let tracing_subscription = TracingSubscription::init();
crate::cleanup_guard::init();
CliRunner {
tracing_subscription,
app: crate::commands::default_app(),
extra_configs: vec![],
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_fn: Box::new(crate::commands::run_command),
start_hook_fns: vec![],
process_global_args_fns: vec![],
}
}
/// Set the name of the CLI application to be displayed in help messages.
pub fn name(mut self, name: &str) -> Self {
self.app = self.app.name(name.to_string());
self
}
/// Set the about message to be displayed in help messages.
pub fn about(mut self, about: &str) -> Self {
self.app = self.app.about(about.to_string());
self
}
/// Set the version to be displayed by `jj version`.
pub fn version(mut self, version: &str) -> Self {
self.app = self.app.version(version.to_string());
self
}
/// Adds default configs in addition to the normal defaults.
pub fn add_extra_config(mut self, extra_configs: config::Config) -> Self {
self.extra_configs.push(extra_configs);
self
}
/// Adds `StoreFactories` to be used.
pub fn add_store_factories(mut self, store_factories: StoreFactories) -> Self {
self.store_factories.merge(store_factories);
self
}
/// Adds working copy factories to be used.
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_start_hook(mut self, start_hook_fn: CliDispatchFn) -> Self {
self.start_hook_fns.push(start_hook_fn);
self
}
/// Registers new subcommands in addition to the default ones.
pub fn add_subcommand<C, F>(mut self, custom_dispatch_fn: F) -> Self
where
C: clap::Subcommand,
F: FnOnce(&mut Ui, &CommandHelper, C) -> Result<(), CommandError> + 'static,
{
let old_dispatch_fn = self.dispatch_fn;
let new_dispatch_fn =
move |ui: &mut Ui, command_helper: &CommandHelper| match C::from_arg_matches(
command_helper.matches(),
) {
Ok(command) => custom_dispatch_fn(ui, command_helper, command),
Err(_) => old_dispatch_fn(ui, command_helper),
};
self.app = C::augment_subcommands(self.app);
self.dispatch_fn = Box::new(new_dispatch_fn);
self
}
/// Registers new global arguments in addition to the default ones.
pub fn add_global_args<A, F>(mut self, process_before: F) -> Self
where
A: clap::Args,
F: FnOnce(&mut Ui, A) -> Result<(), CommandError> + 'static,
{
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)]
fn run_internal(
self,
ui: &mut Ui,
mut layered_configs: LayeredConfigs,
) -> Result<(), CommandError> {
// `cwd` is canonicalized for consistency with `Workspace::workspace_root()` and
// to easily compute relative paths between them.
let cwd = env::current_dir()
.and_then(|cwd| cwd.canonicalize())
.map_err(|_| {
user_error_with_hint(
"Could not determine current directory",
"Did you update to a commit where the directory doesn't exist?",
)
})?;
// Use cwd-relative workspace configs to resolve default command and
// aliases. WorkspaceLoader::init() won't do any heavy lifting other
// than the path resolution.
let maybe_cwd_workspace_loader = self
.workspace_loader_factory
.create(find_workspace_dir(&cwd))
.map_err(|err| map_workspace_load_error(err, None));
layered_configs.read_user_config()?;
let mut repo_config_path = None;
if let Ok(loader) = &maybe_cwd_workspace_loader {
layered_configs.read_repo_config(loader.repo_path())?;
repo_config_path = Some(layered_configs.repo_config_path(loader.repo_path()));
}
let config = layered_configs.merge();
ui.reset(&config).map_err(|e| {
let user_config_path = layered_configs.user_config_path().unwrap_or(None);
let paths = [repo_config_path, user_config_path]
.into_iter()
.flatten()
.map(|path| format!("- {}", path.display()))
.join("\n");
e.hinted(format!("Check the following config files:\n{paths}"))
})?;
let string_args = expand_args(ui, &self.app, env::args_os(), &config)?;
let (matches, args) = parse_args(
ui,
&self.app,
&self.tracing_subscription,
&string_args,
&mut layered_configs,
)
.map_err(|err| map_clap_cli_error(err, ui, &layered_configs))?;
for process_global_args_fn in self.process_global_args_fns {
process_global_args_fn(ui, &matches)?;
}
let maybe_workspace_loader = if let Some(path) = &args.global_args.repository {
// Invalid -R path is an error. No need to proceed.
let loader = self
.workspace_loader_factory
.create(&cwd.join(path))
.map_err(|err| map_workspace_load_error(err, Some(path)))?;
layered_configs.read_repo_config(loader.repo_path())?;
Ok(loader)
} else {
maybe_cwd_workspace_loader
};
// Apply workspace configs and --config-toml arguments.
let config = layered_configs.merge();
ui.reset(&config)?;
// If -R is specified, check if the expanded arguments differ. Aliases
// can also be injected by --config-toml, but that's obviously wrong.
if args.global_args.repository.is_some() {
let new_string_args = expand_args(ui, &self.app, env::args_os(), &config).ok();
if new_string_args.as_ref() != Some(&string_args) {
writeln!(
ui.warning_default(),
"Command aliases cannot be loaded from -R/--repository path"
)?;
}
}
let settings = UserSettings::from_config(config);
let command_helper_data = CommandHelperData {
app: self.app,
cwd,
string_args,
matches,
global_args: args.global_args,
settings,
layered_configs,
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,
};
let command_helper = CommandHelper {
data: Rc::new(command_helper_data),
};
for start_hook_fn in self.start_hook_fns {
start_hook_fn(ui, &command_helper)?;
}
(self.dispatch_fn)(ui, &command_helper)
}
#[must_use]
#[instrument(skip(self))]
pub fn run(mut self) -> ExitCode {
let builder = config::Config::builder().add_source(crate::config::default_config());
let config = self
.extra_configs
.drain(..)
.fold(builder, |builder, config| builder.add_source(config))
.build()
.unwrap();
let layered_configs = LayeredConfigs::from_environment(config);
let mut ui = Ui::with_config(&layered_configs.merge())
.expect("default config should be valid, env vars are stringly typed");
let result = self.run_internal(&mut ui, layered_configs);
let exit_code = handle_command_result(&mut ui, result);
ui.finalize_pager();
exit_code
}
}
fn map_clap_cli_error(
mut cmd_err: CommandError,
ui: &Ui,
layered_configs: &LayeredConfigs,
) -> CommandError {
let Some(err) = cmd_err.error.downcast_ref::<clap::Error>() else {
return cmd_err;
};
if let (Some(ContextValue::String(arg)), Some(ContextValue::String(value))) = (
err.get(ContextKind::InvalidArg),
err.get(ContextKind::InvalidValue),
) {
if arg.as_str() == "--template <TEMPLATE>" && value.is_empty() {
// Suppress the error, it's less important than the original error.
if let Ok(template_aliases) = load_template_aliases(ui, layered_configs) {
cmd_err.add_hint(format_template_aliases_hint(&template_aliases));
}
}
}
cmd_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
}