Skip to main content

jj_cli/
cli_util.rs

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