jj_cli/
cli_util.rs

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