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