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