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