jj_cli/
cli_util.rs

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