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