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