Skip to main content

jj_cli/
cli_util.rs

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