Skip to main content

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