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