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