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::FromStr;
32use std::sync::Arc;
33use std::time::SystemTime;
34
35use bstr::ByteVec as _;
36use chrono::TimeZone as _;
37use clap::ArgAction;
38use clap::ArgMatches;
39use clap::Command;
40use clap::FromArgMatches as _;
41use clap::builder::MapValueParser;
42use clap::builder::NonEmptyStringValueParser;
43use clap::builder::TypedValueParser as _;
44use clap::builder::ValueParserFactory;
45use clap::error::ContextKind;
46use clap::error::ContextValue;
47use clap_complete::ArgValueCandidates;
48use clap_complete::ArgValueCompleter;
49use indexmap::IndexMap;
50use indexmap::IndexSet;
51use indoc::indoc;
52use indoc::writedoc;
53use itertools::Itertools as _;
54use jj_lib::backend::BackendResult;
55use jj_lib::backend::ChangeId;
56use jj_lib::backend::CommitId;
57use jj_lib::backend::MergedTreeId;
58use jj_lib::backend::TreeValue;
59use jj_lib::commit::Commit;
60use jj_lib::config::ConfigGetError;
61use jj_lib::config::ConfigGetResultExt as _;
62use jj_lib::config::ConfigLayer;
63use jj_lib::config::ConfigMigrationRule;
64use jj_lib::config::ConfigNamePathBuf;
65use jj_lib::config::ConfigSource;
66use jj_lib::config::StackedConfig;
67use jj_lib::conflicts::ConflictMarkerStyle;
68use jj_lib::fileset;
69use jj_lib::fileset::FilesetDiagnostics;
70use jj_lib::fileset::FilesetExpression;
71use jj_lib::gitignore::GitIgnoreError;
72use jj_lib::gitignore::GitIgnoreFile;
73use jj_lib::id_prefix::IdPrefixContext;
74use jj_lib::matchers::Matcher;
75use jj_lib::merge::MergedTreeValue;
76use jj_lib::merged_tree::MergedTree;
77use jj_lib::object_id::ObjectId as _;
78use jj_lib::op_heads_store;
79use jj_lib::op_store::OpStoreError;
80use jj_lib::op_store::OperationId;
81use jj_lib::op_store::RefTarget;
82use jj_lib::op_walk;
83use jj_lib::op_walk::OpsetEvaluationError;
84use jj_lib::operation::Operation;
85use jj_lib::ref_name::RefName;
86use jj_lib::ref_name::RefNameBuf;
87use jj_lib::ref_name::WorkspaceName;
88use jj_lib::ref_name::WorkspaceNameBuf;
89use jj_lib::repo::CheckOutCommitError;
90use jj_lib::repo::EditCommitError;
91use jj_lib::repo::MutableRepo;
92use jj_lib::repo::ReadonlyRepo;
93use jj_lib::repo::Repo;
94use jj_lib::repo::RepoLoader;
95use jj_lib::repo::StoreFactories;
96use jj_lib::repo::StoreLoadError;
97use jj_lib::repo::merge_factories_map;
98use jj_lib::repo_path::RepoPath;
99use jj_lib::repo_path::RepoPathBuf;
100use jj_lib::repo_path::RepoPathUiConverter;
101use jj_lib::repo_path::UiPathParseError;
102use jj_lib::revset;
103use jj_lib::revset::ResolvedRevsetExpression;
104use jj_lib::revset::RevsetAliasesMap;
105use jj_lib::revset::RevsetDiagnostics;
106use jj_lib::revset::RevsetExpression;
107use jj_lib::revset::RevsetExtensions;
108use jj_lib::revset::RevsetFilterPredicate;
109use jj_lib::revset::RevsetFunction;
110use jj_lib::revset::RevsetIteratorExt as _;
111use jj_lib::revset::RevsetModifier;
112use jj_lib::revset::RevsetParseContext;
113use jj_lib::revset::RevsetWorkspaceContext;
114use jj_lib::revset::SymbolResolverExtension;
115use jj_lib::revset::UserRevsetExpression;
116use jj_lib::rewrite::restore_tree;
117use jj_lib::settings::HumanByteSize;
118use jj_lib::settings::UserSettings;
119use jj_lib::str_util::StringPattern;
120use jj_lib::transaction::Transaction;
121use jj_lib::view::View;
122use jj_lib::working_copy;
123use jj_lib::working_copy::CheckoutStats;
124use jj_lib::working_copy::SnapshotOptions;
125use jj_lib::working_copy::SnapshotStats;
126use jj_lib::working_copy::UntrackedReason;
127use jj_lib::working_copy::WorkingCopy;
128use jj_lib::working_copy::WorkingCopyFactory;
129use jj_lib::working_copy::WorkingCopyFreshness;
130use jj_lib::workspace::DefaultWorkspaceLoaderFactory;
131use jj_lib::workspace::LockedWorkspace;
132use jj_lib::workspace::WorkingCopyFactories;
133use jj_lib::workspace::Workspace;
134use jj_lib::workspace::WorkspaceLoadError;
135use jj_lib::workspace::WorkspaceLoader;
136use jj_lib::workspace::WorkspaceLoaderFactory;
137use jj_lib::workspace::default_working_copy_factories;
138use jj_lib::workspace::get_working_copy_factory;
139use pollster::FutureExt as _;
140use tracing::instrument;
141use tracing_chrome::ChromeLayerBuilder;
142use tracing_subscriber::prelude::*;
143
144use crate::command_error::CommandError;
145use crate::command_error::cli_error;
146use crate::command_error::config_error_with_message;
147use crate::command_error::handle_command_result;
148use crate::command_error::internal_error;
149use crate::command_error::internal_error_with_message;
150use crate::command_error::print_parse_diagnostics;
151use crate::command_error::user_error;
152use crate::command_error::user_error_with_hint;
153use crate::commit_templater::CommitTemplateLanguage;
154use crate::commit_templater::CommitTemplateLanguageExtension;
155use crate::complete;
156use crate::config::ConfigArgKind;
157use crate::config::ConfigEnv;
158use crate::config::RawConfig;
159use crate::config::config_from_environment;
160use crate::config::parse_config_args;
161use crate::description_util::TextEditor;
162use crate::diff_util;
163use crate::diff_util::DiffFormat;
164use crate::diff_util::DiffFormatArgs;
165use crate::diff_util::DiffRenderer;
166use crate::formatter::FormatRecorder;
167use crate::formatter::Formatter;
168use crate::formatter::FormatterExt as _;
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: Arc<UserRevsetExpression>,
786    short_prefixes_expression: Option<Arc<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) -> Arc<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) -> &Arc<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<Arc<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<Arc<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 HumanByteSize(mut max_new_file_size) = self
1371            .settings()
1372            .get_value_with("snapshot.max-new-file-size", TryInto::try_into)?;
1373        if max_new_file_size == 0 {
1374            max_new_file_size = u64::MAX;
1375        }
1376        Ok(SnapshotOptions {
1377            base_ignores,
1378            progress: None,
1379            start_tracking_matcher,
1380            max_new_file_size,
1381        })
1382    }
1383
1384    pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
1385        self.env.path_converter()
1386    }
1387
1388    #[cfg(not(feature = "git"))]
1389    pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1390        Ok(GitIgnoreFile::empty())
1391    }
1392
1393    #[cfg(feature = "git")]
1394    #[instrument(skip_all)]
1395    pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1396        let get_excludes_file_path = |config: &gix::config::File| -> Option<PathBuf> {
1397            // TODO: maybe use path() and interpolate(), which can process non-utf-8
1398            // path on Unix.
1399            if let Some(value) = config.string("core.excludesFile") {
1400                let path = str::from_utf8(&value)
1401                    .ok()
1402                    .map(jj_lib::file_util::expand_home_path)?;
1403                // The configured path is usually absolute, but if it's relative,
1404                // the "git" command would read the file at the work-tree directory.
1405                Some(self.workspace_root().join(path))
1406            } else {
1407                xdg_config_home().ok().map(|x| x.join("git").join("ignore"))
1408            }
1409        };
1410
1411        fn xdg_config_home() -> Result<PathBuf, std::env::VarError> {
1412            if let Ok(x) = std::env::var("XDG_CONFIG_HOME")
1413                && !x.is_empty()
1414            {
1415                return Ok(PathBuf::from(x));
1416            }
1417            std::env::var("HOME").map(|x| Path::new(&x).join(".config"))
1418        }
1419
1420        let mut git_ignores = GitIgnoreFile::empty();
1421        if let Ok(git_backend) = jj_lib::git::get_git_backend(self.repo().store()) {
1422            let git_repo = git_backend.git_repo();
1423            if let Some(excludes_file_path) = get_excludes_file_path(&git_repo.config_snapshot()) {
1424                git_ignores = git_ignores.chain_with_file("", excludes_file_path)?;
1425            }
1426            git_ignores = git_ignores
1427                .chain_with_file("", git_backend.git_repo_path().join("info").join("exclude"))?;
1428        } else if let Ok(git_config) = gix::config::File::from_globals()
1429            && let Some(excludes_file_path) = get_excludes_file_path(&git_config)
1430        {
1431            git_ignores = git_ignores.chain_with_file("", excludes_file_path)?;
1432        }
1433        Ok(git_ignores)
1434    }
1435
1436    /// Creates textual diff renderer of the specified `formats`.
1437    pub fn diff_renderer(&self, formats: Vec<DiffFormat>) -> DiffRenderer<'_> {
1438        DiffRenderer::new(
1439            self.repo().as_ref(),
1440            self.path_converter(),
1441            self.env.conflict_marker_style(),
1442            formats,
1443        )
1444    }
1445
1446    /// Loads textual diff renderer from the settings and command arguments.
1447    pub fn diff_renderer_for(
1448        &self,
1449        args: &DiffFormatArgs,
1450    ) -> Result<DiffRenderer<'_>, CommandError> {
1451        let formats = diff_util::diff_formats_for(self.settings(), args)?;
1452        Ok(self.diff_renderer(formats))
1453    }
1454
1455    /// Loads textual diff renderer from the settings and log-like command
1456    /// arguments. Returns `Ok(None)` if there are no command arguments that
1457    /// enable patch output.
1458    pub fn diff_renderer_for_log(
1459        &self,
1460        args: &DiffFormatArgs,
1461        patch: bool,
1462    ) -> Result<Option<DiffRenderer<'_>>, CommandError> {
1463        let formats = diff_util::diff_formats_for_log(self.settings(), args, patch)?;
1464        Ok((!formats.is_empty()).then(|| self.diff_renderer(formats)))
1465    }
1466
1467    /// Loads diff editor from the settings.
1468    ///
1469    /// If the `tool_name` isn't specified, the default editor will be returned.
1470    pub fn diff_editor(
1471        &self,
1472        ui: &Ui,
1473        tool_name: Option<&str>,
1474    ) -> Result<DiffEditor, CommandError> {
1475        let base_ignores = self.base_ignores()?;
1476        let conflict_marker_style = self.env.conflict_marker_style();
1477        if let Some(name) = tool_name {
1478            Ok(DiffEditor::with_name(
1479                name,
1480                self.settings(),
1481                base_ignores,
1482                conflict_marker_style,
1483            )?)
1484        } else {
1485            Ok(DiffEditor::from_settings(
1486                ui,
1487                self.settings(),
1488                base_ignores,
1489                conflict_marker_style,
1490            )?)
1491        }
1492    }
1493
1494    /// Conditionally loads diff editor from the settings.
1495    ///
1496    /// If the `tool_name` is specified, interactive session is implied.
1497    pub fn diff_selector(
1498        &self,
1499        ui: &Ui,
1500        tool_name: Option<&str>,
1501        force_interactive: bool,
1502    ) -> Result<DiffSelector, CommandError> {
1503        if tool_name.is_some() || force_interactive {
1504            Ok(DiffSelector::Interactive(self.diff_editor(ui, tool_name)?))
1505        } else {
1506            Ok(DiffSelector::NonInteractive)
1507        }
1508    }
1509
1510    /// Loads 3-way merge editor from the settings.
1511    ///
1512    /// If the `tool_name` isn't specified, the default editor will be returned.
1513    pub fn merge_editor(
1514        &self,
1515        ui: &Ui,
1516        tool_name: Option<&str>,
1517    ) -> Result<MergeEditor, MergeToolConfigError> {
1518        let conflict_marker_style = self.env.conflict_marker_style();
1519        if let Some(name) = tool_name {
1520            MergeEditor::with_name(
1521                name,
1522                self.settings(),
1523                self.path_converter().clone(),
1524                conflict_marker_style,
1525            )
1526        } else {
1527            MergeEditor::from_settings(
1528                ui,
1529                self.settings(),
1530                self.path_converter().clone(),
1531                conflict_marker_style,
1532            )
1533        }
1534    }
1535
1536    /// Loads text editor from the settings.
1537    pub fn text_editor(&self) -> Result<TextEditor, ConfigGetError> {
1538        TextEditor::from_settings(self.settings())
1539    }
1540
1541    pub fn resolve_single_op(&self, op_str: &str) -> Result<Operation, OpsetEvaluationError> {
1542        op_walk::resolve_op_with_repo(self.repo(), op_str)
1543    }
1544
1545    /// Resolve a revset to a single revision. Return an error if the revset is
1546    /// empty or has multiple revisions.
1547    pub fn resolve_single_rev(
1548        &self,
1549        ui: &Ui,
1550        revision_arg: &RevisionArg,
1551    ) -> Result<Commit, CommandError> {
1552        let expression = self.parse_revset(ui, revision_arg)?;
1553        revset_util::evaluate_revset_to_single_commit(revision_arg.as_ref(), &expression, || {
1554            self.commit_summary_template()
1555        })
1556    }
1557
1558    /// Evaluates revset expressions to non-empty set of commit IDs. The
1559    /// returned set preserves the order of the input expressions.
1560    pub fn resolve_some_revsets_default_single(
1561        &self,
1562        ui: &Ui,
1563        revision_args: &[RevisionArg],
1564    ) -> Result<IndexSet<CommitId>, CommandError> {
1565        let mut all_commits = IndexSet::new();
1566        for revision_arg in revision_args {
1567            let (expression, modifier) = self.parse_revset_with_modifier(ui, revision_arg)?;
1568            let all = match modifier {
1569                Some(RevsetModifier::All) => true,
1570                None => self.settings().get_bool("ui.always-allow-large-revsets")?,
1571            };
1572            if all {
1573                for commit_id in expression.evaluate_to_commit_ids()? {
1574                    all_commits.insert(commit_id?);
1575                }
1576            } else {
1577                let commit = revset_util::evaluate_revset_to_single_commit(
1578                    revision_arg.as_ref(),
1579                    &expression,
1580                    || self.commit_summary_template(),
1581                )?;
1582                if !all_commits.insert(commit.id().clone()) {
1583                    let commit_hash = short_commit_hash(commit.id());
1584                    return Err(user_error(format!(
1585                        r#"More than one revset resolved to revision {commit_hash}"#,
1586                    )));
1587                }
1588            }
1589        }
1590        if all_commits.is_empty() {
1591            Err(user_error("Empty revision set"))
1592        } else {
1593            Ok(all_commits)
1594        }
1595    }
1596
1597    pub fn parse_revset(
1598        &self,
1599        ui: &Ui,
1600        revision_arg: &RevisionArg,
1601    ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1602        let (expression, modifier) = self.parse_revset_with_modifier(ui, revision_arg)?;
1603        // Whether the caller accepts multiple revisions or not, "all:" should
1604        // be valid. For example, "all:@" is a valid single-rev expression.
1605        let (None | Some(RevsetModifier::All)) = modifier;
1606        Ok(expression)
1607    }
1608
1609    fn parse_revset_with_modifier(
1610        &self,
1611        ui: &Ui,
1612        revision_arg: &RevisionArg,
1613    ) -> Result<(RevsetExpressionEvaluator<'_>, Option<RevsetModifier>), CommandError> {
1614        let mut diagnostics = RevsetDiagnostics::new();
1615        let context = self.env.revset_parse_context();
1616        let (expression, modifier) =
1617            revset::parse_with_modifier(&mut diagnostics, revision_arg.as_ref(), &context)?;
1618        print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1619        Ok((self.attach_revset_evaluator(expression), modifier))
1620    }
1621
1622    /// Parses the given revset expressions and concatenates them all.
1623    pub fn parse_union_revsets(
1624        &self,
1625        ui: &Ui,
1626        revision_args: &[RevisionArg],
1627    ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1628        let mut diagnostics = RevsetDiagnostics::new();
1629        let context = self.env.revset_parse_context();
1630        let expressions: Vec<_> = revision_args
1631            .iter()
1632            .map(|arg| revset::parse_with_modifier(&mut diagnostics, arg.as_ref(), &context))
1633            .map_ok(|(expression, None | Some(RevsetModifier::All))| expression)
1634            .try_collect()?;
1635        print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1636        let expression = RevsetExpression::union_all(&expressions);
1637        Ok(self.attach_revset_evaluator(expression))
1638    }
1639
1640    pub fn attach_revset_evaluator(
1641        &self,
1642        expression: Arc<UserRevsetExpression>,
1643    ) -> RevsetExpressionEvaluator<'_> {
1644        RevsetExpressionEvaluator::new(
1645            self.repo().as_ref(),
1646            self.env.command.revset_extensions().clone(),
1647            self.id_prefix_context(),
1648            expression,
1649        )
1650    }
1651
1652    pub fn id_prefix_context(&self) -> &IdPrefixContext {
1653        self.user_repo
1654            .id_prefix_context
1655            .get_or_init(|| self.env.new_id_prefix_context())
1656    }
1657
1658    /// Parses template of the given language into evaluation tree.
1659    pub fn parse_template<'a, C, L>(
1660        &self,
1661        ui: &Ui,
1662        language: &L,
1663        template_text: &str,
1664    ) -> Result<TemplateRenderer<'a, C>, CommandError>
1665    where
1666        C: Clone + 'a,
1667        L: TemplateLanguage<'a> + ?Sized,
1668        L::Property: WrapTemplateProperty<'a, C>,
1669    {
1670        self.env.parse_template(ui, language, template_text)
1671    }
1672
1673    /// Parses template that is validated by `Self::new()`.
1674    fn reparse_valid_template<'a, C, L>(
1675        &self,
1676        language: &L,
1677        template_text: &str,
1678    ) -> TemplateRenderer<'a, C>
1679    where
1680        C: Clone + 'a,
1681        L: TemplateLanguage<'a> + ?Sized,
1682        L::Property: WrapTemplateProperty<'a, C>,
1683    {
1684        template_builder::parse(
1685            language,
1686            &mut TemplateDiagnostics::new(),
1687            template_text,
1688            &self.env.template_aliases_map,
1689        )
1690        .expect("parse error should be confined by WorkspaceCommandHelper::new()")
1691    }
1692
1693    /// Parses commit template into evaluation tree.
1694    pub fn parse_commit_template(
1695        &self,
1696        ui: &Ui,
1697        template_text: &str,
1698    ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
1699        let language = self.commit_template_language();
1700        self.parse_template(ui, &language, template_text)
1701    }
1702
1703    /// Parses commit template into evaluation tree.
1704    pub fn parse_operation_template(
1705        &self,
1706        ui: &Ui,
1707        template_text: &str,
1708    ) -> Result<TemplateRenderer<'_, Operation>, CommandError> {
1709        let language = self.operation_template_language();
1710        self.parse_template(ui, &language, template_text)
1711    }
1712
1713    /// Creates commit template language environment for this workspace.
1714    pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
1715        self.env
1716            .commit_template_language(self.repo().as_ref(), self.id_prefix_context())
1717    }
1718
1719    /// Creates operation template language environment for this workspace.
1720    pub fn operation_template_language(&self) -> OperationTemplateLanguage {
1721        OperationTemplateLanguage::new(
1722            self.workspace.repo_loader(),
1723            Some(self.repo().op_id()),
1724            self.env.operation_template_extensions(),
1725        )
1726    }
1727
1728    /// Template for one-line summary of a commit.
1729    pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
1730        let language = self.commit_template_language();
1731        self.reparse_valid_template(&language, &self.commit_summary_template_text)
1732            .labeled(["commit"])
1733    }
1734
1735    /// Template for one-line summary of an operation.
1736    pub fn operation_summary_template(&self) -> TemplateRenderer<'_, Operation> {
1737        let language = self.operation_template_language();
1738        self.reparse_valid_template(&language, &self.op_summary_template_text)
1739            .labeled(["operation"])
1740    }
1741
1742    pub fn short_change_id_template(&self) -> TemplateRenderer<'_, Commit> {
1743        let language = self.commit_template_language();
1744        self.reparse_valid_template(&language, SHORT_CHANGE_ID_TEMPLATE_TEXT)
1745            .labeled(["commit"])
1746    }
1747
1748    /// Returns one-line summary of the given `commit`.
1749    ///
1750    /// Use `write_commit_summary()` to get colorized output. Use
1751    /// `commit_summary_template()` if you have many commits to process.
1752    pub fn format_commit_summary(&self, commit: &Commit) -> String {
1753        let output = self.commit_summary_template().format_plain_text(commit);
1754        output.into_string_lossy()
1755    }
1756
1757    /// Writes one-line summary of the given `commit`.
1758    ///
1759    /// Use `commit_summary_template()` if you have many commits to process.
1760    #[instrument(skip_all)]
1761    pub fn write_commit_summary(
1762        &self,
1763        formatter: &mut dyn Formatter,
1764        commit: &Commit,
1765    ) -> std::io::Result<()> {
1766        self.commit_summary_template().format(commit, formatter)
1767    }
1768
1769    pub fn check_rewritable<'a>(
1770        &self,
1771        commits: impl IntoIterator<Item = &'a CommitId>,
1772    ) -> Result<(), CommandError> {
1773        let repo = self.repo().as_ref();
1774        let commit_ids = commits.into_iter().cloned().collect_vec();
1775        let Some(commit_id) = self.env.find_immutable_commit(repo, &commit_ids)? else {
1776            return Ok(());
1777        };
1778        let error = if &commit_id == repo.store().root_commit_id() {
1779            user_error(format!("The root commit {commit_id:.12} is immutable"))
1780        } else {
1781            let mut error = user_error(format!("Commit {commit_id:.12} is immutable"));
1782            let commit = repo.store().get_commit(&commit_id)?;
1783            error.add_formatted_hint_with(|formatter| {
1784                write!(formatter, "Could not modify commit: ")?;
1785                self.write_commit_summary(formatter, &commit)?;
1786                Ok(())
1787            });
1788            error.add_hint("Immutable commits are used to protect shared history.");
1789            error.add_hint(indoc::indoc! {"
1790                For more information, see:
1791                      - https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
1792                      - `jj help -k config`, \"Set of immutable commits\""});
1793
1794            // Not using self.id_prefix_context() for consistency with
1795            // find_immutable_commit().
1796            let id_prefix_context =
1797                IdPrefixContext::new(self.env.command.revset_extensions().clone());
1798            let to_rewrite_expr = RevsetExpression::commits(commit_ids);
1799            let (lower_bound, upper_bound) = RevsetExpressionEvaluator::new(
1800                repo,
1801                self.env.command.revset_extensions().clone(),
1802                &id_prefix_context,
1803                self.env
1804                    .immutable_expression()
1805                    .intersection(&to_rewrite_expr.descendants()),
1806            )
1807            .evaluate()?
1808            .count_estimate()?;
1809            let exact = upper_bound == Some(lower_bound);
1810            let or_more = if exact { "" } else { " or more" };
1811            error.add_hint(format!(
1812                "This operation would rewrite {lower_bound}{or_more} immutable commits."
1813            ));
1814
1815            error
1816        };
1817        Err(error)
1818    }
1819
1820    #[instrument(skip_all)]
1821    fn snapshot_working_copy(
1822        &mut self,
1823        ui: &Ui,
1824    ) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
1825        let workspace_name = self.workspace_name().to_owned();
1826        let get_wc_commit = |repo: &ReadonlyRepo| -> Result<Option<_>, _> {
1827            repo.view()
1828                .get_wc_commit_id(&workspace_name)
1829                .map(|id| repo.store().get_commit(id))
1830                .transpose()
1831                .map_err(snapshot_command_error)
1832        };
1833        let repo = self.repo().clone();
1834        let Some(wc_commit) = get_wc_commit(&repo)? else {
1835            // If the workspace has been deleted, it's unclear what to do, so we just skip
1836            // committing the working copy.
1837            return Ok(SnapshotStats::default());
1838        };
1839        let auto_tracking_matcher = self
1840            .auto_tracking_matcher(ui)
1841            .map_err(snapshot_command_error)?;
1842        let options = self
1843            .snapshot_options_with_start_tracking_matcher(&auto_tracking_matcher)
1844            .map_err(snapshot_command_error)?;
1845
1846        // Compare working-copy tree and operation with repo's, and reload as needed.
1847        let mut locked_ws = self
1848            .workspace
1849            .start_working_copy_mutation()
1850            .map_err(snapshot_command_error)?;
1851        let old_op_id = locked_ws.locked_wc().old_operation_id().clone();
1852
1853        let (repo, wc_commit) =
1854            match WorkingCopyFreshness::check_stale(locked_ws.locked_wc(), &wc_commit, &repo) {
1855                Ok(WorkingCopyFreshness::Fresh) => (repo, wc_commit),
1856                Ok(WorkingCopyFreshness::Updated(wc_operation)) => {
1857                    let repo = repo
1858                        .reload_at(&wc_operation)
1859                        .map_err(snapshot_command_error)?;
1860                    let wc_commit = if let Some(wc_commit) = get_wc_commit(&repo)? {
1861                        wc_commit
1862                    } else {
1863                        // The workspace has been deleted (see above)
1864                        return Ok(SnapshotStats::default());
1865                    };
1866                    (repo, wc_commit)
1867                }
1868                Ok(WorkingCopyFreshness::WorkingCopyStale) => {
1869                    return Err(SnapshotWorkingCopyError::StaleWorkingCopy(
1870                        user_error_with_hint(
1871                            format!(
1872                                "The working copy is stale (not updated since operation {}).",
1873                                short_operation_hash(&old_op_id)
1874                            ),
1875                            "Run `jj workspace update-stale` to update it.
1876See https://jj-vcs.github.io/jj/latest/working-copy/#stale-working-copy \
1877                             for more information.",
1878                        ),
1879                    ));
1880                }
1881                Ok(WorkingCopyFreshness::SiblingOperation) => {
1882                    return Err(SnapshotWorkingCopyError::StaleWorkingCopy(internal_error(
1883                        format!(
1884                            "The repo was loaded at operation {}, which seems to be a sibling of \
1885                             the working copy's operation {}",
1886                            short_operation_hash(repo.op_id()),
1887                            short_operation_hash(&old_op_id)
1888                        ),
1889                    )));
1890                }
1891                Err(OpStoreError::ObjectNotFound { .. }) => {
1892                    return Err(SnapshotWorkingCopyError::StaleWorkingCopy(
1893                        user_error_with_hint(
1894                            "Could not read working copy's operation.",
1895                            "Run `jj workspace update-stale` to recover.
1896See https://jj-vcs.github.io/jj/latest/working-copy/#stale-working-copy \
1897                             for more information.",
1898                        ),
1899                    ));
1900                }
1901                Err(e) => return Err(snapshot_command_error(e)),
1902            };
1903        self.user_repo = ReadonlyUserRepo::new(repo);
1904        let (new_tree_id, stats) = {
1905            let mut options = options;
1906            let progress = crate::progress::snapshot_progress(ui);
1907            options.progress = progress.as_ref().map(|x| x as _);
1908            locked_ws
1909                .locked_wc()
1910                .snapshot(&options)
1911                .map_err(snapshot_command_error)?
1912        };
1913        if new_tree_id != *wc_commit.tree_id() {
1914            let mut tx =
1915                start_repo_transaction(&self.user_repo.repo, self.env.command.string_args());
1916            tx.set_is_snapshot(true);
1917            let mut_repo = tx.repo_mut();
1918            let commit = mut_repo
1919                .rewrite_commit(&wc_commit)
1920                .set_tree_id(new_tree_id)
1921                .write()
1922                .map_err(snapshot_command_error)?;
1923            mut_repo
1924                .set_wc_commit(workspace_name, commit.id().clone())
1925                .map_err(snapshot_command_error)?;
1926
1927            // Rebase descendants
1928            let num_rebased = mut_repo
1929                .rebase_descendants()
1930                .map_err(snapshot_command_error)?;
1931            if num_rebased > 0 {
1932                writeln!(
1933                    ui.status(),
1934                    "Rebased {num_rebased} descendant commits onto updated working copy"
1935                )
1936                .map_err(snapshot_command_error)?;
1937            }
1938
1939            #[cfg(feature = "git")]
1940            if self.working_copy_shared_with_git {
1941                let old_tree = wc_commit.tree().map_err(snapshot_command_error)?;
1942                let new_tree = commit.tree().map_err(snapshot_command_error)?;
1943                jj_lib::git::update_intent_to_add(
1944                    self.user_repo.repo.as_ref(),
1945                    &old_tree,
1946                    &new_tree,
1947                )
1948                .map_err(snapshot_command_error)?;
1949
1950                let stats = jj_lib::git::export_refs(mut_repo).map_err(snapshot_command_error)?;
1951                crate::git_util::print_git_export_stats(ui, &stats)
1952                    .map_err(snapshot_command_error)?;
1953            }
1954
1955            let repo = tx
1956                .commit("snapshot working copy")
1957                .map_err(snapshot_command_error)?;
1958            self.user_repo = ReadonlyUserRepo::new(repo);
1959        }
1960        locked_ws
1961            .finish(self.user_repo.repo.op_id().clone())
1962            .map_err(snapshot_command_error)?;
1963        Ok(stats)
1964    }
1965
1966    fn update_working_copy(
1967        &mut self,
1968        ui: &Ui,
1969        maybe_old_commit: Option<&Commit>,
1970        new_commit: &Commit,
1971    ) -> Result<(), CommandError> {
1972        assert!(self.may_update_working_copy);
1973        let stats = update_working_copy(
1974            &self.user_repo.repo,
1975            &mut self.workspace,
1976            maybe_old_commit,
1977            new_commit,
1978        )?;
1979        self.print_updated_working_copy_stats(ui, maybe_old_commit, new_commit, &stats)
1980    }
1981
1982    fn print_updated_working_copy_stats(
1983        &self,
1984        ui: &Ui,
1985        maybe_old_commit: Option<&Commit>,
1986        new_commit: &Commit,
1987        stats: &CheckoutStats,
1988    ) -> Result<(), CommandError> {
1989        if Some(new_commit) != maybe_old_commit
1990            && let Some(mut formatter) = ui.status_formatter()
1991        {
1992            let template = self.commit_summary_template();
1993            write!(formatter, "Working copy  (@) now at: ")?;
1994            template.format(new_commit, formatter.as_mut())?;
1995            writeln!(formatter)?;
1996            for parent in new_commit.parents() {
1997                let parent = parent?;
1998                //                "Working copy  (@) now at: "
1999                write!(formatter, "Parent commit (@-)      : ")?;
2000                template.format(&parent, formatter.as_mut())?;
2001                writeln!(formatter)?;
2002            }
2003        }
2004        print_checkout_stats(ui, stats, new_commit)?;
2005        if Some(new_commit) != maybe_old_commit
2006            && let Some(mut formatter) = ui.status_formatter()
2007            && new_commit.has_conflict()?
2008        {
2009            let conflicts = new_commit.tree()?.conflicts().collect_vec();
2010            writeln!(
2011                formatter.labeled("warning").with_heading("Warning: "),
2012                "There are unresolved conflicts at these paths:"
2013            )?;
2014            print_conflicted_paths(conflicts, formatter.as_mut(), self)?;
2015        }
2016        Ok(())
2017    }
2018
2019    pub fn start_transaction(&mut self) -> WorkspaceCommandTransaction<'_> {
2020        let tx = start_repo_transaction(self.repo(), self.env.command.string_args());
2021        let id_prefix_context = mem::take(&mut self.user_repo.id_prefix_context);
2022        WorkspaceCommandTransaction {
2023            helper: self,
2024            tx,
2025            id_prefix_context,
2026        }
2027    }
2028
2029    fn finish_transaction(
2030        &mut self,
2031        ui: &Ui,
2032        mut tx: Transaction,
2033        description: impl Into<String>,
2034    ) -> Result<(), CommandError> {
2035        if !tx.repo().has_changes() {
2036            writeln!(ui.status(), "Nothing changed.")?;
2037            return Ok(());
2038        }
2039        let num_rebased = tx.repo_mut().rebase_descendants()?;
2040        if num_rebased > 0 {
2041            writeln!(ui.status(), "Rebased {num_rebased} descendant commits")?;
2042        }
2043
2044        for (name, wc_commit_id) in &tx.repo().view().wc_commit_ids().clone() {
2045            if self
2046                .env
2047                .find_immutable_commit(tx.repo(), slice::from_ref(wc_commit_id))?
2048                .is_some()
2049            {
2050                let wc_commit = tx.repo().store().get_commit(wc_commit_id)?;
2051                tx.repo_mut().check_out(name.clone(), &wc_commit)?;
2052                writeln!(
2053                    ui.warning_default(),
2054                    "The working-copy commit in workspace '{name}' became immutable, so a new \
2055                     commit has been created on top of it.",
2056                    name = name.as_symbol()
2057                )?;
2058            }
2059        }
2060
2061        let old_repo = tx.base_repo().clone();
2062
2063        let maybe_old_wc_commit = old_repo
2064            .view()
2065            .get_wc_commit_id(self.workspace_name())
2066            .map(|commit_id| tx.base_repo().store().get_commit(commit_id))
2067            .transpose()?;
2068        let maybe_new_wc_commit = tx
2069            .repo()
2070            .view()
2071            .get_wc_commit_id(self.workspace_name())
2072            .map(|commit_id| tx.repo().store().get_commit(commit_id))
2073            .transpose()?;
2074
2075        #[cfg(feature = "git")]
2076        if self.working_copy_shared_with_git {
2077            use std::error::Error as _;
2078            if let Some(wc_commit) = &maybe_new_wc_commit {
2079                // This can fail if HEAD was updated concurrently. In that case,
2080                // the actual state will be imported on the next snapshot.
2081                match jj_lib::git::reset_head(tx.repo_mut(), wc_commit) {
2082                    Ok(()) => {}
2083                    Err(err @ jj_lib::git::GitResetHeadError::UpdateHeadRef(_)) => {
2084                        writeln!(ui.warning_default(), "{err}")?;
2085                        crate::command_error::print_error_sources(ui, err.source())?;
2086                    }
2087                    Err(err) => return Err(err.into()),
2088                }
2089            }
2090            let stats = jj_lib::git::export_refs(tx.repo_mut())?;
2091            crate::git_util::print_git_export_stats(ui, &stats)?;
2092        }
2093
2094        self.user_repo = ReadonlyUserRepo::new(tx.commit(description)?);
2095
2096        // Update working copy before reporting repo changes, so that
2097        // potential errors while reporting changes (broken pipe, etc)
2098        // don't leave the working copy in a stale state.
2099        if self.may_update_working_copy {
2100            if let Some(new_commit) = &maybe_new_wc_commit {
2101                self.update_working_copy(ui, maybe_old_wc_commit.as_ref(), new_commit)?;
2102            } else {
2103                // It seems the workspace was deleted, so we shouldn't try to
2104                // update it.
2105            }
2106        }
2107
2108        self.report_repo_changes(ui, &old_repo)?;
2109
2110        let settings = self.settings();
2111        let missing_user_name = settings.user_name().is_empty();
2112        let missing_user_mail = settings.user_email().is_empty();
2113        if missing_user_name || missing_user_mail {
2114            let not_configured_msg = match (missing_user_name, missing_user_mail) {
2115                (true, true) => "Name and email not configured.",
2116                (true, false) => "Name not configured.",
2117                (false, true) => "Email not configured.",
2118                _ => unreachable!(),
2119            };
2120            writeln!(
2121                ui.warning_default(),
2122                "{not_configured_msg} Until configured, your commits will be created with the \
2123                 empty identity, and can't be pushed to remotes."
2124            )?;
2125            writeln!(ui.hint_default(), "To configure, run:")?;
2126            if missing_user_name {
2127                writeln!(
2128                    ui.hint_no_heading(),
2129                    r#"  jj config set --user user.name "Some One""#
2130                )?;
2131            }
2132            if missing_user_mail {
2133                writeln!(
2134                    ui.hint_no_heading(),
2135                    r#"  jj config set --user user.email "someone@example.com""#
2136                )?;
2137            }
2138        }
2139        Ok(())
2140    }
2141
2142    /// Inform the user about important changes to the repo since the previous
2143    /// operation (when `old_repo` was loaded).
2144    fn report_repo_changes(
2145        &self,
2146        ui: &Ui,
2147        old_repo: &Arc<ReadonlyRepo>,
2148    ) -> Result<(), CommandError> {
2149        let Some(mut fmt) = ui.status_formatter() else {
2150            return Ok(());
2151        };
2152        let old_view = old_repo.view();
2153        let new_repo = self.repo().as_ref();
2154        let new_view = new_repo.view();
2155        let old_heads = RevsetExpression::commits(old_view.heads().iter().cloned().collect());
2156        let new_heads = RevsetExpression::commits(new_view.heads().iter().cloned().collect());
2157        // Filter the revsets by conflicts instead of reading all commits and doing the
2158        // filtering here. That way, we can afford to evaluate the revset even if there
2159        // are millions of commits added to the repo, assuming the revset engine can
2160        // efficiently skip non-conflicting commits. Filter out empty commits mostly so
2161        // `jj new <conflicted commit>` doesn't result in a message about new conflicts.
2162        let conflicts = RevsetExpression::filter(RevsetFilterPredicate::HasConflict)
2163            .filtered(RevsetFilterPredicate::File(FilesetExpression::all()));
2164        let removed_conflicts_expr = new_heads.range(&old_heads).intersection(&conflicts);
2165        let added_conflicts_expr = old_heads.range(&new_heads).intersection(&conflicts);
2166
2167        let get_commits =
2168            |expr: Arc<ResolvedRevsetExpression>| -> Result<Vec<Commit>, CommandError> {
2169                let commits = expr
2170                    .evaluate(new_repo)?
2171                    .iter()
2172                    .commits(new_repo.store())
2173                    .try_collect()?;
2174                Ok(commits)
2175            };
2176        let removed_conflict_commits = get_commits(removed_conflicts_expr)?;
2177        let added_conflict_commits = get_commits(added_conflicts_expr)?;
2178
2179        fn commits_by_change_id(commits: &[Commit]) -> IndexMap<&ChangeId, Vec<&Commit>> {
2180            let mut result: IndexMap<&ChangeId, Vec<&Commit>> = IndexMap::new();
2181            for commit in commits {
2182                result.entry(commit.change_id()).or_default().push(commit);
2183            }
2184            result
2185        }
2186        let removed_conflicts_by_change_id = commits_by_change_id(&removed_conflict_commits);
2187        let added_conflicts_by_change_id = commits_by_change_id(&added_conflict_commits);
2188        let mut resolved_conflicts_by_change_id = removed_conflicts_by_change_id.clone();
2189        resolved_conflicts_by_change_id
2190            .retain(|change_id, _commits| !added_conflicts_by_change_id.contains_key(change_id));
2191        let mut new_conflicts_by_change_id = added_conflicts_by_change_id.clone();
2192        new_conflicts_by_change_id
2193            .retain(|change_id, _commits| !removed_conflicts_by_change_id.contains_key(change_id));
2194
2195        // TODO: Also report new divergence and maybe resolved divergence
2196        if !resolved_conflicts_by_change_id.is_empty() {
2197            // TODO: Report resolved and abandoned numbers separately. However,
2198            // that involves resolving the change_id among the visible commits in the new
2199            // repo, which isn't currently supported by Google's revset engine.
2200            let num_resolved: usize = resolved_conflicts_by_change_id
2201                .values()
2202                .map(|commits| commits.len())
2203                .sum();
2204            writeln!(
2205                fmt,
2206                "Existing conflicts were resolved or abandoned from {num_resolved} commits."
2207            )?;
2208        }
2209        if !new_conflicts_by_change_id.is_empty() {
2210            let num_conflicted: usize = new_conflicts_by_change_id
2211                .values()
2212                .map(|commits| commits.len())
2213                .sum();
2214            writeln!(fmt, "New conflicts appeared in {num_conflicted} commits:")?;
2215            print_updated_commits(
2216                fmt.as_mut(),
2217                &self.commit_summary_template(),
2218                new_conflicts_by_change_id.values().flatten().copied(),
2219            )?;
2220        }
2221
2222        // Hint that the user might want to `jj new` to the first conflict commit to
2223        // resolve conflicts. Only show the hints if there were any new or resolved
2224        // conflicts, and only if there are still some conflicts.
2225        if !(added_conflict_commits.is_empty()
2226            || resolved_conflicts_by_change_id.is_empty() && new_conflicts_by_change_id.is_empty())
2227        {
2228            // If the user just resolved some conflict and squashed them in, there won't be
2229            // any new conflicts. Clarify to them that there are still some other conflicts
2230            // to resolve. (We don't mention conflicts in commits that weren't affected by
2231            // the operation, however.)
2232            if new_conflicts_by_change_id.is_empty() {
2233                writeln!(
2234                    fmt,
2235                    "There are still unresolved conflicts in rebased descendants.",
2236                )?;
2237            }
2238
2239            self.report_repo_conflicts(
2240                fmt.as_mut(),
2241                new_repo,
2242                added_conflict_commits
2243                    .iter()
2244                    .map(|commit| commit.id().clone())
2245                    .collect(),
2246            )?;
2247        }
2248        revset_util::warn_unresolvable_trunk(ui, new_repo, &self.env.revset_parse_context())?;
2249
2250        Ok(())
2251    }
2252
2253    pub fn report_repo_conflicts(
2254        &self,
2255        fmt: &mut dyn Formatter,
2256        repo: &ReadonlyRepo,
2257        conflicted_commits: Vec<CommitId>,
2258    ) -> Result<(), CommandError> {
2259        if !self.settings().get_bool("hints.resolving-conflicts")? || conflicted_commits.is_empty()
2260        {
2261            return Ok(());
2262        }
2263
2264        let only_one_conflicted_commit = conflicted_commits.len() == 1;
2265        let root_conflicts_revset = RevsetExpression::commits(conflicted_commits)
2266            .roots()
2267            .evaluate(repo)?;
2268
2269        let root_conflict_commits: Vec<_> = root_conflicts_revset
2270            .iter()
2271            .commits(repo.store())
2272            .try_collect()?;
2273
2274        // The common part of these strings is not extracted, to avoid i18n issues.
2275        let instruction = if only_one_conflicted_commit {
2276            indoc! {"
2277            To resolve the conflicts, start by creating a commit on top of
2278            the conflicted commit:
2279            "}
2280        } else if root_conflict_commits.len() == 1 {
2281            indoc! {"
2282            To resolve the conflicts, start by creating a commit on top of
2283            the first conflicted commit:
2284            "}
2285        } else {
2286            indoc! {"
2287            To resolve the conflicts, start by creating a commit on top of
2288            one of the first conflicted commits:
2289            "}
2290        };
2291        write!(fmt.labeled("hint").with_heading("Hint: "), "{instruction}")?;
2292        let format_short_change_id = self.short_change_id_template();
2293        {
2294            let mut fmt = fmt.labeled("hint");
2295            for commit in &root_conflict_commits {
2296                write!(fmt, "  jj new ")?;
2297                format_short_change_id.format(commit, *fmt)?;
2298                writeln!(fmt)?;
2299            }
2300        }
2301        writedoc!(
2302            fmt.labeled("hint"),
2303            "
2304            Then use `jj resolve`, or edit the conflict markers in the file directly.
2305            Once the conflicts are resolved, you can inspect the result with `jj diff`.
2306            Then run `jj squash` to move the resolution into the conflicted commit.
2307            ",
2308        )?;
2309        Ok(())
2310    }
2311
2312    /// Identifies bookmarks which are eligible to be moved automatically
2313    /// during `jj commit` and `jj new`. Whether a bookmark is eligible is
2314    /// determined by its target and the user and repo config for
2315    /// "advance-bookmarks".
2316    ///
2317    /// Returns a Vec of bookmarks in `repo` that point to any of the `from`
2318    /// commits and that are eligible to advance. The `from` commits are
2319    /// typically the parents of the target commit of `jj commit` or `jj new`.
2320    ///
2321    /// Bookmarks are not moved until
2322    /// `WorkspaceCommandTransaction::advance_bookmarks()` is called with the
2323    /// `AdvanceableBookmark`s returned by this function.
2324    ///
2325    /// Returns an empty `std::Vec` if no bookmarks are eligible to advance.
2326    pub fn get_advanceable_bookmarks<'a>(
2327        &self,
2328        from: impl IntoIterator<Item = &'a CommitId>,
2329    ) -> Result<Vec<AdvanceableBookmark>, CommandError> {
2330        let ab_settings = AdvanceBookmarksSettings::from_settings(self.settings())?;
2331        if !ab_settings.feature_enabled() {
2332            // Return early if we know that there's no work to do.
2333            return Ok(Vec::new());
2334        }
2335
2336        let mut advanceable_bookmarks = Vec::new();
2337        for from_commit in from {
2338            for (name, _) in self.repo().view().local_bookmarks_for_commit(from_commit) {
2339                if ab_settings.bookmark_is_eligible(name) {
2340                    advanceable_bookmarks.push(AdvanceableBookmark {
2341                        name: name.to_owned(),
2342                        old_commit_id: from_commit.clone(),
2343                    });
2344                }
2345            }
2346        }
2347
2348        Ok(advanceable_bookmarks)
2349    }
2350}
2351
2352/// An ongoing [`Transaction`] tied to a particular workspace.
2353///
2354/// `WorkspaceCommandTransaction`s are created with
2355/// [`WorkspaceCommandHelper::start_transaction`] and committed with
2356/// [`WorkspaceCommandTransaction::finish`]. The inner `Transaction` can also be
2357/// extracted using [`WorkspaceCommandTransaction::into_inner`] in situations
2358/// where finer-grained control over the `Transaction` is necessary.
2359#[must_use]
2360pub struct WorkspaceCommandTransaction<'a> {
2361    helper: &'a mut WorkspaceCommandHelper,
2362    tx: Transaction,
2363    /// Cache of index built against the current MutableRepo state.
2364    id_prefix_context: OnceCell<IdPrefixContext>,
2365}
2366
2367impl WorkspaceCommandTransaction<'_> {
2368    /// Workspace helper that may use the base repo.
2369    pub fn base_workspace_helper(&self) -> &WorkspaceCommandHelper {
2370        self.helper
2371    }
2372
2373    /// Settings for this workspace.
2374    pub fn settings(&self) -> &UserSettings {
2375        self.helper.settings()
2376    }
2377
2378    pub fn base_repo(&self) -> &Arc<ReadonlyRepo> {
2379        self.tx.base_repo()
2380    }
2381
2382    pub fn repo(&self) -> &MutableRepo {
2383        self.tx.repo()
2384    }
2385
2386    pub fn repo_mut(&mut self) -> &mut MutableRepo {
2387        self.id_prefix_context.take(); // invalidate
2388        self.tx.repo_mut()
2389    }
2390
2391    pub fn check_out(&mut self, commit: &Commit) -> Result<Commit, CheckOutCommitError> {
2392        let name = self.helper.workspace_name().to_owned();
2393        self.id_prefix_context.take(); // invalidate
2394        self.tx.repo_mut().check_out(name, commit)
2395    }
2396
2397    pub fn edit(&mut self, commit: &Commit) -> Result<(), EditCommitError> {
2398        let name = self.helper.workspace_name().to_owned();
2399        self.id_prefix_context.take(); // invalidate
2400        self.tx.repo_mut().edit(name, commit)
2401    }
2402
2403    pub fn format_commit_summary(&self, commit: &Commit) -> String {
2404        let output = self.commit_summary_template().format_plain_text(commit);
2405        output.into_string_lossy()
2406    }
2407
2408    pub fn write_commit_summary(
2409        &self,
2410        formatter: &mut dyn Formatter,
2411        commit: &Commit,
2412    ) -> std::io::Result<()> {
2413        self.commit_summary_template().format(commit, formatter)
2414    }
2415
2416    /// Template for one-line summary of a commit within transaction.
2417    pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
2418        let language = self.commit_template_language();
2419        self.helper
2420            .reparse_valid_template(&language, &self.helper.commit_summary_template_text)
2421            .labeled(["commit"])
2422    }
2423
2424    /// Creates commit template language environment capturing the current
2425    /// transaction state.
2426    pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
2427        let id_prefix_context = self
2428            .id_prefix_context
2429            .get_or_init(|| self.helper.env.new_id_prefix_context());
2430        self.helper
2431            .env
2432            .commit_template_language(self.tx.repo(), id_prefix_context)
2433    }
2434
2435    /// Parses commit template with the current transaction state.
2436    pub fn parse_commit_template(
2437        &self,
2438        ui: &Ui,
2439        template_text: &str,
2440    ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
2441        let language = self.commit_template_language();
2442        self.helper.env.parse_template(ui, &language, template_text)
2443    }
2444
2445    pub fn finish(self, ui: &Ui, description: impl Into<String>) -> Result<(), CommandError> {
2446        self.helper.finish_transaction(ui, self.tx, description)
2447    }
2448
2449    /// Returns the wrapped [`Transaction`] for circumstances where
2450    /// finer-grained control is needed. The caller becomes responsible for
2451    /// finishing the `Transaction`, including rebasing descendants and updating
2452    /// the working copy, if applicable.
2453    pub fn into_inner(self) -> Transaction {
2454        self.tx
2455    }
2456
2457    /// Moves each bookmark in `bookmarks` from an old commit it's associated
2458    /// with (configured by `get_advanceable_bookmarks`) to the `move_to`
2459    /// commit. If the bookmark is conflicted before the update, it will
2460    /// remain conflicted after the update, but the conflict will involve
2461    /// the `move_to` commit instead of the old commit.
2462    pub fn advance_bookmarks(&mut self, bookmarks: Vec<AdvanceableBookmark>, move_to: &CommitId) {
2463        for bookmark in bookmarks {
2464            // This removes the old commit ID from the bookmark's RefTarget and
2465            // replaces it with the `move_to` ID.
2466            self.repo_mut().merge_local_bookmark(
2467                &bookmark.name,
2468                &RefTarget::normal(bookmark.old_commit_id),
2469                &RefTarget::normal(move_to.clone()),
2470            );
2471        }
2472    }
2473}
2474
2475pub fn find_workspace_dir(cwd: &Path) -> &Path {
2476    cwd.ancestors()
2477        .find(|path| path.join(".jj").is_dir())
2478        .unwrap_or(cwd)
2479}
2480
2481fn map_workspace_load_error(err: WorkspaceLoadError, user_wc_path: Option<&str>) -> CommandError {
2482    match err {
2483        WorkspaceLoadError::NoWorkspaceHere(wc_path) => {
2484            // Prefer user-specified path instead of absolute wc_path if any.
2485            let short_wc_path = user_wc_path.map_or(wc_path.as_ref(), Path::new);
2486            let message = format!(r#"There is no jj repo in "{}""#, short_wc_path.display());
2487            let git_dir = wc_path.join(".git");
2488            if git_dir.is_dir() {
2489                user_error_with_hint(
2490                    message,
2491                    "It looks like this is a git repo. You can create a jj repo backed by it by \
2492                     running this:
2493jj git init",
2494                )
2495            } else {
2496                user_error(message)
2497            }
2498        }
2499        WorkspaceLoadError::RepoDoesNotExist(repo_dir) => user_error(format!(
2500            "The repository directory at {} is missing. Was it moved?",
2501            repo_dir.display(),
2502        )),
2503        WorkspaceLoadError::StoreLoadError(err @ StoreLoadError::UnsupportedType { .. }) => {
2504            internal_error_with_message(
2505                "This version of the jj binary doesn't support this type of repo",
2506                err,
2507            )
2508        }
2509        WorkspaceLoadError::StoreLoadError(
2510            err @ (StoreLoadError::ReadError { .. } | StoreLoadError::Backend(_)),
2511        ) => internal_error_with_message("The repository appears broken or inaccessible", err),
2512        WorkspaceLoadError::StoreLoadError(StoreLoadError::Signing(err)) => user_error(err),
2513        WorkspaceLoadError::WorkingCopyState(err) => internal_error(err),
2514        WorkspaceLoadError::DecodeRepoPath(_) | WorkspaceLoadError::Path(_) => user_error(err),
2515    }
2516}
2517
2518pub fn start_repo_transaction(repo: &Arc<ReadonlyRepo>, string_args: &[String]) -> Transaction {
2519    let mut tx = repo.start_transaction();
2520    // TODO: Either do better shell-escaping here or store the values in some list
2521    // type (which we currently don't have).
2522    let shell_escape = |arg: &String| {
2523        if arg.as_bytes().iter().all(|b| {
2524            matches!(b,
2525                b'A'..=b'Z'
2526                | b'a'..=b'z'
2527                | b'0'..=b'9'
2528                | b','
2529                | b'-'
2530                | b'.'
2531                | b'/'
2532                | b':'
2533                | b'@'
2534                | b'_'
2535            )
2536        }) {
2537            arg.clone()
2538        } else {
2539            format!("'{}'", arg.replace('\'', "\\'"))
2540        }
2541    };
2542    let mut quoted_strings = vec!["jj".to_string()];
2543    quoted_strings.extend(string_args.iter().skip(1).map(shell_escape));
2544    tx.set_tag("args".to_string(), quoted_strings.join(" "));
2545    tx
2546}
2547
2548fn update_stale_working_copy(
2549    mut locked_ws: LockedWorkspace,
2550    op_id: OperationId,
2551    stale_commit: &Commit,
2552    new_commit: &Commit,
2553) -> Result<CheckoutStats, CommandError> {
2554    // The same check as start_working_copy_mutation(), but with the stale
2555    // working-copy commit.
2556    if stale_commit.tree_id() != locked_ws.locked_wc().old_tree_id() {
2557        return Err(user_error("Concurrent working copy operation. Try again."));
2558    }
2559    let stats = locked_ws.locked_wc().check_out(new_commit).map_err(|err| {
2560        internal_error_with_message(
2561            format!("Failed to check out commit {}", new_commit.id().hex()),
2562            err,
2563        )
2564    })?;
2565    locked_ws.finish(op_id)?;
2566
2567    Ok(stats)
2568}
2569
2570/// Prints a list of commits by the given summary template. The list may be
2571/// elided. Use this to show created, rewritten, or abandoned commits.
2572pub fn print_updated_commits<'a>(
2573    formatter: &mut dyn Formatter,
2574    template: &TemplateRenderer<Commit>,
2575    commits: impl IntoIterator<Item = &'a Commit>,
2576) -> io::Result<()> {
2577    let mut commits = commits.into_iter().fuse();
2578    for commit in commits.by_ref().take(10) {
2579        write!(formatter, "  ")?;
2580        template.format(commit, formatter)?;
2581        writeln!(formatter)?;
2582    }
2583    if commits.next().is_some() {
2584        writeln!(formatter, "  ...")?;
2585    }
2586    Ok(())
2587}
2588
2589#[instrument(skip_all)]
2590pub fn print_conflicted_paths(
2591    conflicts: Vec<(RepoPathBuf, BackendResult<MergedTreeValue>)>,
2592    formatter: &mut dyn Formatter,
2593    workspace_command: &WorkspaceCommandHelper,
2594) -> Result<(), CommandError> {
2595    let formatted_paths = conflicts
2596        .iter()
2597        .map(|(path, _conflict)| workspace_command.format_file_path(path))
2598        .collect_vec();
2599    let max_path_len = formatted_paths.iter().map(|p| p.len()).max().unwrap_or(0);
2600    let formatted_paths = formatted_paths
2601        .into_iter()
2602        .map(|p| format!("{:width$}", p, width = max_path_len.min(32) + 3));
2603
2604    for ((_, conflict), formatted_path) in std::iter::zip(conflicts, formatted_paths) {
2605        // TODO: Display the error for the path instead of failing the whole command if
2606        // `conflict` is an error?
2607        let conflict = conflict?.simplify();
2608        let sides = conflict.num_sides();
2609        let n_adds = conflict.adds().flatten().count();
2610        let deletions = sides - n_adds;
2611
2612        let mut seen_objects = BTreeMap::new(); // Sort for consistency and easier testing
2613        if deletions > 0 {
2614            seen_objects.insert(
2615                format!(
2616                    // Starting with a number sorts this first
2617                    "{deletions} deletion{}",
2618                    if deletions > 1 { "s" } else { "" }
2619                ),
2620                "normal", // Deletions don't interfere with `jj resolve` or diff display
2621            );
2622        }
2623        // TODO: We might decide it's OK for `jj resolve` to ignore special files in the
2624        // `removes` of a conflict (see e.g. https://github.com/jj-vcs/jj/pull/978). In
2625        // that case, `conflict.removes` should be removed below.
2626        for term in itertools::chain(conflict.removes(), conflict.adds()).flatten() {
2627            seen_objects.insert(
2628                match term {
2629                    TreeValue::File {
2630                        executable: false, ..
2631                    } => continue,
2632                    TreeValue::File {
2633                        executable: true, ..
2634                    } => "an executable",
2635                    TreeValue::Symlink(_) => "a symlink",
2636                    TreeValue::Tree(_) => "a directory",
2637                    TreeValue::GitSubmodule(_) => "a git submodule",
2638                }
2639                .to_string(),
2640                "difficult",
2641            );
2642        }
2643
2644        write!(formatter, "{formatted_path} ")?;
2645        {
2646            let mut formatter = formatter.labeled("conflict_description");
2647            let print_pair = |formatter: &mut dyn Formatter, (text, label): &(String, &str)| {
2648                write!(formatter.labeled(label), "{text}")
2649            };
2650            print_pair(
2651                *formatter,
2652                &(
2653                    format!("{sides}-sided"),
2654                    if sides > 2 { "difficult" } else { "normal" },
2655                ),
2656            )?;
2657            write!(formatter, " conflict")?;
2658
2659            if !seen_objects.is_empty() {
2660                write!(formatter, " including ")?;
2661                let seen_objects = seen_objects.into_iter().collect_vec();
2662                match &seen_objects[..] {
2663                    [] => unreachable!(),
2664                    [only] => print_pair(*formatter, only)?,
2665                    [first, middle @ .., last] => {
2666                        print_pair(*formatter, first)?;
2667                        for pair in middle {
2668                            write!(formatter, ", ")?;
2669                            print_pair(*formatter, pair)?;
2670                        }
2671                        write!(formatter, " and ")?;
2672                        print_pair(*formatter, last)?;
2673                    }
2674                }
2675            }
2676        }
2677        writeln!(formatter)?;
2678    }
2679    Ok(())
2680}
2681
2682/// Build human-readable messages explaining why the file was not tracked
2683fn build_untracked_reason_message(reason: &UntrackedReason) -> Option<String> {
2684    match reason {
2685        UntrackedReason::FileTooLarge { size, max_size } => {
2686            // Show both exact and human bytes sizes to avoid something
2687            // like '1.0MiB, maximum size allowed is ~1.0MiB'
2688            let size_approx = HumanByteSize(*size);
2689            let max_size_approx = HumanByteSize(*max_size);
2690            Some(format!(
2691                "{size_approx} ({size} bytes); the maximum size allowed is {max_size_approx} \
2692                 ({max_size} bytes)",
2693            ))
2694        }
2695        // Paths with UntrackedReason::FileNotAutoTracked shouldn't be warned about
2696        // every time we make a snapshot. These paths will be printed by
2697        // "jj status" instead.
2698        UntrackedReason::FileNotAutoTracked => None,
2699    }
2700}
2701
2702/// Print a warning to the user, listing untracked files that he may care about
2703pub fn print_untracked_files(
2704    ui: &Ui,
2705    untracked_paths: &BTreeMap<RepoPathBuf, UntrackedReason>,
2706    path_converter: &RepoPathUiConverter,
2707) -> io::Result<()> {
2708    let mut untracked_paths = untracked_paths
2709        .iter()
2710        .filter_map(|(path, reason)| build_untracked_reason_message(reason).map(|m| (path, m)))
2711        .peekable();
2712
2713    if untracked_paths.peek().is_some() {
2714        writeln!(ui.warning_default(), "Refused to snapshot some files:")?;
2715        let mut formatter = ui.stderr_formatter();
2716        for (path, message) in untracked_paths {
2717            let ui_path = path_converter.format_file_path(path);
2718            writeln!(formatter, "  {ui_path}: {message}")?;
2719        }
2720    }
2721
2722    Ok(())
2723}
2724
2725pub fn print_snapshot_stats(
2726    ui: &Ui,
2727    stats: &SnapshotStats,
2728    path_converter: &RepoPathUiConverter,
2729) -> io::Result<()> {
2730    print_untracked_files(ui, &stats.untracked_paths, path_converter)?;
2731
2732    let large_files_sizes = stats
2733        .untracked_paths
2734        .values()
2735        .filter_map(|reason| match reason {
2736            UntrackedReason::FileTooLarge { size, .. } => Some(size),
2737            UntrackedReason::FileNotAutoTracked => None,
2738        });
2739    if let Some(size) = large_files_sizes.max() {
2740        writedoc!(
2741            ui.hint_default(),
2742            r"
2743            This is to prevent large files from being added by accident. You can fix this by:
2744              - Adding the file to `.gitignore`
2745              - Run `jj config set --repo snapshot.max-new-file-size {size}`
2746                This will increase the maximum file size allowed for new files, in this repository only.
2747              - Run `jj --config snapshot.max-new-file-size={size} st`
2748                This will increase the maximum file size allowed for new files, for this command only.
2749            "
2750        )?;
2751    }
2752    Ok(())
2753}
2754
2755pub fn print_checkout_stats(
2756    ui: &Ui,
2757    stats: &CheckoutStats,
2758    new_commit: &Commit,
2759) -> Result<(), std::io::Error> {
2760    if stats.added_files > 0 || stats.updated_files > 0 || stats.removed_files > 0 {
2761        writeln!(
2762            ui.status(),
2763            "Added {} files, modified {} files, removed {} files",
2764            stats.added_files,
2765            stats.updated_files,
2766            stats.removed_files
2767        )?;
2768    }
2769    if stats.skipped_files != 0 {
2770        writeln!(
2771            ui.warning_default(),
2772            "{} of those updates were skipped because there were conflicting changes in the \
2773             working copy.",
2774            stats.skipped_files
2775        )?;
2776        writeln!(
2777            ui.hint_default(),
2778            "Inspect the changes compared to the intended target with `jj diff --from {}`.
2779Discard the conflicting changes with `jj restore --from {}`.",
2780            short_commit_hash(new_commit.id()),
2781            short_commit_hash(new_commit.id())
2782        )?;
2783    }
2784    Ok(())
2785}
2786
2787/// Prints warning about explicit paths that don't match any of the tree
2788/// entries.
2789pub fn print_unmatched_explicit_paths<'a>(
2790    ui: &Ui,
2791    workspace_command: &WorkspaceCommandHelper,
2792    expression: &FilesetExpression,
2793    trees: impl IntoIterator<Item = &'a MergedTree>,
2794) -> io::Result<()> {
2795    let mut explicit_paths = expression.explicit_paths().collect_vec();
2796    for tree in trees {
2797        // TODO: propagate errors
2798        explicit_paths.retain(|&path| tree.path_value(path).unwrap().is_absent());
2799        if explicit_paths.is_empty() {
2800            return Ok(());
2801        }
2802    }
2803    let ui_paths = explicit_paths
2804        .iter()
2805        .map(|&path| workspace_command.format_file_path(path))
2806        .join(", ");
2807    writeln!(
2808        ui.warning_default(),
2809        "No matching entries for paths: {ui_paths}"
2810    )?;
2811    Ok(())
2812}
2813
2814pub fn update_working_copy(
2815    repo: &Arc<ReadonlyRepo>,
2816    workspace: &mut Workspace,
2817    old_commit: Option<&Commit>,
2818    new_commit: &Commit,
2819) -> Result<CheckoutStats, CommandError> {
2820    let old_tree_id = old_commit.map(|commit| commit.tree_id().clone());
2821    // TODO: CheckoutError::ConcurrentCheckout should probably just result in a
2822    // warning for most commands (but be an error for the checkout command)
2823    let stats = workspace
2824        .check_out(repo.op_id().clone(), old_tree_id.as_ref(), new_commit)
2825        .map_err(|err| {
2826            internal_error_with_message(
2827                format!("Failed to check out commit {}", new_commit.id().hex()),
2828                err,
2829            )
2830        })?;
2831    Ok(stats)
2832}
2833
2834/// Whether or not the `bookmark` has any tracked remotes (i.e. is a tracking
2835/// local bookmark.)
2836pub fn has_tracked_remote_bookmarks(view: &View, bookmark: &RefName) -> bool {
2837    view.remote_bookmarks_matching(
2838        &StringPattern::exact(bookmark),
2839        &StringPattern::everything(),
2840    )
2841    .filter(|&(symbol, _)| !jj_lib::git::is_special_git_remote(symbol.remote))
2842    .any(|(_, remote_ref)| remote_ref.is_tracked())
2843}
2844
2845pub fn load_template_aliases(
2846    ui: &Ui,
2847    stacked_config: &StackedConfig,
2848) -> Result<TemplateAliasesMap, CommandError> {
2849    let table_name = ConfigNamePathBuf::from_iter(["template-aliases"]);
2850    let mut aliases_map = TemplateAliasesMap::new();
2851    // Load from all config layers in order. 'f(x)' in default layer should be
2852    // overridden by 'f(a)' in user.
2853    for layer in stacked_config.layers() {
2854        let table = match layer.look_up_table(&table_name) {
2855            Ok(Some(table)) => table,
2856            Ok(None) => continue,
2857            Err(item) => {
2858                return Err(ConfigGetError::Type {
2859                    name: table_name.to_string(),
2860                    error: format!("Expected a table, but is {}", item.type_name()).into(),
2861                    source_path: layer.path.clone(),
2862                }
2863                .into());
2864            }
2865        };
2866        for (decl, item) in table.iter() {
2867            let r = item
2868                .as_str()
2869                .ok_or_else(|| format!("Expected a string, but is {}", item.type_name()))
2870                .and_then(|v| aliases_map.insert(decl, v).map_err(|e| e.to_string()));
2871            if let Err(s) = r {
2872                writeln!(
2873                    ui.warning_default(),
2874                    "Failed to load `{table_name}.{decl}`: {s}"
2875                )?;
2876            }
2877        }
2878    }
2879    Ok(aliases_map)
2880}
2881
2882/// Helper to reformat content of log-like commands.
2883#[derive(Clone, Debug)]
2884pub struct LogContentFormat {
2885    width: usize,
2886    word_wrap: bool,
2887}
2888
2889impl LogContentFormat {
2890    /// Creates new formatting helper for the terminal.
2891    pub fn new(ui: &Ui, settings: &UserSettings) -> Result<Self, ConfigGetError> {
2892        Ok(Self {
2893            width: ui.term_width(),
2894            word_wrap: settings.get_bool("ui.log-word-wrap")?,
2895        })
2896    }
2897
2898    /// Subtracts the given `width` and returns new formatting helper.
2899    #[must_use]
2900    pub fn sub_width(&self, width: usize) -> Self {
2901        Self {
2902            width: self.width.saturating_sub(width),
2903            word_wrap: self.word_wrap,
2904        }
2905    }
2906
2907    /// Current width available to content.
2908    pub fn width(&self) -> usize {
2909        self.width
2910    }
2911
2912    /// Writes content which will optionally be wrapped at the current width.
2913    pub fn write<E: From<io::Error>>(
2914        &self,
2915        formatter: &mut dyn Formatter,
2916        content_fn: impl FnOnce(&mut dyn Formatter) -> Result<(), E>,
2917    ) -> Result<(), E> {
2918        if self.word_wrap {
2919            let mut recorder = FormatRecorder::new();
2920            content_fn(&mut recorder)?;
2921            text_util::write_wrapped(formatter, &recorder, self.width)?;
2922        } else {
2923            content_fn(formatter)?;
2924        }
2925        Ok(())
2926    }
2927}
2928
2929pub fn short_commit_hash(commit_id: &CommitId) -> String {
2930    format!("{commit_id:.12}")
2931}
2932
2933pub fn short_change_hash(change_id: &ChangeId) -> String {
2934    format!("{change_id:.12}")
2935}
2936
2937pub fn short_operation_hash(operation_id: &OperationId) -> String {
2938    format!("{operation_id:.12}")
2939}
2940
2941/// Wrapper around a `DiffEditor` to conditionally start interactive session.
2942#[derive(Clone, Debug)]
2943pub enum DiffSelector {
2944    NonInteractive,
2945    Interactive(DiffEditor),
2946}
2947
2948impl DiffSelector {
2949    pub fn is_interactive(&self) -> bool {
2950        matches!(self, Self::Interactive(_))
2951    }
2952
2953    /// Restores diffs from the `right_tree` to the `left_tree` by using an
2954    /// interactive editor if enabled.
2955    ///
2956    /// Only files matching the `matcher` will be copied to the new tree.
2957    pub fn select(
2958        &self,
2959        [left_tree, right_tree]: [&MergedTree; 2],
2960        matcher: &dyn Matcher,
2961        format_instructions: impl FnOnce() -> String,
2962    ) -> Result<MergedTreeId, CommandError> {
2963        let selected_tree_id = restore_tree(right_tree, left_tree, matcher).block_on()?;
2964        match self {
2965            Self::NonInteractive => Ok(selected_tree_id),
2966            Self::Interactive(editor) => {
2967                // edit_diff_external() is designed to edit the right tree,
2968                // whereas we want to update the left tree. Unmatched paths
2969                // shouldn't be based off the right tree.
2970                let right_tree = right_tree.store().get_root_tree(&selected_tree_id)?;
2971                Ok(editor.edit([left_tree, &right_tree], matcher, format_instructions)?)
2972            }
2973        }
2974    }
2975}
2976
2977#[derive(Clone, Debug)]
2978pub struct RemoteBookmarkNamePattern {
2979    pub bookmark: StringPattern,
2980    pub remote: StringPattern,
2981}
2982
2983impl FromStr for RemoteBookmarkNamePattern {
2984    type Err = String;
2985
2986    fn from_str(src: &str) -> Result<Self, Self::Err> {
2987        // The kind prefix applies to both bookmark and remote fragments. It's
2988        // weird that unanchored patterns like substring:bookmark@remote is split
2989        // into two, but I can't think of a better syntax.
2990        // TODO: should we disable substring pattern? what if we added regex?
2991        let (maybe_kind, pat) = src
2992            .split_once(':')
2993            .map_or((None, src), |(kind, pat)| (Some(kind), pat));
2994        let to_pattern = |pat: &str| {
2995            if let Some(kind) = maybe_kind {
2996                StringPattern::from_str_kind(pat, kind).map_err(|err| err.to_string())
2997            } else {
2998                Ok(StringPattern::exact(pat))
2999            }
3000        };
3001        // TODO: maybe reuse revset parser to handle bookmark/remote name containing @
3002        let (bookmark, remote) = pat.rsplit_once('@').ok_or_else(|| {
3003            "remote bookmark must be specified in bookmark@remote form".to_owned()
3004        })?;
3005        Ok(Self {
3006            bookmark: to_pattern(bookmark)?,
3007            remote: to_pattern(remote)?,
3008        })
3009    }
3010}
3011
3012impl RemoteBookmarkNamePattern {
3013    pub fn is_exact(&self) -> bool {
3014        self.bookmark.is_exact() && self.remote.is_exact()
3015    }
3016}
3017
3018impl fmt::Display for RemoteBookmarkNamePattern {
3019    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3020        // TODO: use revset::format_remote_symbol() if FromStr is migrated to
3021        // the revset parser.
3022        let Self { bookmark, remote } = self;
3023        write!(f, "{bookmark}@{remote}")
3024    }
3025}
3026
3027/// Computes the location (new parents and new children) to place commits.
3028///
3029/// The `destination` argument is mutually exclusive to the `insert_after` and
3030/// `insert_before` arguments.
3031pub fn compute_commit_location(
3032    ui: &Ui,
3033    workspace_command: &WorkspaceCommandHelper,
3034    destination: Option<&[RevisionArg]>,
3035    insert_after: Option<&[RevisionArg]>,
3036    insert_before: Option<&[RevisionArg]>,
3037    commit_type: &str,
3038) -> Result<(Vec<CommitId>, Vec<CommitId>), CommandError> {
3039    let resolve_revisions =
3040        |revisions: Option<&[RevisionArg]>| -> Result<Option<Vec<CommitId>>, CommandError> {
3041            if let Some(revisions) = revisions {
3042                Ok(Some(
3043                    workspace_command
3044                        .resolve_some_revsets_default_single(ui, revisions)?
3045                        .into_iter()
3046                        .collect_vec(),
3047                ))
3048            } else {
3049                Ok(None)
3050            }
3051        };
3052    let destination_commit_ids = resolve_revisions(destination)?;
3053    let after_commit_ids = resolve_revisions(insert_after)?;
3054    let before_commit_ids = resolve_revisions(insert_before)?;
3055
3056    let (new_parent_ids, new_child_ids) =
3057        match (destination_commit_ids, after_commit_ids, before_commit_ids) {
3058            (Some(destination_commit_ids), None, None) => (destination_commit_ids, vec![]),
3059            (None, Some(after_commit_ids), Some(before_commit_ids)) => {
3060                (after_commit_ids, before_commit_ids)
3061            }
3062            (None, Some(after_commit_ids), None) => {
3063                let new_child_ids: Vec<_> = RevsetExpression::commits(after_commit_ids.clone())
3064                    .children()
3065                    .evaluate(workspace_command.repo().as_ref())?
3066                    .iter()
3067                    .try_collect()?;
3068
3069                (after_commit_ids, new_child_ids)
3070            }
3071            (None, None, Some(before_commit_ids)) => {
3072                let before_commits: Vec<_> = before_commit_ids
3073                    .iter()
3074                    .map(|id| workspace_command.repo().store().get_commit(id))
3075                    .try_collect()?;
3076                // Not using `RevsetExpression::parents` here to persist the order of parents
3077                // specified in `before_commits`.
3078                let new_parent_ids = before_commits
3079                    .iter()
3080                    .flat_map(|commit| commit.parent_ids())
3081                    .unique()
3082                    .cloned()
3083                    .collect_vec();
3084
3085                (new_parent_ids, before_commit_ids)
3086            }
3087            (Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
3088                panic!("destination cannot be used with insert_after/insert_before")
3089            }
3090            (None, None, None) => {
3091                panic!("expected at least one of destination or insert_after/insert_before")
3092            }
3093        };
3094
3095    if !new_child_ids.is_empty() {
3096        workspace_command.check_rewritable(new_child_ids.iter())?;
3097        ensure_no_commit_loop(
3098            workspace_command.repo().as_ref(),
3099            &RevsetExpression::commits(new_child_ids.clone()),
3100            &RevsetExpression::commits(new_parent_ids.clone()),
3101            commit_type,
3102        )?;
3103    }
3104
3105    Ok((new_parent_ids, new_child_ids))
3106}
3107
3108/// Ensure that there is no possible cycle between the potential children and
3109/// parents of the given commits.
3110fn ensure_no_commit_loop(
3111    repo: &ReadonlyRepo,
3112    children_expression: &Arc<ResolvedRevsetExpression>,
3113    parents_expression: &Arc<ResolvedRevsetExpression>,
3114    commit_type: &str,
3115) -> Result<(), CommandError> {
3116    if let Some(commit_id) = children_expression
3117        .dag_range_to(parents_expression)
3118        .evaluate(repo)?
3119        .iter()
3120        .next()
3121    {
3122        let commit_id = commit_id?;
3123        return Err(user_error(format!(
3124            "Refusing to create a loop: commit {} would be both an ancestor and a descendant of \
3125             the {commit_type}",
3126            short_commit_hash(&commit_id),
3127        )));
3128    }
3129    Ok(())
3130}
3131
3132/// Jujutsu (An experimental VCS)
3133///
3134/// To get started, see the tutorial [`jj help -k tutorial`].
3135///
3136/// [`jj help -k tutorial`]:
3137///     https://jj-vcs.github.io/jj/latest/tutorial/
3138#[derive(clap::Parser, Clone, Debug)]
3139#[command(name = "jj")]
3140pub struct Args {
3141    #[command(flatten)]
3142    pub global_args: GlobalArgs,
3143}
3144
3145#[derive(clap::Args, Clone, Debug)]
3146#[command(next_help_heading = "Global Options")]
3147pub struct GlobalArgs {
3148    /// Path to repository to operate on
3149    ///
3150    /// By default, Jujutsu searches for the closest .jj/ directory in an
3151    /// ancestor of the current working directory.
3152    #[arg(long, short = 'R', global = true, value_hint = clap::ValueHint::DirPath)]
3153    pub repository: Option<String>,
3154    /// Don't snapshot the working copy, and don't update it
3155    ///
3156    /// By default, Jujutsu snapshots the working copy at the beginning of every
3157    /// command. The working copy is also updated at the end of the command,
3158    /// if the command modified the working-copy commit (`@`). If you want
3159    /// to avoid snapshotting the working copy and instead see a possibly
3160    /// stale working-copy commit, you can use `--ignore-working-copy`.
3161    /// This may be useful e.g. in a command prompt, especially if you have
3162    /// another process that commits the working copy.
3163    ///
3164    /// Loading the repository at a specific operation with `--at-operation`
3165    /// implies `--ignore-working-copy`.
3166    #[arg(long, global = true)]
3167    pub ignore_working_copy: bool,
3168    /// Allow rewriting immutable commits
3169    ///
3170    /// By default, Jujutsu prevents rewriting commits in the configured set of
3171    /// immutable commits. This option disables that check and lets you rewrite
3172    /// any commit but the root commit.
3173    ///
3174    /// This option only affects the check. It does not affect the
3175    /// `immutable_heads()` revset or the `immutable` template keyword.
3176    #[arg(long, global = true)]
3177    pub ignore_immutable: bool,
3178    /// Operation to load the repo at
3179    ///
3180    /// Operation to load the repo at. By default, Jujutsu loads the repo at the
3181    /// most recent operation, or at the merge of the divergent operations if
3182    /// any.
3183    ///
3184    /// You can use `--at-op=<operation ID>` to see what the repo looked like at
3185    /// an earlier operation. For example `jj --at-op=<operation ID> st` will
3186    /// show you what `jj st` would have shown you when the given operation had
3187    /// just finished. `--at-op=@` is pretty much the same as the default except
3188    /// that divergent operations will never be merged.
3189    ///
3190    /// Use `jj op log` to find the operation ID you want. Any unambiguous
3191    /// prefix of the operation ID is enough.
3192    ///
3193    /// When loading the repo at an earlier operation, the working copy will be
3194    /// ignored, as if `--ignore-working-copy` had been specified.
3195    ///
3196    /// It is possible to run mutating commands when loading the repo at an
3197    /// earlier operation. Doing that is equivalent to having run concurrent
3198    /// commands starting at the earlier operation. There's rarely a reason to
3199    /// do that, but it is possible.
3200    #[arg(
3201        long,
3202        visible_alias = "at-op",
3203        global = true,
3204        add = ArgValueCandidates::new(complete::operations),
3205    )]
3206    pub at_operation: Option<String>,
3207    /// Enable debug logging
3208    #[arg(long, global = true)]
3209    pub debug: bool,
3210
3211    #[command(flatten)]
3212    pub early_args: EarlyArgs,
3213}
3214
3215#[derive(clap::Args, Clone, Debug)]
3216pub struct EarlyArgs {
3217    /// When to colorize output
3218    #[arg(long, value_name = "WHEN", global = true)]
3219    pub color: Option<ColorChoice>,
3220    /// Silence non-primary command output
3221    ///
3222    /// For example, `jj file list` will still list files, but it won't tell
3223    /// you if the working copy was snapshotted or if descendants were rebased.
3224    ///
3225    /// Warnings and errors will still be printed.
3226    #[arg(long, global = true, action = ArgAction::SetTrue)]
3227    // Parsing with ignore_errors will crash if this is bool, so use
3228    // Option<bool>.
3229    pub quiet: Option<bool>,
3230    /// Disable the pager
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 no_pager: Option<bool>,
3235    /// Additional configuration options (can be repeated)
3236    ///
3237    /// The name should be specified as TOML dotted keys. The value should be
3238    /// specified as a TOML expression. If string value isn't enclosed by any
3239    /// TOML constructs (such as array notation), quotes can be omitted.
3240    #[arg(long, value_name = "NAME=VALUE", global = true, add = ArgValueCompleter::new(complete::leaf_config_key_value))]
3241    pub config: Vec<String>,
3242    /// Additional configuration files (can be repeated)
3243    #[arg(long, value_name = "PATH", global = true, value_hint = clap::ValueHint::FilePath)]
3244    pub config_file: Vec<String>,
3245}
3246
3247impl EarlyArgs {
3248    pub(crate) fn merged_config_args(&self, matches: &ArgMatches) -> Vec<(ConfigArgKind, &str)> {
3249        merge_args_with(
3250            matches,
3251            &[("config", &self.config), ("config_file", &self.config_file)],
3252            |id, value| match id {
3253                "config" => (ConfigArgKind::Item, value.as_ref()),
3254                "config_file" => (ConfigArgKind::File, value.as_ref()),
3255                _ => unreachable!("unexpected id {id:?}"),
3256            },
3257        )
3258    }
3259
3260    fn has_config_args(&self) -> bool {
3261        !self.config.is_empty() || !self.config_file.is_empty()
3262    }
3263}
3264
3265/// Wrapper around revset expression argument.
3266///
3267/// An empty string is rejected early by the CLI value parser, but it's still
3268/// allowed to construct an empty `RevisionArg` from a config value for
3269/// example. An empty expression will be rejected by the revset parser.
3270#[derive(Clone, Debug)]
3271pub struct RevisionArg(Cow<'static, str>);
3272
3273impl RevisionArg {
3274    /// The working-copy symbol, which is the default of the most commands.
3275    pub const AT: Self = Self(Cow::Borrowed("@"));
3276}
3277
3278impl From<String> for RevisionArg {
3279    fn from(s: String) -> Self {
3280        Self(s.into())
3281    }
3282}
3283
3284impl AsRef<str> for RevisionArg {
3285    fn as_ref(&self) -> &str {
3286        &self.0
3287    }
3288}
3289
3290impl fmt::Display for RevisionArg {
3291    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3292        write!(f, "{}", self.0)
3293    }
3294}
3295
3296impl ValueParserFactory for RevisionArg {
3297    type Parser = MapValueParser<NonEmptyStringValueParser, fn(String) -> Self>;
3298
3299    fn value_parser() -> Self::Parser {
3300        NonEmptyStringValueParser::new().map(Self::from)
3301    }
3302}
3303
3304/// Merges multiple clap args in order of appearance.
3305///
3306/// The `id_values` is a list of `(id, values)` pairs, where `id` is the name of
3307/// the clap `Arg`, and `values` are the parsed values for that arg. The
3308/// `convert` function transforms each `(id, value)` pair to e.g. an enum.
3309///
3310/// This is a workaround for <https://github.com/clap-rs/clap/issues/3146>.
3311pub fn merge_args_with<'k, 'v, T, U>(
3312    matches: &ArgMatches,
3313    id_values: &[(&'k str, &'v [T])],
3314    mut convert: impl FnMut(&'k str, &'v T) -> U,
3315) -> Vec<U> {
3316    let mut pos_values: Vec<(usize, U)> = Vec::new();
3317    for (id, values) in id_values {
3318        pos_values.extend(itertools::zip_eq(
3319            matches.indices_of(id).into_iter().flatten(),
3320            values.iter().map(|v| convert(id, v)),
3321        ));
3322    }
3323    pos_values.sort_unstable_by_key(|&(pos, _)| pos);
3324    pos_values.into_iter().map(|(_, value)| value).collect()
3325}
3326
3327fn get_string_or_array(
3328    config: &StackedConfig,
3329    key: &'static str,
3330) -> Result<Vec<String>, ConfigGetError> {
3331    config
3332        .get(key)
3333        .map(|string| vec![string])
3334        .or_else(|_| config.get::<Vec<String>>(key))
3335}
3336
3337fn resolve_default_command(
3338    ui: &Ui,
3339    config: &StackedConfig,
3340    app: &Command,
3341    mut string_args: Vec<String>,
3342) -> Result<Vec<String>, CommandError> {
3343    const PRIORITY_FLAGS: &[&str] = &["--help", "-h", "--version", "-V"];
3344
3345    let has_priority_flag = string_args
3346        .iter()
3347        .any(|arg| PRIORITY_FLAGS.contains(&arg.as_str()));
3348    if has_priority_flag {
3349        return Ok(string_args);
3350    }
3351
3352    let app_clone = app
3353        .clone()
3354        .allow_external_subcommands(true)
3355        .ignore_errors(true);
3356    let matches = app_clone.try_get_matches_from(&string_args).ok();
3357
3358    if let Some(matches) = matches
3359        && matches.subcommand_name().is_none()
3360    {
3361        let args = get_string_or_array(config, "ui.default-command").optional()?;
3362        if args.is_none() {
3363            writeln!(
3364                ui.hint_default(),
3365                "Use `jj -h` for a list of available commands."
3366            )?;
3367            writeln!(
3368                ui.hint_no_heading(),
3369                "Run `jj config set --user ui.default-command log` to disable this message."
3370            )?;
3371        }
3372        let default_command = args.unwrap_or_else(|| vec!["log".to_string()]);
3373
3374        // Insert the default command directly after the path to the binary.
3375        string_args.splice(1..1, default_command);
3376    }
3377    Ok(string_args)
3378}
3379
3380fn resolve_aliases(
3381    ui: &Ui,
3382    config: &StackedConfig,
3383    app: &Command,
3384    mut string_args: Vec<String>,
3385) -> Result<Vec<String>, CommandError> {
3386    let defined_aliases: HashSet<_> = config.table_keys("aliases").collect();
3387    let mut resolved_aliases = HashSet::new();
3388    let mut real_commands = HashSet::new();
3389    for command in app.get_subcommands() {
3390        real_commands.insert(command.get_name());
3391        for alias in command.get_all_aliases() {
3392            real_commands.insert(alias);
3393        }
3394    }
3395    for alias in defined_aliases.intersection(&real_commands).sorted() {
3396        writeln!(
3397            ui.warning_default(),
3398            "Cannot define an alias that overrides the built-in command '{alias}'"
3399        )?;
3400    }
3401
3402    loop {
3403        let app_clone = app.clone().allow_external_subcommands(true);
3404        let matches = app_clone.try_get_matches_from(&string_args).ok();
3405        if let Some((command_name, submatches)) = matches.as_ref().and_then(|m| m.subcommand())
3406            && !real_commands.contains(command_name)
3407        {
3408            let alias_name = command_name.to_string();
3409            let alias_args = submatches
3410                .get_many::<OsString>("")
3411                .unwrap_or_default()
3412                .map(|arg| arg.to_str().unwrap().to_string())
3413                .collect_vec();
3414            if resolved_aliases.contains(&*alias_name) {
3415                return Err(user_error(format!(
3416                    "Recursive alias definition involving `{alias_name}`"
3417                )));
3418            }
3419            if let Some(&alias_name) = defined_aliases.get(&*alias_name) {
3420                let alias_definition: Vec<String> = config.get(["aliases", alias_name])?;
3421                assert!(string_args.ends_with(&alias_args));
3422                string_args.truncate(string_args.len() - 1 - alias_args.len());
3423                string_args.extend(alias_definition);
3424                string_args.extend_from_slice(&alias_args);
3425                resolved_aliases.insert(alias_name);
3426                continue;
3427            } else {
3428                // Not a real command and not an alias, so return what we've resolved so far
3429                return Ok(string_args);
3430            }
3431        }
3432        // No more alias commands, or hit unknown option
3433        return Ok(string_args);
3434    }
3435}
3436
3437/// Parse args that must be interpreted early, e.g. before printing help.
3438fn parse_early_args(
3439    app: &Command,
3440    args: &[String],
3441) -> Result<(EarlyArgs, Vec<ConfigLayer>), CommandError> {
3442    // ignore_errors() bypasses errors like missing subcommand
3443    let early_matches = app
3444        .clone()
3445        .disable_version_flag(true)
3446        // Do not emit DisplayHelp error
3447        .disable_help_flag(true)
3448        // Do not stop parsing at -h/--help
3449        .arg(
3450            clap::Arg::new("help")
3451                .short('h')
3452                .long("help")
3453                .global(true)
3454                .action(ArgAction::Count),
3455        )
3456        .ignore_errors(true)
3457        .try_get_matches_from(args)?;
3458    let args = EarlyArgs::from_arg_matches(&early_matches).unwrap();
3459
3460    let mut config_layers = parse_config_args(&args.merged_config_args(&early_matches))?;
3461    // Command arguments overrides any other configuration including the
3462    // variables loaded from --config* arguments.
3463    let mut layer = ConfigLayer::empty(ConfigSource::CommandArg);
3464    if let Some(choice) = args.color {
3465        layer.set_value("ui.color", choice.to_string()).unwrap();
3466    }
3467    if args.quiet.unwrap_or_default() {
3468        layer.set_value("ui.quiet", true).unwrap();
3469    }
3470    if args.no_pager.unwrap_or_default() {
3471        layer.set_value("ui.paginate", "never").unwrap();
3472    }
3473    if !layer.is_empty() {
3474        config_layers.push(layer);
3475    }
3476    Ok((args, config_layers))
3477}
3478
3479fn handle_shell_completion(
3480    ui: &Ui,
3481    app: &Command,
3482    config: &StackedConfig,
3483    cwd: &Path,
3484) -> Result<(), CommandError> {
3485    let mut orig_args = env::args_os();
3486
3487    let mut args = vec![];
3488    // Take the first two arguments as is, they must be passed to clap_complete
3489    // without any changes. They are usually "jj --".
3490    args.extend(orig_args.by_ref().take(2));
3491
3492    // Make sure aliases are expanded before passing them to clap_complete. We
3493    // skip the first two args ("jj" and "--") for alias resolution, then we
3494    // stitch the args back together, like clap_complete expects them.
3495    if orig_args.len() > 0 {
3496        let complete_index: Option<usize> = env::var("_CLAP_COMPLETE_INDEX")
3497            .ok()
3498            .and_then(|s| s.parse().ok());
3499        let resolved_aliases = if let Some(index) = complete_index {
3500            // As of clap_complete 4.5.38, zsh completion script doesn't pad an
3501            // empty arg at the complete position. If the args doesn't include a
3502            // command name, the default command would be expanded at that
3503            // position. Therefore, no other command names would be suggested.
3504            let pad_len = usize::saturating_sub(index + 1, orig_args.len());
3505            let padded_args = orig_args
3506                .by_ref()
3507                .chain(std::iter::repeat_n(OsString::new(), pad_len));
3508
3509            // Expand aliases left of the completion index.
3510            let mut expanded_args = expand_args(ui, app, padded_args.take(index + 1), config)?;
3511
3512            // Adjust env var to compensate for shift of the completion point in the
3513            // expanded command line.
3514            // SAFETY: Program is running single-threaded at this point.
3515            unsafe {
3516                env::set_var(
3517                    "_CLAP_COMPLETE_INDEX",
3518                    (expanded_args.len() - 1).to_string(),
3519                );
3520            }
3521
3522            // Remove extra padding again to align with clap_complete's expectations for
3523            // zsh.
3524            let split_off_padding = expanded_args.split_off(expanded_args.len() - pad_len);
3525            assert!(
3526                split_off_padding.iter().all(|s| s.is_empty()),
3527                "split-off padding should only consist of empty strings but was \
3528                 {split_off_padding:?}",
3529            );
3530
3531            // Append the remaining arguments to the right of the completion point.
3532            expanded_args.extend(to_string_args(orig_args)?);
3533            expanded_args
3534        } else {
3535            expand_args(ui, app, orig_args, config)?
3536        };
3537        args.extend(resolved_aliases.into_iter().map(OsString::from));
3538    }
3539    let ran_completion = clap_complete::CompleteEnv::with_factory(|| {
3540        app.clone()
3541            // for completing aliases
3542            .allow_external_subcommands(true)
3543    })
3544    .try_complete(args.iter(), Some(cwd))?;
3545    assert!(
3546        ran_completion,
3547        "This function should not be called without the COMPLETE variable set."
3548    );
3549    Ok(())
3550}
3551
3552pub fn expand_args(
3553    ui: &Ui,
3554    app: &Command,
3555    args_os: impl IntoIterator<Item = OsString>,
3556    config: &StackedConfig,
3557) -> Result<Vec<String>, CommandError> {
3558    let string_args = to_string_args(args_os)?;
3559    let string_args = resolve_default_command(ui, config, app, string_args)?;
3560    resolve_aliases(ui, config, app, string_args)
3561}
3562
3563fn to_string_args(
3564    args_os: impl IntoIterator<Item = OsString>,
3565) -> Result<Vec<String>, CommandError> {
3566    args_os
3567        .into_iter()
3568        .map(|arg_os| {
3569            arg_os
3570                .into_string()
3571                .map_err(|_| cli_error("Non-UTF-8 argument"))
3572        })
3573        .collect()
3574}
3575
3576fn parse_args(app: &Command, string_args: &[String]) -> Result<(ArgMatches, Args), clap::Error> {
3577    let matches = app
3578        .clone()
3579        .arg_required_else_help(true)
3580        .subcommand_required(true)
3581        .try_get_matches_from(string_args)?;
3582    let args = Args::from_arg_matches(&matches).unwrap();
3583    Ok((matches, args))
3584}
3585
3586fn command_name(mut matches: &ArgMatches) -> String {
3587    let mut command = String::new();
3588    while let Some((subcommand, new_matches)) = matches.subcommand() {
3589        if !command.is_empty() {
3590            command.push(' ');
3591        }
3592        command.push_str(subcommand);
3593        matches = new_matches;
3594    }
3595    command
3596}
3597
3598pub fn format_template<C: Clone>(ui: &Ui, arg: &C, template: &TemplateRenderer<C>) -> String {
3599    let mut output = vec![];
3600    template
3601        .format(arg, ui.new_formatter(&mut output).as_mut())
3602        .expect("write() to vec backed formatter should never fail");
3603    // Template output is usually UTF-8, but it can contain file content.
3604    output.into_string_lossy()
3605}
3606
3607/// CLI command builder and runner.
3608#[must_use]
3609pub struct CliRunner<'a> {
3610    tracing_subscription: TracingSubscription,
3611    app: Command,
3612    config_layers: Vec<ConfigLayer>,
3613    config_migrations: Vec<ConfigMigrationRule>,
3614    store_factories: StoreFactories,
3615    working_copy_factories: WorkingCopyFactories,
3616    workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
3617    revset_extensions: RevsetExtensions,
3618    commit_template_extensions: Vec<Arc<dyn CommitTemplateLanguageExtension>>,
3619    operation_template_extensions: Vec<Arc<dyn OperationTemplateLanguageExtension>>,
3620    dispatch_fn: CliDispatchFn<'a>,
3621    dispatch_hook_fns: Vec<CliDispatchHookFn<'a>>,
3622    process_global_args_fns: Vec<ProcessGlobalArgsFn<'a>>,
3623}
3624
3625pub type CliDispatchFn<'a> =
3626    Box<dyn FnOnce(&mut Ui, &CommandHelper) -> Result<(), CommandError> + 'a>;
3627
3628type CliDispatchHookFn<'a> =
3629    Box<dyn FnOnce(&mut Ui, &CommandHelper, CliDispatchFn<'a>) -> Result<(), CommandError> + 'a>;
3630
3631type ProcessGlobalArgsFn<'a> =
3632    Box<dyn FnOnce(&mut Ui, &ArgMatches) -> Result<(), CommandError> + 'a>;
3633
3634impl<'a> CliRunner<'a> {
3635    /// Initializes CLI environment and returns a builder. This should be called
3636    /// as early as possible.
3637    pub fn init() -> Self {
3638        let tracing_subscription = TracingSubscription::init();
3639        crate::cleanup_guard::init();
3640        Self {
3641            tracing_subscription,
3642            app: crate::commands::default_app(),
3643            config_layers: crate::config::default_config_layers(),
3644            config_migrations: crate::config::default_config_migrations(),
3645            store_factories: StoreFactories::default(),
3646            working_copy_factories: default_working_copy_factories(),
3647            workspace_loader_factory: Box::new(DefaultWorkspaceLoaderFactory),
3648            revset_extensions: Default::default(),
3649            commit_template_extensions: vec![],
3650            operation_template_extensions: vec![],
3651            dispatch_fn: Box::new(crate::commands::run_command),
3652            dispatch_hook_fns: vec![],
3653            process_global_args_fns: vec![],
3654        }
3655    }
3656
3657    /// Set the name of the CLI application to be displayed in help messages.
3658    pub fn name(mut self, name: &str) -> Self {
3659        self.app = self.app.name(name.to_string());
3660        self
3661    }
3662
3663    /// Set the about message to be displayed in help messages.
3664    pub fn about(mut self, about: &str) -> Self {
3665        self.app = self.app.about(about.to_string());
3666        self
3667    }
3668
3669    /// Set the version to be displayed by `jj version`.
3670    pub fn version(mut self, version: &str) -> Self {
3671        self.app = self.app.version(version.to_string());
3672        self
3673    }
3674
3675    /// Adds default configs in addition to the normal defaults.
3676    ///
3677    /// The `layer.source` must be `Default`. Other sources such as `User` would
3678    /// be replaced by loaded configuration.
3679    pub fn add_extra_config(mut self, layer: ConfigLayer) -> Self {
3680        assert_eq!(layer.source, ConfigSource::Default);
3681        self.config_layers.push(layer);
3682        self
3683    }
3684
3685    /// Adds config migration rule in addition to the default rules.
3686    pub fn add_extra_config_migration(mut self, rule: ConfigMigrationRule) -> Self {
3687        self.config_migrations.push(rule);
3688        self
3689    }
3690
3691    /// Adds `StoreFactories` to be used.
3692    pub fn add_store_factories(mut self, store_factories: StoreFactories) -> Self {
3693        self.store_factories.merge(store_factories);
3694        self
3695    }
3696
3697    /// Adds working copy factories to be used.
3698    pub fn add_working_copy_factories(
3699        mut self,
3700        working_copy_factories: WorkingCopyFactories,
3701    ) -> Self {
3702        merge_factories_map(&mut self.working_copy_factories, working_copy_factories);
3703        self
3704    }
3705
3706    pub fn set_workspace_loader_factory(
3707        mut self,
3708        workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
3709    ) -> Self {
3710        self.workspace_loader_factory = workspace_loader_factory;
3711        self
3712    }
3713
3714    pub fn add_symbol_resolver_extension(
3715        mut self,
3716        symbol_resolver: Box<dyn SymbolResolverExtension>,
3717    ) -> Self {
3718        self.revset_extensions.add_symbol_resolver(symbol_resolver);
3719        self
3720    }
3721
3722    pub fn add_revset_function_extension(
3723        mut self,
3724        name: &'static str,
3725        func: RevsetFunction,
3726    ) -> Self {
3727        self.revset_extensions.add_custom_function(name, func);
3728        self
3729    }
3730
3731    pub fn add_commit_template_extension(
3732        mut self,
3733        commit_template_extension: Box<dyn CommitTemplateLanguageExtension>,
3734    ) -> Self {
3735        self.commit_template_extensions
3736            .push(commit_template_extension.into());
3737        self
3738    }
3739
3740    pub fn add_operation_template_extension(
3741        mut self,
3742        operation_template_extension: Box<dyn OperationTemplateLanguageExtension>,
3743    ) -> Self {
3744        self.operation_template_extensions
3745            .push(operation_template_extension.into());
3746        self
3747    }
3748
3749    /// Add a hook that gets called when it's time to run the command. It is
3750    /// the hook's responsibility to call the given inner dispatch function to
3751    /// run the command.
3752    pub fn add_dispatch_hook<F>(mut self, dispatch_hook_fn: F) -> Self
3753    where
3754        F: FnOnce(&mut Ui, &CommandHelper, CliDispatchFn) -> Result<(), CommandError> + 'a,
3755    {
3756        self.dispatch_hook_fns.push(Box::new(dispatch_hook_fn));
3757        self
3758    }
3759
3760    /// Registers new subcommands in addition to the default ones.
3761    pub fn add_subcommand<C, F>(mut self, custom_dispatch_fn: F) -> Self
3762    where
3763        C: clap::Subcommand,
3764        F: FnOnce(&mut Ui, &CommandHelper, C) -> Result<(), CommandError> + 'a,
3765    {
3766        let old_dispatch_fn = self.dispatch_fn;
3767        let new_dispatch_fn =
3768            move |ui: &mut Ui, command_helper: &CommandHelper| match C::from_arg_matches(
3769                command_helper.matches(),
3770            ) {
3771                Ok(command) => custom_dispatch_fn(ui, command_helper, command),
3772                Err(_) => old_dispatch_fn(ui, command_helper),
3773            };
3774        self.app = C::augment_subcommands(self.app);
3775        self.dispatch_fn = Box::new(new_dispatch_fn);
3776        self
3777    }
3778
3779    /// Registers new global arguments in addition to the default ones.
3780    pub fn add_global_args<A, F>(mut self, process_before: F) -> Self
3781    where
3782        A: clap::Args,
3783        F: FnOnce(&mut Ui, A) -> Result<(), CommandError> + 'a,
3784    {
3785        let process_global_args_fn = move |ui: &mut Ui, matches: &ArgMatches| {
3786            let custom_args = A::from_arg_matches(matches).unwrap();
3787            process_before(ui, custom_args)
3788        };
3789        self.app = A::augment_args(self.app);
3790        self.process_global_args_fns
3791            .push(Box::new(process_global_args_fn));
3792        self
3793    }
3794
3795    #[instrument(skip_all)]
3796    fn run_internal(self, ui: &mut Ui, mut raw_config: RawConfig) -> Result<(), CommandError> {
3797        // `cwd` is canonicalized for consistency with `Workspace::workspace_root()` and
3798        // to easily compute relative paths between them.
3799        let cwd = env::current_dir()
3800            .and_then(dunce::canonicalize)
3801            .map_err(|_| {
3802                user_error_with_hint(
3803                    "Could not determine current directory",
3804                    "Did you update to a commit where the directory doesn't exist?",
3805                )
3806            })?;
3807        let mut config_env = ConfigEnv::from_environment(ui);
3808        let mut last_config_migration_descriptions = Vec::new();
3809        let mut migrate_config = |config: &mut StackedConfig| -> Result<(), CommandError> {
3810            last_config_migration_descriptions =
3811                jj_lib::config::migrate(config, &self.config_migrations)?;
3812            Ok(())
3813        };
3814        // Use cwd-relative workspace configs to resolve default command and
3815        // aliases. WorkspaceLoader::init() won't do any heavy lifting other
3816        // than the path resolution.
3817        let maybe_cwd_workspace_loader = self
3818            .workspace_loader_factory
3819            .create(find_workspace_dir(&cwd))
3820            .map_err(|err| map_workspace_load_error(err, Some(".")));
3821        config_env.reload_user_config(&mut raw_config)?;
3822        if let Ok(loader) = &maybe_cwd_workspace_loader {
3823            config_env.reset_repo_path(loader.repo_path());
3824            config_env.reload_repo_config(&mut raw_config)?;
3825        }
3826        let mut config = config_env.resolve_config(&raw_config)?;
3827        migrate_config(&mut config)?;
3828        ui.reset(&config)?;
3829
3830        if env::var_os("COMPLETE").is_some() {
3831            return handle_shell_completion(&Ui::null(), &self.app, &config, &cwd);
3832        }
3833
3834        let string_args = expand_args(ui, &self.app, env::args_os(), &config)?;
3835        let (args, config_layers) = parse_early_args(&self.app, &string_args)?;
3836        if !config_layers.is_empty() {
3837            raw_config.as_mut().extend_layers(config_layers);
3838            config = config_env.resolve_config(&raw_config)?;
3839            migrate_config(&mut config)?;
3840            ui.reset(&config)?;
3841        }
3842
3843        if args.has_config_args() {
3844            warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
3845        }
3846
3847        let (matches, args) = parse_args(&self.app, &string_args)
3848            .map_err(|err| map_clap_cli_error(err, ui, &config))?;
3849        if args.global_args.debug {
3850            // TODO: set up debug logging as early as possible
3851            self.tracing_subscription.enable_debug_logging()?;
3852        }
3853        for process_global_args_fn in self.process_global_args_fns {
3854            process_global_args_fn(ui, &matches)?;
3855        }
3856        config_env.set_command_name(command_name(&matches));
3857
3858        let maybe_workspace_loader = if let Some(path) = &args.global_args.repository {
3859            // TODO: maybe path should be canonicalized by WorkspaceLoader?
3860            let abs_path = cwd.join(path);
3861            let abs_path = dunce::canonicalize(&abs_path).unwrap_or(abs_path);
3862            // Invalid -R path is an error. No need to proceed.
3863            let loader = self
3864                .workspace_loader_factory
3865                .create(&abs_path)
3866                .map_err(|err| map_workspace_load_error(err, Some(path)))?;
3867            config_env.reset_repo_path(loader.repo_path());
3868            config_env.reload_repo_config(&mut raw_config)?;
3869            Ok(loader)
3870        } else {
3871            maybe_cwd_workspace_loader
3872        };
3873
3874        // Apply workspace configs, --config arguments, and --when.commands.
3875        config = config_env.resolve_config(&raw_config)?;
3876        migrate_config(&mut config)?;
3877        ui.reset(&config)?;
3878
3879        // Print only the last migration messages to omit duplicates.
3880        for (source, desc) in &last_config_migration_descriptions {
3881            let source_str = match source {
3882                ConfigSource::Default => "default-provided",
3883                ConfigSource::EnvBase | ConfigSource::EnvOverrides => "environment-provided",
3884                ConfigSource::User => "user-level",
3885                ConfigSource::Repo => "repo-level",
3886                ConfigSource::CommandArg => "CLI-provided",
3887            };
3888            writeln!(
3889                ui.warning_default(),
3890                "Deprecated {source_str} config: {desc}"
3891            )?;
3892        }
3893
3894        if args.global_args.repository.is_some() {
3895            warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
3896        }
3897
3898        let settings = UserSettings::from_config(config)?;
3899        let command_helper_data = CommandHelperData {
3900            app: self.app,
3901            cwd,
3902            string_args,
3903            matches,
3904            global_args: args.global_args,
3905            config_env,
3906            config_migrations: self.config_migrations,
3907            raw_config,
3908            settings,
3909            revset_extensions: self.revset_extensions.into(),
3910            commit_template_extensions: self.commit_template_extensions,
3911            operation_template_extensions: self.operation_template_extensions,
3912            maybe_workspace_loader,
3913            store_factories: self.store_factories,
3914            working_copy_factories: self.working_copy_factories,
3915            workspace_loader_factory: self.workspace_loader_factory,
3916        };
3917        let command_helper = CommandHelper {
3918            data: Rc::new(command_helper_data),
3919        };
3920        let dispatch_fn = self.dispatch_hook_fns.into_iter().fold(
3921            self.dispatch_fn,
3922            |old_dispatch_fn, dispatch_hook_fn| {
3923                Box::new(move |ui: &mut Ui, command_helper: &CommandHelper| {
3924                    dispatch_hook_fn(ui, command_helper, old_dispatch_fn)
3925                })
3926            },
3927        );
3928        (dispatch_fn)(ui, &command_helper)
3929    }
3930
3931    #[must_use]
3932    #[instrument(skip(self))]
3933    pub fn run(mut self) -> u8 {
3934        // Tell crossterm to ignore NO_COLOR (we check it ourselves)
3935        crossterm::style::force_color_output(true);
3936        let config = config_from_environment(self.config_layers.drain(..));
3937        // Set up ui assuming the default config has no conditional variables.
3938        // If it had, the configuration will be fixed by the next ui.reset().
3939        let mut ui = Ui::with_config(config.as_ref())
3940            .expect("default config should be valid, env vars are stringly typed");
3941        let result = self.run_internal(&mut ui, config);
3942        let exit_code = handle_command_result(&mut ui, result);
3943        ui.finalize_pager();
3944        exit_code
3945    }
3946}
3947
3948fn map_clap_cli_error(err: clap::Error, ui: &Ui, config: &StackedConfig) -> CommandError {
3949    if let Some(ContextValue::String(cmd)) = err.get(ContextKind::InvalidSubcommand) {
3950        let remove_useless_error_context = |mut err: clap::Error| {
3951            // Clap suggests unhelpful subcommands, e.g. `config` for `clone`.
3952            // We don't want suggestions when we know this isn't a misspelling.
3953            err.remove(ContextKind::SuggestedSubcommand);
3954            err.remove(ContextKind::Suggested); // Remove an empty line
3955            err.remove(ContextKind::Usage); // Also unhelpful for these errors.
3956            err
3957        };
3958        match cmd.as_str() {
3959            // git commands that a brand-new user might type during their first
3960            // experiments with `jj`
3961            "clone" | "init" => {
3962                let cmd = cmd.clone();
3963                return CommandError::from(remove_useless_error_context(err))
3964                    .hinted(format!(
3965                        "You probably want `jj git {cmd}`. See also `jj help git`."
3966                    ))
3967                    .hinted(format!(
3968                        r#"You can configure `aliases.{cmd} = ["git", "{cmd}"]` if you want `jj {cmd}` to work and always use the Git backend."#
3969                    ));
3970            }
3971            "amend" => {
3972                return CommandError::from(remove_useless_error_context(err))
3973                    .hinted(
3974                        r#"You probably want `jj squash`. You can configure `aliases.amend = ["squash"]` if you want `jj amend` to work."#);
3975            }
3976            _ => {}
3977        }
3978    }
3979    if let (Some(ContextValue::String(arg)), Some(ContextValue::String(value))) = (
3980        err.get(ContextKind::InvalidArg),
3981        err.get(ContextKind::InvalidValue),
3982    ) && arg.as_str() == "--template <TEMPLATE>"
3983        && value.is_empty()
3984    {
3985        // Suppress the error, it's less important than the original error.
3986        if let Ok(template_aliases) = load_template_aliases(ui, config) {
3987            return CommandError::from(err).hinted(format_template_aliases_hint(&template_aliases));
3988        }
3989    }
3990    CommandError::from(err)
3991}
3992
3993fn format_template_aliases_hint(template_aliases: &TemplateAliasesMap) -> String {
3994    let mut hint = String::from("The following template aliases are defined:\n");
3995    hint.push_str(
3996        &template_aliases
3997            .symbol_names()
3998            .sorted_unstable()
3999            .map(|name| format!("- {name}"))
4000            .join("\n"),
4001    );
4002    hint
4003}
4004
4005// If -R or --config* is specified, check if the expanded arguments differ.
4006fn warn_if_args_mismatch(
4007    ui: &Ui,
4008    app: &Command,
4009    config: &StackedConfig,
4010    expected_args: &[String],
4011) -> Result<(), CommandError> {
4012    let new_string_args = expand_args(ui, app, env::args_os(), config).ok();
4013    if new_string_args.as_deref() != Some(expected_args) {
4014        writeln!(
4015            ui.warning_default(),
4016            "Command aliases cannot be loaded from -R/--repository path or --config/--config-file \
4017             arguments."
4018        )?;
4019    }
4020    Ok(())
4021}
4022
4023#[cfg(test)]
4024mod tests {
4025    use clap::CommandFactory as _;
4026
4027    use super::*;
4028
4029    #[derive(clap::Parser, Clone, Debug)]
4030    pub struct TestArgs {
4031        #[arg(long)]
4032        pub foo: Vec<u32>,
4033        #[arg(long)]
4034        pub bar: Vec<u32>,
4035        #[arg(long)]
4036        pub baz: bool,
4037    }
4038
4039    #[test]
4040    fn test_merge_args_with() {
4041        let command = TestArgs::command();
4042        let parse = |args: &[&str]| -> Vec<(&'static str, u32)> {
4043            let matches = command.clone().try_get_matches_from(args).unwrap();
4044            let args = TestArgs::from_arg_matches(&matches).unwrap();
4045            merge_args_with(
4046                &matches,
4047                &[("foo", &args.foo), ("bar", &args.bar)],
4048                |id, value| (id, *value),
4049            )
4050        };
4051
4052        assert_eq!(parse(&["jj"]), vec![]);
4053        assert_eq!(parse(&["jj", "--foo=1"]), vec![("foo", 1)]);
4054        assert_eq!(
4055            parse(&["jj", "--foo=1", "--bar=2"]),
4056            vec![("foo", 1), ("bar", 2)]
4057        );
4058        assert_eq!(
4059            parse(&["jj", "--foo=1", "--baz", "--bar=2", "--foo", "3"]),
4060            vec![("foo", 1), ("bar", 2), ("foo", 3)]
4061        );
4062    }
4063}