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