Skip to main content

jj_cli/
cli_util.rs

1// Copyright 2022 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::borrow::Cow;
16use std::cell::OnceCell;
17use std::collections::BTreeMap;
18use std::collections::BTreeSet;
19use std::collections::HashMap;
20use std::collections::HashSet;
21use std::env;
22use std::ffi::OsString;
23use std::fmt;
24use std::fmt::Debug;
25use std::io;
26use std::io::Write as _;
27use std::mem;
28use std::ops::Range;
29use std::path::Path;
30use std::path::PathBuf;
31use std::pin::Pin;
32use std::rc::Rc;
33use std::sync::Arc;
34use std::sync::LazyLock;
35use std::time::SystemTime;
36
37use bstr::ByteVec as _;
38use chrono::TimeZone as _;
39use clap::ArgAction;
40use clap::ArgMatches;
41use clap::Command;
42use clap::FromArgMatches as _;
43use clap::builder::MapValueParser;
44use clap::builder::NonEmptyStringValueParser;
45use clap::builder::TypedValueParser as _;
46use clap::builder::ValueParserFactory;
47use clap::error::ContextKind;
48use clap::error::ContextValue;
49use clap_complete::ArgValueCandidates;
50use clap_complete::ArgValueCompleter;
51use futures::TryStreamExt as _;
52use futures::future::try_join_all;
53use indexmap::IndexMap;
54use indexmap::IndexSet;
55use indoc::indoc;
56use indoc::writedoc;
57use itertools::Itertools as _;
58use jj_lib::backend::BackendResult;
59use jj_lib::backend::ChangeId;
60use jj_lib::backend::CommitId;
61use jj_lib::backend::TreeValue;
62use jj_lib::commit::Commit;
63use jj_lib::config::ConfigGetError;
64use jj_lib::config::ConfigGetResultExt as _;
65use jj_lib::config::ConfigLayer;
66use jj_lib::config::ConfigMigrationRule;
67use jj_lib::config::ConfigNamePathBuf;
68use jj_lib::config::ConfigSource;
69use jj_lib::config::ConfigValue;
70use jj_lib::config::StackedConfig;
71use jj_lib::conflicts::ConflictMarkerStyle;
72use jj_lib::default_backend_factories::default_backend_factories;
73use jj_lib::default_backend_factories::default_working_copy_factories;
74use jj_lib::fileset;
75use jj_lib::fileset::FilesetAliasesMap;
76use jj_lib::fileset::FilesetDiagnostics;
77use jj_lib::fileset::FilesetExpression;
78use jj_lib::fileset::FilesetParseContext;
79use jj_lib::gitignore::GitIgnoreError;
80use jj_lib::gitignore::GitIgnoreFile;
81use jj_lib::id_prefix::IdPrefixContext;
82use jj_lib::lock::FileLock;
83use jj_lib::matchers::Matcher;
84use jj_lib::matchers::NothingMatcher;
85use jj_lib::merge::Diff;
86use jj_lib::merge::MergedTreeValue;
87use jj_lib::merged_tree::MergedTree;
88use jj_lib::object_id::ObjectId as _;
89use jj_lib::op_heads_store;
90use jj_lib::op_store::OpStoreError;
91use jj_lib::op_store::OperationId;
92use jj_lib::op_store::RefTarget;
93use jj_lib::op_walk;
94use jj_lib::op_walk::OpsetEvaluationError;
95use jj_lib::operation::Operation;
96use jj_lib::ref_name::RefName;
97use jj_lib::ref_name::RefNameBuf;
98use jj_lib::ref_name::RemoteName;
99use jj_lib::ref_name::WorkspaceName;
100use jj_lib::ref_name::WorkspaceNameBuf;
101use jj_lib::repo::CheckOutCommitError;
102use jj_lib::repo::EditCommitError;
103use jj_lib::repo::MutableRepo;
104use jj_lib::repo::ReadonlyRepo;
105use jj_lib::repo::Repo;
106use jj_lib::repo::RepoLoader;
107use jj_lib::repo::StoreFactories;
108use jj_lib::repo::StoreLoadError;
109use jj_lib::repo::merge_factories_map;
110use jj_lib::repo_path::RepoPath;
111use jj_lib::repo_path::RepoPathBuf;
112use jj_lib::repo_path::RepoPathUiConverter;
113use jj_lib::repo_path::UiPathParseError;
114use jj_lib::revset;
115use jj_lib::revset::ResolvedRevsetExpression;
116use jj_lib::revset::RevsetAliasesMap;
117use jj_lib::revset::RevsetDiagnostics;
118use jj_lib::revset::RevsetExpression;
119use jj_lib::revset::RevsetExtensions;
120use jj_lib::revset::RevsetFilterPredicate;
121use jj_lib::revset::RevsetFunction;
122use jj_lib::revset::RevsetParseContext;
123use jj_lib::revset::RevsetStreamExt as _;
124use jj_lib::revset::RevsetWorkspaceContext;
125use jj_lib::revset::SymbolResolverExtension;
126use jj_lib::revset::UserRevsetExpression;
127use jj_lib::rewrite::RebaseOptions;
128use jj_lib::rewrite::restore_tree;
129use jj_lib::settings::HumanByteSize;
130use jj_lib::settings::UserSettings;
131use jj_lib::store::Store;
132use jj_lib::str_util::StringExpression;
133use jj_lib::str_util::StringMatcher;
134use jj_lib::transaction::Transaction;
135use jj_lib::transaction::TransactionCommitError;
136use jj_lib::working_copy;
137use jj_lib::working_copy::CheckoutStats;
138use jj_lib::working_copy::LockedWorkingCopy;
139use jj_lib::working_copy::SnapshotOptions;
140use jj_lib::working_copy::SnapshotStats;
141use jj_lib::working_copy::UntrackedReason;
142use jj_lib::working_copy::WorkingCopy;
143use jj_lib::working_copy::WorkingCopyFactory;
144use jj_lib::working_copy::WorkingCopyFreshness;
145use jj_lib::workspace::DefaultWorkspaceLoaderFactory;
146use jj_lib::workspace::LockedWorkspace;
147use jj_lib::workspace::WorkingCopyFactories;
148use jj_lib::workspace::Workspace;
149use jj_lib::workspace::WorkspaceLoadError;
150use jj_lib::workspace::WorkspaceLoader;
151use jj_lib::workspace::WorkspaceLoaderFactory;
152use jj_lib::workspace::get_working_copy_factory;
153use pollster::FutureExt as _;
154use tracing::instrument;
155use tracing_chrome::ChromeLayerBuilder;
156use tracing_subscriber::prelude::*;
157
158use crate::command_error::CommandError;
159use crate::command_error::cli_error;
160use crate::command_error::config_error_with_message;
161use crate::command_error::handle_command_result;
162use crate::command_error::internal_error;
163use crate::command_error::internal_error_with_message;
164use crate::command_error::print_error_sources;
165use crate::command_error::print_parse_diagnostics;
166use crate::command_error::user_error;
167use crate::command_error::user_error_with_message;
168use crate::commit_templater::CommitTemplateLanguage;
169use crate::commit_templater::CommitTemplateLanguageExtension;
170use crate::complete;
171use crate::config::ConfigArgKind;
172use crate::config::ConfigEnv;
173use crate::config::RawConfig;
174use crate::config::config_from_environment;
175use crate::config::load_aliases_map;
176use crate::config::parse_config_args;
177use crate::description_util::TextEditor;
178use crate::diff_util;
179use crate::diff_util::DiffFormat;
180use crate::diff_util::DiffFormatArgs;
181use crate::diff_util::DiffRenderer;
182use crate::formatter::FormatRecorder;
183use crate::formatter::Formatter;
184use crate::formatter::FormatterExt as _;
185use crate::merge_tools::DiffEditor;
186use crate::merge_tools::MergeEditor;
187use crate::merge_tools::MergeToolConfigError;
188use crate::operation_templater::OperationTemplateLanguage;
189use crate::operation_templater::OperationTemplateLanguageExtension;
190use crate::revset_util;
191use crate::revset_util::RevsetExpressionEvaluator;
192use crate::revset_util::parse_union_name_patterns;
193use crate::template_builder;
194use crate::template_builder::TemplateLanguage;
195use crate::template_parser::TemplateAliasesMap;
196use crate::template_parser::TemplateDiagnostics;
197use crate::templater::TemplateRenderer;
198use crate::templater::WrapTemplateProperty;
199use crate::text_util;
200use crate::ui::ColorChoice;
201use crate::ui::Ui;
202
203const SHORT_CHANGE_ID_TEMPLATE_TEXT: &str = "format_short_change_id_with_change_offset(self)";
204
205#[derive(Clone)]
206struct ChromeTracingFlushGuard {
207    _inner: Option<Rc<tracing_chrome::FlushGuard>>,
208}
209
210impl Debug for ChromeTracingFlushGuard {
211    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
212        let Self { _inner } = self;
213        f.debug_struct("ChromeTracingFlushGuard")
214            .finish_non_exhaustive()
215    }
216}
217
218/// Handle to initialize or change tracing subscription.
219#[derive(Clone, Debug)]
220pub struct TracingSubscription {
221    reload_log_filter: tracing_subscriber::reload::Handle<
222        tracing_subscriber::EnvFilter,
223        tracing_subscriber::Registry,
224    >,
225    _chrome_tracing_flush_guard: ChromeTracingFlushGuard,
226}
227
228impl TracingSubscription {
229    const ENV_VAR_NAME: &str = "JJ_LOG";
230
231    /// Initializes tracing with the default configuration. This should be
232    /// called as early as possible.
233    pub fn init() -> Self {
234        let filter = tracing_subscriber::EnvFilter::builder()
235            .with_default_directive(tracing::metadata::LevelFilter::ERROR.into())
236            .with_env_var(Self::ENV_VAR_NAME)
237            .from_env_lossy();
238        let (filter, reload_log_filter) = tracing_subscriber::reload::Layer::new(filter);
239
240        let (chrome_tracing_layer, chrome_tracing_flush_guard) = match std::env::var("JJ_TRACE") {
241            Ok(filename) => {
242                let filename = if filename.is_empty() {
243                    format!(
244                        "jj-trace-{}.json",
245                        SystemTime::now()
246                            .duration_since(SystemTime::UNIX_EPOCH)
247                            .unwrap()
248                            .as_secs(),
249                    )
250                } else {
251                    filename
252                };
253                let include_args = std::env::var("JJ_TRACE_INCLUDE_ARGS").is_ok();
254                let (layer, guard) = ChromeLayerBuilder::new()
255                    .file(filename)
256                    .include_args(include_args)
257                    .build();
258                (
259                    Some(layer),
260                    ChromeTracingFlushGuard {
261                        _inner: Some(Rc::new(guard)),
262                    },
263                )
264            }
265            Err(_) => (None, ChromeTracingFlushGuard { _inner: None }),
266        };
267
268        tracing_subscriber::registry()
269            .with(
270                tracing_subscriber::fmt::Layer::default()
271                    .with_writer(std::io::stderr)
272                    .with_filter(filter),
273            )
274            .with(chrome_tracing_layer)
275            .init();
276        Self {
277            reload_log_filter,
278            _chrome_tracing_flush_guard: chrome_tracing_flush_guard,
279        }
280    }
281
282    pub fn enable_debug_logging(&self) -> Result<(), CommandError> {
283        self.reload_log_filter
284            .modify(|filter| {
285                // The default is INFO.
286                // jj-lib and jj-cli are whitelisted for DEBUG logging.
287                // This ensures that other crates' logging doesn't show up by default.
288                *filter = tracing_subscriber::EnvFilter::builder()
289                    .with_default_directive(tracing::metadata::LevelFilter::INFO.into())
290                    .with_env_var(Self::ENV_VAR_NAME)
291                    .from_env_lossy()
292                    .add_directive("jj_lib=debug".parse().unwrap())
293                    .add_directive("jj_cli=debug".parse().unwrap());
294            })
295            .map_err(|err| internal_error_with_message("failed to enable debug logging", err))?;
296        tracing::info!("debug logging enabled");
297        Ok(())
298    }
299}
300
301#[derive(Clone)]
302pub struct CommandHelper {
303    data: Rc<CommandHelperData>,
304}
305
306struct CommandHelperData {
307    app: Command,
308    cwd: PathBuf,
309    string_args: Vec<String>,
310    matches: ArgMatches,
311    global_args: GlobalArgs,
312    config_env: ConfigEnv,
313    config_migrations: Vec<ConfigMigrationRule>,
314    raw_config: RawConfig,
315    settings: UserSettings,
316    revset_extensions: Arc<RevsetExtensions>,
317    commit_template_extensions: Vec<Arc<dyn CommitTemplateLanguageExtension>>,
318    operation_template_extensions: Vec<Arc<dyn OperationTemplateLanguageExtension>>,
319    maybe_workspace_loader: Result<Box<dyn WorkspaceLoader>, CommandError>,
320    store_factories: StoreFactories,
321    working_copy_factories: WorkingCopyFactories,
322    workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
323}
324
325impl CommandHelper {
326    pub fn app(&self) -> &Command {
327        &self.data.app
328    }
329
330    /// Canonical form of the current working directory path.
331    ///
332    /// A loaded `Workspace::workspace_root()` also returns a canonical path, so
333    /// relative paths can be easily computed from these paths.
334    pub fn cwd(&self) -> &Path {
335        &self.data.cwd
336    }
337
338    pub fn string_args(&self) -> &Vec<String> {
339        &self.data.string_args
340    }
341
342    pub fn matches(&self) -> &ArgMatches {
343        &self.data.matches
344    }
345
346    pub fn global_args(&self) -> &GlobalArgs {
347        &self.data.global_args
348    }
349
350    pub fn config_env(&self) -> &ConfigEnv {
351        &self.data.config_env
352    }
353
354    /// Unprocessed (or unresolved) configuration data.
355    ///
356    /// Use this only if the unmodified config data is needed. For example, `jj
357    /// config set` should use this to write updated data back to file.
358    pub fn raw_config(&self) -> &RawConfig {
359        &self.data.raw_config
360    }
361
362    /// Settings for the current command and workspace.
363    ///
364    /// This may be different from the settings for new workspace created by
365    /// e.g. `jj git init`. There may be conditional variables and repo config
366    /// loaded for the cwd workspace.
367    pub fn settings(&self) -> &UserSettings {
368        &self.data.settings
369    }
370
371    /// Resolves configuration for new workspace located at the specified path.
372    pub fn settings_for_new_workspace(
373        &self,
374        ui: &Ui,
375        workspace_root: &Path,
376    ) -> Result<(UserSettings, ConfigEnv), CommandError> {
377        let mut config_env = self.data.config_env.clone();
378        let mut raw_config = self.data.raw_config.clone();
379        let repo_path = workspace_root.join(".jj").join("repo");
380        config_env.reset_repo_path(&repo_path);
381        config_env.reload_repo_config(ui, &mut raw_config)?;
382        config_env.reset_workspace_path(workspace_root);
383        config_env.reload_workspace_config(ui, &mut raw_config)?;
384        let mut config = config_env.resolve_config(&raw_config)?;
385        // No migration messages here, which would usually be emitted before.
386        jj_lib::config::migrate(&mut config, &self.data.config_migrations)?;
387        Ok((self.data.settings.with_new_config(config)?, config_env))
388    }
389
390    /// Loads text editor from the settings.
391    pub fn text_editor(&self) -> Result<TextEditor, ConfigGetError> {
392        TextEditor::from_settings(self.settings())
393    }
394
395    pub fn revset_extensions(&self) -> &Arc<RevsetExtensions> {
396        &self.data.revset_extensions
397    }
398
399    /// Parses template of the given language into evaluation tree.
400    ///
401    /// This function also loads template aliases from the settings. Use
402    /// `WorkspaceCommandHelper::parse_template()` if you've already
403    /// instantiated the workspace helper.
404    pub fn parse_template<'a, C, L>(
405        &self,
406        ui: &Ui,
407        language: &L,
408        template_text: &str,
409    ) -> Result<TemplateRenderer<'a, C>, CommandError>
410    where
411        C: Clone + 'a,
412        L: TemplateLanguage<'a> + ?Sized,
413        L::Property: WrapTemplateProperty<'a, C>,
414    {
415        let mut diagnostics = TemplateDiagnostics::new();
416        let aliases = load_template_aliases(ui, self.settings().config())?;
417        let template =
418            template_builder::parse(language, &mut diagnostics, template_text, &aliases)?;
419        print_parse_diagnostics(ui, "In template expression", &diagnostics)?;
420        Ok(template)
421    }
422
423    pub fn should_commit_transaction(&self) -> bool {
424        !self.global_args().no_integrate_operation
425    }
426
427    async fn maybe_commit_transaction(
428        &self,
429        tx: Transaction,
430        description: impl Into<String>,
431    ) -> Result<Arc<ReadonlyRepo>, TransactionCommitError> {
432        let unpublished_op = tx.write(description).await?;
433        if self.should_commit_transaction() {
434            unpublished_op.publish().await
435        } else {
436            Ok(unpublished_op.leave_unpublished())
437        }
438    }
439
440    pub fn workspace_loader(&self) -> Result<&dyn WorkspaceLoader, CommandError> {
441        self.data
442            .maybe_workspace_loader
443            .as_deref()
444            .map_err(Clone::clone)
445    }
446
447    fn new_workspace_loader_at(
448        &self,
449        workspace_root: &Path,
450    ) -> Result<Box<dyn WorkspaceLoader>, CommandError> {
451        self.data
452            .workspace_loader_factory
453            .create(workspace_root)
454            .map_err(|err| map_workspace_load_error(err, None))
455    }
456
457    /// Loads workspace and repo, then snapshots the working copy if allowed.
458    #[instrument(skip(self, ui))]
459    pub async fn workspace_helper(&self, ui: &Ui) -> Result<WorkspaceCommandHelper, CommandError> {
460        let (workspace_command, stats, _) = self.workspace_helper_with_stats(ui).await?;
461        print_snapshot_stats(ui, &stats, workspace_command.env().path_converter())?;
462        Ok(workspace_command)
463    }
464
465    /// Loads workspace and repo, then snapshots the working copy if allowed.
466    /// Returns [`SnapshotStats`] and a bool indicating if a snapshot was taken.
467    ///
468    /// Note that unless you have a good reason not to do so, you should always
469    /// call [`print_snapshot_stats`] with the [`SnapshotStats`] returned by
470    /// this function to present possible untracked files to the user.
471    #[instrument(skip(self, ui))]
472    pub async fn workspace_helper_with_stats(
473        &self,
474        ui: &Ui,
475    ) -> Result<(WorkspaceCommandHelper, SnapshotStats, bool), CommandError> {
476        let workspace = self.load_workspace()?;
477        let env = self.workspace_environment(ui, &workspace)?;
478        // Acquire the lock to ensure that the loaded repo points to the head
479        // operation whose refs should be synchronized with the Git repo. This
480        // prevents races with other processes during Git HEAD and refs
481        // import/export.
482        let git_import_export_lock = self
483            .is_working_copy_writable()
484            .then(|| env.lock_git_import_export(&workspace))
485            .transpose()?;
486        let mut workspace_command = self.load_from_workspace(ui, workspace, env).await?;
487        let Some(git_import_export_lock) = git_import_export_lock else {
488            return Ok((workspace_command, SnapshotStats::default(), false));
489        };
490
491        let old_repo = workspace_command.repo().clone();
492        let (workspace_command, stats) = match workspace_command
493            .snapshot_impl(ui, &git_import_export_lock)
494            .await
495        {
496            Ok(stats) => (workspace_command, stats),
497            Err(SnapshotWorkingCopyError::Command(err)) => return Err(err),
498            Err(SnapshotWorkingCopyError::StaleWorkingCopy(err)) => {
499                let auto_update_stale = self.settings().get_bool("snapshot.auto-update-stale")?;
500                if !auto_update_stale {
501                    return Err(err);
502                }
503
504                // We detected the working copy was stale and the client is configured to
505                // auto-update-stale, so let's do that now. We need to do it up here, not at a
506                // lower level (e.g. inside snapshot_working_copy()) to avoid recursive locking
507                // of the working copy.
508                let WorkspaceCommandHelper { workspace, env, .. } = workspace_command;
509                self.recover_stale_working_copy_impl(ui, workspace, env, &git_import_export_lock)
510                    .await?
511            }
512        };
513
514        let changed = old_repo.op_id() != workspace_command.repo().op_id();
515        Ok((workspace_command, stats, changed))
516    }
517
518    /// Loads workspace and repo, but never snapshots the working copy. Most
519    /// commands should use `workspace_helper()` instead.
520    #[instrument(skip(self, ui))]
521    pub async fn workspace_helper_no_snapshot(
522        &self,
523        ui: &Ui,
524    ) -> Result<WorkspaceCommandHelper, CommandError> {
525        let workspace = self.load_workspace()?;
526        let env = self.workspace_environment(ui, &workspace)?;
527        self.load_from_workspace(ui, workspace, env).await
528    }
529
530    async fn load_from_workspace(
531        &self,
532        ui: &Ui,
533        workspace: Workspace,
534        mut env: WorkspaceCommandEnvironment,
535    ) -> Result<WorkspaceCommandHelper, CommandError> {
536        let op_head =
537            self.resolve_operation(ui, workspace.repo_loader(), workspace.workspace_name())?;
538        let repo = workspace.repo_loader().load_at(&op_head).await?;
539        if let Err(err) =
540            revset_util::try_resolve_trunk_alias(repo.as_ref(), &env.revset_parse_context())
541        {
542            // The fallback can be builtin_trunk() if we're willing to support
543            // inferred trunk forever. (#7990)
544            let fallback = "root()";
545            writeln!(
546                ui.warning_default(),
547                "Failed to resolve `revset-aliases.trunk()`: {err}"
548            )?;
549            writeln!(
550                ui.warning_no_heading(),
551                "The `trunk()` alias is temporarily set to `{fallback}`."
552            )?;
553            writeln!(
554                ui.hint_default(),
555                "Use `jj config edit --repo` to adjust the `trunk()` alias."
556            )?;
557            env.revset_aliases_map
558                .insert("trunk()", fallback, None)
559                .expect("valid syntax");
560            env.reload_revset_expressions(ui)?;
561        }
562        let may_snapshot_working_copy = self.is_working_copy_writable();
563        WorkspaceCommandHelper::new(ui, workspace, repo, env, may_snapshot_working_copy)
564    }
565
566    pub fn get_working_copy_factory(&self) -> Result<&dyn WorkingCopyFactory, CommandError> {
567        let loader = self.workspace_loader()?;
568
569        // We convert StoreLoadError -> WorkspaceLoadError -> CommandError
570        let factory: Result<_, WorkspaceLoadError> =
571            get_working_copy_factory(loader, &self.data.working_copy_factories)
572                .map_err(|e| e.into());
573        let factory = factory.map_err(|err| {
574            map_workspace_load_error(err, self.data.global_args.repository.as_deref())
575        })?;
576        Ok(factory)
577    }
578
579    /// Loads workspace for the current command.
580    #[instrument(skip_all)]
581    pub fn load_workspace(&self) -> Result<Workspace, CommandError> {
582        let loader = self.workspace_loader()?;
583        loader
584            .load(
585                &self.data.settings,
586                &self.data.store_factories,
587                &self.data.working_copy_factories,
588            )
589            .map_err(|err| {
590                map_workspace_load_error(err, self.data.global_args.repository.as_deref())
591            })
592    }
593
594    /// Loads workspace located at the specified path.
595    #[instrument(skip(self, settings))]
596    pub fn load_workspace_at(
597        &self,
598        workspace_root: &Path,
599        settings: &UserSettings,
600    ) -> Result<Workspace, CommandError> {
601        let loader = self.new_workspace_loader_at(workspace_root)?;
602        loader
603            .load(
604                settings,
605                &self.data.store_factories,
606                &self.data.working_copy_factories,
607            )
608            .map_err(|err| map_workspace_load_error(err, None))
609    }
610
611    /// Note that unless you have a good reason not to do so, you should always
612    /// call [`print_snapshot_stats`] with the [`SnapshotStats`] returned by
613    /// this function to present possible untracked files to the user.
614    pub async fn recover_stale_working_copy(
615        &self,
616        ui: &Ui,
617    ) -> Result<(WorkspaceCommandHelper, SnapshotStats), CommandError> {
618        let workspace = self.load_workspace()?;
619        let env = self.workspace_environment(ui, &workspace)?;
620        let git_import_export_lock = env.lock_git_import_export(&workspace)?;
621        self.recover_stale_working_copy_impl(ui, workspace, env, &git_import_export_lock)
622            .await
623    }
624
625    async fn recover_stale_working_copy_impl(
626        &self,
627        ui: &Ui,
628        workspace: Workspace,
629        env: WorkspaceCommandEnvironment,
630        git_import_export_lock: &GitImportExportLock,
631    ) -> Result<(WorkspaceCommandHelper, SnapshotStats), CommandError> {
632        let op_id = workspace.working_copy().operation_id();
633        match workspace.repo_loader().load_operation(op_id).await {
634            Ok(op) => {
635                // self.for_workable_repo(), but reuse loaded env.
636                let repo = workspace.repo_loader().load_at(&op).await?;
637                let may_snapshot_working_copy = !self.global_args().ignore_working_copy;
638                let mut workspace_command = WorkspaceCommandHelper::new(
639                    ui,
640                    workspace,
641                    repo,
642                    env,
643                    may_snapshot_working_copy,
644                )?;
645                workspace_command.check_working_copy_writable()?;
646
647                // Snapshot the current working copy on top of the last known working-copy
648                // operation, then merge the divergent operations. The wc_commit_id of the
649                // merged repo wouldn't change because the old one wins, but it's probably
650                // fine if we picked the new wc_commit_id.
651                let stale_stats = workspace_command
652                    .snapshot_working_copy(ui, git_import_export_lock)
653                    .await
654                    .map_err(|err| err.into_command_error())?;
655
656                let wc_commit_id = workspace_command.get_wc_commit_id().unwrap();
657                let repo = workspace_command.repo();
658                let stale_wc_commit = repo.store().get_commit_async(wc_commit_id).await?;
659
660                let WorkspaceCommandHelper { workspace, env, .. } = workspace_command;
661                let mut workspace_command = self.load_from_workspace(ui, workspace, env).await?;
662
663                let repo = workspace_command.repo().clone();
664                let (mut locked_ws, desired_wc_commit) = workspace_command
665                    .unchecked_start_working_copy_mutation()
666                    .await?;
667                match WorkingCopyFreshness::check_stale(
668                    locked_ws.locked_wc(),
669                    &desired_wc_commit,
670                    &repo,
671                )
672                .await?
673                {
674                    WorkingCopyFreshness::Fresh | WorkingCopyFreshness::Updated(_) => {
675                        drop(locked_ws);
676                        writeln!(
677                            ui.status(),
678                            "Attempted recovery, but the working copy is not stale."
679                        )?;
680                    }
681                    WorkingCopyFreshness::WorkingCopyStale
682                    | WorkingCopyFreshness::SiblingOperation => {
683                        let stats = update_stale_working_copy(
684                            locked_ws,
685                            repo.op_id().clone(),
686                            &stale_wc_commit,
687                            &desired_wc_commit,
688                        )
689                        .await?;
690                        workspace_command.print_updated_working_copy_stats(
691                            ui,
692                            Some(&stale_wc_commit),
693                            &desired_wc_commit,
694                            &stats,
695                        )?;
696                        writeln!(
697                            ui.status(),
698                            "Updated working copy to fresh commit {}",
699                            short_commit_hash(desired_wc_commit.id())
700                        )?;
701                    }
702                }
703
704                // There may be Git refs to import, so snapshot again. Git HEAD
705                // will also be imported if it was updated after the working
706                // copy became stale. The result wouldn't be ideal, but there
707                // should be no data loss at least.
708                let fresh_stats = workspace_command
709                    .snapshot_impl(ui, git_import_export_lock)
710                    .await
711                    .map_err(|err| err.into_command_error())?;
712                let merged_stats = {
713                    let SnapshotStats {
714                        mut untracked_paths,
715                        mut invalid_utf8_paths,
716                    } = stale_stats;
717                    untracked_paths.extend(fresh_stats.untracked_paths);
718                    invalid_utf8_paths.extend(fresh_stats.invalid_utf8_paths);
719                    SnapshotStats {
720                        untracked_paths,
721                        invalid_utf8_paths,
722                    }
723                };
724                Ok((workspace_command, merged_stats))
725            }
726            Err(e @ OpStoreError::ObjectNotFound { .. }) => {
727                writeln!(
728                    ui.status(),
729                    "Failed to read working copy's current operation; attempting recovery. Error \
730                     message from read attempt: {e}"
731                )?;
732
733                let mut workspace_command = self.load_from_workspace(ui, workspace, env).await?;
734                let stats = workspace_command
735                    .create_and_check_out_recovery_commit(ui, git_import_export_lock)
736                    .await?;
737                Ok((workspace_command, stats))
738            }
739            Err(e) => Err(e.into()),
740        }
741    }
742
743    /// Loads command environment for the given `workspace`.
744    pub fn workspace_environment(
745        &self,
746        ui: &Ui,
747        workspace: &Workspace,
748    ) -> Result<WorkspaceCommandEnvironment, CommandError> {
749        WorkspaceCommandEnvironment::new(ui, self, workspace)
750    }
751
752    /// Returns true if the working copy to be loaded is writable, and therefore
753    /// should usually be snapshotted.
754    pub fn is_working_copy_writable(&self) -> bool {
755        self.is_at_head_operation() && !self.data.global_args.ignore_working_copy
756    }
757
758    /// Returns true if the current operation is considered to be the head.
759    pub fn is_at_head_operation(&self) -> bool {
760        // TODO: should we accept --at-op=<head_id> as the head op? or should we
761        // make --at-op=@ imply --ignore-working-copy (i.e. not at the head.)
762        matches!(
763            self.data.global_args.at_operation.as_deref(),
764            None | Some("@")
765        )
766    }
767
768    /// Resolves the current operation from the command-line argument.
769    ///
770    /// If no `--at-operation` is specified, the head operations will be
771    /// loaded. If there are multiple heads, they'll be merged.
772    #[instrument(skip_all)]
773    pub fn resolve_operation(
774        &self,
775        ui: &Ui,
776        repo_loader: &RepoLoader,
777        workspace_name: &WorkspaceName,
778    ) -> Result<Operation, CommandError> {
779        if let Some(op_str) = &self.data.global_args.at_operation {
780            Ok(op_walk::resolve_op_for_load(repo_loader, op_str).block_on()?)
781        } else {
782            op_heads_store::resolve_op_heads(
783                repo_loader.op_heads_store().as_ref(),
784                repo_loader.op_store(),
785                async |op_heads| {
786                    writeln!(
787                        ui.status(),
788                        "Concurrent modification detected, resolving automatically.",
789                    )?;
790                    // TODO: It may be helpful to print each operation we're merging here
791                    let transaction_description = "reconcile divergent operations";
792                    merge_operations(
793                        Some(ui),
794                        repo_loader,
795                        op_heads,
796                        Some(workspace_name),
797                        Some(transaction_description),
798                        &self.data.string_args,
799                    )
800                    .await
801                },
802            )
803            .block_on()
804        }
805    }
806
807    /// Creates helper for the repo whose view is supposed to be in sync with
808    /// the working copy. If `--ignore-working-copy` is not specified, the
809    /// returned helper will attempt to update the working copy.
810    #[instrument(skip_all)]
811    pub fn for_workable_repo(
812        &self,
813        ui: &Ui,
814        workspace: Workspace,
815        repo: Arc<ReadonlyRepo>,
816    ) -> Result<WorkspaceCommandHelper, CommandError> {
817        let env = self.workspace_environment(ui, &workspace)?;
818        // No is_at_head_operation() check here because the repo isn't loaded at
819        // the specified operation.
820        let may_snapshot_working_copy = !self.global_args().ignore_working_copy;
821        WorkspaceCommandHelper::new(ui, workspace, repo, env, may_snapshot_working_copy)
822    }
823}
824
825/// If `operations` is empty returns the root operation, if it contains a single
826/// entry returns that entry, otherwise merges the operations into a single
827/// operation. If `ui` is set, reports the number of rebased descendants.
828pub async fn merge_operations(
829    ui: Option<&Ui>,
830    repo_loader: &RepoLoader,
831    operations: Vec<Operation>,
832    workspace_name: Option<&WorkspaceName>,
833    transaction_description: Option<&str>,
834    command_args: &[String],
835) -> Result<Operation, CommandError> {
836    let transaction_attributes = command_args_to_transaction_attribute(command_args);
837    let (merged_repo, num_rebased) = repo_loader
838        .merge_operations(
839            operations,
840            workspace_name,
841            transaction_description,
842            transaction_attributes,
843        )
844        .await?;
845    if let Some(ui) = ui
846        && num_rebased > 0
847    {
848        writeln!(
849            ui.status(),
850            "Rebased {num_rebased} descendant commits onto commits rewritten by other operation.",
851        )?;
852    }
853    Ok(merged_repo.operation().clone())
854}
855
856/// A ReadonlyRepo along with user-config-dependent derived data. The derived
857/// data is lazily loaded.
858struct ReadonlyUserRepo {
859    repo: Arc<ReadonlyRepo>,
860    id_prefix_context: OnceCell<IdPrefixContext>,
861}
862
863impl ReadonlyUserRepo {
864    fn new(repo: Arc<ReadonlyRepo>) -> Self {
865        Self {
866            repo,
867            id_prefix_context: OnceCell::new(),
868        }
869    }
870}
871
872/// A advanceable bookmark to satisfy the "advance-bookmarks" feature.
873///
874/// This is a helper for `WorkspaceCommandTransaction`. It provides a
875/// type-safe way to separate the work of checking whether a bookmark
876/// can be advanced and actually advancing it. Advancing the bookmark
877/// never fails, but can't be done until the new `CommitId` is
878/// available. Splitting the work in this way also allows us to
879/// identify eligible bookmarks without actually moving them and
880/// return config errors to the user early.
881pub struct AdvanceableBookmark {
882    name: RefNameBuf,
883    old_commit_id: CommitId,
884}
885
886/// Parses advance-bookmarks settings into matcher.
887///
888/// Settings are configured in the jj config.toml as lists of string matcher
889/// expressions for enabled and disabled bookmarks. Example:
890/// ```toml
891/// [experimental-advance-branches]
892/// # Enable the feature for all branches except "main".
893/// enabled-branches = ["*"]
894/// disabled-branches = ["main"]
895/// ```
896fn load_advance_bookmarks_matcher(
897    ui: &Ui,
898    settings: &UserSettings,
899) -> Result<Option<StringMatcher>, CommandError> {
900    let get_setting = |setting_key: &str| -> Result<Vec<String>, _> {
901        let name = ConfigNamePathBuf::from_iter(["experimental-advance-branches", setting_key]);
902        settings.get(&name)
903    };
904    // TODO: When we stabilize this feature, enabled/disabled patterns can be
905    // combined into a single matcher expression.
906    let enabled_names = get_setting("enabled-branches")?;
907    let disabled_names = get_setting("disabled-branches")?;
908    let enabled_expr = parse_union_name_patterns(ui, &enabled_names)?;
909    let disabled_expr = parse_union_name_patterns(ui, &disabled_names)?;
910    if enabled_names.is_empty() {
911        Ok(None)
912    } else {
913        let expr = enabled_expr.intersection(disabled_expr.negated());
914        Ok(Some(expr.to_matcher()))
915    }
916}
917
918/// Metadata and configuration loaded for a specific workspace.
919pub struct WorkspaceCommandEnvironment {
920    command: CommandHelper,
921    settings: UserSettings,
922    fileset_aliases_map: FilesetAliasesMap,
923    revset_aliases_map: RevsetAliasesMap,
924    template_aliases_map: TemplateAliasesMap,
925    default_ignored_remote: Option<&'static RemoteName>,
926    path_converter: RepoPathUiConverter,
927    working_copy_shared_with_git: bool,
928    workspace_name: WorkspaceNameBuf,
929    immutable_heads_expression: Arc<UserRevsetExpression>,
930    short_prefixes_expression: Option<Arc<UserRevsetExpression>>,
931    conflict_marker_style: ConflictMarkerStyle,
932}
933
934impl WorkspaceCommandEnvironment {
935    #[instrument(skip_all)]
936    fn new(ui: &Ui, command: &CommandHelper, workspace: &Workspace) -> Result<Self, CommandError> {
937        let settings = workspace.settings();
938        let fileset_aliases_map = load_fileset_aliases(ui, settings.config())?;
939        let revset_aliases_map = load_revset_aliases(ui, settings.config())?;
940        let template_aliases_map = load_template_aliases(ui, settings.config())?;
941        let default_ignored_remote = default_ignored_remote_name(workspace.repo_loader().store());
942        let path_converter = RepoPathUiConverter::Fs {
943            cwd: command.cwd().to_owned(),
944            base: workspace.workspace_root().to_owned(),
945        };
946        #[cfg(feature = "git")]
947        let working_copy_shared_with_git = crate::git_util::is_colocated_git_workspace(workspace);
948        #[cfg(not(feature = "git"))]
949        let working_copy_shared_with_git = false;
950        let mut env = Self {
951            command: command.clone(),
952            settings: settings.clone(),
953            fileset_aliases_map,
954            revset_aliases_map,
955            template_aliases_map,
956            default_ignored_remote,
957            path_converter,
958            working_copy_shared_with_git,
959            workspace_name: workspace.workspace_name().to_owned(),
960            immutable_heads_expression: RevsetExpression::root(),
961            short_prefixes_expression: None,
962            conflict_marker_style: settings.get("ui.conflict-marker-style")?,
963        };
964        env.reload_revset_expressions(ui)?;
965        Ok(env)
966    }
967
968    pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
969        &self.path_converter
970    }
971
972    pub(crate) fn cwd(&self) -> &Path {
973        let RepoPathUiConverter::Fs { cwd, base: _ } = &self.path_converter;
974        cwd
975    }
976
977    pub fn workspace_name(&self) -> &WorkspaceName {
978        &self.workspace_name
979    }
980
981    /// Acquires a lock for Git import/export operations if the workspace is
982    /// supposed to be colocated.
983    fn lock_git_import_export(
984        &self,
985        workspace: &Workspace,
986    ) -> Result<GitImportExportLock, CommandError> {
987        let lock = if self.working_copy_shared_with_git {
988            let lock_path = workspace.repo_path().join("git_import_export.lock");
989            Some(FileLock::lock(lock_path).map_err(|err| {
990                user_error_with_message("Failed to take lock for Git import/export", err)
991            })?)
992        } else {
993            None
994        };
995        Ok(GitImportExportLock { _lock: lock })
996    }
997
998    /// Parsing context for fileset expressions specified by command arguments.
999    pub(crate) fn fileset_parse_context(&self) -> FilesetParseContext<'_> {
1000        FilesetParseContext {
1001            aliases_map: &self.fileset_aliases_map,
1002            path_converter: &self.path_converter,
1003        }
1004    }
1005
1006    /// Parsing context for fileset expressions loaded from config files.
1007    pub(crate) fn fileset_parse_context_for_config(&self) -> FilesetParseContext<'_> {
1008        // TODO: bump MSRV to 1.91.0 to leverage const PathBuf::new()
1009        static ROOT_PATH_CONVERTER: LazyLock<RepoPathUiConverter> =
1010            LazyLock::new(|| RepoPathUiConverter::Fs {
1011                cwd: PathBuf::new(),
1012                base: PathBuf::new(),
1013            });
1014        FilesetParseContext {
1015            aliases_map: &self.fileset_aliases_map,
1016            path_converter: &ROOT_PATH_CONVERTER,
1017        }
1018    }
1019
1020    pub(crate) fn revset_parse_context(&self) -> RevsetParseContext<'_> {
1021        let workspace_context = RevsetWorkspaceContext {
1022            path_converter: &self.path_converter,
1023            workspace_name: &self.workspace_name,
1024        };
1025        let now = if let Some(timestamp) = self.settings.commit_timestamp() {
1026            chrono::Local
1027                .timestamp_millis_opt(timestamp.timestamp.0)
1028                .unwrap()
1029        } else {
1030            chrono::Local::now()
1031        };
1032        RevsetParseContext {
1033            aliases_map: &self.revset_aliases_map,
1034            local_variables: HashMap::new(),
1035            user_email: self.settings.user_email(),
1036            date_pattern_context: now.into(),
1037            default_ignored_remote: self.default_ignored_remote,
1038            fileset_aliases_map: &self.fileset_aliases_map,
1039            extensions: self.command.revset_extensions(),
1040            workspace: Some(workspace_context),
1041        }
1042    }
1043
1044    /// Creates fresh new context which manages cache of short commit/change ID
1045    /// prefixes. New context should be created per repo view (or operation.)
1046    pub fn new_id_prefix_context(&self) -> IdPrefixContext {
1047        let context = IdPrefixContext::new(self.command.revset_extensions().clone());
1048        match &self.short_prefixes_expression {
1049            None => context,
1050            Some(expression) => context.disambiguate_within(expression.clone()),
1051        }
1052    }
1053
1054    /// Updates parsed revset expressions.
1055    fn reload_revset_expressions(&mut self, ui: &Ui) -> Result<(), CommandError> {
1056        self.immutable_heads_expression = self.load_immutable_heads_expression(ui)?;
1057        self.short_prefixes_expression = self.load_short_prefixes_expression(ui)?;
1058        Ok(())
1059    }
1060
1061    /// User-configured expression defining the immutable set.
1062    pub fn immutable_expression(&self) -> Arc<UserRevsetExpression> {
1063        // Negated ancestors expression `~::(<heads> | root())` is slightly
1064        // easier to optimize than negated union `~(::<heads> | root())`.
1065        self.immutable_heads_expression.ancestors()
1066    }
1067
1068    /// User-configured expression defining the heads of the immutable set.
1069    pub fn immutable_heads_expression(&self) -> &Arc<UserRevsetExpression> {
1070        &self.immutable_heads_expression
1071    }
1072
1073    /// User-configured conflict marker style for materializing conflicts
1074    pub fn conflict_marker_style(&self) -> ConflictMarkerStyle {
1075        self.conflict_marker_style
1076    }
1077
1078    fn load_immutable_heads_expression(
1079        &self,
1080        ui: &Ui,
1081    ) -> Result<Arc<UserRevsetExpression>, CommandError> {
1082        let mut diagnostics = RevsetDiagnostics::new();
1083        let expression = revset_util::parse_immutable_heads_expression(
1084            &mut diagnostics,
1085            &self.revset_parse_context(),
1086        )
1087        .map_err(|e| config_error_with_message("Invalid `revset-aliases.immutable_heads()`", e))?;
1088        print_parse_diagnostics(ui, "In `revset-aliases.immutable_heads()`", &diagnostics)?;
1089        Ok(expression)
1090    }
1091
1092    fn load_short_prefixes_expression(
1093        &self,
1094        ui: &Ui,
1095    ) -> Result<Option<Arc<UserRevsetExpression>>, CommandError> {
1096        let revset_string = self
1097            .settings
1098            .get_string("revsets.short-prefixes")
1099            .optional()?
1100            .map_or_else(|| self.settings.get_string("revsets.log"), Ok)?;
1101        if revset_string.is_empty() {
1102            Ok(None)
1103        } else {
1104            let mut diagnostics = RevsetDiagnostics::new();
1105            let expression = revset::parse(
1106                &mut diagnostics,
1107                &revset_string,
1108                &self.revset_parse_context(),
1109            )
1110            .map_err(|err| config_error_with_message("Invalid `revsets.short-prefixes`", err))?;
1111            print_parse_diagnostics(ui, "In `revsets.short-prefixes`", &diagnostics)?;
1112            Ok(Some(expression))
1113        }
1114    }
1115
1116    /// Resolves the effective `immutable()` expression to test against commits
1117    /// during a rewrite, taking the `--ignore-immutable` flag into account.
1118    fn resolve_immutable_expression(
1119        &self,
1120        repo: &dyn Repo,
1121    ) -> Result<Arc<ResolvedRevsetExpression>, CommandError> {
1122        let immutable_expression = if self.command.global_args().ignore_immutable {
1123            UserRevsetExpression::root()
1124        } else {
1125            self.immutable_expression()
1126        };
1127
1128        // Not using self.id_prefix_context() because the disambiguation data
1129        // must not be calculated and cached against arbitrary repo. It's also
1130        // unlikely that the immutable expression contains short hashes.
1131        let id_prefix_context = IdPrefixContext::new(self.command.revset_extensions().clone());
1132        RevsetExpressionEvaluator::new(
1133            repo,
1134            self.command.revset_extensions().clone(),
1135            &id_prefix_context,
1136            immutable_expression,
1137        )
1138        .resolve()
1139        .map_err(|e| config_error_with_message("Invalid `revset-aliases.immutable_heads()`", e))
1140    }
1141
1142    pub fn template_aliases_map(&self) -> &TemplateAliasesMap {
1143        &self.template_aliases_map
1144    }
1145
1146    /// Parses template of the given language into evaluation tree.
1147    pub fn parse_template<'a, C, L>(
1148        &self,
1149        ui: &Ui,
1150        language: &L,
1151        template_text: &str,
1152    ) -> Result<TemplateRenderer<'a, C>, CommandError>
1153    where
1154        C: Clone + 'a,
1155        L: TemplateLanguage<'a> + ?Sized,
1156        L::Property: WrapTemplateProperty<'a, C>,
1157    {
1158        let mut diagnostics = TemplateDiagnostics::new();
1159        let template = template_builder::parse(
1160            language,
1161            &mut diagnostics,
1162            template_text,
1163            &self.template_aliases_map,
1164        )?;
1165        print_parse_diagnostics(ui, "In template expression", &diagnostics)?;
1166        Ok(template)
1167    }
1168
1169    /// Creates commit template language environment for this workspace and the
1170    /// given `repo`.
1171    pub fn commit_template_language<'a>(
1172        &'a self,
1173        repo: &'a dyn Repo,
1174        id_prefix_context: &'a IdPrefixContext,
1175    ) -> CommitTemplateLanguage<'a> {
1176        CommitTemplateLanguage::new(
1177            repo,
1178            &self.path_converter,
1179            &self.workspace_name,
1180            self.revset_parse_context(),
1181            id_prefix_context,
1182            self.immutable_expression(),
1183            self.conflict_marker_style,
1184            &self.command.data.commit_template_extensions,
1185        )
1186    }
1187
1188    pub fn operation_template_extensions(&self) -> &[Arc<dyn OperationTemplateLanguageExtension>] {
1189        &self.command.data.operation_template_extensions
1190    }
1191}
1192
1193/// A token that holds a lock for git import/export operations in colocated
1194/// repositories. For non-colocated repos, this is an empty token (no actual
1195/// lock held). The lock is automatically released when this token is dropped.
1196pub struct GitImportExportLock {
1197    _lock: Option<FileLock>,
1198}
1199
1200/// Provides utilities for writing a command that works on a [`Workspace`]
1201/// (which most commands do).
1202pub struct WorkspaceCommandHelper {
1203    workspace: Workspace,
1204    user_repo: ReadonlyUserRepo,
1205    env: WorkspaceCommandEnvironment,
1206    // TODO: Parsed template can be cached if it doesn't capture 'repo lifetime
1207    commit_summary_template_text: String,
1208    op_summary_template_text: String,
1209    may_snapshot_working_copy: bool,
1210    may_update_working_copy: bool,
1211}
1212
1213enum SnapshotWorkingCopyError {
1214    Command(CommandError),
1215    StaleWorkingCopy(CommandError),
1216}
1217
1218impl SnapshotWorkingCopyError {
1219    fn into_command_error(self) -> CommandError {
1220        match self {
1221            Self::Command(err) => err,
1222            Self::StaleWorkingCopy(err) => err,
1223        }
1224    }
1225}
1226
1227fn snapshot_command_error<E>(err: E) -> SnapshotWorkingCopyError
1228where
1229    E: Into<CommandError>,
1230{
1231    SnapshotWorkingCopyError::Command(err.into())
1232}
1233
1234impl WorkspaceCommandHelper {
1235    #[instrument(skip_all)]
1236    fn new(
1237        ui: &Ui,
1238        workspace: Workspace,
1239        repo: Arc<ReadonlyRepo>,
1240        env: WorkspaceCommandEnvironment,
1241        may_snapshot_working_copy: bool,
1242    ) -> Result<Self, CommandError> {
1243        let settings = workspace.settings();
1244        let commit_summary_template_text = settings.get_string("templates.commit_summary")?;
1245        let op_summary_template_text = settings.get_string("templates.op_summary")?;
1246        let may_update_working_copy =
1247            may_snapshot_working_copy && env.command.should_commit_transaction();
1248
1249        let helper = Self {
1250            workspace,
1251            user_repo: ReadonlyUserRepo::new(repo),
1252            env,
1253            commit_summary_template_text,
1254            op_summary_template_text,
1255            may_snapshot_working_copy,
1256            may_update_working_copy,
1257        };
1258        // Parse commit_summary template early to report error before starting
1259        // mutable operation.
1260        helper.parse_operation_template(ui, &helper.op_summary_template_text)?;
1261        helper.parse_commit_template(ui, &helper.commit_summary_template_text)?;
1262        helper.parse_commit_template(ui, SHORT_CHANGE_ID_TEMPLATE_TEXT)?;
1263        Ok(helper)
1264    }
1265
1266    /// Settings for this workspace.
1267    pub fn settings(&self) -> &UserSettings {
1268        self.workspace.settings()
1269    }
1270
1271    pub fn check_working_copy_writable(&self) -> Result<(), CommandError> {
1272        if self.may_update_working_copy {
1273            Ok(())
1274        } else {
1275            let hint = if self.env.command.global_args().ignore_working_copy {
1276                "Don't use --ignore-working-copy."
1277            } else if self.env.command.global_args().no_integrate_operation {
1278                "Don't use --no-integrate-operation."
1279            } else {
1280                "Don't use --at-op."
1281            };
1282            Err(user_error("This command must be able to update the working copy.").hinted(hint))
1283        }
1284    }
1285
1286    /// Acquires a lock for git import/export operations if the workspace is
1287    /// colocated with Git. Returns a token that can be passed to functions
1288    /// that need to import from or export to Git. For non-colocated repos,
1289    /// returns a token with no lock inside.
1290    fn lock_git_import_export(&self) -> Result<GitImportExportLock, CommandError> {
1291        self.env.lock_git_import_export(&self.workspace)
1292    }
1293
1294    /// Note that unless you have a good reason not to do so, you should always
1295    /// call [`print_snapshot_stats`] with the [`SnapshotStats`] returned by
1296    /// this function to present possible untracked files to the user.
1297    #[instrument(skip_all)]
1298    async fn snapshot_impl(
1299        &mut self,
1300        ui: &Ui,
1301        git_import_export_lock: &GitImportExportLock,
1302    ) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
1303        assert!(self.may_snapshot_working_copy);
1304        #[cfg(feature = "git")]
1305        if self.env.working_copy_shared_with_git {
1306            self.import_git_head(ui, git_import_export_lock)
1307                .await
1308                .map_err(snapshot_command_error)?;
1309        }
1310        // Because the Git refs (except HEAD) aren't imported yet, the ref
1311        // pointing to the new working-copy commit might not be exported.
1312        // In that situation, the ref would be conflicted anyway, so export
1313        // failure is okay.
1314        let stats = self
1315            .snapshot_working_copy(ui, git_import_export_lock)
1316            .await?;
1317
1318        // import_git_refs() can rebase the working-copy commit.
1319        #[cfg(feature = "git")]
1320        if self.env.working_copy_shared_with_git {
1321            self.import_git_refs(ui, git_import_export_lock)
1322                .await
1323                .map_err(snapshot_command_error)?;
1324        }
1325        Ok(stats)
1326    }
1327
1328    /// Snapshots the working copy if allowed, and imports Git refs if the
1329    /// working copy is colocated with Git.
1330    #[instrument(skip_all)]
1331    pub async fn maybe_snapshot(&mut self, ui: &Ui) -> Result<(), CommandError> {
1332        if !self.may_snapshot_working_copy {
1333            return Ok(());
1334        }
1335        let git_import_export_lock = self.lock_git_import_export()?;
1336        let stats = self
1337            .snapshot_impl(ui, &git_import_export_lock)
1338            .await
1339            .map_err(|err| err.into_command_error())?;
1340        print_snapshot_stats(ui, &stats, self.env().path_converter())?;
1341        Ok(())
1342    }
1343
1344    /// Imports new HEAD from the colocated Git repo.
1345    ///
1346    /// If the Git HEAD has changed, this function checks out the new Git HEAD.
1347    /// The old working-copy commit will be abandoned if it's discardable. The
1348    /// working-copy state will be reset to point to the new Git HEAD. The
1349    /// working-copy contents won't be updated.
1350    #[cfg(feature = "git")]
1351    #[instrument(skip_all)]
1352    async fn import_git_head(
1353        &mut self,
1354        ui: &Ui,
1355        git_import_export_lock: &GitImportExportLock,
1356    ) -> Result<(), CommandError> {
1357        assert!(self.may_snapshot_working_copy);
1358        let mut tx = self.start_transaction();
1359        jj_lib::git::import_head(tx.repo_mut()).await?;
1360        if !tx.repo().has_changes() {
1361            return Ok(());
1362        }
1363
1364        let mut tx = tx.into_inner();
1365        let old_git_head = self.repo().view().git_head().clone();
1366        let new_git_head = tx.repo().view().git_head().clone();
1367        if let Some(new_git_head_id) = new_git_head.as_normal() {
1368            let workspace_name = self.workspace_name().to_owned();
1369            let new_git_head_commit = tx.repo().store().get_commit_async(new_git_head_id).await?;
1370            let wc_commit = tx
1371                .repo_mut()
1372                .check_out(workspace_name, &new_git_head_commit)
1373                .await?;
1374            let mut locked_ws = self.workspace.start_working_copy_mutation().await?;
1375            // The working copy was presumably updated by the git command that updated
1376            // HEAD, so we just need to reset our working copy
1377            // state to it without updating working copy files.
1378            locked_ws.locked_wc().reset(&wc_commit).await?;
1379            tx.repo_mut().rebase_descendants().await?;
1380            self.user_repo = ReadonlyUserRepo::new(
1381                self.env
1382                    .command
1383                    .maybe_commit_transaction(tx, "import git head")
1384                    .await?,
1385            );
1386            if self.env.command.should_commit_transaction() {
1387                locked_ws
1388                    .finish(self.user_repo.repo.op_id().clone())
1389                    .await?;
1390            }
1391            if old_git_head.is_present() {
1392                writeln!(
1393                    ui.status(),
1394                    "Reset the working copy parent to the new Git HEAD."
1395                )?;
1396            } else {
1397                // Don't print verbose message on initial checkout.
1398            }
1399            if !self.env.command.should_commit_transaction() {
1400                writeln!(
1401                    ui.status(),
1402                    "Operation left uncommitted because --no-integrate-operation was requested: {}",
1403                    short_operation_hash(self.repo().op_id())
1404                )?;
1405            }
1406        } else {
1407            // Unlikely, but the HEAD ref got deleted by git?
1408            let num_rebased = tx.repo_mut().rebase_descendants().await?;
1409            if num_rebased > 0 {
1410                writeln!(ui.status(), "Rebased {num_rebased} descendant commits.")?;
1411            }
1412            self.finish_transaction(ui, tx, "import git head", git_import_export_lock)
1413                .await?;
1414        }
1415        Ok(())
1416    }
1417
1418    /// Imports branches and tags from the underlying Git repo, abandons old
1419    /// bookmarks.
1420    ///
1421    /// If the working-copy branch is rebased, and if update is allowed, the
1422    /// new working-copy commit will be checked out.
1423    ///
1424    /// This function does not import the Git HEAD, but the HEAD may be reset to
1425    /// the working copy parent if the repository is colocated.
1426    #[cfg(feature = "git")]
1427    #[instrument(skip_all)]
1428    async fn import_git_refs(
1429        &mut self,
1430        ui: &Ui,
1431        git_import_export_lock: &GitImportExportLock,
1432    ) -> Result<(), CommandError> {
1433        use jj_lib::git;
1434        let git_settings = git::GitSettings::from_settings(self.settings())?;
1435        let remote_settings = self.settings().remote_settings()?;
1436        let import_options =
1437            crate::git_util::load_git_import_options(ui, &git_settings, &remote_settings)?;
1438        let mut tx = self.start_transaction();
1439        let stats = git::import_refs(tx.repo_mut(), &import_options).await?;
1440        crate::git_util::print_git_import_stats_summary(ui, &stats)?;
1441        if !tx.repo().has_changes() {
1442            return Ok(());
1443        }
1444
1445        let mut tx = tx.into_inner();
1446        let num_rebased = rebase_mutable_descendants(&self.env, &mut tx).await?;
1447        if num_rebased > 0 {
1448            writeln!(
1449                ui.status(),
1450                "Rebased {num_rebased} descendant commits off of commits rewritten from Git."
1451            )?;
1452        }
1453        self.finish_transaction(ui, tx, "import git refs", git_import_export_lock)
1454            .await?;
1455        writeln!(
1456            ui.status(),
1457            "Done importing changes from the underlying Git repo."
1458        )?;
1459        Ok(())
1460    }
1461
1462    pub fn repo(&self) -> &Arc<ReadonlyRepo> {
1463        &self.user_repo.repo
1464    }
1465
1466    pub fn repo_path(&self) -> &Path {
1467        self.workspace.repo_path()
1468    }
1469
1470    pub fn workspace(&self) -> &Workspace {
1471        &self.workspace
1472    }
1473
1474    pub fn working_copy(&self) -> &dyn WorkingCopy {
1475        self.workspace.working_copy()
1476    }
1477
1478    pub fn env(&self) -> &WorkspaceCommandEnvironment {
1479        &self.env
1480    }
1481
1482    pub async fn unchecked_start_working_copy_mutation(
1483        &mut self,
1484    ) -> Result<(LockedWorkspace<'_>, Commit), CommandError> {
1485        self.check_working_copy_writable()?;
1486        let wc_commit = if let Some(wc_commit_id) = self.get_wc_commit_id() {
1487            self.repo().store().get_commit_async(wc_commit_id).await?
1488        } else {
1489            return Err(user_error("Nothing checked out in this workspace"));
1490        };
1491
1492        let locked_ws = self.workspace.start_working_copy_mutation().await?;
1493
1494        Ok((locked_ws, wc_commit))
1495    }
1496
1497    pub async fn start_working_copy_mutation(
1498        &mut self,
1499    ) -> Result<(LockedWorkspace<'_>, Commit), CommandError> {
1500        let (mut locked_ws, wc_commit) = self.unchecked_start_working_copy_mutation().await?;
1501        if wc_commit.tree().tree_ids_and_labels()
1502            != locked_ws.locked_wc().old_tree().tree_ids_and_labels()
1503        {
1504            return Err(user_error("Concurrent working copy operation. Try again."));
1505        }
1506        Ok((locked_ws, wc_commit))
1507    }
1508
1509    async fn create_and_check_out_recovery_commit(
1510        &mut self,
1511        ui: &Ui,
1512        git_import_export_lock: &GitImportExportLock,
1513    ) -> Result<SnapshotStats, CommandError> {
1514        self.check_working_copy_writable()?;
1515
1516        let workspace_name = self.workspace_name().to_owned();
1517        let mut locked_ws = self.workspace.start_working_copy_mutation().await?;
1518        let (repo, new_commit) = working_copy::create_and_check_out_recovery_commit(
1519            locked_ws.locked_wc(),
1520            &self.user_repo.repo,
1521            workspace_name,
1522            "RECOVERY COMMIT FROM `jj workspace update-stale`
1523
1524This commit contains changes that were written to the working copy by an
1525operation that was subsequently lost (or was at least unavailable when you ran
1526`jj workspace update-stale`). Because the operation was lost, we don't know
1527what the parent commits are supposed to be. That means that the diff compared
1528to the current parents may contain changes from multiple commits.
1529",
1530        )
1531        .await?;
1532
1533        writeln!(
1534            ui.status(),
1535            "Created and checked out recovery commit {}",
1536            short_commit_hash(new_commit.id())
1537        )?;
1538        locked_ws.finish(repo.op_id().clone()).await?;
1539        self.user_repo = ReadonlyUserRepo::new(repo);
1540
1541        self.snapshot_impl(ui, git_import_export_lock)
1542            .await
1543            .map_err(|err| err.into_command_error())
1544    }
1545
1546    pub fn workspace_root(&self) -> &Path {
1547        self.workspace.workspace_root()
1548    }
1549
1550    pub fn workspace_name(&self) -> &WorkspaceName {
1551        self.workspace.workspace_name()
1552    }
1553
1554    pub fn get_wc_commit_id(&self) -> Option<&CommitId> {
1555        self.repo().view().get_wc_commit_id(self.workspace_name())
1556    }
1557
1558    pub fn working_copy_shared_with_git(&self) -> bool {
1559        self.env.working_copy_shared_with_git
1560    }
1561
1562    pub fn format_file_path(&self, file: &RepoPath) -> String {
1563        self.path_converter().format_file_path(file)
1564    }
1565
1566    /// Parses a path relative to cwd into a RepoPath, which is relative to the
1567    /// workspace root.
1568    pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
1569        self.path_converter().parse_file_path(input)
1570    }
1571
1572    /// Parses the given strings as file patterns.
1573    pub fn parse_file_patterns(
1574        &self,
1575        ui: &Ui,
1576        values: &[String],
1577    ) -> Result<FilesetExpression, CommandError> {
1578        // TODO: This function might be superseded by parse_union_filesets(),
1579        // but it would be weird if parse_union_*() had a special case for the
1580        // empty arguments.
1581        if values.is_empty() {
1582            Ok(FilesetExpression::all())
1583        } else {
1584            self.parse_union_filesets(ui, values)
1585        }
1586    }
1587
1588    /// Parses the given fileset expressions and concatenates them all.
1589    pub fn parse_union_filesets(
1590        &self,
1591        ui: &Ui,
1592        file_args: &[String], // TODO: introduce FileArg newtype?
1593    ) -> Result<FilesetExpression, CommandError> {
1594        let mut diagnostics = FilesetDiagnostics::new();
1595        let context = self.env.fileset_parse_context();
1596        let expressions: Vec<_> = file_args
1597            .iter()
1598            .map(|arg| fileset::parse_maybe_bare(&mut diagnostics, arg, &context))
1599            .try_collect()?;
1600        print_parse_diagnostics(ui, "In fileset expression", &diagnostics)?;
1601        Ok(FilesetExpression::union_all(expressions))
1602    }
1603
1604    pub fn auto_tracking_matcher(&self, ui: &Ui) -> Result<Box<dyn Matcher>, CommandError> {
1605        let mut diagnostics = FilesetDiagnostics::new();
1606        let pattern = self.settings().get_string("snapshot.auto-track")?;
1607        let context = self.env.fileset_parse_context_for_config();
1608        let expression = fileset::parse(&mut diagnostics, &pattern, &context)?;
1609        print_parse_diagnostics(ui, "In `snapshot.auto-track`", &diagnostics)?;
1610        Ok(expression.to_matcher())
1611    }
1612
1613    pub fn snapshot_options_with_start_tracking_matcher<'a>(
1614        &self,
1615        start_tracking_matcher: &'a dyn Matcher,
1616    ) -> Result<SnapshotOptions<'a>, CommandError> {
1617        let base_ignores = self.base_ignores()?;
1618        let HumanByteSize(mut max_new_file_size) = self
1619            .settings()
1620            .get_value_with("snapshot.max-new-file-size", TryInto::try_into)?;
1621        if max_new_file_size == 0 {
1622            max_new_file_size = u64::MAX;
1623        }
1624        Ok(SnapshotOptions {
1625            base_ignores,
1626            progress: None,
1627            start_tracking_matcher,
1628            force_tracking_matcher: &NothingMatcher,
1629            max_new_file_size,
1630        })
1631    }
1632
1633    pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
1634        self.env.path_converter()
1635    }
1636
1637    #[cfg(not(feature = "git"))]
1638    pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1639        Ok(GitIgnoreFile::empty())
1640    }
1641
1642    #[cfg(feature = "git")]
1643    #[instrument(skip_all)]
1644    pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1645        let get_excludes_file_path = |config: &gix::config::File| -> Option<PathBuf> {
1646            // TODO: maybe use path() and interpolate(), which can process non-utf-8
1647            // path on Unix.
1648            if let Some(value) = config.string("core.excludesFile") {
1649                let path = str::from_utf8(&value)
1650                    .ok()
1651                    .map(jj_lib::file_util::expand_home_path)?;
1652                // The configured path is usually absolute, but if it's relative,
1653                // the "git" command would read the file at the work-tree directory.
1654                Some(self.workspace_root().join(path))
1655            } else {
1656                xdg_config_home().map(|x| x.join("git").join("ignore"))
1657            }
1658        };
1659
1660        fn xdg_config_home() -> Option<PathBuf> {
1661            if let Ok(x) = std::env::var("XDG_CONFIG_HOME")
1662                && !x.is_empty()
1663            {
1664                return Some(PathBuf::from(x));
1665            }
1666            etcetera::home_dir().ok().map(|home| home.join(".config"))
1667        }
1668
1669        let mut git_ignores = GitIgnoreFile::empty();
1670        if let Ok(git_backend) = jj_lib::git::get_git_backend(self.repo().store()) {
1671            let git_repo = git_backend.git_repo();
1672            if let Some(excludes_file_path) = get_excludes_file_path(&git_repo.config_snapshot()) {
1673                git_ignores = git_ignores.chain_with_file(RepoPath::root(), excludes_file_path)?;
1674            }
1675            git_ignores = git_ignores.chain_with_file(
1676                RepoPath::root(),
1677                git_backend.git_repo_path().join("info").join("exclude"),
1678            )?;
1679        } else if let Ok(git_config) = gix::config::File::from_globals()
1680            && let Some(excludes_file_path) = get_excludes_file_path(&git_config)
1681        {
1682            git_ignores = git_ignores.chain_with_file(RepoPath::root(), excludes_file_path)?;
1683        }
1684        Ok(git_ignores)
1685    }
1686
1687    /// Creates textual diff renderer of the specified `formats`.
1688    pub fn diff_renderer(&self, formats: Vec<DiffFormat>) -> DiffRenderer<'_> {
1689        DiffRenderer::new(
1690            self.repo().as_ref(),
1691            self.path_converter(),
1692            self.env.conflict_marker_style(),
1693            formats,
1694        )
1695    }
1696
1697    /// Loads textual diff renderer from the settings and command arguments.
1698    pub fn diff_renderer_for(
1699        &self,
1700        args: &DiffFormatArgs,
1701    ) -> Result<DiffRenderer<'_>, CommandError> {
1702        let formats = diff_util::diff_formats_for(self.settings(), args)?;
1703        Ok(self.diff_renderer(formats))
1704    }
1705
1706    /// Loads textual diff renderer from the settings and log-like command
1707    /// arguments. Returns `Ok(None)` if there are no command arguments that
1708    /// enable patch output.
1709    pub fn diff_renderer_for_log(
1710        &self,
1711        args: &DiffFormatArgs,
1712        patch: bool,
1713    ) -> Result<Option<DiffRenderer<'_>>, CommandError> {
1714        let formats = diff_util::diff_formats_for_log(self.settings(), args, patch)?;
1715        Ok((!formats.is_empty()).then(|| self.diff_renderer(formats)))
1716    }
1717
1718    /// Loads diff editor from the settings.
1719    ///
1720    /// If the `tool_name` isn't specified, the default editor will be returned.
1721    pub fn diff_editor(
1722        &self,
1723        ui: &Ui,
1724        tool_name: Option<&str>,
1725    ) -> Result<DiffEditor, CommandError> {
1726        let base_ignores = self.base_ignores()?;
1727        let conflict_marker_style = self.env.conflict_marker_style();
1728        if let Some(name) = tool_name {
1729            Ok(DiffEditor::with_name(
1730                name,
1731                self.settings(),
1732                base_ignores,
1733                conflict_marker_style,
1734            )?)
1735        } else {
1736            Ok(DiffEditor::from_settings(
1737                ui,
1738                self.settings(),
1739                base_ignores,
1740                conflict_marker_style,
1741            )?)
1742        }
1743    }
1744
1745    /// Conditionally loads diff editor from the settings.
1746    ///
1747    /// If the `tool_name` is specified, interactive session is implied.
1748    pub fn diff_selector(
1749        &self,
1750        ui: &Ui,
1751        tool_name: Option<&str>,
1752        force_interactive: bool,
1753    ) -> Result<DiffSelector, CommandError> {
1754        if tool_name.is_some() || force_interactive {
1755            Ok(DiffSelector::Interactive(self.diff_editor(ui, tool_name)?))
1756        } else {
1757            Ok(DiffSelector::NonInteractive)
1758        }
1759    }
1760
1761    /// Loads 3-way merge editor from the settings.
1762    ///
1763    /// If the `tool_name` isn't specified, the default editor will be returned.
1764    pub fn merge_editor(
1765        &self,
1766        ui: &Ui,
1767        tool_name: Option<&str>,
1768    ) -> Result<MergeEditor, MergeToolConfigError> {
1769        let conflict_marker_style = self.env.conflict_marker_style();
1770        if let Some(name) = tool_name {
1771            MergeEditor::with_name(
1772                name,
1773                self.settings(),
1774                self.path_converter().clone(),
1775                conflict_marker_style,
1776            )
1777        } else {
1778            MergeEditor::from_settings(
1779                ui,
1780                self.settings(),
1781                self.path_converter().clone(),
1782                conflict_marker_style,
1783            )
1784        }
1785    }
1786
1787    /// Loads text editor from the settings.
1788    pub fn text_editor(&self) -> Result<TextEditor, ConfigGetError> {
1789        TextEditor::from_settings(self.settings())
1790    }
1791
1792    pub fn resolve_single_op(&self, op_str: &str) -> Result<Operation, OpsetEvaluationError> {
1793        op_walk::resolve_op_with_repo(self.repo(), op_str).block_on()
1794    }
1795
1796    /// Resolve a revset to a single revision. Return an error if the revset is
1797    /// empty or has multiple revisions.
1798    pub async fn resolve_single_rev(
1799        &self,
1800        ui: &Ui,
1801        revision_arg: &RevisionArg,
1802    ) -> Result<Commit, CommandError> {
1803        let expression = self.parse_revset(ui, revision_arg)?;
1804        revset_util::evaluate_revset_to_single_commit(revision_arg.as_ref(), &expression, || {
1805            self.commit_summary_template()
1806        })
1807        .await
1808    }
1809
1810    /// Evaluates revset expressions to set of commit IDs. The
1811    /// returned set preserves the order of the input expressions.
1812    pub async fn resolve_revsets_ordered(
1813        &self,
1814        ui: &Ui,
1815        revision_args: &[RevisionArg],
1816    ) -> Result<IndexSet<CommitId>, CommandError> {
1817        let mut all_commits = IndexSet::new();
1818        for revision_arg in revision_args {
1819            let expression = self.parse_revset(ui, revision_arg)?;
1820            let mut stream = expression.evaluate_to_commit_ids()?;
1821            while let Some(commit_id) = stream.try_next().await? {
1822                all_commits.insert(commit_id);
1823            }
1824        }
1825        Ok(all_commits)
1826    }
1827
1828    /// Evaluates revset expressions to non-empty set of commit IDs. The
1829    /// returned set preserves the order of the input expressions.
1830    pub async fn resolve_some_revsets(
1831        &self,
1832        ui: &Ui,
1833        revision_args: &[RevisionArg],
1834    ) -> Result<IndexSet<CommitId>, CommandError> {
1835        let all_commits = self.resolve_revsets_ordered(ui, revision_args).await?;
1836        if all_commits.is_empty() {
1837            Err(user_error("Empty revision set"))
1838        } else {
1839            Ok(all_commits)
1840        }
1841    }
1842
1843    pub fn parse_revset(
1844        &self,
1845        ui: &Ui,
1846        revision_arg: &RevisionArg,
1847    ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1848        let mut diagnostics = RevsetDiagnostics::new();
1849        let context = self.env.revset_parse_context();
1850        let expression = revset::parse(&mut diagnostics, revision_arg.as_ref(), &context)?;
1851        print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1852        Ok(self.attach_revset_evaluator(expression))
1853    }
1854
1855    /// Parses the given revset expressions and concatenates them all.
1856    pub fn parse_union_revsets(
1857        &self,
1858        ui: &Ui,
1859        revision_args: &[RevisionArg],
1860    ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1861        let mut diagnostics = RevsetDiagnostics::new();
1862        let context = self.env.revset_parse_context();
1863        let expressions: Vec<_> = revision_args
1864            .iter()
1865            .map(|arg| revset::parse(&mut diagnostics, arg.as_ref(), &context))
1866            .try_collect()?;
1867        print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1868        let expression = RevsetExpression::union_all(&expressions);
1869        Ok(self.attach_revset_evaluator(expression))
1870    }
1871
1872    pub fn attach_revset_evaluator(
1873        &self,
1874        expression: Arc<UserRevsetExpression>,
1875    ) -> RevsetExpressionEvaluator<'_> {
1876        RevsetExpressionEvaluator::new(
1877            self.repo().as_ref(),
1878            self.env.command.revset_extensions().clone(),
1879            self.id_prefix_context(),
1880            expression,
1881        )
1882    }
1883
1884    pub fn id_prefix_context(&self) -> &IdPrefixContext {
1885        self.user_repo
1886            .id_prefix_context
1887            .get_or_init(|| self.env.new_id_prefix_context())
1888    }
1889
1890    /// Parses template of the given language into evaluation tree.
1891    pub fn parse_template<'a, C, L>(
1892        &self,
1893        ui: &Ui,
1894        language: &L,
1895        template_text: &str,
1896    ) -> Result<TemplateRenderer<'a, C>, CommandError>
1897    where
1898        C: Clone + 'a,
1899        L: TemplateLanguage<'a> + ?Sized,
1900        L::Property: WrapTemplateProperty<'a, C>,
1901    {
1902        self.env.parse_template(ui, language, template_text)
1903    }
1904
1905    /// Parses template that is validated by `Self::new()`.
1906    fn reparse_valid_template<'a, C, L>(
1907        &self,
1908        language: &L,
1909        template_text: &str,
1910    ) -> TemplateRenderer<'a, C>
1911    where
1912        C: Clone + 'a,
1913        L: TemplateLanguage<'a> + ?Sized,
1914        L::Property: WrapTemplateProperty<'a, C>,
1915    {
1916        template_builder::parse(
1917            language,
1918            &mut TemplateDiagnostics::new(),
1919            template_text,
1920            &self.env.template_aliases_map,
1921        )
1922        .expect("parse error should be confined by WorkspaceCommandHelper::new()")
1923    }
1924
1925    /// Parses commit template into evaluation tree.
1926    pub fn parse_commit_template(
1927        &self,
1928        ui: &Ui,
1929        template_text: &str,
1930    ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
1931        let language = self.commit_template_language();
1932        self.parse_template(ui, &language, template_text)
1933    }
1934
1935    /// Parses commit template into evaluation tree.
1936    pub fn parse_operation_template(
1937        &self,
1938        ui: &Ui,
1939        template_text: &str,
1940    ) -> Result<TemplateRenderer<'_, Operation>, CommandError> {
1941        let language = self.operation_template_language();
1942        self.parse_template(ui, &language, template_text)
1943    }
1944
1945    /// Creates commit template language environment for this workspace.
1946    pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
1947        self.env
1948            .commit_template_language(self.repo().as_ref(), self.id_prefix_context())
1949    }
1950
1951    /// Creates operation template language environment for this workspace.
1952    pub fn operation_template_language(&self) -> OperationTemplateLanguage {
1953        OperationTemplateLanguage::new(
1954            self.workspace.repo_loader(),
1955            Some(self.repo().op_id()),
1956            self.env.cwd(),
1957            self.env.operation_template_extensions(),
1958        )
1959    }
1960
1961    /// Template for one-line summary of a commit.
1962    pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
1963        let language = self.commit_template_language();
1964        self.reparse_valid_template(&language, &self.commit_summary_template_text)
1965            .labeled(["commit"])
1966    }
1967
1968    /// Template for one-line summary of an operation.
1969    pub fn operation_summary_template(&self) -> TemplateRenderer<'_, Operation> {
1970        let language = self.operation_template_language();
1971        self.reparse_valid_template(&language, &self.op_summary_template_text)
1972            .labeled(["operation"])
1973    }
1974
1975    pub fn short_change_id_template(&self) -> TemplateRenderer<'_, Commit> {
1976        let language = self.commit_template_language();
1977        self.reparse_valid_template(&language, SHORT_CHANGE_ID_TEMPLATE_TEXT)
1978            .labeled(["commit"])
1979    }
1980
1981    /// Returns one-line summary of the given `commit`.
1982    ///
1983    /// Use `write_commit_summary()` to get colorized output. Use
1984    /// `commit_summary_template()` if you have many commits to process.
1985    pub fn format_commit_summary(&self, commit: &Commit) -> String {
1986        let output = self.commit_summary_template().format_plain_text(commit);
1987        output.into_string_lossy()
1988    }
1989
1990    /// Writes one-line summary of the given `commit`.
1991    ///
1992    /// Use `commit_summary_template()` if you have many commits to process.
1993    #[instrument(skip_all)]
1994    pub fn write_commit_summary(
1995        &self,
1996        formatter: &mut dyn Formatter,
1997        commit: &Commit,
1998    ) -> std::io::Result<()> {
1999        self.commit_summary_template().format(commit, formatter)
2000    }
2001
2002    pub async fn check_rewritable<'a>(
2003        &self,
2004        commits: impl IntoIterator<Item = &'a CommitId>,
2005    ) -> Result<(), CommandError> {
2006        let commit_ids = commits.into_iter().cloned().collect_vec();
2007        let to_rewrite_expr = RevsetExpression::commits(commit_ids);
2008        self.check_rewritable_expr(&to_rewrite_expr).await
2009    }
2010
2011    pub async fn check_rewritable_expr(
2012        &self,
2013        to_rewrite_expr: &Arc<ResolvedRevsetExpression>,
2014    ) -> Result<(), CommandError> {
2015        let repo = self.repo().as_ref();
2016        let immutable_expr = self.env.resolve_immutable_expression(repo)?;
2017        let Some(commit_id) = immutable_expr
2018            .intersection(to_rewrite_expr)
2019            .evaluate(repo)?
2020            .stream()
2021            .try_next()
2022            .await?
2023        else {
2024            return Ok(());
2025        };
2026        let error = if &commit_id == repo.store().root_commit_id() {
2027            user_error(format!("The root commit {commit_id:.12} is immutable"))
2028        } else {
2029            let mut error = user_error(format!("Commit {commit_id:.12} is immutable"));
2030            let commit = repo.store().get_commit_async(&commit_id).await?;
2031            error.add_formatted_hint_with(|formatter| {
2032                write!(formatter, "Could not modify commit: ")?;
2033                self.write_commit_summary(formatter, &commit)?;
2034                Ok(())
2035            });
2036            error.add_hint("Immutable commits are used to protect shared history.");
2037            error.add_hint(indoc::indoc! {"
2038                For more information, see:
2039                      - https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits
2040                      - `jj help -k config`, \"Set of immutable commits\""});
2041
2042            let (lower_bound, upper_bound) = immutable_expr
2043                .intersection(&to_rewrite_expr.descendants())
2044                .evaluate(repo)?
2045                .count_estimate()?;
2046            let exact = upper_bound == Some(lower_bound);
2047            let or_more = if exact { "" } else { " or more" };
2048            error.add_hint(format!(
2049                "This operation would rewrite {lower_bound}{or_more} immutable commits."
2050            ));
2051
2052            error
2053        };
2054        Err(error)
2055    }
2056
2057    #[instrument(skip_all)]
2058    async fn snapshot_working_copy(
2059        &mut self,
2060        ui: &Ui,
2061        git_import_export_lock: &GitImportExportLock,
2062    ) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
2063        let workspace_name = self.workspace_name().to_owned();
2064        let repo = self.repo().clone();
2065        let auto_tracking_matcher = self
2066            .auto_tracking_matcher(ui)
2067            .map_err(snapshot_command_error)?;
2068        let options = self
2069            .snapshot_options_with_start_tracking_matcher(&auto_tracking_matcher)
2070            .map_err(snapshot_command_error)?;
2071
2072        // Compare working-copy tree and operation with repo's, and reload as needed.
2073        let mut locked_ws = self
2074            .workspace
2075            .start_working_copy_mutation()
2076            .await
2077            .map_err(snapshot_command_error)?;
2078
2079        let Some((repo, wc_commit)) =
2080            handle_stale_working_copy(locked_ws.locked_wc(), repo, &workspace_name).await?
2081        else {
2082            // If the workspace has been deleted, it's unclear what to do, so we just skip
2083            // committing the working copy.
2084            return Ok(SnapshotStats::default());
2085        };
2086
2087        self.user_repo = ReadonlyUserRepo::new(repo);
2088        let (new_tree, stats) = {
2089            let mut options = options;
2090            let progress = crate::progress::snapshot_progress(ui);
2091            options.progress = progress.as_ref().map(|x| x as _);
2092            locked_ws
2093                .locked_wc()
2094                .snapshot(&options)
2095                .await
2096                .map_err(snapshot_command_error)?
2097        };
2098        if new_tree.tree_ids_and_labels() != wc_commit.tree().tree_ids_and_labels() {
2099            let mut tx = start_repo_transaction(
2100                &self.user_repo.repo,
2101                &workspace_name,
2102                self.env.command.string_args(),
2103            );
2104            tx.set_is_snapshot(true);
2105            let immutable_expr = self
2106                .env
2107                .resolve_immutable_expression(tx.repo())
2108                .map_err(snapshot_command_error)?;
2109            let wc_immutable = !immutable_expr
2110                .intersection(&RevsetExpression::commit(wc_commit.id().clone()))
2111                .evaluate(tx.repo())
2112                .map_err(snapshot_command_error)?
2113                .is_empty()
2114                .map_err(snapshot_command_error)?;
2115            let mut_repo = tx.repo_mut();
2116            let new_wc_commit;
2117            if wc_immutable {
2118                new_wc_commit = mut_repo
2119                    .new_commit(vec![wc_commit.id().clone()], new_tree.clone())
2120                    .write()
2121                    .await
2122                    .map_err(snapshot_command_error)?;
2123                writeln!(
2124                    ui.warning_default(),
2125                    "The working-copy commit is immutable; a new commit has been created on top \
2126                     of it.",
2127                )
2128                .map_err(snapshot_command_error)?;
2129            } else {
2130                new_wc_commit = mut_repo
2131                    .rewrite_commit(&wc_commit)
2132                    .set_tree(new_tree.clone())
2133                    .write()
2134                    .await
2135                    .map_err(snapshot_command_error)?;
2136            }
2137            mut_repo
2138                .set_wc_commit(workspace_name, new_wc_commit.id().clone())
2139                .map_err(snapshot_command_error)?;
2140
2141            // Rebase descendants
2142            let num_rebased = mut_repo
2143                .rebase_descendants()
2144                .await
2145                .map_err(snapshot_command_error)?;
2146            if num_rebased > 0 {
2147                writeln!(
2148                    ui.status(),
2149                    "Rebased {num_rebased} descendant commits onto updated working copy."
2150                )
2151                .map_err(snapshot_command_error)?;
2152            }
2153
2154            #[cfg(feature = "git")]
2155            if self.env.working_copy_shared_with_git && self.env.command.should_commit_transaction()
2156            {
2157                if wc_immutable {
2158                    // New working-copy commit is created on top. Reset Git HEAD and index.
2159                    try_reset_git_head(ui, mut_repo, &new_wc_commit, git_import_export_lock)
2160                        .await
2161                        .map_err(snapshot_command_error)?;
2162                    // export_refs() is probably unnecessary because there should be no
2163                    // rewritten descendants, but it's harmless.
2164                    let stats =
2165                        jj_lib::git::export_refs(mut_repo).map_err(snapshot_command_error)?;
2166                    crate::git_util::print_git_export_stats(ui, &stats)
2167                        .map_err(snapshot_command_error)?;
2168                } else {
2169                    let old_tree = wc_commit.tree();
2170                    let new_tree = new_wc_commit.tree();
2171                    export_working_copy_changes_to_git(ui, mut_repo, &old_tree, &new_tree)
2172                        .await
2173                        .map_err(snapshot_command_error)?;
2174                }
2175            }
2176
2177            let repo = self
2178                .env
2179                .command
2180                .maybe_commit_transaction(tx, "snapshot working copy")
2181                .await
2182                .map_err(snapshot_command_error)?;
2183            self.user_repo = ReadonlyUserRepo::new(repo);
2184            if !self.env.command.should_commit_transaction() {
2185                writeln!(
2186                    ui.status(),
2187                    "Snapshot operation left uncommitted because --no-integrate-operation was \
2188                     requested: {}",
2189                    short_operation_hash(self.user_repo.repo.op_id())
2190                )
2191                .map_err(snapshot_command_error)?;
2192            }
2193        }
2194
2195        #[cfg(feature = "git")]
2196        if self.env.working_copy_shared_with_git
2197            && let Ok(resolved_tree) = new_tree
2198                .trees()
2199                .await
2200                .map_err(snapshot_command_error)?
2201                .into_resolved()
2202            && resolved_tree
2203                .entries_non_recursive()
2204                .any(|entry| entry.name().as_internal_str().starts_with(".jjconflict"))
2205        {
2206            writeln!(
2207                ui.warning_default(),
2208                "The working copy contains '.jjconflict' files. These files are used by `jj` \
2209                 internally and should not be present in the working copy."
2210            )
2211            .map_err(snapshot_command_error)?;
2212            writeln!(
2213                ui.hint_default(),
2214                "You may have used a regular `git` command to check out a conflicted commit."
2215            )
2216            .map_err(snapshot_command_error)?;
2217            writeln!(
2218                ui.hint_default(),
2219                "You can use `jj abandon` to discard the working copy changes."
2220            )
2221            .map_err(snapshot_command_error)?;
2222        }
2223
2224        if self.env.command.should_commit_transaction() {
2225            locked_ws
2226                .finish(self.user_repo.repo.op_id().clone())
2227                .await
2228                .map_err(snapshot_command_error)?;
2229        }
2230        Ok(stats)
2231    }
2232
2233    async fn update_working_copy(
2234        &mut self,
2235        ui: &Ui,
2236        maybe_old_commit: Option<&Commit>,
2237        new_commit: &Commit,
2238    ) -> Result<(), CommandError> {
2239        assert!(self.may_update_working_copy);
2240        let stats = update_working_copy(
2241            &self.user_repo.repo,
2242            &mut self.workspace,
2243            maybe_old_commit,
2244            new_commit,
2245        )
2246        .await?;
2247        self.print_updated_working_copy_stats(ui, maybe_old_commit, new_commit, &stats)
2248    }
2249
2250    fn print_updated_working_copy_stats(
2251        &self,
2252        ui: &Ui,
2253        maybe_old_commit: Option<&Commit>,
2254        new_commit: &Commit,
2255        stats: &CheckoutStats,
2256    ) -> Result<(), CommandError> {
2257        if Some(new_commit) != maybe_old_commit
2258            && let Some(mut formatter) = ui.status_formatter()
2259        {
2260            let template = self.commit_summary_template();
2261            write!(formatter, "Working copy  (@) now at: ")?;
2262            template.format(new_commit, formatter.as_mut())?;
2263            writeln!(formatter)?;
2264            for parent in new_commit.parents().block_on()? {
2265                //                "Working copy  (@) now at: "
2266                write!(formatter, "Parent commit (@-)      : ")?;
2267                template.format(&parent, formatter.as_mut())?;
2268                writeln!(formatter)?;
2269            }
2270        }
2271        print_checkout_stats(ui, stats, new_commit)?;
2272        if Some(new_commit) != maybe_old_commit
2273            && let Some(mut formatter) = ui.status_formatter()
2274            && new_commit.has_conflict()
2275        {
2276            let conflicts = new_commit.tree().conflicts().collect_vec();
2277            writeln!(
2278                formatter.labeled("warning").with_heading("Warning: "),
2279                "There are unresolved conflicts at these paths:"
2280            )?;
2281            print_conflicted_paths(conflicts, formatter.as_mut(), self)?;
2282        }
2283        Ok(())
2284    }
2285
2286    pub fn start_transaction(&mut self) -> WorkspaceCommandTransaction<'_> {
2287        let tx = start_repo_transaction(
2288            self.repo(),
2289            self.workspace_name(),
2290            self.env.command.string_args(),
2291        );
2292        let id_prefix_context = mem::take(&mut self.user_repo.id_prefix_context);
2293        WorkspaceCommandTransaction {
2294            helper: self,
2295            tx,
2296            id_prefix_context,
2297        }
2298    }
2299
2300    async fn finish_transaction(
2301        &mut self,
2302        ui: &Ui,
2303        mut tx: Transaction,
2304        description: impl Into<String>,
2305        git_import_export_lock: &GitImportExportLock,
2306    ) -> Result<(), CommandError> {
2307        let old_repo = tx.base_repo().clone();
2308
2309        let maybe_old_wc_commit = old_repo
2310            .view()
2311            .get_wc_commit_id(self.workspace_name())
2312            .map(|commit_id| tx.base_repo().store().get_commit(commit_id))
2313            .transpose()?;
2314        let maybe_new_wc_commit = tx
2315            .repo()
2316            .view()
2317            .get_wc_commit_id(self.workspace_name())
2318            .map(|commit_id| tx.repo().store().get_commit(commit_id))
2319            .transpose()?;
2320        // Create a new mutable working-copy commit to reduce unintended states.
2321        // This isn't strictly required for correctness, so symbol resolution
2322        // failures can be ignored. snapshot_working_copy() ensures that the
2323        // working-copy commit is mutable.
2324        let maybe_new_wc_commit = if let Some(wc_commit) = &maybe_new_wc_commit
2325            && let Ok(immutable_expr) = self.env.resolve_immutable_expression(tx.repo())
2326            && !immutable_expr
2327                .intersection(&RevsetExpression::commit(wc_commit.id().clone()))
2328                .evaluate(tx.repo())?
2329                .is_empty()?
2330        {
2331            let new_wc_commit = tx
2332                .repo_mut()
2333                .new_commit(vec![wc_commit.id().clone()], wc_commit.tree())
2334                .write()
2335                .await?;
2336            tx.repo_mut()
2337                .set_wc_commit(self.workspace_name().to_owned(), new_wc_commit.id().clone())?;
2338            writeln!(
2339                ui.warning_default(),
2340                "The working-copy commit became immutable; a new commit has been created on top \
2341                 of it.",
2342            )?;
2343            Some(new_wc_commit)
2344        } else {
2345            maybe_new_wc_commit
2346        };
2347
2348        #[cfg(feature = "git")]
2349        if self.env.working_copy_shared_with_git && self.env.command.should_commit_transaction() {
2350            if let Some(wc_commit) = &maybe_new_wc_commit {
2351                try_reset_git_head(ui, tx.repo_mut(), wc_commit, git_import_export_lock).await?;
2352            }
2353            let stats = jj_lib::git::export_refs(tx.repo_mut())?;
2354            crate::git_util::print_git_export_stats(ui, &stats)?;
2355        }
2356
2357        self.user_repo = ReadonlyUserRepo::new(
2358            self.env
2359                .command
2360                .maybe_commit_transaction(tx, description)
2361                .await?,
2362        );
2363
2364        // Update working copy before reporting repo changes, so that
2365        // potential errors while reporting changes (broken pipe, etc)
2366        // don't leave the working copy in a stale state.
2367        if self.may_update_working_copy {
2368            if let Some(new_commit) = &maybe_new_wc_commit {
2369                self.update_working_copy(ui, maybe_old_wc_commit.as_ref(), new_commit)
2370                    .await?;
2371            } else {
2372                // It seems the workspace was deleted, so we shouldn't try to
2373                // update it.
2374            }
2375        }
2376
2377        self.report_repo_changes(ui, &old_repo).await?;
2378
2379        if !self.env.command.should_commit_transaction() {
2380            writeln!(
2381                ui.status(),
2382                "Operation left uncommitted because --no-integrate-operation was requested: {}",
2383                short_operation_hash(self.repo().op_id())
2384            )?;
2385        }
2386
2387        let settings = self.settings();
2388        let missing_user_name = settings.user_name().is_empty();
2389        let missing_user_mail = settings.user_email().is_empty();
2390        if missing_user_name || missing_user_mail {
2391            let not_configured_msg = match (missing_user_name, missing_user_mail) {
2392                (true, true) => "Name and email not configured.",
2393                (true, false) => "Name not configured.",
2394                (false, true) => "Email not configured.",
2395                _ => unreachable!(),
2396            };
2397            writeln!(
2398                ui.warning_default(),
2399                "{not_configured_msg} Until configured, your commits will be created with the \
2400                 empty identity, and can't be pushed to remotes."
2401            )?;
2402            writeln!(ui.hint_default(), "To configure, run:")?;
2403            if missing_user_name {
2404                writeln!(
2405                    ui.hint_no_heading(),
2406                    r#"  jj config set --user user.name "Some One""#
2407                )?;
2408            }
2409            if missing_user_mail {
2410                writeln!(
2411                    ui.hint_no_heading(),
2412                    r#"  jj config set --user user.email "someone@example.com""#
2413                )?;
2414            }
2415        }
2416        Ok(())
2417    }
2418
2419    /// Inform the user about important changes to the repo since the previous
2420    /// operation (when `old_repo` was loaded).
2421    async fn report_repo_changes(
2422        &self,
2423        ui: &Ui,
2424        old_repo: &Arc<ReadonlyRepo>,
2425    ) -> Result<(), CommandError> {
2426        let Some(mut fmt) = ui.status_formatter() else {
2427            return Ok(());
2428        };
2429        let old_view = old_repo.view();
2430        let new_repo = self.repo().as_ref();
2431        let new_view = new_repo.view();
2432
2433        let workspace_name = self.workspace_name();
2434        if old_view.wc_commit_ids().contains_key(workspace_name)
2435            && !new_view.wc_commit_ids().contains_key(workspace_name)
2436        {
2437            writeln!(
2438                fmt.labeled("warning").with_heading("Warning: "),
2439                "The current workspace '{}' no longer exists after this operation. The working \
2440                 copy was left untouched.",
2441                workspace_name.as_symbol(),
2442            )?;
2443            writeln!(
2444                fmt.labeled("hint").with_heading("Hint: "),
2445                "Restore to an operation that contains the workspace (e.g. `jj undo` or `jj \
2446                 redo`).",
2447            )?;
2448        }
2449
2450        let old_heads = RevsetExpression::commits(old_view.heads().iter().cloned().collect());
2451        let new_heads = RevsetExpression::commits(new_view.heads().iter().cloned().collect());
2452        // Filter the revsets by conflicts instead of reading all commits and doing the
2453        // filtering here. That way, we can afford to evaluate the revset even if there
2454        // are millions of commits added to the repo, assuming the revset engine can
2455        // efficiently skip non-conflicting commits. Filter out empty commits mostly so
2456        // `jj new <conflicted commit>` doesn't result in a message about new conflicts.
2457        let conflicts = RevsetExpression::filter(RevsetFilterPredicate::HasConflict)
2458            .filtered(RevsetFilterPredicate::File(FilesetExpression::all()));
2459        let removed_conflicts_expr = new_heads.range(&old_heads).intersection(&conflicts);
2460        let added_conflicts_expr = old_heads.range(&new_heads).intersection(&conflicts);
2461
2462        let get_commits =
2463            async |expr: Arc<ResolvedRevsetExpression>| -> Result<Vec<Commit>, CommandError> {
2464                let commits = expr
2465                    .evaluate(new_repo)?
2466                    .stream()
2467                    .commits(new_repo.store())
2468                    .try_collect()
2469                    .await?;
2470                Ok(commits)
2471            };
2472        let removed_conflict_commits = get_commits(removed_conflicts_expr).await?;
2473        let added_conflict_commits = get_commits(added_conflicts_expr).await?;
2474
2475        fn commits_by_change_id(commits: &[Commit]) -> IndexMap<&ChangeId, Vec<&Commit>> {
2476            let mut result: IndexMap<&ChangeId, Vec<&Commit>> = IndexMap::new();
2477            for commit in commits {
2478                result.entry(commit.change_id()).or_default().push(commit);
2479            }
2480            result
2481        }
2482        let removed_conflicts_by_change_id = commits_by_change_id(&removed_conflict_commits);
2483        let added_conflicts_by_change_id = commits_by_change_id(&added_conflict_commits);
2484        let mut resolved_conflicts_by_change_id = removed_conflicts_by_change_id.clone();
2485        resolved_conflicts_by_change_id
2486            .retain(|change_id, _commits| !added_conflicts_by_change_id.contains_key(change_id));
2487        let mut new_conflicts_by_change_id = added_conflicts_by_change_id.clone();
2488        new_conflicts_by_change_id
2489            .retain(|change_id, _commits| !removed_conflicts_by_change_id.contains_key(change_id));
2490
2491        // TODO: Also report new divergence and maybe resolved divergence
2492        if !resolved_conflicts_by_change_id.is_empty() {
2493            // TODO: Report resolved and abandoned numbers separately. However,
2494            // that involves resolving the change_id among the visible commits in the new
2495            // repo, which isn't currently supported by Google's revset engine.
2496            let num_resolved: usize = resolved_conflicts_by_change_id
2497                .values()
2498                .map(|commits| commits.len())
2499                .sum();
2500            writeln!(
2501                fmt,
2502                "Existing conflicts were resolved or abandoned from {num_resolved} commits."
2503            )?;
2504        }
2505        if !new_conflicts_by_change_id.is_empty() {
2506            let num_conflicted: usize = new_conflicts_by_change_id
2507                .values()
2508                .map(|commits| commits.len())
2509                .sum();
2510            writeln!(fmt, "New conflicts appeared in {num_conflicted} commits:")?;
2511            print_updated_commits(
2512                fmt.as_mut(),
2513                &self.commit_summary_template(),
2514                new_conflicts_by_change_id.values().flatten().copied(),
2515            )?;
2516        }
2517
2518        // Hint that the user might want to `jj new` to the first conflict commit to
2519        // resolve conflicts. Only show the hints if there were any new or resolved
2520        // conflicts, and only if there are still some conflicts.
2521        if !(added_conflict_commits.is_empty()
2522            || resolved_conflicts_by_change_id.is_empty() && new_conflicts_by_change_id.is_empty())
2523        {
2524            // If the user just resolved some conflict and squashed them in, there won't be
2525            // any new conflicts. Clarify to them that there are still some other conflicts
2526            // to resolve. (We don't mention conflicts in commits that weren't affected by
2527            // the operation, however.)
2528            if new_conflicts_by_change_id.is_empty() {
2529                writeln!(
2530                    fmt,
2531                    "There are still unresolved conflicts in rebased descendants.",
2532                )?;
2533            }
2534
2535            self.report_repo_conflicts(
2536                fmt.as_mut(),
2537                new_repo,
2538                added_conflict_commits
2539                    .iter()
2540                    .map(|commit| commit.id().clone())
2541                    .collect(),
2542            )
2543            .await?;
2544        }
2545
2546        Ok(())
2547    }
2548
2549    pub async fn report_repo_conflicts(
2550        &self,
2551        fmt: &mut dyn Formatter,
2552        repo: &ReadonlyRepo,
2553        conflicted_commits: Vec<CommitId>,
2554    ) -> Result<(), CommandError> {
2555        if !self.settings().get_bool("hints.resolving-conflicts")? || conflicted_commits.is_empty()
2556        {
2557            return Ok(());
2558        }
2559
2560        let only_one_conflicted_commit = conflicted_commits.len() == 1;
2561        let root_conflicts_revset = RevsetExpression::commits(conflicted_commits)
2562            .roots()
2563            .evaluate(repo)?;
2564
2565        let root_conflict_commits: Vec<_> = root_conflicts_revset
2566            .stream()
2567            .commits(repo.store())
2568            .try_collect()
2569            .await?;
2570
2571        // The common part of these strings is not extracted, to avoid i18n issues.
2572        let instruction = if only_one_conflicted_commit {
2573            indoc! {"
2574            To resolve the conflicts, start by creating a commit on top of
2575            the conflicted commit:
2576            "}
2577        } else if root_conflict_commits.len() == 1 {
2578            indoc! {"
2579            To resolve the conflicts, start by creating a commit on top of
2580            the first conflicted commit:
2581            "}
2582        } else {
2583            indoc! {"
2584            To resolve the conflicts, start by creating a commit on top of
2585            one of the first conflicted commits:
2586            "}
2587        };
2588        write!(fmt.labeled("hint").with_heading("Hint: "), "{instruction}")?;
2589        let format_short_change_id = self.short_change_id_template();
2590        {
2591            let mut fmt = fmt.labeled("hint");
2592            for commit in &root_conflict_commits {
2593                write!(fmt, "  jj new ")?;
2594                format_short_change_id.format(commit, *fmt)?;
2595                writeln!(fmt)?;
2596            }
2597        }
2598        writedoc!(
2599            fmt.labeled("hint"),
2600            "
2601            Then use `jj resolve`, or edit the conflict markers in the file directly.
2602            Once the conflicts are resolved, you can inspect the result with `jj diff`.
2603            Then run `jj squash` to move the resolution into the conflicted commit.
2604            ",
2605        )?;
2606        Ok(())
2607    }
2608
2609    /// Identifies bookmarks which are eligible to be moved automatically
2610    /// during `jj commit` and `jj new`. Whether a bookmark is eligible is
2611    /// determined by its target and the user and repo config for
2612    /// "advance-bookmarks".
2613    ///
2614    /// Returns a Vec of bookmarks in `repo` that point to any of the `from`
2615    /// commits and that are eligible to advance. The `from` commits are
2616    /// typically the parents of the target commit of `jj commit` or `jj new`.
2617    ///
2618    /// Bookmarks are not moved until
2619    /// `WorkspaceCommandTransaction::advance_bookmarks()` is called with the
2620    /// `AdvanceableBookmark`s returned by this function.
2621    ///
2622    /// Returns an empty `std::Vec` if no bookmarks are eligible to advance.
2623    pub fn get_advanceable_bookmarks<'a>(
2624        &self,
2625        ui: &Ui,
2626        from: impl IntoIterator<Item = &'a CommitId>,
2627    ) -> Result<Vec<AdvanceableBookmark>, CommandError> {
2628        let Some(ab_matcher) = load_advance_bookmarks_matcher(ui, self.settings())? else {
2629            // Return early if we know that there's no work to do.
2630            return Ok(Vec::new());
2631        };
2632
2633        let mut advanceable_bookmarks = Vec::new();
2634        for from_commit in from {
2635            for (name, _) in self.repo().view().local_bookmarks_for_commit(from_commit) {
2636                if ab_matcher.is_match(name.as_str()) {
2637                    advanceable_bookmarks.push(AdvanceableBookmark {
2638                        name: name.to_owned(),
2639                        old_commit_id: from_commit.clone(),
2640                    });
2641                }
2642            }
2643        }
2644
2645        Ok(advanceable_bookmarks)
2646    }
2647}
2648
2649#[cfg(feature = "git")]
2650pub async fn export_working_copy_changes_to_git(
2651    ui: &Ui,
2652    mut_repo: &mut MutableRepo,
2653    old_tree: &MergedTree,
2654    new_tree: &MergedTree,
2655) -> Result<(), CommandError> {
2656    let repo = mut_repo.base_repo().as_ref();
2657    jj_lib::git::update_intent_to_add(repo, old_tree, new_tree).await?;
2658    let stats = jj_lib::git::export_refs(mut_repo)?;
2659    crate::git_util::print_git_export_stats(ui, &stats)?;
2660    Ok(())
2661}
2662#[cfg(not(feature = "git"))]
2663pub async fn export_working_copy_changes_to_git(
2664    _ui: &Ui,
2665    _mut_repo: &mut MutableRepo,
2666    _old_tree: &MergedTree,
2667    _new_tree: &MergedTree,
2668) -> Result<(), CommandError> {
2669    Ok(())
2670}
2671
2672#[cfg(feature = "git")]
2673async fn try_reset_git_head(
2674    ui: &Ui,
2675    mut_repo: &mut MutableRepo,
2676    wc_commit: &Commit,
2677    _git_import_export_lock: &GitImportExportLock,
2678) -> Result<(), CommandError> {
2679    use std::error::Error as _;
2680    // Export Git HEAD while holding the git-head lock to prevent races:
2681    // - Between two finish_transaction calls updating HEAD
2682    // - With import_git_head importing HEAD concurrently
2683    // This can still fail if HEAD was updated concurrently by another JJ process
2684    // (overlapping transaction) or a non-JJ process (e.g., git checkout). In that
2685    // case, the actual state will be imported on the next snapshot.
2686    match jj_lib::git::reset_head(mut_repo, wc_commit).await {
2687        Ok(()) => Ok(()),
2688        Err(err @ jj_lib::git::GitResetHeadError::UpdateHeadRef(_)) => {
2689            writeln!(ui.warning_default(), "{err}")?;
2690            print_error_sources(ui, err.source())?;
2691            Ok(())
2692        }
2693        Err(err) => Err(err.into()),
2694    }
2695}
2696
2697/// An ongoing [`Transaction`] tied to a particular workspace.
2698///
2699/// `WorkspaceCommandTransaction`s are created with
2700/// [`WorkspaceCommandHelper::start_transaction`] and committed with
2701/// [`WorkspaceCommandTransaction::finish`]. The inner `Transaction` can also be
2702/// extracted using [`WorkspaceCommandTransaction::into_inner`] in situations
2703/// where finer-grained control over the `Transaction` is necessary.
2704#[must_use]
2705pub struct WorkspaceCommandTransaction<'a> {
2706    helper: &'a mut WorkspaceCommandHelper,
2707    tx: Transaction,
2708    /// Cache of index built against the current MutableRepo state.
2709    id_prefix_context: OnceCell<IdPrefixContext>,
2710}
2711
2712impl WorkspaceCommandTransaction<'_> {
2713    /// Workspace helper that may use the base repo.
2714    pub fn base_workspace_helper(&self) -> &WorkspaceCommandHelper {
2715        self.helper
2716    }
2717
2718    /// Settings for this workspace.
2719    pub fn settings(&self) -> &UserSettings {
2720        self.helper.settings()
2721    }
2722
2723    pub fn base_repo(&self) -> &Arc<ReadonlyRepo> {
2724        self.tx.base_repo()
2725    }
2726
2727    pub fn repo(&self) -> &MutableRepo {
2728        self.tx.repo()
2729    }
2730
2731    pub fn repo_mut(&mut self) -> &mut MutableRepo {
2732        self.id_prefix_context.take(); // invalidate
2733        self.tx.repo_mut()
2734    }
2735
2736    pub fn check_out(&mut self, commit: &Commit) -> Result<Commit, CheckOutCommitError> {
2737        let name = self.helper.workspace_name().to_owned();
2738        self.id_prefix_context.take(); // invalidate
2739        self.tx.repo_mut().check_out(name, commit).block_on()
2740    }
2741
2742    pub fn edit(&mut self, commit: &Commit) -> Result<(), EditCommitError> {
2743        let name = self.helper.workspace_name().to_owned();
2744        self.id_prefix_context.take(); // invalidate
2745        self.tx.repo_mut().edit(name, commit).block_on()
2746    }
2747
2748    pub fn format_commit_summary(&self, commit: &Commit) -> String {
2749        let output = self.commit_summary_template().format_plain_text(commit);
2750        output.into_string_lossy()
2751    }
2752
2753    pub fn write_commit_summary(
2754        &self,
2755        formatter: &mut dyn Formatter,
2756        commit: &Commit,
2757    ) -> std::io::Result<()> {
2758        self.commit_summary_template().format(commit, formatter)
2759    }
2760
2761    /// Template for one-line summary of a commit within transaction.
2762    pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
2763        let language = self.commit_template_language();
2764        self.helper
2765            .reparse_valid_template(&language, &self.helper.commit_summary_template_text)
2766            .labeled(["commit"])
2767    }
2768
2769    /// Creates commit template language environment capturing the current
2770    /// transaction state.
2771    pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
2772        let id_prefix_context = self
2773            .id_prefix_context
2774            .get_or_init(|| self.helper.env.new_id_prefix_context());
2775        self.helper
2776            .env
2777            .commit_template_language(self.tx.repo(), id_prefix_context)
2778    }
2779
2780    /// Parses commit template with the current transaction state.
2781    pub fn parse_commit_template(
2782        &self,
2783        ui: &Ui,
2784        template_text: &str,
2785    ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
2786        let language = self.commit_template_language();
2787        self.helper.env.parse_template(ui, &language, template_text)
2788    }
2789
2790    pub async fn finish(self, ui: &Ui, description: impl Into<String>) -> Result<(), CommandError> {
2791        let Self { helper, mut tx, .. } = self;
2792        if !tx.repo().has_changes() {
2793            writeln!(ui.status(), "Nothing changed.")?;
2794            return Ok(());
2795        }
2796        let num_rebased = rebase_mutable_descendants(&helper.env, &mut tx).await?;
2797        if num_rebased > 0 {
2798            writeln!(ui.status(), "Rebased {num_rebased} descendant commits.")?;
2799        }
2800        // Acquire git import/export lock before finishing the transaction to ensure
2801        // Git HEAD export happens atomically with the transaction commit.
2802        let git_import_export_lock = helper.lock_git_import_export()?;
2803        helper
2804            .finish_transaction(ui, tx, description, &git_import_export_lock)
2805            .await
2806    }
2807
2808    /// Returns the wrapped [`Transaction`] for circumstances where
2809    /// finer-grained control is needed. The caller becomes responsible for
2810    /// finishing the `Transaction`, including rebasing descendants and updating
2811    /// the working copy, if applicable.
2812    pub fn into_inner(self) -> Transaction {
2813        self.tx
2814    }
2815
2816    /// Moves each bookmark in `bookmarks` from an old commit it's associated
2817    /// with (configured by `get_advanceable_bookmarks`) to the `move_to`
2818    /// commit. If the bookmark is conflicted before the update, it will
2819    /// remain conflicted after the update, but the conflict will involve
2820    /// the `move_to` commit instead of the old commit.
2821    pub async fn advance_bookmarks(
2822        &mut self,
2823        bookmarks: Vec<AdvanceableBookmark>,
2824        move_to: &CommitId,
2825    ) -> Result<(), CommandError> {
2826        for bookmark in bookmarks {
2827            // This removes the old commit ID from the bookmark's RefTarget and
2828            // replaces it with the `move_to` ID.
2829            self.repo_mut()
2830                .merge_local_bookmark(
2831                    &bookmark.name,
2832                    &RefTarget::normal(bookmark.old_commit_id),
2833                    &RefTarget::normal(move_to.clone()),
2834                )
2835                .await?;
2836        }
2837        Ok(())
2838    }
2839}
2840
2841pub fn find_workspace_dir(cwd: &Path) -> &Path {
2842    cwd.ancestors()
2843        .find(|path| path.join(".jj").is_dir())
2844        .unwrap_or(cwd)
2845}
2846
2847fn map_workspace_load_error(err: WorkspaceLoadError, user_wc_path: Option<&str>) -> CommandError {
2848    match err {
2849        WorkspaceLoadError::NoWorkspaceHere(wc_path) => {
2850            // Prefer user-specified path instead of absolute wc_path if any.
2851            let short_wc_path = user_wc_path.map_or(wc_path.as_ref(), Path::new);
2852            let message = format!(r#"There is no jj repo in "{}""#, short_wc_path.display());
2853            let git_dir = wc_path.join(".git");
2854            if git_dir.is_dir() {
2855                user_error(message).hinted(
2856                    "It looks like this is a git repo. You can create a jj repo backed by it by \
2857                     running this:
2858jj git init",
2859                )
2860            } else {
2861                user_error(message)
2862            }
2863        }
2864        WorkspaceLoadError::RepoDoesNotExist(repo_dir) => user_error(format!(
2865            "The repository directory at {} is missing. Was it moved?",
2866            repo_dir.display(),
2867        )),
2868        WorkspaceLoadError::StoreLoadError(err @ StoreLoadError::UnsupportedType { .. }) => {
2869            internal_error_with_message(
2870                "This version of the jj binary doesn't support this type of repo",
2871                err,
2872            )
2873        }
2874        WorkspaceLoadError::StoreLoadError(
2875            err @ (StoreLoadError::ReadError { .. } | StoreLoadError::Backend(_)),
2876        ) => internal_error_with_message("The repository appears broken or inaccessible", err),
2877        WorkspaceLoadError::StoreLoadError(StoreLoadError::Signing(err)) => user_error(err),
2878        WorkspaceLoadError::WorkingCopyState(err) => internal_error(err),
2879        WorkspaceLoadError::DecodeRepoPath(_) | WorkspaceLoadError::Path(_) => user_error(err),
2880    }
2881}
2882
2883pub fn start_repo_transaction(
2884    repo: &Arc<ReadonlyRepo>,
2885    workspace_name: &WorkspaceName,
2886    string_args: &[String],
2887) -> Transaction {
2888    let mut tx = repo.start_transaction();
2889    tx.set_workspace_name(workspace_name);
2890    for (key, value) in command_args_to_transaction_attribute(string_args) {
2891        tx.set_attribute(key, value);
2892    }
2893    tx
2894}
2895
2896fn command_args_to_transaction_attribute(command_args: &[String]) -> Vec<(String, String)> {
2897    if command_args.is_empty() {
2898        return vec![];
2899    }
2900    // TODO: Either do better shell-escaping here or store the values in some list
2901    // type (which we currently don't have).
2902    let shell_escape = |arg: &String| {
2903        if arg.as_bytes().iter().all(|b| {
2904            matches!(b,
2905                b'A'..=b'Z'
2906                | b'a'..=b'z'
2907                | b'0'..=b'9'
2908                | b','
2909                | b'-'
2910                | b'.'
2911                | b'/'
2912                | b':'
2913                | b'@'
2914                | b'_'
2915            )
2916        }) {
2917            arg.clone()
2918        } else {
2919            format!("'{}'", arg.replace('\'', "\\'"))
2920        }
2921    };
2922    let mut quoted_strings = vec!["jj".to_string()];
2923    quoted_strings.extend(command_args.iter().skip(1).map(shell_escape));
2924    vec![("args".to_string(), quoted_strings.join(" "))]
2925}
2926
2927async fn rebase_mutable_descendants(
2928    env: &WorkspaceCommandEnvironment,
2929    tx: &mut Transaction,
2930) -> Result<usize, CommandError> {
2931    // Commands like "jj git fetch" can update immutable commits to reflect the
2932    // remote changes. Their immutable descendants shouldn't be rebased. We use
2933    // tx.base_repo() here because we're interested in existing immutable
2934    // commits that are still reachable.
2935    let mut num_rebased = 0;
2936    let immutable = env.resolve_immutable_expression(tx.base_repo().as_ref())?;
2937    tx.repo_mut()
2938        .rebase_descendants_with_options(
2939            &immutable,
2940            &RebaseOptions::default(),
2941            |_old_commit, _rebased_commit| num_rebased += 1,
2942        )
2943        .await?;
2944    Ok(num_rebased)
2945}
2946
2947/// Check if the working copy is stale and reload the repo if the repo is ahead
2948/// of the working copy.
2949///
2950/// Returns Ok(None) if the workspace doesn't exist in the repo (presumably
2951/// because it was deleted).
2952async fn handle_stale_working_copy(
2953    locked_wc: &mut dyn LockedWorkingCopy,
2954    repo: Arc<ReadonlyRepo>,
2955    workspace_name: &WorkspaceName,
2956) -> Result<Option<(Arc<ReadonlyRepo>, Commit)>, SnapshotWorkingCopyError> {
2957    let get_wc_commit = |repo: &ReadonlyRepo| -> Result<Option<_>, _> {
2958        repo.view()
2959            .get_wc_commit_id(workspace_name)
2960            .map(|id| repo.store().get_commit(id))
2961            .transpose()
2962            .map_err(snapshot_command_error)
2963    };
2964    let Some(wc_commit) = get_wc_commit(&repo)? else {
2965        return Ok(None);
2966    };
2967    let old_op_id = locked_wc.old_operation_id().clone();
2968    match WorkingCopyFreshness::check_stale(locked_wc, &wc_commit, &repo).await {
2969        Ok(WorkingCopyFreshness::Fresh) => Ok(Some((repo, wc_commit))),
2970        Ok(WorkingCopyFreshness::Updated(wc_operation)) => {
2971            let repo = repo
2972                .reload_at(&wc_operation)
2973                .await
2974                .map_err(snapshot_command_error)?;
2975            if let Some(wc_commit) = get_wc_commit(&repo)? {
2976                Ok(Some((repo, wc_commit)))
2977            } else {
2978                Ok(None)
2979            }
2980        }
2981        Ok(WorkingCopyFreshness::WorkingCopyStale) => {
2982            Err(SnapshotWorkingCopyError::StaleWorkingCopy(
2983                user_error(format!(
2984                    "The working copy is stale (not updated since operation {}).",
2985                    short_operation_hash(&old_op_id)
2986                ))
2987                .hinted(
2988                    "Run `jj workspace update-stale` to update it.
2989See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy \
2990                     for more information.",
2991                ),
2992            ))
2993        }
2994        Ok(WorkingCopyFreshness::SiblingOperation) => {
2995            Err(SnapshotWorkingCopyError::StaleWorkingCopy(
2996                internal_error(format!(
2997                    "The repo was loaded at operation {}, which seems to be a sibling of the \
2998                     working copy's operation {}",
2999                    short_operation_hash(repo.op_id()),
3000                    short_operation_hash(&old_op_id)
3001                ))
3002                .hinted(format!(
3003                    "Run `jj op integrate {}` to add the working copy's operation to the \
3004                     operation log.",
3005                    short_operation_hash(&old_op_id)
3006                )),
3007            ))
3008        }
3009        Err(OpStoreError::ObjectNotFound { .. }) => {
3010            Err(SnapshotWorkingCopyError::StaleWorkingCopy(
3011                user_error("Could not read working copy's operation.").hinted(
3012                    "Run `jj workspace update-stale` to recover.
3013See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy \
3014                     for more information.",
3015                ),
3016            ))
3017        }
3018        Err(e) => Err(snapshot_command_error(e)),
3019    }
3020}
3021
3022async fn update_stale_working_copy(
3023    mut locked_ws: LockedWorkspace<'_>,
3024    op_id: OperationId,
3025    stale_commit: &Commit,
3026    new_commit: &Commit,
3027) -> Result<CheckoutStats, CommandError> {
3028    // The same check as start_working_copy_mutation(), but with the stale
3029    // working-copy commit.
3030    if stale_commit.tree().tree_ids_and_labels()
3031        != locked_ws.locked_wc().old_tree().tree_ids_and_labels()
3032    {
3033        return Err(user_error("Concurrent working copy operation. Try again."));
3034    }
3035    let stats = locked_ws
3036        .locked_wc()
3037        .check_out(new_commit)
3038        .await
3039        .map_err(|err| {
3040            internal_error_with_message(
3041                format!("Failed to check out commit {}", new_commit.id().hex()),
3042                err,
3043            )
3044        })?;
3045    locked_ws.finish(op_id).await?;
3046
3047    Ok(stats)
3048}
3049
3050/// Prints a list of commits by the given summary template. The list may be
3051/// elided. Use this to show created, rewritten, or abandoned commits.
3052pub fn print_updated_commits<'a>(
3053    formatter: &mut dyn Formatter,
3054    template: &TemplateRenderer<Commit>,
3055    commits: impl IntoIterator<Item = &'a Commit>,
3056) -> io::Result<()> {
3057    let mut commits = commits.into_iter().fuse();
3058    for commit in commits.by_ref().take(10) {
3059        write!(formatter, "  ")?;
3060        template.format(commit, formatter)?;
3061        writeln!(formatter)?;
3062    }
3063    if commits.next().is_some() {
3064        writeln!(formatter, "  ...")?;
3065    }
3066    Ok(())
3067}
3068
3069#[instrument(skip_all)]
3070pub fn print_conflicted_paths(
3071    conflicts: Vec<(RepoPathBuf, BackendResult<MergedTreeValue>)>,
3072    formatter: &mut dyn Formatter,
3073    workspace_command: &WorkspaceCommandHelper,
3074) -> Result<(), CommandError> {
3075    let formatted_paths = conflicts
3076        .iter()
3077        .map(|(path, _conflict)| workspace_command.format_file_path(path))
3078        .collect_vec();
3079    let max_path_len = formatted_paths.iter().map(|p| p.len()).max().unwrap_or(0);
3080    let formatted_paths = formatted_paths
3081        .into_iter()
3082        .map(|p| format!("{:width$}", p, width = max_path_len.min(32) + 3));
3083
3084    for ((_, conflict), formatted_path) in std::iter::zip(conflicts, formatted_paths) {
3085        // TODO: Display the error for the path instead of failing the whole command if
3086        // `conflict` is an error?
3087        let conflict = conflict?.simplify();
3088        let sides = conflict.num_sides();
3089        let n_adds = conflict.adds().flatten().count();
3090        let deletions = sides - n_adds;
3091
3092        let mut seen_objects = BTreeMap::new(); // Sort for consistency and easier testing
3093        if deletions > 0 {
3094            seen_objects.insert(
3095                format!(
3096                    // Starting with a number sorts this first
3097                    "{deletions} deletion{}",
3098                    if deletions > 1 { "s" } else { "" }
3099                ),
3100                "normal", // Deletions don't interfere with `jj resolve` or diff display
3101            );
3102        }
3103        // TODO: We might decide it's OK for `jj resolve` to ignore special files in the
3104        // `removes` of a conflict (see e.g. https://github.com/jj-vcs/jj/pull/978). In
3105        // that case, `conflict.removes` should be removed below.
3106        for term in itertools::chain(conflict.removes(), conflict.adds()).flatten() {
3107            seen_objects.insert(
3108                match term {
3109                    TreeValue::File {
3110                        executable: false, ..
3111                    } => continue,
3112                    TreeValue::File {
3113                        executable: true, ..
3114                    } => "an executable",
3115                    TreeValue::Symlink(_) => "a symlink",
3116                    TreeValue::Tree(_) => "a directory",
3117                    TreeValue::GitSubmodule(_) => "a git submodule",
3118                }
3119                .to_string(),
3120                "difficult",
3121            );
3122        }
3123
3124        write!(formatter, "{formatted_path} ")?;
3125        {
3126            let mut formatter = formatter.labeled("conflict_description");
3127            let print_pair = |formatter: &mut dyn Formatter, (text, label): &(String, &str)| {
3128                write!(formatter.labeled(label), "{text}")
3129            };
3130            print_pair(
3131                *formatter,
3132                &(
3133                    format!("{sides}-sided"),
3134                    if sides > 2 { "difficult" } else { "normal" },
3135                ),
3136            )?;
3137            write!(formatter, " conflict")?;
3138
3139            if !seen_objects.is_empty() {
3140                write!(formatter, " including ")?;
3141                let seen_objects = seen_objects.into_iter().collect_vec();
3142                match &seen_objects[..] {
3143                    [] => unreachable!(),
3144                    [only] => print_pair(*formatter, only)?,
3145                    [first, middle @ .., last] => {
3146                        print_pair(*formatter, first)?;
3147                        for pair in middle {
3148                            write!(formatter, ", ")?;
3149                            print_pair(*formatter, pair)?;
3150                        }
3151                        write!(formatter, " and ")?;
3152                        print_pair(*formatter, last)?;
3153                    }
3154                }
3155            }
3156        }
3157        writeln!(formatter)?;
3158    }
3159    Ok(())
3160}
3161
3162/// Build human-readable messages explaining why the file was not tracked
3163fn build_untracked_reason_message(reason: &UntrackedReason) -> Option<String> {
3164    match reason {
3165        UntrackedReason::FileTooLarge { size, max_size } => {
3166            // Show both exact and human bytes sizes to avoid something
3167            // like '1.0MiB, maximum size allowed is ~1.0MiB'
3168            let size_approx = HumanByteSize(*size);
3169            let max_size_approx = HumanByteSize(*max_size);
3170            Some(format!(
3171                "{size_approx} ({size} bytes); the maximum size allowed is {max_size_approx} \
3172                 ({max_size} bytes)",
3173            ))
3174        }
3175        // Paths with UntrackedReason::FileNotAutoTracked shouldn't be warned about
3176        // every time we make a snapshot. These paths will be printed by
3177        // "jj status" instead.
3178        UntrackedReason::FileNotAutoTracked => None,
3179    }
3180}
3181
3182/// Print a warning to the user, listing untracked files that he may care about
3183pub fn print_untracked_files(
3184    ui: &Ui,
3185    untracked_paths: &BTreeMap<RepoPathBuf, UntrackedReason>,
3186    path_converter: &RepoPathUiConverter,
3187) -> io::Result<()> {
3188    let mut untracked_paths = untracked_paths
3189        .iter()
3190        .filter_map(|(path, reason)| build_untracked_reason_message(reason).map(|m| (path, m)))
3191        .peekable();
3192
3193    if untracked_paths.peek().is_some() {
3194        writeln!(ui.warning_default(), "Refused to snapshot some files:")?;
3195        let mut formatter = ui.stderr_formatter();
3196        for (path, message) in untracked_paths {
3197            let ui_path = path_converter.format_file_path(path);
3198            writeln!(formatter, "  {ui_path}: {message}")?;
3199        }
3200    }
3201
3202    Ok(())
3203}
3204
3205/// Print a warning listing paths that were skipped because their names aren't
3206/// valid UTF-8.
3207fn print_invalid_utf8_paths(
3208    ui: &Ui,
3209    paths: &BTreeSet<(RepoPathBuf, OsString)>,
3210    path_converter: &RepoPathUiConverter,
3211) -> io::Result<()> {
3212    if paths.is_empty() {
3213        return Ok(());
3214    }
3215    writeln!(
3216        ui.warning_default(),
3217        "Skipped some paths because they are not valid UTF-8:"
3218    )?;
3219    let mut formatter = ui.stderr_formatter();
3220    for (dir, name) in paths {
3221        writeln!(
3222            formatter,
3223            "  {}: {name:?}",
3224            path_converter.format_file_path(dir)
3225        )?;
3226    }
3227    Ok(())
3228}
3229
3230pub fn print_snapshot_stats(
3231    ui: &Ui,
3232    stats: &SnapshotStats,
3233    path_converter: &RepoPathUiConverter,
3234) -> io::Result<()> {
3235    print_untracked_files(ui, &stats.untracked_paths, path_converter)?;
3236    print_invalid_utf8_paths(ui, &stats.invalid_utf8_paths, path_converter)?;
3237
3238    let large_files_sizes = stats
3239        .untracked_paths
3240        .values()
3241        .filter_map(|reason| match reason {
3242            UntrackedReason::FileTooLarge { size, .. } => Some(size),
3243            UntrackedReason::FileNotAutoTracked => None,
3244        });
3245    if let Some(size) = large_files_sizes.max() {
3246        print_large_file_hint(ui, *size, None)?;
3247    }
3248    Ok(())
3249}
3250
3251/// Prints a hint about how to handle large files that were refused during
3252/// snapshot.
3253///
3254/// If `large_files` is provided, the hint will include file-track-specific
3255/// options like `--include-ignored`. Otherwise, it shows a simpler hint
3256/// suitable for general snapshot operations.
3257pub fn print_large_file_hint(
3258    ui: &Ui,
3259    max_size: u64,
3260    large_files: Option<&[String]>,
3261) -> io::Result<()> {
3262    let (command, extra) = large_files
3263        .map(|files| {
3264            let files_list = files.iter().map(|s| shell_quote(s)).join(" ");
3265            let command = format!("file track {files_list}");
3266            let extra = format!(
3267                r"
3268  * Run `jj file track --include-ignored {files_list}`
3269    This will track the file(s) regardless of size."
3270            );
3271            (command, extra)
3272        })
3273        .unwrap_or(("status".to_string(), String::new()));
3274
3275    writedoc!(
3276        ui.hint_default(),
3277        r"
3278        This is to prevent large files from being added by accident. To fix this:
3279          * Add the file(s) to `.gitignore`
3280          * Run `jj config set --repo snapshot.max-new-file-size {max_size}`
3281            This will increase the maximum file size allowed for new files, in this repository only.
3282          * Run `jj --config snapshot.max-new-file-size={max_size} {command}`
3283            This will increase the maximum file size allowed for new files, for this command only.{extra}
3284        "
3285    )?;
3286    Ok(())
3287}
3288
3289pub fn print_checkout_stats(
3290    ui: &Ui,
3291    stats: &CheckoutStats,
3292    new_commit: &Commit,
3293) -> Result<(), std::io::Error> {
3294    if stats.added_files > 0 || stats.updated_files > 0 || stats.removed_files > 0 {
3295        writeln!(
3296            ui.status(),
3297            "Added {} files, modified {} files, removed {} files",
3298            stats.added_files,
3299            stats.updated_files,
3300            stats.removed_files
3301        )?;
3302    }
3303    if stats.skipped_files != 0 {
3304        writeln!(
3305            ui.warning_default(),
3306            "{} of those updates were skipped because there were conflicting changes in the \
3307             working copy.",
3308            stats.skipped_files
3309        )?;
3310        writeln!(
3311            ui.hint_default(),
3312            "Inspect the changes compared to the intended target with `jj diff --from {}`.
3313Discard the conflicting changes with `jj restore --from {}`.",
3314            short_commit_hash(new_commit.id()),
3315            short_commit_hash(new_commit.id())
3316        )?;
3317    }
3318    Ok(())
3319}
3320
3321/// Prints warning about explicit paths that don't match any of the tree
3322/// entries.
3323pub fn print_unmatched_explicit_paths<'a>(
3324    ui: &Ui,
3325    workspace_command: &WorkspaceCommandHelper,
3326    expression: &FilesetExpression,
3327    trees: impl IntoIterator<Item = &'a MergedTree>,
3328) -> io::Result<()> {
3329    let mut explicit_paths = expression.explicit_paths().collect_vec();
3330    for tree in trees {
3331        // TODO: propagate errors
3332        explicit_paths.retain(|&path| tree.path_value(path).block_on().unwrap().is_absent());
3333    }
3334
3335    if !explicit_paths.is_empty() {
3336        let ui_paths = explicit_paths
3337            .iter()
3338            .map(|&path| workspace_command.format_file_path(path))
3339            .join(", ");
3340        writeln!(
3341            ui.warning_default(),
3342            "No matching entries for paths: {ui_paths}"
3343        )?;
3344    }
3345
3346    Ok(())
3347}
3348
3349pub async fn update_working_copy(
3350    repo: &Arc<ReadonlyRepo>,
3351    workspace: &mut Workspace,
3352    old_commit: Option<&Commit>,
3353    new_commit: &Commit,
3354) -> Result<CheckoutStats, CommandError> {
3355    let old_tree = old_commit.map(|commit| commit.tree());
3356    // TODO: CheckoutError::ConcurrentCheckout should probably just result in a
3357    // warning for most commands (but be an error for the checkout command)
3358    let stats = workspace
3359        .check_out(repo.op_id().clone(), old_tree.as_ref(), new_commit)
3360        .await
3361        .map_err(|err| {
3362            internal_error_with_message(
3363                format!("Failed to check out commit {}", new_commit.id().hex()),
3364                err,
3365            )
3366        })?;
3367    Ok(stats)
3368}
3369
3370/// Returns the special remote name that should be ignored by default.
3371#[cfg_attr(not(feature = "git"), expect(unused_variables))]
3372pub fn default_ignored_remote_name(store: &Store) -> Option<&'static RemoteName> {
3373    #[cfg(feature = "git")]
3374    {
3375        use jj_lib::git;
3376        if git::get_git_backend(store).is_ok() {
3377            return Some(git::REMOTE_NAME_FOR_LOCAL_GIT_REPO);
3378        }
3379    }
3380    None
3381}
3382
3383/// Whether or not the `bookmark` has any tracked remotes (i.e. is a tracking
3384/// local bookmark.)
3385pub fn has_tracked_remote_bookmarks(repo: &dyn Repo, bookmark: &RefName) -> bool {
3386    let remote_matcher = match default_ignored_remote_name(repo.store()) {
3387        Some(remote) => StringExpression::exact(remote).negated().to_matcher(),
3388        None => StringMatcher::all(),
3389    };
3390    repo.view()
3391        .remote_bookmarks_matching(&StringMatcher::exact(bookmark), &remote_matcher)
3392        .any(|(_, remote_ref)| remote_ref.is_tracked())
3393}
3394
3395/// Whether or not the `tag` has any tracked remotes (i.e. is a tracking local
3396/// tag.)
3397pub fn has_tracked_remote_tags(repo: &dyn Repo, tag: &RefName) -> bool {
3398    let remote_matcher = match default_ignored_remote_name(repo.store()) {
3399        Some(remote) => StringExpression::exact(remote).negated().to_matcher(),
3400        None => StringMatcher::all(),
3401    };
3402    repo.view()
3403        .remote_tags_matching(&StringMatcher::exact(tag), &remote_matcher)
3404        .any(|(_, remote_ref)| remote_ref.is_tracked())
3405}
3406
3407pub fn load_fileset_aliases(
3408    ui: &Ui,
3409    config: &StackedConfig,
3410) -> Result<FilesetAliasesMap, CommandError> {
3411    let table_name = ConfigNamePathBuf::from_iter(["fileset-aliases"]);
3412    load_aliases_map(ui, config, &table_name)
3413}
3414
3415pub fn load_revset_aliases(
3416    ui: &Ui,
3417    config: &StackedConfig,
3418) -> Result<RevsetAliasesMap, CommandError> {
3419    let table_name = ConfigNamePathBuf::from_iter(["revset-aliases"]);
3420    let aliases_map = load_aliases_map(ui, config, &table_name)?;
3421    revset_util::warn_user_redefined_builtin(ui, config, &table_name)?;
3422    Ok(aliases_map)
3423}
3424
3425pub fn load_template_aliases(
3426    ui: &Ui,
3427    config: &StackedConfig,
3428) -> Result<TemplateAliasesMap, CommandError> {
3429    let table_name = ConfigNamePathBuf::from_iter(["template-aliases"]);
3430    load_aliases_map(ui, config, &table_name)
3431}
3432
3433/// Helper to reformat content of log-like commands.
3434#[derive(Clone, Debug)]
3435pub struct LogContentFormat {
3436    width: usize,
3437    word_wrap: bool,
3438}
3439
3440impl LogContentFormat {
3441    /// Creates new formatting helper for the terminal.
3442    pub fn new(ui: &Ui, settings: &UserSettings) -> Result<Self, ConfigGetError> {
3443        Ok(Self {
3444            width: ui.term_width(),
3445            word_wrap: settings.get_bool("ui.log-word-wrap")?,
3446        })
3447    }
3448
3449    /// Subtracts the given `width` and returns new formatting helper.
3450    #[must_use]
3451    pub fn sub_width(&self, width: usize) -> Self {
3452        Self {
3453            width: self.width.saturating_sub(width),
3454            word_wrap: self.word_wrap,
3455        }
3456    }
3457
3458    /// Current width available to content.
3459    pub fn width(&self) -> usize {
3460        self.width
3461    }
3462
3463    /// Writes content which will optionally be wrapped at the current width.
3464    pub async fn write<E: From<io::Error>>(
3465        &self,
3466        formatter: &mut dyn Formatter,
3467        content_fn: impl AsyncFnOnce(&mut dyn Formatter) -> Result<(), E>,
3468    ) -> Result<(), E> {
3469        if self.word_wrap {
3470            let mut recorder = FormatRecorder::new(formatter.maybe_color());
3471            content_fn(&mut recorder).await?;
3472            text_util::write_wrapped(formatter, &recorder, self.width)?;
3473        } else {
3474            content_fn(formatter).await?;
3475        }
3476        Ok(())
3477    }
3478}
3479
3480pub fn short_commit_hash(commit_id: &CommitId) -> String {
3481    format!("{commit_id:.12}")
3482}
3483
3484pub fn short_change_hash(change_id: &ChangeId) -> String {
3485    format!("{change_id:.12}")
3486}
3487
3488pub fn short_operation_hash(operation_id: &OperationId) -> String {
3489    format!("{operation_id:.12}")
3490}
3491
3492/// Wrapper around a `DiffEditor` to conditionally start interactive session.
3493#[derive(Clone, Debug)]
3494pub enum DiffSelector {
3495    NonInteractive,
3496    Interactive(DiffEditor),
3497}
3498
3499impl DiffSelector {
3500    pub fn is_interactive(&self) -> bool {
3501        matches!(self, Self::Interactive(_))
3502    }
3503
3504    /// Restores diffs from the `right_tree` to the `left_tree` by using an
3505    /// interactive editor if enabled.
3506    ///
3507    /// Only files matching the `matcher` will be copied to the new tree.
3508    pub async fn select(
3509        &self,
3510        ui: &Ui,
3511        trees: Diff<&MergedTree>,
3512        tree_labels: Diff<String>,
3513        matcher: &dyn Matcher,
3514        format_instructions: impl FnOnce() -> String,
3515    ) -> Result<MergedTree, CommandError> {
3516        let selected_tree = restore_tree(
3517            trees.after,
3518            trees.before,
3519            tree_labels.after,
3520            tree_labels.before,
3521            matcher,
3522        )
3523        .await?;
3524        match self {
3525            Self::NonInteractive => Ok(selected_tree),
3526            Self::Interactive(editor) => {
3527                if selected_tree.tree_ids() == trees.before.tree_ids() {
3528                    writeln!(ui.warning_default(), "Empty diff - won't run diff editor.")?;
3529                    Ok(selected_tree)
3530                } else {
3531                    // edit_diff_external() is designed to edit the right tree,
3532                    // whereas we want to update the left tree. Unmatched paths
3533                    // shouldn't be based off the right tree.
3534                    Ok(editor
3535                        .edit(
3536                            Diff::new(trees.before, &selected_tree),
3537                            matcher,
3538                            format_instructions,
3539                        )
3540                        .await?)
3541                }
3542            }
3543        }
3544    }
3545}
3546
3547/// Computes the location (new parents and new children) to place commits.
3548///
3549/// The `destination` argument is mutually exclusive to the `insert_after` and
3550/// `insert_before` arguments.
3551pub async fn compute_commit_location(
3552    ui: &Ui,
3553    workspace_command: &WorkspaceCommandHelper,
3554    destination: Option<&[RevisionArg]>,
3555    insert_after: Option<&[RevisionArg]>,
3556    insert_before: Option<&[RevisionArg]>,
3557    commit_type: &str,
3558) -> Result<(Vec<CommitId>, Vec<CommitId>), CommandError> {
3559    let resolve_revisions =
3560        async |revisions: Option<&[RevisionArg]>| -> Result<Option<Vec<CommitId>>, CommandError> {
3561            if let Some(revisions) = revisions {
3562                Ok(Some(
3563                    workspace_command
3564                        .resolve_revsets_ordered(ui, revisions)
3565                        .await?
3566                        .into_iter()
3567                        .collect_vec(),
3568                ))
3569            } else {
3570                Ok(None)
3571            }
3572        };
3573    let destination_commit_ids = resolve_revisions(destination).await?;
3574    let after_commit_ids = resolve_revisions(insert_after).await?;
3575    let before_commit_ids = resolve_revisions(insert_before).await?;
3576
3577    let (new_parent_ids, new_child_ids) =
3578        match (destination_commit_ids, after_commit_ids, before_commit_ids) {
3579            (Some(destination_commit_ids), None, None) => (destination_commit_ids, vec![]),
3580            (None, Some(after_commit_ids), Some(before_commit_ids)) => {
3581                (after_commit_ids, before_commit_ids)
3582            }
3583            (None, Some(after_commit_ids), None) => {
3584                let new_child_ids = RevsetExpression::commits(after_commit_ids.clone())
3585                    .children()
3586                    .evaluate(workspace_command.repo().as_ref())?
3587                    .stream()
3588                    .try_collect()
3589                    .await?;
3590
3591                (after_commit_ids, new_child_ids)
3592            }
3593            (None, None, Some(before_commit_ids)) => {
3594                let before_commits = try_join_all(
3595                    before_commit_ids
3596                        .iter()
3597                        .map(|id| workspace_command.repo().store().get_commit_async(id)),
3598                )
3599                .await?;
3600                // Not using `RevsetExpression::parents` here to persist the order of parents
3601                // specified in `before_commits`.
3602                let new_parent_ids = before_commits
3603                    .iter()
3604                    .flat_map(|commit| commit.parent_ids())
3605                    .unique()
3606                    .cloned()
3607                    .collect_vec();
3608
3609                (new_parent_ids, before_commit_ids)
3610            }
3611            (Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
3612                panic!("destination cannot be used with insert_after/insert_before")
3613            }
3614            (None, None, None) => {
3615                panic!("expected at least one of destination or insert_after/insert_before")
3616            }
3617        };
3618
3619    if !new_child_ids.is_empty() {
3620        workspace_command
3621            .check_rewritable(new_child_ids.iter())
3622            .await?;
3623        ensure_no_commit_loop(
3624            workspace_command.repo().as_ref(),
3625            &RevsetExpression::commits(new_child_ids.clone()),
3626            &RevsetExpression::commits(new_parent_ids.clone()),
3627            commit_type,
3628        )
3629        .await?;
3630    }
3631
3632    if new_parent_ids.is_empty() {
3633        return Err(user_error("No revisions found to use as parent"));
3634    }
3635
3636    Ok((new_parent_ids, new_child_ids))
3637}
3638
3639/// Ensure that there is no possible cycle between the potential children and
3640/// parents of the given commits.
3641async fn ensure_no_commit_loop(
3642    repo: &ReadonlyRepo,
3643    children_expression: &Arc<ResolvedRevsetExpression>,
3644    parents_expression: &Arc<ResolvedRevsetExpression>,
3645    commit_type: &str,
3646) -> Result<(), CommandError> {
3647    if let Some(commit_id) = children_expression
3648        .dag_range_to(parents_expression)
3649        .evaluate(repo)?
3650        .stream()
3651        .try_next()
3652        .await?
3653    {
3654        return Err(user_error(format!(
3655            "Refusing to create a loop: commit {} would be both an ancestor and a descendant of \
3656             the {commit_type}",
3657            short_commit_hash(&commit_id),
3658        )));
3659    }
3660    Ok(())
3661}
3662
3663/// Jujutsu (An experimental VCS)
3664///
3665/// To get started, see the tutorial [`jj help -k tutorial`].
3666///
3667/// [`jj help -k tutorial`]:
3668///     https://docs.jj-vcs.dev/latest/tutorial/
3669#[derive(clap::Parser, Clone, Debug)]
3670#[command(name = "jj")]
3671pub struct Args {
3672    #[command(flatten)]
3673    pub global_args: GlobalArgs,
3674}
3675
3676#[derive(clap::Args, Clone, Debug)]
3677#[command(next_help_heading = "Global Options")]
3678pub struct GlobalArgs {
3679    /// Path to repository to operate on
3680    ///
3681    /// By default, Jujutsu searches for the closest .jj/ directory in an
3682    /// ancestor of the current working directory.
3683    #[arg(long, short = 'R', global = true, value_hint = clap::ValueHint::DirPath)]
3684    pub repository: Option<String>,
3685
3686    /// Don't snapshot the working copy, and don't update it
3687    ///
3688    /// By default, Jujutsu snapshots the working copy at the beginning of every
3689    /// command. The working copy is also updated at the end of the command,
3690    /// if the command modified the working-copy commit (`@`). If you want
3691    /// to avoid snapshotting the working copy and instead see a possibly
3692    /// stale working-copy commit, you can use `--ignore-working-copy`.
3693    /// This may be useful e.g. in a command prompt, especially if you have
3694    /// another process that commits the working copy.
3695    ///
3696    /// Loading the repository at a specific operation with `--at-operation`
3697    /// implies `--ignore-working-copy`.
3698    #[arg(long, global = true)]
3699    pub ignore_working_copy: bool,
3700
3701    /// Run the command as usual but don't integrate any operations
3702    ///
3703    /// When this option is given, the operations will still be created as usual
3704    /// but they will not be integrated to the operation log. The working copy
3705    /// will also not be updated.
3706    ///
3707    /// The command will print the resulting operation ID. You can pass that to
3708    /// e.g. `jj --at-op` to inspect the resulting repo state, or you can pass
3709    /// it to `jj op restore` to restore the repo to that state. You can also
3710    /// pass the ID to `jj op integrate` to integrate the operation.
3711    ///
3712    /// Note that this does *not* prevent side effects outside the repo. For
3713    /// example, `jj git push --no-integrate-operation` will still perform the
3714    /// push.
3715    #[arg(long, global = true)]
3716    pub no_integrate_operation: bool,
3717
3718    /// Allow rewriting immutable commits
3719    ///
3720    /// By default, Jujutsu prevents rewriting commits in the configured set of
3721    /// immutable commits. This option disables that check and lets you rewrite
3722    /// any commit but the root commit.
3723    ///
3724    /// This option only affects the check. It does not affect the
3725    /// `immutable_heads()` revset or the `immutable` template keyword.
3726    #[arg(long, global = true)]
3727    pub ignore_immutable: bool,
3728
3729    /// Operation to load the repo at
3730    ///
3731    /// Operation to load the repo at. By default, Jujutsu loads the repo at the
3732    /// most recent operation, or at the merge of the divergent operations if
3733    /// any.
3734    ///
3735    /// You can use `--at-op=<operation ID>` to see what the repo looked like at
3736    /// an earlier operation. For example `jj --at-op=<operation ID> st` will
3737    /// show you what `jj st` would have shown you when the given operation had
3738    /// just finished. `--at-op=@` is pretty much the same as the default except
3739    /// that divergent operations will never be merged.
3740    ///
3741    /// Use `jj op log` to find the operation ID you want. Any unambiguous
3742    /// prefix of the operation ID is enough.
3743    ///
3744    /// When loading the repo at an earlier operation, the working copy will be
3745    /// ignored, as if `--ignore-working-copy` had been specified.
3746    ///
3747    /// It is possible to run mutating commands when loading the repo at an
3748    /// earlier operation. Doing that is equivalent to having run concurrent
3749    /// commands starting at the earlier operation. There's rarely a reason to
3750    /// do that, but it is possible.
3751    #[arg(long, visible_alias = "at-op", global = true)]
3752    #[arg(add = ArgValueCandidates::new(complete::operations))]
3753    pub at_operation: Option<String>,
3754
3755    /// Enable debug logging
3756    #[arg(long, global = true)]
3757    pub debug: bool,
3758
3759    #[command(flatten)]
3760    pub early_args: EarlyArgs,
3761}
3762
3763#[derive(clap::Args, Clone, Debug)]
3764pub struct EarlyArgs {
3765    /// When to colorize output
3766    #[arg(long, value_name = "WHEN", global = true)]
3767    pub color: Option<ColorChoice>,
3768
3769    /// Silence non-primary command output
3770    ///
3771    /// For example, `jj file list` will still list files, but it won't tell
3772    /// you if the working copy was snapshotted or if descendants were rebased.
3773    ///
3774    /// Warnings and errors will still be printed.
3775    #[arg(long, global = true, action = ArgAction::SetTrue)]
3776    // Parsing with ignore_errors will crash if this is bool, so use
3777    // Option<bool>.
3778    pub quiet: Option<bool>,
3779
3780    /// Disable the pager
3781    #[arg(long, global = true, action = ArgAction::SetTrue)]
3782    // Parsing with ignore_errors will crash if this is bool, so use
3783    // Option<bool>.
3784    pub no_pager: Option<bool>,
3785
3786    /// Additional configuration options (can be repeated)
3787    ///
3788    /// The name should be specified as TOML dotted keys. The value should be
3789    /// specified as a TOML expression. If string value isn't enclosed by any
3790    /// TOML constructs (such as array notation), quotes can be omitted.
3791    #[arg(long, value_name = "NAME=VALUE", global = true)]
3792    #[arg(add = ArgValueCompleter::new(complete::leaf_config_key_value))]
3793    pub config: Vec<String>,
3794
3795    /// Additional configuration files (can be repeated)
3796    #[arg(long, value_name = "PATH", global = true, value_hint = clap::ValueHint::FilePath)]
3797    pub config_file: Vec<String>,
3798}
3799
3800impl EarlyArgs {
3801    pub(crate) fn merged_config_args(&self, matches: &ArgMatches) -> Vec<(ConfigArgKind, &str)> {
3802        merge_args_with(
3803            matches,
3804            &[("config", &self.config), ("config_file", &self.config_file)],
3805            |id, value| match id {
3806                "config" => (ConfigArgKind::Item, value.as_ref()),
3807                "config_file" => (ConfigArgKind::File, value.as_ref()),
3808                _ => unreachable!("unexpected id {id:?}"),
3809            },
3810        )
3811    }
3812
3813    fn has_config_args(&self) -> bool {
3814        !self.config.is_empty() || !self.config_file.is_empty()
3815    }
3816}
3817
3818/// Wrapper around revset expression argument.
3819///
3820/// An empty string is rejected early by the CLI value parser, but it's still
3821/// allowed to construct an empty `RevisionArg` from a config value for
3822/// example. An empty expression will be rejected by the revset parser.
3823#[derive(Clone, Debug)]
3824pub struct RevisionArg(Cow<'static, str>);
3825
3826impl RevisionArg {
3827    /// The working-copy symbol, which is the default of the most commands.
3828    pub const AT: Self = Self(Cow::Borrowed("@"));
3829}
3830
3831impl From<String> for RevisionArg {
3832    fn from(s: String) -> Self {
3833        Self(s.into())
3834    }
3835}
3836
3837impl AsRef<str> for RevisionArg {
3838    fn as_ref(&self) -> &str {
3839        &self.0
3840    }
3841}
3842
3843impl fmt::Display for RevisionArg {
3844    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3845        write!(f, "{}", self.0)
3846    }
3847}
3848
3849impl ValueParserFactory for RevisionArg {
3850    type Parser = MapValueParser<NonEmptyStringValueParser, fn(String) -> Self>;
3851
3852    fn value_parser() -> Self::Parser {
3853        NonEmptyStringValueParser::new().map(Self::from)
3854    }
3855}
3856
3857/// Merges multiple clap args in order of appearance.
3858///
3859/// The `id_values` is a list of `(id, values)` pairs, where `id` is the name of
3860/// the clap `Arg`, and `values` are the parsed values for that arg. The
3861/// `convert` function transforms each `(id, value)` pair to e.g. an enum.
3862///
3863/// This is a workaround for <https://github.com/clap-rs/clap/issues/3146>.
3864pub fn merge_args_with<'k, 'v, T, U>(
3865    matches: &ArgMatches,
3866    id_values: &[(&'k str, &'v [T])],
3867    mut convert: impl FnMut(&'k str, &'v T) -> U,
3868) -> Vec<U> {
3869    let mut pos_values: Vec<(usize, U)> = Vec::new();
3870    for (id, values) in id_values {
3871        pos_values.extend(itertools::zip_eq(
3872            matches.indices_of(id).into_iter().flatten(),
3873            values.iter().map(|v| convert(id, v)),
3874        ));
3875    }
3876    pos_values.sort_unstable_by_key(|&(pos, _)| pos);
3877    pos_values.into_iter().map(|(_, value)| value).collect()
3878}
3879
3880fn resolve_default_command(
3881    ui: &Ui,
3882    config: &StackedConfig,
3883    app: &Command,
3884    mut string_args: Vec<String>,
3885) -> Result<Vec<String>, CommandError> {
3886    const PRIORITY_FLAGS: &[&str] = &["--help", "-h", "--version", "-V"];
3887
3888    let has_priority_flag = string_args
3889        .iter()
3890        .any(|arg| PRIORITY_FLAGS.contains(&arg.as_str()));
3891    if has_priority_flag {
3892        return Ok(string_args);
3893    }
3894
3895    let app_clone = app
3896        .clone()
3897        .allow_external_subcommands(true)
3898        .ignore_errors(true);
3899    let matches = app_clone.try_get_matches_from(&string_args).ok();
3900
3901    if let Some(matches) = matches
3902        && matches.subcommand_name().is_none()
3903    {
3904        // Try loading as either a string or an array.
3905        // Only use `.optional()?` on the latter call to `config.get` - if it
3906        // was used on both, the type error from the first check would return
3907        // early.
3908        let default_command = if let Ok(string) = config.get::<String>("ui.default-command") {
3909            // Warn when the user has misconfigured their default command as
3910            // "log -n 5" instead of ["log", "-n", "5"]
3911            if string.contains(' ') {
3912                let elements: ConfigValue = string.split_whitespace().collect();
3913                writeln!(
3914                    ui.warning_default(),
3915                    "To include flags/arguments in `ui.default-command`, use an array instead of \
3916                     a string: `ui.default-command = {elements}`"
3917                )?;
3918            }
3919
3920            vec![string]
3921        } else if let Some(array) = config.get::<Vec<String>>("ui.default-command").optional()? {
3922            array
3923        } else {
3924            writeln!(
3925                ui.hint_default(),
3926                "Use `jj -h` for a list of available commands."
3927            )?;
3928            writeln!(
3929                ui.hint_no_heading(),
3930                "Run `jj config set --user ui.default-command log` to disable this message."
3931            )?;
3932
3933            vec!["log".to_string()]
3934        };
3935
3936        // Insert the default command directly after the path to the binary.
3937        string_args.splice(1..1, default_command);
3938    }
3939    Ok(string_args)
3940}
3941
3942fn load_aliases<'config>(
3943    ui: &Ui,
3944    config: &'config StackedConfig,
3945    app: &Command,
3946) -> Result<HashSet<&'config str>, CommandError> {
3947    let mut defined_aliases: HashSet<_> = config.table_keys("aliases").collect();
3948    let mut real_commands = HashSet::new();
3949    for command in app.get_subcommands() {
3950        real_commands.insert(command.get_name());
3951        for alias in command.get_all_aliases() {
3952            real_commands.insert(alias);
3953        }
3954    }
3955    for alias in defined_aliases
3956        .extract_if(|a| real_commands.contains(a))
3957        .sorted()
3958    {
3959        writeln!(
3960            ui.warning_default(),
3961            "Cannot define an alias that overrides the built-in command '{alias}'."
3962        )?;
3963    }
3964    Ok(defined_aliases)
3965}
3966
3967fn resolve_aliases(
3968    config: &StackedConfig,
3969    app: &Command,
3970    defined_aliases: &HashSet<&str>,
3971    mut string_args: Vec<String>,
3972) -> Result<Vec<String>, CommandError> {
3973    let mut recursion_check_stack: Vec<(&str, Range<usize>)> = Vec::new();
3974
3975    loop {
3976        let app_clone = app.clone().allow_external_subcommands(true);
3977        let matches = app_clone.try_get_matches_from(&string_args).ok();
3978        let Some((command_name, submatches)) = matches.as_ref().and_then(|m| m.subcommand()) else {
3979            // No more alias commands, or hit unknown option
3980            return Ok(string_args);
3981        };
3982        let alias_name = command_name.to_string();
3983        let alias_args = submatches
3984            .get_many::<OsString>("")
3985            .unwrap_or_default()
3986            .map(|arg| arg.to_str().unwrap().to_string())
3987            .collect_vec();
3988        let Some(&alias_name) = defined_aliases.get(&*alias_name) else {
3989            // Not a real command and not an alias, so return what we've resolved so far
3990            return Ok(string_args);
3991        };
3992        let alias_definition: Vec<String> = match config.get(["aliases", alias_name]) {
3993            Ok(definition) => definition,
3994            Err(original_err) => config
3995                .get(["aliases", alias_name, "definition"])
3996                .map_err(|_| original_err)?,
3997        };
3998        let alias_position = string_args.len() - 1 - alias_args.len();
3999
4000        // recursion check
4001        while let Some((_, check_range)) = recursion_check_stack.last() {
4002            if check_range.contains(&alias_position) {
4003                // The tracked chain of alias expansions produced the current
4004                // alias. Check for recursion.
4005                if recursion_check_stack.iter().any(|&(a, _)| a == alias_name) {
4006                    return Err(user_error(format!(
4007                        "Recursive alias definition involving `{alias_name}`"
4008                    )));
4009                }
4010                break;
4011            }
4012            // Last tracked alias did not produce the currently expanding alias.
4013            // Remove it from stack and fixup the range of the next one.
4014            let check_range = check_range.clone();
4015            recursion_check_stack.pop();
4016            if let Some((_, next_range)) = recursion_check_stack.last_mut() {
4017                // Increase next range by the length of the current one, minus
4018                // one to account for the removed alias name.
4019                next_range.end += check_range.end - check_range.start - 1;
4020            }
4021        }
4022        recursion_check_stack.push((
4023            alias_name,
4024            alias_position..(alias_position + alias_definition.len()),
4025        ));
4026
4027        assert!(string_args.ends_with(&alias_args));
4028        string_args.truncate(alias_position);
4029        string_args.extend(alias_definition);
4030        string_args.extend_from_slice(&alias_args);
4031    }
4032}
4033
4034/// Parse args that must be interpreted early, e.g. before printing help.
4035fn parse_early_args(
4036    app: &Command,
4037    args: &[String],
4038) -> Result<(EarlyArgs, Vec<ConfigLayer>), CommandError> {
4039    // ignore_errors() bypasses errors like missing subcommand
4040    let early_matches = app
4041        .clone()
4042        .disable_version_flag(true)
4043        // Do not emit DisplayHelp error
4044        .disable_help_flag(true)
4045        // Do not stop parsing at -h/--help
4046        .arg(
4047            clap::Arg::new("help")
4048                .short('h')
4049                .long("help")
4050                .global(true)
4051                .action(ArgAction::Count),
4052        )
4053        .ignore_errors(true)
4054        .try_get_matches_from(args)?;
4055    let args = EarlyArgs::from_arg_matches(&early_matches).unwrap();
4056
4057    let mut config_layers = parse_config_args(&args.merged_config_args(&early_matches))?;
4058    // Command arguments overrides any other configuration including the
4059    // variables loaded from --config* arguments.
4060    let mut layer = ConfigLayer::empty(ConfigSource::CommandArg);
4061    if let Some(choice) = args.color {
4062        layer.set_value("ui.color", choice.to_string()).unwrap();
4063    }
4064    if args.quiet.unwrap_or_default() {
4065        layer.set_value("ui.quiet", true).unwrap();
4066    }
4067    if args.no_pager.unwrap_or_default() {
4068        layer.set_value("ui.paginate", "never").unwrap();
4069    }
4070    if !layer.is_empty() {
4071        config_layers.push(layer);
4072    }
4073    Ok((args, config_layers))
4074}
4075
4076fn handle_shell_completion(
4077    ui: &Ui,
4078    app: &Command,
4079    config: &StackedConfig,
4080    cwd: &Path,
4081) -> Result<(), CommandError> {
4082    let mut orig_args = env::args_os();
4083
4084    let mut args = vec![];
4085    // Take the first two arguments as is, they must be passed to clap_complete
4086    // without any changes. They are usually "jj --".
4087    args.extend(orig_args.by_ref().take(2));
4088
4089    // Make sure aliases are expanded before passing them to clap_complete. We
4090    // skip the first two args ("jj" and "--") for alias resolution, then we
4091    // stitch the args back together, like clap_complete expects them.
4092    if orig_args.len() > 0 {
4093        let complete_index: Option<usize> = env::var("_CLAP_COMPLETE_INDEX")
4094            .ok()
4095            .and_then(|s| s.parse().ok());
4096        let resolved_aliases = if let Some(index) = complete_index {
4097            // As of clap_complete 4.5.38, zsh completion script doesn't pad an
4098            // empty arg at the complete position. If the args doesn't include a
4099            // command name, the default command would be expanded at that
4100            // position. Therefore, no other command names would be suggested.
4101            let pad_len = usize::saturating_sub(index + 1, orig_args.len());
4102            let padded_args = orig_args
4103                .by_ref()
4104                .chain(std::iter::repeat_n(OsString::new(), pad_len));
4105
4106            // Expand aliases left of the completion index.
4107            let mut expanded_args =
4108                expand_args_for_completion(ui, app, padded_args.take(index + 1), config)?;
4109
4110            // Adjust env var to compensate for shift of the completion point in the
4111            // expanded command line.
4112            // SAFETY: Program is running single-threaded at this point.
4113            unsafe {
4114                env::set_var(
4115                    "_CLAP_COMPLETE_INDEX",
4116                    (expanded_args.len() - 1).to_string(),
4117                );
4118            }
4119
4120            // Remove extra padding again to align with clap_complete's expectations for
4121            // zsh.
4122            let split_off_padding = expanded_args.split_off(expanded_args.len() - pad_len);
4123            assert!(
4124                split_off_padding.iter().all(|s| s.is_empty()),
4125                "split-off padding should only consist of empty strings but was \
4126                 {split_off_padding:?}",
4127            );
4128
4129            // Append the remaining arguments to the right of the completion point.
4130            expanded_args.extend(to_string_args(orig_args)?);
4131            expanded_args
4132        } else {
4133            expand_args_for_completion(ui, app, orig_args, config)?
4134        };
4135        args.extend(resolved_aliases.into_iter().map(OsString::from));
4136    }
4137    let ran_completion = clap_complete::CompleteEnv::with_factory(|| {
4138        let mut app = app.clone();
4139        // Dynamic completer can produce completion for hidden aliases, so we
4140        // can remove short subcommand names from the completion candidates.
4141        hide_short_subcommand_aliases(&mut app);
4142        // for completing aliases
4143        app.allow_external_subcommands(true)
4144    })
4145    .try_complete(args.iter(), Some(cwd))?;
4146    assert!(
4147        ran_completion,
4148        "This function should not be called without the COMPLETE variable set."
4149    );
4150    Ok(())
4151}
4152
4153/// Removes prefix command names (e.g. "c" for "create") from visible aliases,
4154/// and adds them to (hidden) aliases.
4155fn hide_short_subcommand_aliases(cmd: &mut Command) {
4156    for cmd in cmd.get_subcommands_mut() {
4157        hide_short_subcommand_aliases(cmd);
4158    }
4159    let (short_aliases, new_visible_aliases) = cmd
4160        .get_visible_aliases()
4161        .map(|name| name.to_owned())
4162        .partition::<Vec<_>, _>(|name| cmd.get_name().starts_with(name));
4163    if short_aliases.is_empty() {
4164        return;
4165    }
4166    *cmd = mem::take(cmd)
4167        // clear existing visible aliases and add new
4168        .visible_alias(None)
4169        .visible_aliases(new_visible_aliases)
4170        // add to hidden aliases
4171        .aliases(short_aliases);
4172}
4173
4174pub fn expand_args(
4175    ui: &Ui,
4176    app: &Command,
4177    args_os: impl IntoIterator<Item = OsString>,
4178    config: &StackedConfig,
4179) -> Result<Vec<String>, CommandError> {
4180    let mut string_args = to_string_args(args_os)?;
4181    let aliases = load_aliases(ui, config, app)?;
4182    string_args = resolve_aliases(config, app, &aliases, string_args)?;
4183    string_args = resolve_default_command(ui, config, app, string_args)?;
4184    string_args = resolve_aliases(config, app, &aliases, string_args)?;
4185    Ok(string_args)
4186}
4187
4188fn expand_args_for_completion(
4189    ui: &Ui,
4190    app: &Command,
4191    args_os: impl IntoIterator<Item = OsString>,
4192    config: &StackedConfig,
4193) -> Result<Vec<String>, CommandError> {
4194    let mut string_args = to_string_args(args_os)?;
4195    let aliases = load_aliases(ui, config, app)?;
4196
4197    // Resolution of subcommand aliases must not consider the argument that is being
4198    // completed.
4199    let cursor_arg = string_args.pop();
4200    string_args = resolve_aliases(config, app, &aliases, string_args)?;
4201    string_args.extend(cursor_arg);
4202
4203    // If a subcommand has been given, including the potentially incomplete argument
4204    // that is being completed, the default command is not resolved and the
4205    // completion candidates for the subcommand are prioritized.
4206    string_args = resolve_default_command(ui, config, app, string_args)?;
4207
4208    let cursor_arg = string_args.pop();
4209    string_args = resolve_aliases(config, app, &aliases, string_args)?;
4210    string_args.extend(cursor_arg);
4211
4212    Ok(string_args)
4213}
4214
4215fn to_string_args(
4216    args_os: impl IntoIterator<Item = OsString>,
4217) -> Result<Vec<String>, CommandError> {
4218    args_os
4219        .into_iter()
4220        .map(|arg_os| {
4221            arg_os
4222                .into_string()
4223                .map_err(|_| cli_error("Non-UTF-8 argument"))
4224        })
4225        .collect()
4226}
4227
4228fn parse_args(app: &Command, string_args: &[String]) -> Result<(ArgMatches, Args), clap::Error> {
4229    let matches = app
4230        .clone()
4231        .arg_required_else_help(true)
4232        .subcommand_required(true)
4233        .try_get_matches_from(string_args)?;
4234    let args = Args::from_arg_matches(&matches).unwrap();
4235    Ok((matches, args))
4236}
4237
4238fn command_name(mut matches: &ArgMatches) -> String {
4239    let mut command = String::new();
4240    while let Some((subcommand, new_matches)) = matches.subcommand() {
4241        if !command.is_empty() {
4242            command.push(' ');
4243        }
4244        command.push_str(subcommand);
4245        matches = new_matches;
4246    }
4247    command
4248}
4249
4250pub fn format_template<C: Clone>(ui: &Ui, arg: &C, template: &TemplateRenderer<C>) -> String {
4251    let mut output = vec![];
4252    template
4253        .format(arg, ui.new_formatter(&mut output).as_mut())
4254        .expect("write() to vec backed formatter should never fail");
4255    // Template output is usually UTF-8, but it can contain file content.
4256    output.into_string_lossy()
4257}
4258
4259// Like `BoxFuture<_>`, but doesn't require `Send`.
4260type BoxedCliDispatchFuture<'a> = Pin<Box<dyn Future<Output = Result<(), CommandError>> + 'a>>;
4261pub type BoxedAsyncCliDispatch<'a> = Box<dyn AsyncCliDispatch + 'a>;
4262type BoxedAsyncCliDispatchHook<'a> = Box<dyn AsyncCliDispatchHook + 'a>;
4263
4264/// Object-safe trait for async command dispatch function.
4265pub trait AsyncCliDispatch {
4266    fn call<'a>(
4267        self: Box<Self>,
4268        ui: &'a mut Ui,
4269        command_helper: &'a CommandHelper,
4270    ) -> BoxedCliDispatchFuture<'a>
4271    where
4272        Self: 'a;
4273}
4274
4275/// Object-safe trait for async command dispatch hook function.
4276trait AsyncCliDispatchHook {
4277    fn call<'a>(
4278        self: Box<Self>,
4279        ui: &'a mut Ui,
4280        command_helper: &'a CommandHelper,
4281        old_dispatch: BoxedAsyncCliDispatch<'a>,
4282    ) -> BoxedCliDispatchFuture<'a>
4283    where
4284        Self: 'a;
4285}
4286
4287/// Object-safe wrapper for async command dispatch function.
4288struct AsyncCliDispatchFn<F>(F);
4289
4290impl<F> AsyncCliDispatch for AsyncCliDispatchFn<F>
4291where
4292    F: AsyncFnOnce(&mut Ui, &CommandHelper) -> Result<(), CommandError>,
4293{
4294    fn call<'a>(
4295        self: Box<Self>,
4296        ui: &'a mut Ui,
4297        command_helper: &'a CommandHelper,
4298    ) -> BoxedCliDispatchFuture<'a>
4299    where
4300        Self: 'a,
4301    {
4302        Box::pin((self.0)(ui, command_helper))
4303    }
4304}
4305
4306/// Object-safe wrapper for async command dispatch hook function.
4307struct AsyncCliDispatchHookFn<F>(F);
4308
4309impl<F> AsyncCliDispatchHook for AsyncCliDispatchHookFn<F>
4310where
4311    F: AsyncFnOnce(&mut Ui, &CommandHelper, BoxedAsyncCliDispatch<'_>) -> Result<(), CommandError>,
4312{
4313    fn call<'a>(
4314        self: Box<Self>,
4315        ui: &'a mut Ui,
4316        command_helper: &'a CommandHelper,
4317        old_dispatch: BoxedAsyncCliDispatch<'a>,
4318    ) -> BoxedCliDispatchFuture<'a>
4319    where
4320        Self: 'a,
4321    {
4322        Box::pin((self.0)(ui, command_helper, old_dispatch))
4323    }
4324}
4325
4326/// CLI command builder and runner.
4327#[must_use]
4328pub struct CliRunner<'a> {
4329    tracing_subscription: TracingSubscription,
4330    app: Command,
4331    config_layers: Vec<ConfigLayer>,
4332    config_migrations: Vec<ConfigMigrationRule>,
4333    store_factories: StoreFactories,
4334    working_copy_factories: WorkingCopyFactories,
4335    workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
4336    revset_extensions: RevsetExtensions,
4337    commit_template_extensions: Vec<Arc<dyn CommitTemplateLanguageExtension>>,
4338    operation_template_extensions: Vec<Arc<dyn OperationTemplateLanguageExtension>>,
4339    dispatch: BoxedAsyncCliDispatch<'a>,
4340    dispatch_hooks: Vec<BoxedAsyncCliDispatchHook<'a>>,
4341    process_global_args_fns: Vec<ProcessGlobalArgsFn<'a>>,
4342}
4343
4344type ProcessGlobalArgsFn<'a> =
4345    Box<dyn FnOnce(&mut Ui, &ArgMatches) -> Result<(), CommandError> + 'a>;
4346
4347impl<'a> CliRunner<'a> {
4348    /// Initializes CLI environment and returns a builder. This should be called
4349    /// as early as possible.
4350    pub fn init() -> Self {
4351        let tracing_subscription = TracingSubscription::init();
4352        crate::cleanup_guard::init();
4353        Self {
4354            tracing_subscription,
4355            app: crate::commands::default_app(),
4356            config_layers: crate::config::default_config_layers(),
4357            config_migrations: crate::config::default_config_migrations(),
4358            store_factories: default_backend_factories(),
4359            working_copy_factories: default_working_copy_factories(),
4360            workspace_loader_factory: Box::new(DefaultWorkspaceLoaderFactory),
4361            revset_extensions: Default::default(),
4362            commit_template_extensions: vec![],
4363            operation_template_extensions: vec![],
4364            dispatch: Box::new(AsyncCliDispatchFn(crate::commands::run_command)),
4365            dispatch_hooks: vec![],
4366            process_global_args_fns: vec![],
4367        }
4368    }
4369
4370    /// Set the name of the CLI application to be displayed in help messages.
4371    pub fn name(mut self, name: &str) -> Self {
4372        self.app = self.app.name(name.to_string());
4373        self
4374    }
4375
4376    /// Set the about message to be displayed in help messages.
4377    pub fn about(mut self, about: &str) -> Self {
4378        self.app = self.app.about(about.to_string());
4379        self
4380    }
4381
4382    /// Set the version to be displayed by `jj version`.
4383    pub fn version(mut self, version: &str) -> Self {
4384        self.app = self.app.version(version.to_string());
4385        self
4386    }
4387
4388    /// Adds default configs in addition to the normal defaults.
4389    ///
4390    /// The `layer.source` must be `Default`. Other sources such as `User` would
4391    /// be replaced by loaded configuration.
4392    pub fn add_extra_config(mut self, layer: ConfigLayer) -> Self {
4393        assert_eq!(layer.source, ConfigSource::Default);
4394        self.config_layers.push(layer);
4395        self
4396    }
4397
4398    /// Adds config migration rule in addition to the default rules.
4399    pub fn add_extra_config_migration(mut self, rule: ConfigMigrationRule) -> Self {
4400        self.config_migrations.push(rule);
4401        self
4402    }
4403
4404    /// Adds `StoreFactories` to be used.
4405    pub fn add_store_factories(mut self, store_factories: StoreFactories) -> Self {
4406        self.store_factories.merge(store_factories);
4407        self
4408    }
4409
4410    /// Adds working copy factories to be used.
4411    pub fn add_working_copy_factories(
4412        mut self,
4413        working_copy_factories: WorkingCopyFactories,
4414    ) -> Self {
4415        merge_factories_map(&mut self.working_copy_factories, working_copy_factories);
4416        self
4417    }
4418
4419    pub fn set_workspace_loader_factory(
4420        mut self,
4421        workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
4422    ) -> Self {
4423        self.workspace_loader_factory = workspace_loader_factory;
4424        self
4425    }
4426
4427    pub fn add_symbol_resolver_extension(
4428        mut self,
4429        symbol_resolver: Box<dyn SymbolResolverExtension>,
4430    ) -> Self {
4431        self.revset_extensions.add_symbol_resolver(symbol_resolver);
4432        self
4433    }
4434
4435    pub fn add_revset_function_extension(
4436        mut self,
4437        name: &'static str,
4438        func: RevsetFunction,
4439    ) -> Self {
4440        self.revset_extensions.add_custom_function(name, func);
4441        self
4442    }
4443
4444    pub fn add_commit_template_extension(
4445        mut self,
4446        commit_template_extension: Box<dyn CommitTemplateLanguageExtension>,
4447    ) -> Self {
4448        self.commit_template_extensions
4449            .push(commit_template_extension.into());
4450        self
4451    }
4452
4453    pub fn add_operation_template_extension(
4454        mut self,
4455        operation_template_extension: Box<dyn OperationTemplateLanguageExtension>,
4456    ) -> Self {
4457        self.operation_template_extensions
4458            .push(operation_template_extension.into());
4459        self
4460    }
4461
4462    /// Add a hook that gets called when it's time to run the command. It is
4463    /// the hook's responsibility to call the given inner dispatch function to
4464    /// run the command.
4465    pub fn add_dispatch_hook<F>(mut self, dispatch_hook_fn: F) -> Self
4466    where
4467        F: AsyncFnOnce(&mut Ui, &CommandHelper, BoxedAsyncCliDispatch) -> Result<(), CommandError>
4468            + 'a,
4469    {
4470        self.dispatch_hooks
4471            .push(Box::new(AsyncCliDispatchHookFn(dispatch_hook_fn)));
4472        self
4473    }
4474
4475    /// Registers new subcommands in addition to the default ones.
4476    pub fn add_subcommand<C, F>(mut self, custom_dispatch_fn: F) -> Self
4477    where
4478        C: clap::Subcommand,
4479        F: AsyncFnOnce(&mut Ui, &CommandHelper, C) -> Result<(), CommandError> + 'a,
4480    {
4481        let old_dispatch = self.dispatch;
4482        let new_dispatch_fn =
4483            async move |ui: &mut Ui, command_helper: &CommandHelper| match C::from_arg_matches(
4484                command_helper.matches(),
4485            ) {
4486                Ok(command) => custom_dispatch_fn(ui, command_helper, command).await,
4487                Err(_) => old_dispatch.call(ui, command_helper).await,
4488            };
4489        self.app = C::augment_subcommands(self.app);
4490        self.dispatch = Box::new(AsyncCliDispatchFn(new_dispatch_fn));
4491        self
4492    }
4493
4494    /// Registers new global arguments in addition to the default ones.
4495    pub fn add_global_args<A, F>(mut self, process_before: F) -> Self
4496    where
4497        A: clap::Args,
4498        F: FnOnce(&mut Ui, A) -> Result<(), CommandError> + 'a,
4499    {
4500        let process_global_args_fn = move |ui: &mut Ui, matches: &ArgMatches| {
4501            let custom_args = A::from_arg_matches(matches).unwrap();
4502            process_before(ui, custom_args)
4503        };
4504        self.app = A::augment_args(self.app);
4505        self.process_global_args_fns
4506            .push(Box::new(process_global_args_fn));
4507        self
4508    }
4509
4510    #[instrument(skip_all)]
4511    async fn run_internal(
4512        self,
4513        ui: &mut Ui,
4514        mut raw_config: RawConfig,
4515    ) -> Result<(), CommandError> {
4516        // `cwd` is canonicalized for consistency with `Workspace::workspace_root()` and
4517        // to easily compute relative paths between them.
4518        let cwd = env::current_dir()
4519            .and_then(dunce::canonicalize)
4520            .map_err(|_| {
4521                user_error("Could not determine current directory").hinted(
4522                    "Did you update to a commit where the directory doesn't exist or can't be \
4523                     accessed?",
4524                )
4525            })?;
4526        let mut config_env = ConfigEnv::from_environment();
4527        let mut last_config_migration_descriptions = Vec::new();
4528        let mut migrate_config = |config: &mut StackedConfig| -> Result<(), CommandError> {
4529            last_config_migration_descriptions =
4530                jj_lib::config::migrate(config, &self.config_migrations)?;
4531            Ok(())
4532        };
4533
4534        // Initial load: user, repo, and workspace-level configs for
4535        // alias/default-command resolution
4536        // Use cwd-relative workspace configs to resolve default command and
4537        // aliases. WorkspaceLoader::init() won't do any heavy lifting other
4538        // than the path resolution.
4539        let maybe_cwd_workspace_loader = self
4540            .workspace_loader_factory
4541            .create(find_workspace_dir(&cwd))
4542            .map_err(|err| map_workspace_load_error(err, Some(".")));
4543        config_env.reload_system_config(&mut raw_config)?;
4544        config_env.reload_user_config(&mut raw_config)?;
4545        if let Ok(loader) = &maybe_cwd_workspace_loader {
4546            config_env.reset_repo_path(loader.repo_path());
4547            config_env.reload_repo_config(ui, &mut raw_config)?;
4548            config_env.reset_workspace_path(loader.workspace_root());
4549            config_env.reload_workspace_config(ui, &mut raw_config)?;
4550        }
4551        let mut config = config_env.resolve_config(&raw_config)?;
4552        migrate_config(&mut config)?;
4553        ui.reset(&config)?;
4554
4555        if env::var_os("COMPLETE").is_some_and(|v| !v.is_empty() && v != "0") {
4556            return handle_shell_completion(&Ui::null(), &self.app, &config, &cwd);
4557        }
4558
4559        let string_args = expand_args(ui, &self.app, env::args_os(), &config)?;
4560        let (args, config_layers) = parse_early_args(&self.app, &string_args)?;
4561        if !config_layers.is_empty() {
4562            raw_config.as_mut().extend_layers(config_layers);
4563            config = config_env.resolve_config(&raw_config)?;
4564            migrate_config(&mut config)?;
4565            ui.reset(&config)?;
4566        }
4567
4568        if args.has_config_args() {
4569            warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
4570        }
4571
4572        let (matches, args) = parse_args(&self.app, &string_args)
4573            .map_err(|err| map_clap_cli_error(err, ui, &config))?;
4574        if args.global_args.debug {
4575            // TODO: set up debug logging as early as possible
4576            self.tracing_subscription.enable_debug_logging()?;
4577        }
4578        for process_global_args_fn in self.process_global_args_fns {
4579            process_global_args_fn(ui, &matches)?;
4580        }
4581        config_env.set_command_name(command_name(&matches));
4582
4583        let maybe_workspace_loader = if let Some(path) = &args.global_args.repository {
4584            // TODO: maybe path should be canonicalized by WorkspaceLoader?
4585            let abs_path = cwd.join(path);
4586            let abs_path = dunce::canonicalize(&abs_path).unwrap_or(abs_path);
4587            // Invalid -R path is an error. No need to proceed.
4588            let loader = self
4589                .workspace_loader_factory
4590                .create(&abs_path)
4591                .map_err(|err| map_workspace_load_error(err, Some(path)))?;
4592            config_env.reset_repo_path(loader.repo_path());
4593            config_env.reload_repo_config(ui, &mut raw_config)?;
4594            config_env.reset_workspace_path(loader.workspace_root());
4595            config_env.reload_workspace_config(ui, &mut raw_config)?;
4596            Ok(loader)
4597        } else {
4598            maybe_cwd_workspace_loader
4599        };
4600
4601        // Apply workspace configs, --config arguments, and --when.commands.
4602        config = config_env.resolve_config(&raw_config)?;
4603        migrate_config(&mut config)?;
4604        ui.reset(&config)?;
4605
4606        // Print only the last migration messages to omit duplicates.
4607        for (source, desc) in &last_config_migration_descriptions {
4608            let source_str = match source {
4609                ConfigSource::Default => "default-provided",
4610                ConfigSource::System => "system-level",
4611                ConfigSource::EnvBase | ConfigSource::EnvOverrides => "environment-provided",
4612                ConfigSource::User => "user-level",
4613                ConfigSource::Repo => "repo-level",
4614                ConfigSource::Workspace => "workspace-level",
4615                ConfigSource::CommandArg => "CLI-provided",
4616            };
4617            writeln!(
4618                ui.warning_default(),
4619                "Deprecated {source_str} config: {desc}"
4620            )?;
4621        }
4622
4623        if args.global_args.repository.is_some() {
4624            warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
4625        }
4626
4627        let settings = UserSettings::from_config(config)?;
4628        let command_helper_data = CommandHelperData {
4629            app: self.app,
4630            cwd,
4631            string_args,
4632            matches,
4633            global_args: args.global_args,
4634            config_env,
4635            config_migrations: self.config_migrations,
4636            raw_config,
4637            settings,
4638            revset_extensions: self.revset_extensions.into(),
4639            commit_template_extensions: self.commit_template_extensions,
4640            operation_template_extensions: self.operation_template_extensions,
4641            maybe_workspace_loader,
4642            store_factories: self.store_factories,
4643            working_copy_factories: self.working_copy_factories,
4644            workspace_loader_factory: self.workspace_loader_factory,
4645        };
4646        let command_helper = CommandHelper {
4647            data: Rc::new(command_helper_data),
4648        };
4649        let dispatch =
4650            self.dispatch_hooks
4651                .into_iter()
4652                .fold(self.dispatch, |old_dispatch, dispatch_hook| {
4653                    let f = async move |ui: &mut Ui, command_helper: &CommandHelper| {
4654                        dispatch_hook.call(ui, command_helper, old_dispatch).await
4655                    };
4656                    Box::new(AsyncCliDispatchFn(f))
4657                });
4658        dispatch.call(ui, &command_helper).await
4659    }
4660
4661    #[must_use]
4662    #[instrument(skip(self))]
4663    pub fn run(mut self) -> u8 {
4664        // Tell crossterm to ignore NO_COLOR (we check it ourselves)
4665        crossterm::style::force_color_output(true);
4666        let config = config_from_environment(self.config_layers.drain(..));
4667        // Set up ui assuming the default config has no conditional variables.
4668        // If it had, the configuration will be fixed by the next ui.reset().
4669        let mut ui = Ui::with_config(config.as_ref())
4670            .expect("default config should be valid, env vars are stringly typed");
4671        let result = self.run_internal(&mut ui, config).block_on();
4672        let exit_code = handle_command_result(&mut ui, result);
4673        ui.finalize_pager();
4674        exit_code
4675    }
4676}
4677
4678fn map_clap_cli_error(err: clap::Error, ui: &Ui, config: &StackedConfig) -> CommandError {
4679    if let Some(ContextValue::String(cmd)) = err.get(ContextKind::InvalidSubcommand) {
4680        let remove_useless_error_context = |mut err: clap::Error| {
4681            // Clap suggests unhelpful subcommands, e.g. `config` for `clone`.
4682            // We don't want suggestions when we know this isn't a misspelling.
4683            err.remove(ContextKind::SuggestedSubcommand);
4684            err.remove(ContextKind::Suggested); // Remove an empty line
4685            err.remove(ContextKind::Usage); // Also unhelpful for these errors.
4686            err
4687        };
4688        match cmd.as_str() {
4689            // git commands that a brand-new user might type during their first
4690            // experiments with `jj`
4691            "clone" | "init" => {
4692                let cmd = cmd.clone();
4693                return CommandError::from(remove_useless_error_context(err))
4694                    .hinted(format!(
4695                        "You probably want `jj git {cmd}`. See also `jj help git`."
4696                    ))
4697                    .hinted(format!(
4698                        r#"You can configure `aliases.{cmd} = ["git", "{cmd}"]` if you want `jj {cmd}` to work and always use the Git backend."#
4699                    ));
4700            }
4701            "amend" => {
4702                return CommandError::from(remove_useless_error_context(err))
4703                    .hinted(
4704                        r#"You probably want `jj squash`. You can configure `aliases.amend = ["squash"]` if you want `jj amend` to work."#);
4705            }
4706            _ => {}
4707        }
4708    }
4709    if let (Some(ContextValue::String(arg)), Some(ContextValue::String(value))) = (
4710        err.get(ContextKind::InvalidArg),
4711        err.get(ContextKind::InvalidValue),
4712    ) && arg.as_str() == "--template <TEMPLATE>"
4713        && value.is_empty()
4714    {
4715        // Suppress the error, it's less important than the original error.
4716        if let Ok(template_aliases) = load_template_aliases(ui, config) {
4717            return CommandError::from(err).hinted(format_template_aliases_hint(&template_aliases));
4718        }
4719    }
4720    CommandError::from(err)
4721}
4722
4723fn format_template_aliases_hint(template_aliases: &TemplateAliasesMap) -> String {
4724    let mut hint = String::from("The following template aliases are defined:\n");
4725    hint.push_str(
4726        &template_aliases
4727            .symbol_names()
4728            .sorted_unstable()
4729            .map(|name| format!("- {name}"))
4730            .join("\n"),
4731    );
4732    hint
4733}
4734
4735// If -R or --config* is specified, check if the expanded arguments differ.
4736fn warn_if_args_mismatch(
4737    ui: &Ui,
4738    app: &Command,
4739    config: &StackedConfig,
4740    expected_args: &[String],
4741) -> Result<(), CommandError> {
4742    let new_string_args = expand_args(ui, app, env::args_os(), config).ok();
4743    if new_string_args.as_deref() != Some(expected_args) {
4744        writeln!(
4745            ui.warning_default(),
4746            "Command aliases cannot be loaded from -R/--repository path or --config/--config-file \
4747             arguments."
4748        )?;
4749    }
4750    Ok(())
4751}
4752
4753pub fn shell_quote(s: &str) -> Cow<'_, str> {
4754    // shlex::try_quote fails if `s` has a nul byte, which
4755    // shouldn't usually happen. Fall back to unquoted on error.
4756    shlex::try_quote(s).unwrap_or(s.into())
4757}
4758
4759#[cfg(test)]
4760mod tests {
4761    use clap::CommandFactory as _;
4762
4763    use super::*;
4764
4765    #[derive(clap::Parser, Clone, Debug)]
4766    pub struct TestArgs {
4767        #[arg(long)]
4768        pub foo: Vec<u32>,
4769        #[arg(long)]
4770        pub bar: Vec<u32>,
4771        #[arg(long)]
4772        pub baz: bool,
4773    }
4774
4775    #[test]
4776    fn test_merge_args_with() {
4777        let command = TestArgs::command();
4778        let parse = |args: &[&str]| -> Vec<(&'static str, u32)> {
4779            let matches = command.clone().try_get_matches_from(args).unwrap();
4780            let args = TestArgs::from_arg_matches(&matches).unwrap();
4781            merge_args_with(
4782                &matches,
4783                &[("foo", &args.foo), ("bar", &args.bar)],
4784                |id, value| (id, *value),
4785            )
4786        };
4787
4788        assert_eq!(parse(&["jj"]), vec![]);
4789        assert_eq!(parse(&["jj", "--foo=1"]), vec![("foo", 1)]);
4790        assert_eq!(
4791            parse(&["jj", "--foo=1", "--bar=2"]),
4792            vec![("foo", 1), ("bar", 2)]
4793        );
4794        assert_eq!(
4795            parse(&["jj", "--foo=1", "--baz", "--bar=2", "--foo", "3"]),
4796            vec![("foo", 1), ("bar", 2), ("foo", 3)]
4797        );
4798    }
4799}