1use 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::pin::Pin;
30use std::rc::Rc;
31use std::str::FromStr;
32use std::sync::Arc;
33use std::sync::LazyLock;
34use std::time::SystemTime;
35
36use bstr::ByteVec as _;
37use chrono::TimeZone as _;
38use clap::ArgAction;
39use clap::ArgMatches;
40use clap::Command;
41use clap::FromArgMatches as _;
42use clap::builder::MapValueParser;
43use clap::builder::NonEmptyStringValueParser;
44use clap::builder::TypedValueParser as _;
45use clap::builder::ValueParserFactory;
46use clap::error::ContextKind;
47use clap::error::ContextValue;
48use clap_complete::ArgValueCandidates;
49use clap_complete::ArgValueCompleter;
50use futures::TryStreamExt as _;
51use indexmap::IndexMap;
52use indexmap::IndexSet;
53use indoc::indoc;
54use indoc::writedoc;
55use itertools::Itertools as _;
56use jj_lib::backend::BackendResult;
57use jj_lib::backend::ChangeId;
58use jj_lib::backend::CommitId;
59use jj_lib::backend::TreeValue;
60use jj_lib::commit::Commit;
61use jj_lib::config::ConfigGetError;
62use jj_lib::config::ConfigGetResultExt as _;
63use jj_lib::config::ConfigLayer;
64use jj_lib::config::ConfigMigrationRule;
65use jj_lib::config::ConfigNamePathBuf;
66use jj_lib::config::ConfigSource;
67use jj_lib::config::ConfigValue;
68use jj_lib::config::StackedConfig;
69use jj_lib::conflicts::ConflictMarkerStyle;
70use jj_lib::fileset;
71use jj_lib::fileset::FilesetAliasesMap;
72use jj_lib::fileset::FilesetDiagnostics;
73use jj_lib::fileset::FilesetExpression;
74use jj_lib::fileset::FilesetParseContext;
75use jj_lib::gitignore::GitIgnoreError;
76use jj_lib::gitignore::GitIgnoreFile;
77use jj_lib::id_prefix::IdPrefixContext;
78use jj_lib::lock::FileLock;
79use jj_lib::matchers::Matcher;
80use jj_lib::matchers::NothingMatcher;
81use jj_lib::merge::Diff;
82use jj_lib::merge::MergedTreeValue;
83use jj_lib::merged_tree::MergedTree;
84use jj_lib::object_id::ObjectId as _;
85use jj_lib::op_heads_store;
86use jj_lib::op_store::OpStoreError;
87use jj_lib::op_store::OperationId;
88use jj_lib::op_store::RefTarget;
89use jj_lib::op_walk;
90use jj_lib::op_walk::OpsetEvaluationError;
91use jj_lib::operation::Operation;
92use jj_lib::ref_name::RefName;
93use jj_lib::ref_name::RefNameBuf;
94use jj_lib::ref_name::RemoteName;
95use jj_lib::ref_name::RemoteRefSymbol;
96use jj_lib::ref_name::WorkspaceName;
97use jj_lib::ref_name::WorkspaceNameBuf;
98use jj_lib::repo::CheckOutCommitError;
99use jj_lib::repo::EditCommitError;
100use jj_lib::repo::MutableRepo;
101use jj_lib::repo::ReadonlyRepo;
102use jj_lib::repo::Repo;
103use jj_lib::repo::RepoLoader;
104use jj_lib::repo::StoreFactories;
105use jj_lib::repo::StoreLoadError;
106use jj_lib::repo::merge_factories_map;
107use jj_lib::repo_path::RepoPath;
108use jj_lib::repo_path::RepoPathBuf;
109use jj_lib::repo_path::RepoPathUiConverter;
110use jj_lib::repo_path::UiPathParseError;
111use jj_lib::revset;
112use jj_lib::revset::ResolvedRevsetExpression;
113use jj_lib::revset::RevsetAliasesMap;
114use jj_lib::revset::RevsetDiagnostics;
115use jj_lib::revset::RevsetExpression;
116use jj_lib::revset::RevsetExtensions;
117use jj_lib::revset::RevsetFilterPredicate;
118use jj_lib::revset::RevsetFunction;
119use jj_lib::revset::RevsetParseContext;
120use jj_lib::revset::RevsetStreamExt as _;
121use jj_lib::revset::RevsetWorkspaceContext;
122use jj_lib::revset::SymbolResolverExtension;
123use jj_lib::revset::UserRevsetExpression;
124use jj_lib::rewrite::restore_tree;
125use jj_lib::settings::HumanByteSize;
126use jj_lib::settings::UserSettings;
127use jj_lib::store::Store;
128use jj_lib::str_util::StringExpression;
129use jj_lib::str_util::StringMatcher;
130use jj_lib::str_util::StringPattern;
131use jj_lib::transaction::Transaction;
132use jj_lib::working_copy;
133use jj_lib::working_copy::CheckoutStats;
134use jj_lib::working_copy::LockedWorkingCopy;
135use jj_lib::working_copy::SnapshotOptions;
136use jj_lib::working_copy::SnapshotStats;
137use jj_lib::working_copy::UntrackedReason;
138use jj_lib::working_copy::WorkingCopy;
139use jj_lib::working_copy::WorkingCopyFactory;
140use jj_lib::working_copy::WorkingCopyFreshness;
141use jj_lib::workspace::DefaultWorkspaceLoaderFactory;
142use jj_lib::workspace::LockedWorkspace;
143use jj_lib::workspace::WorkingCopyFactories;
144use jj_lib::workspace::Workspace;
145use jj_lib::workspace::WorkspaceLoadError;
146use jj_lib::workspace::WorkspaceLoader;
147use jj_lib::workspace::WorkspaceLoaderFactory;
148use jj_lib::workspace::default_working_copy_factories;
149use jj_lib::workspace::get_working_copy_factory;
150use pollster::FutureExt as _;
151use tracing::instrument;
152use tracing_chrome::ChromeLayerBuilder;
153use tracing_subscriber::prelude::*;
154
155use crate::command_error::CommandError;
156use crate::command_error::cli_error;
157use crate::command_error::config_error_with_message;
158use crate::command_error::handle_command_result;
159use crate::command_error::internal_error;
160use crate::command_error::internal_error_with_message;
161use crate::command_error::print_error_sources;
162use crate::command_error::print_parse_diagnostics;
163use crate::command_error::user_error;
164use crate::command_error::user_error_with_message;
165use crate::commit_templater::CommitTemplateLanguage;
166use crate::commit_templater::CommitTemplateLanguageExtension;
167use crate::complete;
168use crate::config::ConfigArgKind;
169use crate::config::ConfigEnv;
170use crate::config::RawConfig;
171use crate::config::config_from_environment;
172use crate::config::load_aliases_map;
173use crate::config::parse_config_args;
174use crate::description_util::TextEditor;
175use crate::diff_util;
176use crate::diff_util::DiffFormat;
177use crate::diff_util::DiffFormatArgs;
178use crate::diff_util::DiffRenderer;
179use crate::formatter::FormatRecorder;
180use crate::formatter::Formatter;
181use crate::formatter::FormatterExt as _;
182use crate::merge_tools::DiffEditor;
183use crate::merge_tools::MergeEditor;
184use crate::merge_tools::MergeToolConfigError;
185use crate::operation_templater::OperationTemplateLanguage;
186use crate::operation_templater::OperationTemplateLanguageExtension;
187use crate::revset_util;
188use crate::revset_util::RevsetExpressionEvaluator;
189use crate::revset_util::parse_union_name_patterns;
190use crate::template_builder;
191use crate::template_builder::TemplateLanguage;
192use crate::template_parser::TemplateAliasesMap;
193use crate::template_parser::TemplateDiagnostics;
194use crate::templater::TemplateRenderer;
195use crate::templater::WrapTemplateProperty;
196use crate::text_util;
197use crate::ui::ColorChoice;
198use crate::ui::Ui;
199
200const SHORT_CHANGE_ID_TEMPLATE_TEXT: &str = "format_short_change_id_with_change_offset(self)";
201
202#[derive(Clone)]
203struct ChromeTracingFlushGuard {
204 _inner: Option<Rc<tracing_chrome::FlushGuard>>,
205}
206
207impl Debug for ChromeTracingFlushGuard {
208 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
209 let Self { _inner } = self;
210 f.debug_struct("ChromeTracingFlushGuard")
211 .finish_non_exhaustive()
212 }
213}
214
215#[derive(Clone, Debug)]
217pub struct TracingSubscription {
218 reload_log_filter: tracing_subscriber::reload::Handle<
219 tracing_subscriber::EnvFilter,
220 tracing_subscriber::Registry,
221 >,
222 _chrome_tracing_flush_guard: ChromeTracingFlushGuard,
223}
224
225impl TracingSubscription {
226 const ENV_VAR_NAME: &str = "JJ_LOG";
227
228 pub fn init() -> Self {
231 let filter = tracing_subscriber::EnvFilter::builder()
232 .with_default_directive(tracing::metadata::LevelFilter::ERROR.into())
233 .with_env_var(Self::ENV_VAR_NAME)
234 .from_env_lossy();
235 let (filter, reload_log_filter) = tracing_subscriber::reload::Layer::new(filter);
236
237 let (chrome_tracing_layer, chrome_tracing_flush_guard) = match std::env::var("JJ_TRACE") {
238 Ok(filename) => {
239 let filename = if filename.is_empty() {
240 format!(
241 "jj-trace-{}.json",
242 SystemTime::now()
243 .duration_since(SystemTime::UNIX_EPOCH)
244 .unwrap()
245 .as_secs(),
246 )
247 } else {
248 filename
249 };
250 let include_args = std::env::var("JJ_TRACE_INCLUDE_ARGS").is_ok();
251 let (layer, guard) = ChromeLayerBuilder::new()
252 .file(filename)
253 .include_args(include_args)
254 .build();
255 (
256 Some(layer),
257 ChromeTracingFlushGuard {
258 _inner: Some(Rc::new(guard)),
259 },
260 )
261 }
262 Err(_) => (None, ChromeTracingFlushGuard { _inner: None }),
263 };
264
265 tracing_subscriber::registry()
266 .with(
267 tracing_subscriber::fmt::Layer::default()
268 .with_writer(std::io::stderr)
269 .with_filter(filter),
270 )
271 .with(chrome_tracing_layer)
272 .init();
273 Self {
274 reload_log_filter,
275 _chrome_tracing_flush_guard: chrome_tracing_flush_guard,
276 }
277 }
278
279 pub fn enable_debug_logging(&self) -> Result<(), CommandError> {
280 self.reload_log_filter
281 .modify(|filter| {
282 *filter = tracing_subscriber::EnvFilter::builder()
286 .with_default_directive(tracing::metadata::LevelFilter::INFO.into())
287 .with_env_var(Self::ENV_VAR_NAME)
288 .from_env_lossy()
289 .add_directive("jj_lib=debug".parse().unwrap())
290 .add_directive("jj_cli=debug".parse().unwrap());
291 })
292 .map_err(|err| internal_error_with_message("failed to enable debug logging", err))?;
293 tracing::info!("debug logging enabled");
294 Ok(())
295 }
296}
297
298#[derive(Clone)]
299pub struct CommandHelper {
300 data: Rc<CommandHelperData>,
301}
302
303struct CommandHelperData {
304 app: Command,
305 cwd: PathBuf,
306 string_args: Vec<String>,
307 matches: ArgMatches,
308 global_args: GlobalArgs,
309 config_env: ConfigEnv,
310 config_migrations: Vec<ConfigMigrationRule>,
311 raw_config: RawConfig,
312 settings: UserSettings,
313 revset_extensions: Arc<RevsetExtensions>,
314 commit_template_extensions: Vec<Arc<dyn CommitTemplateLanguageExtension>>,
315 operation_template_extensions: Vec<Arc<dyn OperationTemplateLanguageExtension>>,
316 maybe_workspace_loader: Result<Box<dyn WorkspaceLoader>, CommandError>,
317 store_factories: StoreFactories,
318 working_copy_factories: WorkingCopyFactories,
319 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
320}
321
322impl CommandHelper {
323 pub fn app(&self) -> &Command {
324 &self.data.app
325 }
326
327 pub fn cwd(&self) -> &Path {
332 &self.data.cwd
333 }
334
335 pub fn string_args(&self) -> &Vec<String> {
336 &self.data.string_args
337 }
338
339 pub fn matches(&self) -> &ArgMatches {
340 &self.data.matches
341 }
342
343 pub fn global_args(&self) -> &GlobalArgs {
344 &self.data.global_args
345 }
346
347 pub fn config_env(&self) -> &ConfigEnv {
348 &self.data.config_env
349 }
350
351 pub fn raw_config(&self) -> &RawConfig {
356 &self.data.raw_config
357 }
358
359 pub fn settings(&self) -> &UserSettings {
365 &self.data.settings
366 }
367
368 pub fn settings_for_new_workspace(
370 &self,
371 ui: &Ui,
372 workspace_root: &Path,
373 ) -> Result<(UserSettings, ConfigEnv), CommandError> {
374 let mut config_env = self.data.config_env.clone();
375 let mut raw_config = self.data.raw_config.clone();
376 let repo_path = workspace_root.join(".jj").join("repo");
377 config_env.reset_repo_path(&repo_path);
378 config_env.reload_repo_config(ui, &mut raw_config)?;
379 config_env.reset_workspace_path(workspace_root);
380 config_env.reload_workspace_config(ui, &mut raw_config)?;
381 let mut config = config_env.resolve_config(&raw_config)?;
382 jj_lib::config::migrate(&mut config, &self.data.config_migrations)?;
384 Ok((self.data.settings.with_new_config(config)?, config_env))
385 }
386
387 pub fn text_editor(&self) -> Result<TextEditor, ConfigGetError> {
389 TextEditor::from_settings(self.settings())
390 }
391
392 pub fn revset_extensions(&self) -> &Arc<RevsetExtensions> {
393 &self.data.revset_extensions
394 }
395
396 pub fn parse_template<'a, C, L>(
402 &self,
403 ui: &Ui,
404 language: &L,
405 template_text: &str,
406 ) -> Result<TemplateRenderer<'a, C>, CommandError>
407 where
408 C: Clone + 'a,
409 L: TemplateLanguage<'a> + ?Sized,
410 L::Property: WrapTemplateProperty<'a, C>,
411 {
412 let mut diagnostics = TemplateDiagnostics::new();
413 let aliases = load_template_aliases(ui, self.settings().config())?;
414 let template =
415 template_builder::parse(language, &mut diagnostics, template_text, &aliases)?;
416 print_parse_diagnostics(ui, "In template expression", &diagnostics)?;
417 Ok(template)
418 }
419
420 pub fn workspace_loader(&self) -> Result<&dyn WorkspaceLoader, CommandError> {
421 self.data
422 .maybe_workspace_loader
423 .as_deref()
424 .map_err(Clone::clone)
425 }
426
427 fn new_workspace_loader_at(
428 &self,
429 workspace_root: &Path,
430 ) -> Result<Box<dyn WorkspaceLoader>, CommandError> {
431 self.data
432 .workspace_loader_factory
433 .create(workspace_root)
434 .map_err(|err| map_workspace_load_error(err, None))
435 }
436
437 #[instrument(skip(self, ui))]
439 pub fn workspace_helper(&self, ui: &Ui) -> Result<WorkspaceCommandHelper, CommandError> {
440 let (workspace_command, stats) = self.workspace_helper_with_stats(ui)?;
441 print_snapshot_stats(ui, &stats, workspace_command.env().path_converter())?;
442 Ok(workspace_command)
443 }
444
445 #[instrument(skip(self, ui))]
452 pub fn workspace_helper_with_stats(
453 &self,
454 ui: &Ui,
455 ) -> Result<(WorkspaceCommandHelper, SnapshotStats), CommandError> {
456 let mut workspace_command = self.workspace_helper_no_snapshot(ui)?;
457
458 let (workspace_command, stats) = match workspace_command.maybe_snapshot_impl(ui).block_on()
459 {
460 Ok(stats) => (workspace_command, stats),
461 Err(SnapshotWorkingCopyError::Command(err)) => return Err(err),
462 Err(SnapshotWorkingCopyError::StaleWorkingCopy(err)) => {
463 let auto_update_stale = self.settings().get_bool("snapshot.auto-update-stale")?;
464 if !auto_update_stale {
465 return Err(err);
466 }
467
468 self.recover_stale_working_copy(ui).block_on()?
473 }
474 };
475
476 Ok((workspace_command, stats))
477 }
478
479 #[instrument(skip(self, ui))]
482 pub fn workspace_helper_no_snapshot(
483 &self,
484 ui: &Ui,
485 ) -> Result<WorkspaceCommandHelper, CommandError> {
486 let workspace = self.load_workspace()?;
487 let op_head =
488 self.resolve_operation(ui, workspace.repo_loader(), workspace.workspace_name())?;
489 let repo = workspace.repo_loader().load_at(&op_head).block_on()?;
490 let mut env = self.workspace_environment(ui, &workspace)?;
491 if let Err(err) =
492 revset_util::try_resolve_trunk_alias(repo.as_ref(), &env.revset_parse_context())
493 {
494 let fallback = "root()";
497 writeln!(
498 ui.warning_default(),
499 "Failed to resolve `revset-aliases.trunk()`: {err}"
500 )?;
501 writeln!(
502 ui.warning_no_heading(),
503 "The `trunk()` alias is temporarily set to `{fallback}`."
504 )?;
505 writeln!(
506 ui.hint_default(),
507 "Use `jj config edit --repo` to adjust the `trunk()` alias."
508 )?;
509 env.revset_aliases_map
510 .insert("trunk()", fallback)
511 .expect("valid syntax");
512 env.reload_revset_expressions(ui)?;
513 }
514 WorkspaceCommandHelper::new(ui, workspace, repo, env, self.is_at_head_operation())
515 }
516
517 pub fn get_working_copy_factory(&self) -> Result<&dyn WorkingCopyFactory, CommandError> {
518 let loader = self.workspace_loader()?;
519
520 let factory: Result<_, WorkspaceLoadError> =
522 get_working_copy_factory(loader, &self.data.working_copy_factories)
523 .map_err(|e| e.into());
524 let factory = factory.map_err(|err| {
525 map_workspace_load_error(err, self.data.global_args.repository.as_deref())
526 })?;
527 Ok(factory)
528 }
529
530 #[instrument(skip_all)]
532 pub fn load_workspace(&self) -> Result<Workspace, CommandError> {
533 let loader = self.workspace_loader()?;
534 loader
535 .load(
536 &self.data.settings,
537 &self.data.store_factories,
538 &self.data.working_copy_factories,
539 )
540 .map_err(|err| {
541 map_workspace_load_error(err, self.data.global_args.repository.as_deref())
542 })
543 }
544
545 #[instrument(skip(self, settings))]
547 pub fn load_workspace_at(
548 &self,
549 workspace_root: &Path,
550 settings: &UserSettings,
551 ) -> Result<Workspace, CommandError> {
552 let loader = self.new_workspace_loader_at(workspace_root)?;
553 loader
554 .load(
555 settings,
556 &self.data.store_factories,
557 &self.data.working_copy_factories,
558 )
559 .map_err(|err| map_workspace_load_error(err, None))
560 }
561
562 pub async fn recover_stale_working_copy(
566 &self,
567 ui: &Ui,
568 ) -> Result<(WorkspaceCommandHelper, SnapshotStats), CommandError> {
569 let workspace = self.load_workspace()?;
570 let op_id = workspace.working_copy().operation_id();
571
572 match workspace.repo_loader().load_operation(op_id).await {
573 Ok(op) => {
574 let repo = workspace.repo_loader().load_at(&op).await?;
575 let mut workspace_command = self.for_workable_repo(ui, workspace, repo)?;
576 workspace_command.check_working_copy_writable()?;
577
578 let stale_stats = workspace_command
583 .snapshot_working_copy(ui)
584 .await
585 .map_err(|err| err.into_command_error())?;
586
587 let wc_commit_id = workspace_command.get_wc_commit_id().unwrap();
588 let repo = workspace_command.repo().clone();
589 let stale_wc_commit = repo.store().get_commit_async(wc_commit_id).await?;
590
591 let mut workspace_command = self.workspace_helper_no_snapshot(ui)?;
592
593 let repo = workspace_command.repo().clone();
594 let (mut locked_ws, desired_wc_commit) =
595 workspace_command.unchecked_start_working_copy_mutation()?;
596 match WorkingCopyFreshness::check_stale(
597 locked_ws.locked_wc(),
598 &desired_wc_commit,
599 &repo,
600 )
601 .await?
602 {
603 WorkingCopyFreshness::Fresh | WorkingCopyFreshness::Updated(_) => {
604 drop(locked_ws);
605 writeln!(
606 ui.status(),
607 "Attempted recovery, but the working copy is not stale"
608 )?;
609 }
610 WorkingCopyFreshness::WorkingCopyStale
611 | WorkingCopyFreshness::SiblingOperation => {
612 let stats = update_stale_working_copy(
613 locked_ws,
614 repo.op_id().clone(),
615 &stale_wc_commit,
616 &desired_wc_commit,
617 )
618 .await?;
619 workspace_command.print_updated_working_copy_stats(
620 ui,
621 Some(&stale_wc_commit),
622 &desired_wc_commit,
623 &stats,
624 )?;
625 writeln!(
626 ui.status(),
627 "Updated working copy to fresh commit {}",
628 short_commit_hash(desired_wc_commit.id())
629 )?;
630 }
631 }
632
633 let fresh_stats = workspace_command
638 .maybe_snapshot_impl(ui)
639 .await
640 .map_err(|err| err.into_command_error())?;
641 let merged_stats = {
642 let SnapshotStats {
643 mut untracked_paths,
644 } = stale_stats;
645 untracked_paths.extend(fresh_stats.untracked_paths);
646 SnapshotStats { untracked_paths }
647 };
648 Ok((workspace_command, merged_stats))
649 }
650 Err(e @ OpStoreError::ObjectNotFound { .. }) => {
651 writeln!(
652 ui.status(),
653 "Failed to read working copy's current operation; attempting recovery. Error \
654 message from read attempt: {e}"
655 )?;
656
657 let mut workspace_command = self.workspace_helper_no_snapshot(ui)?;
658 let stats = workspace_command
659 .create_and_check_out_recovery_commit(ui)
660 .await?;
661 Ok((workspace_command, stats))
662 }
663 Err(e) => Err(e.into()),
664 }
665 }
666
667 pub fn workspace_environment(
669 &self,
670 ui: &Ui,
671 workspace: &Workspace,
672 ) -> Result<WorkspaceCommandEnvironment, CommandError> {
673 WorkspaceCommandEnvironment::new(ui, self, workspace)
674 }
675
676 pub fn is_working_copy_writable(&self) -> bool {
679 self.is_at_head_operation() && !self.data.global_args.ignore_working_copy
680 }
681
682 pub fn is_at_head_operation(&self) -> bool {
684 matches!(
687 self.data.global_args.at_operation.as_deref(),
688 None | Some("@")
689 )
690 }
691
692 #[instrument(skip_all)]
697 pub fn resolve_operation(
698 &self,
699 ui: &Ui,
700 repo_loader: &RepoLoader,
701 workspace_name: &WorkspaceName,
702 ) -> Result<Operation, CommandError> {
703 if let Some(op_str) = &self.data.global_args.at_operation {
704 Ok(op_walk::resolve_op_for_load(repo_loader, op_str).block_on()?)
705 } else {
706 op_heads_store::resolve_op_heads(
707 repo_loader.op_heads_store().as_ref(),
708 repo_loader.op_store(),
709 async |op_heads| {
710 writeln!(
711 ui.status(),
712 "Concurrent modification detected, resolving automatically.",
713 )?;
714 let base_repo = repo_loader.load_at(&op_heads[0]).block_on()?;
715 let mut tx =
717 start_repo_transaction(&base_repo, workspace_name, &self.data.string_args);
718 for other_op_head in op_heads.into_iter().skip(1) {
719 tx.merge_operation(other_op_head).await?;
720 let num_rebased = tx.repo_mut().rebase_descendants().await?;
721 if num_rebased > 0 {
722 writeln!(
723 ui.status(),
724 "Rebased {num_rebased} descendant commits onto commits rewritten \
725 by other operation"
726 )?;
727 }
728 }
729 Ok(tx
730 .write("reconcile divergent operations")
731 .await?
732 .leave_unpublished()
733 .operation()
734 .clone())
735 },
736 )
737 .block_on()
738 }
739 }
740
741 #[instrument(skip_all)]
745 pub fn for_workable_repo(
746 &self,
747 ui: &Ui,
748 workspace: Workspace,
749 repo: Arc<ReadonlyRepo>,
750 ) -> Result<WorkspaceCommandHelper, CommandError> {
751 let env = self.workspace_environment(ui, &workspace)?;
752 let loaded_at_head = true;
753 WorkspaceCommandHelper::new(ui, workspace, repo, env, loaded_at_head)
754 }
755}
756
757struct ReadonlyUserRepo {
760 repo: Arc<ReadonlyRepo>,
761 id_prefix_context: OnceCell<IdPrefixContext>,
762}
763
764impl ReadonlyUserRepo {
765 fn new(repo: Arc<ReadonlyRepo>) -> Self {
766 Self {
767 repo,
768 id_prefix_context: OnceCell::new(),
769 }
770 }
771}
772
773pub struct AdvanceableBookmark {
783 name: RefNameBuf,
784 old_commit_id: CommitId,
785}
786
787fn load_advance_bookmarks_matcher(
798 ui: &Ui,
799 settings: &UserSettings,
800) -> Result<Option<StringMatcher>, CommandError> {
801 let get_setting = |setting_key: &str| -> Result<Vec<String>, _> {
802 let name = ConfigNamePathBuf::from_iter(["experimental-advance-branches", setting_key]);
803 settings.get(&name)
804 };
805 let enabled_names = get_setting("enabled-branches")?;
808 let disabled_names = get_setting("disabled-branches")?;
809 let enabled_expr = parse_union_name_patterns(ui, &enabled_names)?;
810 let disabled_expr = parse_union_name_patterns(ui, &disabled_names)?;
811 if enabled_names.is_empty() {
812 Ok(None)
813 } else {
814 let expr = enabled_expr.intersection(disabled_expr.negated());
815 Ok(Some(expr.to_matcher()))
816 }
817}
818
819pub struct WorkspaceCommandEnvironment {
821 command: CommandHelper,
822 settings: UserSettings,
823 fileset_aliases_map: FilesetAliasesMap,
824 revset_aliases_map: RevsetAliasesMap,
825 template_aliases_map: TemplateAliasesMap,
826 default_ignored_remote: Option<&'static RemoteName>,
827 revsets_use_glob_by_default: bool,
828 path_converter: RepoPathUiConverter,
829 workspace_name: WorkspaceNameBuf,
830 immutable_heads_expression: Arc<UserRevsetExpression>,
831 short_prefixes_expression: Option<Arc<UserRevsetExpression>>,
832 conflict_marker_style: ConflictMarkerStyle,
833}
834
835impl WorkspaceCommandEnvironment {
836 #[instrument(skip_all)]
837 fn new(ui: &Ui, command: &CommandHelper, workspace: &Workspace) -> Result<Self, CommandError> {
838 let settings = workspace.settings();
839 let fileset_aliases_map = load_fileset_aliases(ui, settings.config())?;
840 let revset_aliases_map = load_revset_aliases(ui, settings.config())?;
841 let template_aliases_map = load_template_aliases(ui, settings.config())?;
842 let default_ignored_remote = default_ignored_remote_name(workspace.repo_loader().store());
843 let path_converter = RepoPathUiConverter::Fs {
844 cwd: command.cwd().to_owned(),
845 base: workspace.workspace_root().to_owned(),
846 };
847 let mut env = Self {
848 command: command.clone(),
849 settings: settings.clone(),
850 fileset_aliases_map,
851 revset_aliases_map,
852 template_aliases_map,
853 default_ignored_remote,
854 revsets_use_glob_by_default: settings.get("ui.revsets-use-glob-by-default")?,
855 path_converter,
856 workspace_name: workspace.workspace_name().to_owned(),
857 immutable_heads_expression: RevsetExpression::root(),
858 short_prefixes_expression: None,
859 conflict_marker_style: settings.get("ui.conflict-marker-style")?,
860 };
861 env.reload_revset_expressions(ui)?;
862 Ok(env)
863 }
864
865 pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
866 &self.path_converter
867 }
868
869 pub fn workspace_name(&self) -> &WorkspaceName {
870 &self.workspace_name
871 }
872
873 pub(crate) fn fileset_parse_context(&self) -> FilesetParseContext<'_> {
875 FilesetParseContext {
876 aliases_map: &self.fileset_aliases_map,
877 path_converter: &self.path_converter,
878 }
879 }
880
881 pub(crate) fn fileset_parse_context_for_config(&self) -> FilesetParseContext<'_> {
883 static ROOT_PATH_CONVERTER: LazyLock<RepoPathUiConverter> =
885 LazyLock::new(|| RepoPathUiConverter::Fs {
886 cwd: PathBuf::new(),
887 base: PathBuf::new(),
888 });
889 FilesetParseContext {
890 aliases_map: &self.fileset_aliases_map,
891 path_converter: &ROOT_PATH_CONVERTER,
892 }
893 }
894
895 pub(crate) fn revset_parse_context(&self) -> RevsetParseContext<'_> {
896 let workspace_context = RevsetWorkspaceContext {
897 path_converter: &self.path_converter,
898 workspace_name: &self.workspace_name,
899 };
900 let now = if let Some(timestamp) = self.settings.commit_timestamp() {
901 chrono::Local
902 .timestamp_millis_opt(timestamp.timestamp.0)
903 .unwrap()
904 } else {
905 chrono::Local::now()
906 };
907 RevsetParseContext {
908 aliases_map: &self.revset_aliases_map,
909 local_variables: HashMap::new(),
910 user_email: self.settings.user_email(),
911 date_pattern_context: now.into(),
912 default_ignored_remote: self.default_ignored_remote,
913 fileset_aliases_map: &self.fileset_aliases_map,
914 use_glob_by_default: self.revsets_use_glob_by_default,
915 extensions: self.command.revset_extensions(),
916 workspace: Some(workspace_context),
917 }
918 }
919
920 pub fn new_id_prefix_context(&self) -> IdPrefixContext {
923 let context = IdPrefixContext::new(self.command.revset_extensions().clone());
924 match &self.short_prefixes_expression {
925 None => context,
926 Some(expression) => context.disambiguate_within(expression.clone()),
927 }
928 }
929
930 fn reload_revset_expressions(&mut self, ui: &Ui) -> Result<(), CommandError> {
932 self.immutable_heads_expression = self.load_immutable_heads_expression(ui)?;
933 self.short_prefixes_expression = self.load_short_prefixes_expression(ui)?;
934 Ok(())
935 }
936
937 pub fn immutable_expression(&self) -> Arc<UserRevsetExpression> {
939 self.immutable_heads_expression.ancestors()
942 }
943
944 pub fn immutable_heads_expression(&self) -> &Arc<UserRevsetExpression> {
946 &self.immutable_heads_expression
947 }
948
949 pub fn conflict_marker_style(&self) -> ConflictMarkerStyle {
951 self.conflict_marker_style
952 }
953
954 fn load_immutable_heads_expression(
955 &self,
956 ui: &Ui,
957 ) -> Result<Arc<UserRevsetExpression>, CommandError> {
958 let mut diagnostics = RevsetDiagnostics::new();
959 let expression = revset_util::parse_immutable_heads_expression(
960 &mut diagnostics,
961 &self.revset_parse_context(),
962 )
963 .map_err(|e| config_error_with_message("Invalid `revset-aliases.immutable_heads()`", e))?;
964 print_parse_diagnostics(ui, "In `revset-aliases.immutable_heads()`", &diagnostics)?;
965 Ok(expression)
966 }
967
968 fn load_short_prefixes_expression(
969 &self,
970 ui: &Ui,
971 ) -> Result<Option<Arc<UserRevsetExpression>>, CommandError> {
972 let revset_string = self
973 .settings
974 .get_string("revsets.short-prefixes")
975 .optional()?
976 .map_or_else(|| self.settings.get_string("revsets.log"), Ok)?;
977 if revset_string.is_empty() {
978 Ok(None)
979 } else {
980 let mut diagnostics = RevsetDiagnostics::new();
981 let expression = revset::parse(
982 &mut diagnostics,
983 &revset_string,
984 &self.revset_parse_context(),
985 )
986 .map_err(|err| config_error_with_message("Invalid `revsets.short-prefixes`", err))?;
987 print_parse_diagnostics(ui, "In `revsets.short-prefixes`", &diagnostics)?;
988 Ok(Some(expression))
989 }
990 }
991
992 async fn find_immutable_commit(
994 &self,
995 repo: &dyn Repo,
996 to_rewrite_expr: &Arc<ResolvedRevsetExpression>,
997 ) -> Result<Option<CommitId>, CommandError> {
998 let immutable_expression = if self.command.global_args().ignore_immutable {
999 UserRevsetExpression::root()
1000 } else {
1001 self.immutable_expression()
1002 };
1003
1004 let id_prefix_context = IdPrefixContext::new(self.command.revset_extensions().clone());
1008 let immutable_expr = RevsetExpressionEvaluator::new(
1009 repo,
1010 self.command.revset_extensions().clone(),
1011 &id_prefix_context,
1012 immutable_expression,
1013 )
1014 .resolve()
1015 .map_err(|e| config_error_with_message("Invalid `revset-aliases.immutable_heads()`", e))?;
1016
1017 let mut commit_id_iter = immutable_expr
1018 .intersection(to_rewrite_expr)
1019 .evaluate(repo)?
1020 .stream();
1021 Ok(commit_id_iter.try_next().await?)
1022 }
1023
1024 pub fn template_aliases_map(&self) -> &TemplateAliasesMap {
1025 &self.template_aliases_map
1026 }
1027
1028 pub fn parse_template<'a, C, L>(
1030 &self,
1031 ui: &Ui,
1032 language: &L,
1033 template_text: &str,
1034 ) -> Result<TemplateRenderer<'a, C>, CommandError>
1035 where
1036 C: Clone + 'a,
1037 L: TemplateLanguage<'a> + ?Sized,
1038 L::Property: WrapTemplateProperty<'a, C>,
1039 {
1040 let mut diagnostics = TemplateDiagnostics::new();
1041 let template = template_builder::parse(
1042 language,
1043 &mut diagnostics,
1044 template_text,
1045 &self.template_aliases_map,
1046 )?;
1047 print_parse_diagnostics(ui, "In template expression", &diagnostics)?;
1048 Ok(template)
1049 }
1050
1051 pub fn commit_template_language<'a>(
1054 &'a self,
1055 repo: &'a dyn Repo,
1056 id_prefix_context: &'a IdPrefixContext,
1057 ) -> CommitTemplateLanguage<'a> {
1058 CommitTemplateLanguage::new(
1059 repo,
1060 &self.path_converter,
1061 &self.workspace_name,
1062 self.revset_parse_context(),
1063 id_prefix_context,
1064 self.immutable_expression(),
1065 self.conflict_marker_style,
1066 &self.command.data.commit_template_extensions,
1067 )
1068 }
1069
1070 pub fn operation_template_extensions(&self) -> &[Arc<dyn OperationTemplateLanguageExtension>] {
1071 &self.command.data.operation_template_extensions
1072 }
1073}
1074
1075pub struct GitImportExportLock {
1079 _lock: Option<FileLock>,
1080}
1081
1082pub struct WorkspaceCommandHelper {
1085 workspace: Workspace,
1086 user_repo: ReadonlyUserRepo,
1087 env: WorkspaceCommandEnvironment,
1088 commit_summary_template_text: String,
1090 op_summary_template_text: String,
1091 may_update_working_copy: bool,
1092 working_copy_shared_with_git: bool,
1093}
1094
1095enum SnapshotWorkingCopyError {
1096 Command(CommandError),
1097 StaleWorkingCopy(CommandError),
1098}
1099
1100impl SnapshotWorkingCopyError {
1101 fn into_command_error(self) -> CommandError {
1102 match self {
1103 Self::Command(err) => err,
1104 Self::StaleWorkingCopy(err) => err,
1105 }
1106 }
1107}
1108
1109fn snapshot_command_error<E>(err: E) -> SnapshotWorkingCopyError
1110where
1111 E: Into<CommandError>,
1112{
1113 SnapshotWorkingCopyError::Command(err.into())
1114}
1115
1116impl WorkspaceCommandHelper {
1117 #[instrument(skip_all)]
1118 fn new(
1119 ui: &Ui,
1120 workspace: Workspace,
1121 repo: Arc<ReadonlyRepo>,
1122 env: WorkspaceCommandEnvironment,
1123 loaded_at_head: bool,
1124 ) -> Result<Self, CommandError> {
1125 let settings = workspace.settings();
1126 let commit_summary_template_text = settings.get_string("templates.commit_summary")?;
1127 let op_summary_template_text = settings.get_string("templates.op_summary")?;
1128 let may_update_working_copy =
1129 loaded_at_head && !env.command.global_args().ignore_working_copy;
1130 let working_copy_shared_with_git =
1131 crate::git_util::is_colocated_git_workspace(&workspace, &repo);
1132
1133 let helper = Self {
1134 workspace,
1135 user_repo: ReadonlyUserRepo::new(repo),
1136 env,
1137 commit_summary_template_text,
1138 op_summary_template_text,
1139 may_update_working_copy,
1140 working_copy_shared_with_git,
1141 };
1142 helper.parse_operation_template(ui, &helper.op_summary_template_text)?;
1145 helper.parse_commit_template(ui, &helper.commit_summary_template_text)?;
1146 helper.parse_commit_template(ui, SHORT_CHANGE_ID_TEMPLATE_TEXT)?;
1147 Ok(helper)
1148 }
1149
1150 pub fn settings(&self) -> &UserSettings {
1152 self.workspace.settings()
1153 }
1154
1155 pub fn check_working_copy_writable(&self) -> Result<(), CommandError> {
1156 if self.may_update_working_copy {
1157 Ok(())
1158 } else {
1159 let hint = if self.env.command.global_args().ignore_working_copy {
1160 "Don't use --ignore-working-copy."
1161 } else {
1162 "Don't use --at-op."
1163 };
1164 Err(user_error("This command must be able to update the working copy.").hinted(hint))
1165 }
1166 }
1167
1168 fn lock_git_import_export(&self) -> Result<GitImportExportLock, CommandError> {
1173 let lock = if self.working_copy_shared_with_git {
1174 let lock_path = self.workspace.repo_path().join("git_import_export.lock");
1175 Some(FileLock::lock(lock_path.clone()).map_err(|err| {
1176 user_error_with_message("Failed to take lock for Git import/export", err)
1177 })?)
1178 } else {
1179 None
1180 };
1181 Ok(GitImportExportLock { _lock: lock })
1182 }
1183
1184 #[instrument(skip_all)]
1188 async fn maybe_snapshot_impl(
1189 &mut self,
1190 ui: &Ui,
1191 ) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
1192 if !self.may_update_working_copy {
1193 return Ok(SnapshotStats::default());
1194 }
1195
1196 #[cfg_attr(not(feature = "git"), allow(unused_variables))]
1200 let git_import_export_lock = self
1201 .lock_git_import_export()
1202 .map_err(snapshot_command_error)?;
1203
1204 if self.working_copy_shared_with_git {
1207 let repo = self.repo().clone();
1208 let op_heads_store = repo.loader().op_heads_store();
1209 let op_heads = op_heads_store
1210 .get_op_heads()
1211 .await
1212 .map_err(snapshot_command_error)?;
1213 if std::slice::from_ref(repo.op_id()) != op_heads {
1214 let op = self
1215 .env
1216 .command
1217 .resolve_operation(ui, repo.loader(), self.workspace_name())
1218 .map_err(snapshot_command_error)?;
1219 let current_repo = repo
1220 .loader()
1221 .load_at(&op)
1222 .await
1223 .map_err(snapshot_command_error)?;
1224 self.user_repo = ReadonlyUserRepo::new(current_repo);
1225 }
1226 }
1227
1228 #[cfg(feature = "git")]
1229 if self.working_copy_shared_with_git {
1230 self.import_git_head(ui, &git_import_export_lock)
1231 .await
1232 .map_err(snapshot_command_error)?;
1233 }
1234 let stats = self.snapshot_working_copy(ui).await?;
1239
1240 #[cfg(feature = "git")]
1242 if self.working_copy_shared_with_git {
1243 self.import_git_refs(ui, &git_import_export_lock)
1244 .await
1245 .map_err(snapshot_command_error)?;
1246 }
1247 Ok(stats)
1248 }
1249
1250 #[instrument(skip_all)]
1255 pub async fn maybe_snapshot(&mut self, ui: &Ui) -> Result<bool, CommandError> {
1256 let op_id_before = self.repo().op_id().clone();
1257 let stats = self
1258 .maybe_snapshot_impl(ui)
1259 .await
1260 .map_err(|err| err.into_command_error())?;
1261 print_snapshot_stats(ui, &stats, self.env().path_converter())?;
1262 let op_id_after = self.repo().op_id();
1263 Ok(op_id_before != *op_id_after)
1264 }
1265
1266 #[cfg(feature = "git")]
1273 #[instrument(skip_all)]
1274 async fn import_git_head(
1275 &mut self,
1276 ui: &Ui,
1277 git_import_export_lock: &GitImportExportLock,
1278 ) -> Result<(), CommandError> {
1279 assert!(self.may_update_working_copy);
1280 let mut tx = self.start_transaction();
1281 jj_lib::git::import_head(tx.repo_mut()).await?;
1282 if !tx.repo().has_changes() {
1283 return Ok(());
1284 }
1285
1286 let mut tx = tx.into_inner();
1287 let old_git_head = self.repo().view().git_head().clone();
1288 let new_git_head = tx.repo().view().git_head().clone();
1289 if let Some(new_git_head_id) = new_git_head.as_normal() {
1290 let workspace_name = self.workspace_name().to_owned();
1291 let new_git_head_commit = tx.repo().store().get_commit(new_git_head_id)?;
1292 let wc_commit = tx
1293 .repo_mut()
1294 .check_out(workspace_name, &new_git_head_commit)
1295 .await?;
1296 let mut locked_ws = self.workspace.start_working_copy_mutation()?;
1297 locked_ws.locked_wc().reset(&wc_commit).await?;
1301 tx.repo_mut().rebase_descendants().await?;
1302 self.user_repo = ReadonlyUserRepo::new(tx.commit("import git head").await?);
1303 locked_ws
1304 .finish(self.user_repo.repo.op_id().clone())
1305 .await?;
1306 if old_git_head.is_present() {
1307 writeln!(
1308 ui.status(),
1309 "Reset the working copy parent to the new Git HEAD."
1310 )?;
1311 } else {
1312 }
1314 } else {
1315 self.finish_transaction(ui, tx, "import git head", git_import_export_lock)
1317 .await?;
1318 }
1319 Ok(())
1320 }
1321
1322 #[cfg(feature = "git")]
1331 #[instrument(skip_all)]
1332 async fn import_git_refs(
1333 &mut self,
1334 ui: &Ui,
1335 git_import_export_lock: &GitImportExportLock,
1336 ) -> Result<(), CommandError> {
1337 use jj_lib::git;
1338 let git_settings = git::GitSettings::from_settings(self.settings())?;
1339 let remote_settings = self.settings().remote_settings()?;
1340 let import_options =
1341 crate::git_util::load_git_import_options(ui, &git_settings, &remote_settings)?;
1342 let mut tx = self.start_transaction();
1343 let stats = git::import_refs(tx.repo_mut(), &import_options).await?;
1344 crate::git_util::print_git_import_stats_summary(ui, &stats)?;
1345 if !tx.repo().has_changes() {
1346 return Ok(());
1347 }
1348
1349 let mut tx = tx.into_inner();
1350 let num_rebased = tx.repo_mut().rebase_descendants().await?;
1352 if num_rebased > 0 {
1353 writeln!(
1354 ui.status(),
1355 "Rebased {num_rebased} descendant commits off of commits rewritten from git"
1356 )?;
1357 }
1358 self.finish_transaction(ui, tx, "import git refs", git_import_export_lock)
1359 .await?;
1360 writeln!(
1361 ui.status(),
1362 "Done importing changes from the underlying Git repo."
1363 )?;
1364 Ok(())
1365 }
1366
1367 pub fn repo(&self) -> &Arc<ReadonlyRepo> {
1368 &self.user_repo.repo
1369 }
1370
1371 pub fn repo_path(&self) -> &Path {
1372 self.workspace.repo_path()
1373 }
1374
1375 pub fn workspace(&self) -> &Workspace {
1376 &self.workspace
1377 }
1378
1379 pub fn working_copy(&self) -> &dyn WorkingCopy {
1380 self.workspace.working_copy()
1381 }
1382
1383 pub fn env(&self) -> &WorkspaceCommandEnvironment {
1384 &self.env
1385 }
1386
1387 pub fn unchecked_start_working_copy_mutation(
1388 &mut self,
1389 ) -> Result<(LockedWorkspace<'_>, Commit), CommandError> {
1390 self.check_working_copy_writable()?;
1391 let wc_commit = if let Some(wc_commit_id) = self.get_wc_commit_id() {
1392 self.repo().store().get_commit(wc_commit_id)?
1393 } else {
1394 return Err(user_error("Nothing checked out in this workspace"));
1395 };
1396
1397 let locked_ws = self.workspace.start_working_copy_mutation()?;
1398
1399 Ok((locked_ws, wc_commit))
1400 }
1401
1402 pub fn start_working_copy_mutation(
1403 &mut self,
1404 ) -> Result<(LockedWorkspace<'_>, Commit), CommandError> {
1405 let (mut locked_ws, wc_commit) = self.unchecked_start_working_copy_mutation()?;
1406 if wc_commit.tree().tree_ids_and_labels()
1407 != locked_ws.locked_wc().old_tree().tree_ids_and_labels()
1408 {
1409 return Err(user_error("Concurrent working copy operation. Try again."));
1410 }
1411 Ok((locked_ws, wc_commit))
1412 }
1413
1414 async fn create_and_check_out_recovery_commit(
1415 &mut self,
1416 ui: &Ui,
1417 ) -> Result<SnapshotStats, CommandError> {
1418 self.check_working_copy_writable()?;
1419
1420 let workspace_name = self.workspace_name().to_owned();
1421 let mut locked_ws = self.workspace.start_working_copy_mutation()?;
1422 let (repo, new_commit) = working_copy::create_and_check_out_recovery_commit(
1423 locked_ws.locked_wc(),
1424 &self.user_repo.repo,
1425 workspace_name,
1426 "RECOVERY COMMIT FROM `jj workspace update-stale`
1427
1428This commit contains changes that were written to the working copy by an
1429operation that was subsequently lost (or was at least unavailable when you ran
1430`jj workspace update-stale`). Because the operation was lost, we don't know
1431what the parent commits are supposed to be. That means that the diff compared
1432to the current parents may contain changes from multiple commits.
1433",
1434 )
1435 .await?;
1436
1437 writeln!(
1438 ui.status(),
1439 "Created and checked out recovery commit {}",
1440 short_commit_hash(new_commit.id())
1441 )?;
1442 locked_ws.finish(repo.op_id().clone()).await?;
1443 self.user_repo = ReadonlyUserRepo::new(repo);
1444
1445 self.maybe_snapshot_impl(ui)
1446 .await
1447 .map_err(|err| err.into_command_error())
1448 }
1449
1450 pub fn workspace_root(&self) -> &Path {
1451 self.workspace.workspace_root()
1452 }
1453
1454 pub fn workspace_name(&self) -> &WorkspaceName {
1455 self.workspace.workspace_name()
1456 }
1457
1458 pub fn get_wc_commit_id(&self) -> Option<&CommitId> {
1459 self.repo().view().get_wc_commit_id(self.workspace_name())
1460 }
1461
1462 pub fn working_copy_shared_with_git(&self) -> bool {
1463 self.working_copy_shared_with_git
1464 }
1465
1466 pub fn format_file_path(&self, file: &RepoPath) -> String {
1467 self.path_converter().format_file_path(file)
1468 }
1469
1470 pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
1473 self.path_converter().parse_file_path(input)
1474 }
1475
1476 pub fn parse_file_patterns(
1478 &self,
1479 ui: &Ui,
1480 values: &[String],
1481 ) -> Result<FilesetExpression, CommandError> {
1482 if values.is_empty() {
1486 Ok(FilesetExpression::all())
1487 } else {
1488 self.parse_union_filesets(ui, values)
1489 }
1490 }
1491
1492 pub fn parse_union_filesets(
1494 &self,
1495 ui: &Ui,
1496 file_args: &[String], ) -> Result<FilesetExpression, CommandError> {
1498 let mut diagnostics = FilesetDiagnostics::new();
1499 let context = self.env.fileset_parse_context();
1500 let expressions: Vec<_> = file_args
1501 .iter()
1502 .map(|arg| fileset::parse_maybe_bare(&mut diagnostics, arg, &context))
1503 .try_collect()?;
1504 print_parse_diagnostics(ui, "In fileset expression", &diagnostics)?;
1505 Ok(FilesetExpression::union_all(expressions))
1506 }
1507
1508 pub fn auto_tracking_matcher(&self, ui: &Ui) -> Result<Box<dyn Matcher>, CommandError> {
1509 let mut diagnostics = FilesetDiagnostics::new();
1510 let pattern = self.settings().get_string("snapshot.auto-track")?;
1511 let context = self.env.fileset_parse_context_for_config();
1512 let expression = fileset::parse(&mut diagnostics, &pattern, &context)?;
1513 print_parse_diagnostics(ui, "In `snapshot.auto-track`", &diagnostics)?;
1514 Ok(expression.to_matcher())
1515 }
1516
1517 pub fn snapshot_options_with_start_tracking_matcher<'a>(
1518 &self,
1519 start_tracking_matcher: &'a dyn Matcher,
1520 ) -> Result<SnapshotOptions<'a>, CommandError> {
1521 let base_ignores = self.base_ignores()?;
1522 let HumanByteSize(mut max_new_file_size) = self
1523 .settings()
1524 .get_value_with("snapshot.max-new-file-size", TryInto::try_into)?;
1525 if max_new_file_size == 0 {
1526 max_new_file_size = u64::MAX;
1527 }
1528 Ok(SnapshotOptions {
1529 base_ignores,
1530 progress: None,
1531 start_tracking_matcher,
1532 force_tracking_matcher: &NothingMatcher,
1533 max_new_file_size,
1534 })
1535 }
1536
1537 pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
1538 self.env.path_converter()
1539 }
1540
1541 #[cfg(not(feature = "git"))]
1542 pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1543 Ok(GitIgnoreFile::empty())
1544 }
1545
1546 #[cfg(feature = "git")]
1547 #[instrument(skip_all)]
1548 pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1549 let get_excludes_file_path = |config: &gix::config::File| -> Option<PathBuf> {
1550 if let Some(value) = config.string("core.excludesFile") {
1553 let path = str::from_utf8(&value)
1554 .ok()
1555 .map(jj_lib::file_util::expand_home_path)?;
1556 Some(self.workspace_root().join(path))
1559 } else {
1560 xdg_config_home().map(|x| x.join("git").join("ignore"))
1561 }
1562 };
1563
1564 fn xdg_config_home() -> Option<PathBuf> {
1565 if let Ok(x) = std::env::var("XDG_CONFIG_HOME")
1566 && !x.is_empty()
1567 {
1568 return Some(PathBuf::from(x));
1569 }
1570 etcetera::home_dir().ok().map(|home| home.join(".config"))
1571 }
1572
1573 let mut git_ignores = GitIgnoreFile::empty();
1574 if let Ok(git_backend) = jj_lib::git::get_git_backend(self.repo().store()) {
1575 let git_repo = git_backend.git_repo();
1576 if let Some(excludes_file_path) = get_excludes_file_path(&git_repo.config_snapshot()) {
1577 git_ignores = git_ignores.chain_with_file("", excludes_file_path)?;
1578 }
1579 git_ignores = git_ignores
1580 .chain_with_file("", git_backend.git_repo_path().join("info").join("exclude"))?;
1581 } else if let Ok(git_config) = gix::config::File::from_globals()
1582 && let Some(excludes_file_path) = get_excludes_file_path(&git_config)
1583 {
1584 git_ignores = git_ignores.chain_with_file("", excludes_file_path)?;
1585 }
1586 Ok(git_ignores)
1587 }
1588
1589 pub fn diff_renderer(&self, formats: Vec<DiffFormat>) -> DiffRenderer<'_> {
1591 DiffRenderer::new(
1592 self.repo().as_ref(),
1593 self.path_converter(),
1594 self.env.conflict_marker_style(),
1595 formats,
1596 )
1597 }
1598
1599 pub fn diff_renderer_for(
1601 &self,
1602 args: &DiffFormatArgs,
1603 ) -> Result<DiffRenderer<'_>, CommandError> {
1604 let formats = diff_util::diff_formats_for(self.settings(), args)?;
1605 Ok(self.diff_renderer(formats))
1606 }
1607
1608 pub fn diff_renderer_for_log(
1612 &self,
1613 args: &DiffFormatArgs,
1614 patch: bool,
1615 ) -> Result<Option<DiffRenderer<'_>>, CommandError> {
1616 let formats = diff_util::diff_formats_for_log(self.settings(), args, patch)?;
1617 Ok((!formats.is_empty()).then(|| self.diff_renderer(formats)))
1618 }
1619
1620 pub fn diff_editor(
1624 &self,
1625 ui: &Ui,
1626 tool_name: Option<&str>,
1627 ) -> Result<DiffEditor, CommandError> {
1628 let base_ignores = self.base_ignores()?;
1629 let conflict_marker_style = self.env.conflict_marker_style();
1630 if let Some(name) = tool_name {
1631 Ok(DiffEditor::with_name(
1632 name,
1633 self.settings(),
1634 base_ignores,
1635 conflict_marker_style,
1636 )?)
1637 } else {
1638 Ok(DiffEditor::from_settings(
1639 ui,
1640 self.settings(),
1641 base_ignores,
1642 conflict_marker_style,
1643 )?)
1644 }
1645 }
1646
1647 pub fn diff_selector(
1651 &self,
1652 ui: &Ui,
1653 tool_name: Option<&str>,
1654 force_interactive: bool,
1655 ) -> Result<DiffSelector, CommandError> {
1656 if tool_name.is_some() || force_interactive {
1657 Ok(DiffSelector::Interactive(self.diff_editor(ui, tool_name)?))
1658 } else {
1659 Ok(DiffSelector::NonInteractive)
1660 }
1661 }
1662
1663 pub fn merge_editor(
1667 &self,
1668 ui: &Ui,
1669 tool_name: Option<&str>,
1670 ) -> Result<MergeEditor, MergeToolConfigError> {
1671 let conflict_marker_style = self.env.conflict_marker_style();
1672 if let Some(name) = tool_name {
1673 MergeEditor::with_name(
1674 name,
1675 self.settings(),
1676 self.path_converter().clone(),
1677 conflict_marker_style,
1678 )
1679 } else {
1680 MergeEditor::from_settings(
1681 ui,
1682 self.settings(),
1683 self.path_converter().clone(),
1684 conflict_marker_style,
1685 )
1686 }
1687 }
1688
1689 pub fn text_editor(&self) -> Result<TextEditor, ConfigGetError> {
1691 TextEditor::from_settings(self.settings())
1692 }
1693
1694 pub fn resolve_single_op(&self, op_str: &str) -> Result<Operation, OpsetEvaluationError> {
1695 op_walk::resolve_op_with_repo(self.repo(), op_str).block_on()
1696 }
1697
1698 pub async fn resolve_single_rev(
1701 &self,
1702 ui: &Ui,
1703 revision_arg: &RevisionArg,
1704 ) -> Result<Commit, CommandError> {
1705 let expression = self.parse_revset(ui, revision_arg)?;
1706 revset_util::evaluate_revset_to_single_commit(revision_arg.as_ref(), &expression, || {
1707 self.commit_summary_template()
1708 })
1709 .await
1710 }
1711
1712 pub async fn resolve_revsets_ordered(
1715 &self,
1716 ui: &Ui,
1717 revision_args: &[RevisionArg],
1718 ) -> Result<IndexSet<CommitId>, CommandError> {
1719 let mut all_commits = IndexSet::new();
1720 for revision_arg in revision_args {
1721 let expression = self.parse_revset(ui, revision_arg)?;
1722 let mut stream = expression.evaluate_to_commit_ids()?;
1723 while let Some(commit_id) = stream.try_next().await? {
1724 all_commits.insert(commit_id);
1725 }
1726 }
1727 Ok(all_commits)
1728 }
1729
1730 pub async fn resolve_some_revsets(
1733 &self,
1734 ui: &Ui,
1735 revision_args: &[RevisionArg],
1736 ) -> Result<IndexSet<CommitId>, CommandError> {
1737 let all_commits = self.resolve_revsets_ordered(ui, revision_args).await?;
1738 if all_commits.is_empty() {
1739 Err(user_error("Empty revision set"))
1740 } else {
1741 Ok(all_commits)
1742 }
1743 }
1744
1745 pub fn parse_revset(
1746 &self,
1747 ui: &Ui,
1748 revision_arg: &RevisionArg,
1749 ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1750 let mut diagnostics = RevsetDiagnostics::new();
1751 let context = self.env.revset_parse_context();
1752 let expression = revset::parse(&mut diagnostics, revision_arg.as_ref(), &context)?;
1753 print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1754 Ok(self.attach_revset_evaluator(expression))
1755 }
1756
1757 pub fn parse_union_revsets(
1759 &self,
1760 ui: &Ui,
1761 revision_args: &[RevisionArg],
1762 ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1763 let mut diagnostics = RevsetDiagnostics::new();
1764 let context = self.env.revset_parse_context();
1765 let expressions: Vec<_> = revision_args
1766 .iter()
1767 .map(|arg| revset::parse(&mut diagnostics, arg.as_ref(), &context))
1768 .try_collect()?;
1769 print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1770 let expression = RevsetExpression::union_all(&expressions);
1771 Ok(self.attach_revset_evaluator(expression))
1772 }
1773
1774 pub fn attach_revset_evaluator(
1775 &self,
1776 expression: Arc<UserRevsetExpression>,
1777 ) -> RevsetExpressionEvaluator<'_> {
1778 RevsetExpressionEvaluator::new(
1779 self.repo().as_ref(),
1780 self.env.command.revset_extensions().clone(),
1781 self.id_prefix_context(),
1782 expression,
1783 )
1784 }
1785
1786 pub fn id_prefix_context(&self) -> &IdPrefixContext {
1787 self.user_repo
1788 .id_prefix_context
1789 .get_or_init(|| self.env.new_id_prefix_context())
1790 }
1791
1792 pub fn parse_template<'a, C, L>(
1794 &self,
1795 ui: &Ui,
1796 language: &L,
1797 template_text: &str,
1798 ) -> Result<TemplateRenderer<'a, C>, CommandError>
1799 where
1800 C: Clone + 'a,
1801 L: TemplateLanguage<'a> + ?Sized,
1802 L::Property: WrapTemplateProperty<'a, C>,
1803 {
1804 self.env.parse_template(ui, language, template_text)
1805 }
1806
1807 fn reparse_valid_template<'a, C, L>(
1809 &self,
1810 language: &L,
1811 template_text: &str,
1812 ) -> TemplateRenderer<'a, C>
1813 where
1814 C: Clone + 'a,
1815 L: TemplateLanguage<'a> + ?Sized,
1816 L::Property: WrapTemplateProperty<'a, C>,
1817 {
1818 template_builder::parse(
1819 language,
1820 &mut TemplateDiagnostics::new(),
1821 template_text,
1822 &self.env.template_aliases_map,
1823 )
1824 .expect("parse error should be confined by WorkspaceCommandHelper::new()")
1825 }
1826
1827 pub fn parse_commit_template(
1829 &self,
1830 ui: &Ui,
1831 template_text: &str,
1832 ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
1833 let language = self.commit_template_language();
1834 self.parse_template(ui, &language, template_text)
1835 }
1836
1837 pub fn parse_operation_template(
1839 &self,
1840 ui: &Ui,
1841 template_text: &str,
1842 ) -> Result<TemplateRenderer<'_, Operation>, CommandError> {
1843 let language = self.operation_template_language();
1844 self.parse_template(ui, &language, template_text)
1845 }
1846
1847 pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
1849 self.env
1850 .commit_template_language(self.repo().as_ref(), self.id_prefix_context())
1851 }
1852
1853 pub fn operation_template_language(&self) -> OperationTemplateLanguage {
1855 OperationTemplateLanguage::new(
1856 self.workspace.repo_loader(),
1857 Some(self.repo().op_id()),
1858 self.env.operation_template_extensions(),
1859 )
1860 }
1861
1862 pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
1864 let language = self.commit_template_language();
1865 self.reparse_valid_template(&language, &self.commit_summary_template_text)
1866 .labeled(["commit"])
1867 }
1868
1869 pub fn operation_summary_template(&self) -> TemplateRenderer<'_, Operation> {
1871 let language = self.operation_template_language();
1872 self.reparse_valid_template(&language, &self.op_summary_template_text)
1873 .labeled(["operation"])
1874 }
1875
1876 pub fn short_change_id_template(&self) -> TemplateRenderer<'_, Commit> {
1877 let language = self.commit_template_language();
1878 self.reparse_valid_template(&language, SHORT_CHANGE_ID_TEMPLATE_TEXT)
1879 .labeled(["commit"])
1880 }
1881
1882 pub fn format_commit_summary(&self, commit: &Commit) -> String {
1887 let output = self.commit_summary_template().format_plain_text(commit);
1888 output.into_string_lossy()
1889 }
1890
1891 #[instrument(skip_all)]
1895 pub fn write_commit_summary(
1896 &self,
1897 formatter: &mut dyn Formatter,
1898 commit: &Commit,
1899 ) -> std::io::Result<()> {
1900 self.commit_summary_template().format(commit, formatter)
1901 }
1902
1903 pub async fn check_rewritable<'a>(
1904 &self,
1905 commits: impl IntoIterator<Item = &'a CommitId>,
1906 ) -> Result<(), CommandError> {
1907 let commit_ids = commits.into_iter().cloned().collect_vec();
1908 let to_rewrite_expr = RevsetExpression::commits(commit_ids);
1909 self.check_rewritable_expr(&to_rewrite_expr).await
1910 }
1911
1912 pub async fn check_rewritable_expr(
1913 &self,
1914 to_rewrite_expr: &Arc<ResolvedRevsetExpression>,
1915 ) -> Result<(), CommandError> {
1916 let repo = self.repo().as_ref();
1917 let Some(commit_id) = self
1918 .env
1919 .find_immutable_commit(repo, to_rewrite_expr)
1920 .await?
1921 else {
1922 return Ok(());
1923 };
1924 let error = if &commit_id == repo.store().root_commit_id() {
1925 user_error(format!("The root commit {commit_id:.12} is immutable"))
1926 } else {
1927 let mut error = user_error(format!("Commit {commit_id:.12} is immutable"));
1928 let commit = repo.store().get_commit_async(&commit_id).await?;
1929 error.add_formatted_hint_with(|formatter| {
1930 write!(formatter, "Could not modify commit: ")?;
1931 self.write_commit_summary(formatter, &commit)?;
1932 Ok(())
1933 });
1934 error.add_hint("Immutable commits are used to protect shared history.");
1935 error.add_hint(indoc::indoc! {"
1936 For more information, see:
1937 - https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits
1938 - `jj help -k config`, \"Set of immutable commits\""});
1939
1940 let id_prefix_context =
1943 IdPrefixContext::new(self.env.command.revset_extensions().clone());
1944 let (lower_bound, upper_bound) = RevsetExpressionEvaluator::new(
1945 repo,
1946 self.env.command.revset_extensions().clone(),
1947 &id_prefix_context,
1948 self.env.immutable_expression(),
1949 )
1950 .resolve()?
1951 .intersection(&to_rewrite_expr.descendants())
1952 .evaluate(repo)?
1953 .count_estimate()?;
1954 let exact = upper_bound == Some(lower_bound);
1955 let or_more = if exact { "" } else { " or more" };
1956 error.add_hint(format!(
1957 "This operation would rewrite {lower_bound}{or_more} immutable commits."
1958 ));
1959
1960 error
1961 };
1962 Err(error)
1963 }
1964
1965 #[instrument(skip_all)]
1966 async fn snapshot_working_copy(
1967 &mut self,
1968 ui: &Ui,
1969 ) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
1970 let workspace_name = self.workspace_name().to_owned();
1971 let repo = self.repo().clone();
1972 let auto_tracking_matcher = self
1973 .auto_tracking_matcher(ui)
1974 .map_err(snapshot_command_error)?;
1975 let options = self
1976 .snapshot_options_with_start_tracking_matcher(&auto_tracking_matcher)
1977 .map_err(snapshot_command_error)?;
1978
1979 let mut locked_ws = self
1981 .workspace
1982 .start_working_copy_mutation()
1983 .map_err(snapshot_command_error)?;
1984
1985 let Some((repo, wc_commit)) =
1986 handle_stale_working_copy(locked_ws.locked_wc(), repo, &workspace_name).await?
1987 else {
1988 return Ok(SnapshotStats::default());
1991 };
1992
1993 self.user_repo = ReadonlyUserRepo::new(repo);
1994 let (new_tree, stats) = {
1995 let mut options = options;
1996 let progress = crate::progress::snapshot_progress(ui);
1997 options.progress = progress.as_ref().map(|x| x as _);
1998 locked_ws
1999 .locked_wc()
2000 .snapshot(&options)
2001 .await
2002 .map_err(snapshot_command_error)?
2003 };
2004 if new_tree.tree_ids_and_labels() != wc_commit.tree().tree_ids_and_labels() {
2005 let mut tx = start_repo_transaction(
2006 &self.user_repo.repo,
2007 &workspace_name,
2008 self.env.command.string_args(),
2009 );
2010 tx.set_is_snapshot(true);
2011 let mut_repo = tx.repo_mut();
2012 let commit = mut_repo
2013 .rewrite_commit(&wc_commit)
2014 .set_tree(new_tree.clone())
2015 .write()
2016 .await
2017 .map_err(snapshot_command_error)?;
2018 mut_repo
2019 .set_wc_commit(workspace_name, commit.id().clone())
2020 .map_err(snapshot_command_error)?;
2021
2022 let num_rebased = mut_repo
2024 .rebase_descendants()
2025 .await
2026 .map_err(snapshot_command_error)?;
2027 if num_rebased > 0 {
2028 writeln!(
2029 ui.status(),
2030 "Rebased {num_rebased} descendant commits onto updated working copy"
2031 )
2032 .map_err(snapshot_command_error)?;
2033 }
2034
2035 #[cfg(feature = "git")]
2036 if self.working_copy_shared_with_git {
2037 let old_tree = wc_commit.tree();
2038 let new_tree = commit.tree();
2039 export_working_copy_changes_to_git(ui, mut_repo, &old_tree, &new_tree)
2040 .await
2041 .map_err(snapshot_command_error)?;
2042 }
2043
2044 let repo = tx
2045 .commit("snapshot working copy")
2046 .await
2047 .map_err(snapshot_command_error)?;
2048 self.user_repo = ReadonlyUserRepo::new(repo);
2049 }
2050
2051 #[cfg(feature = "git")]
2052 if self.working_copy_shared_with_git
2053 && let Ok(resolved_tree) = new_tree
2054 .trees()
2055 .await
2056 .map_err(snapshot_command_error)?
2057 .into_resolved()
2058 && resolved_tree
2059 .entries_non_recursive()
2060 .any(|entry| entry.name().as_internal_str().starts_with(".jjconflict"))
2061 {
2062 writeln!(
2063 ui.warning_default(),
2064 "The working copy contains '.jjconflict' files. These files are used by `jj` \
2065 internally and should not be present in the working copy."
2066 )
2067 .map_err(snapshot_command_error)?;
2068 writeln!(
2069 ui.hint_default(),
2070 "You may have used a regular `git` command to check out a conflicted commit."
2071 )
2072 .map_err(snapshot_command_error)?;
2073 writeln!(
2074 ui.hint_default(),
2075 "You can use `jj abandon` to discard the working copy changes."
2076 )
2077 .map_err(snapshot_command_error)?;
2078 }
2079
2080 locked_ws
2081 .finish(self.user_repo.repo.op_id().clone())
2082 .await
2083 .map_err(snapshot_command_error)?;
2084 Ok(stats)
2085 }
2086
2087 async fn update_working_copy(
2088 &mut self,
2089 ui: &Ui,
2090 maybe_old_commit: Option<&Commit>,
2091 new_commit: &Commit,
2092 ) -> Result<(), CommandError> {
2093 assert!(self.may_update_working_copy);
2094 let stats = update_working_copy(
2095 &self.user_repo.repo,
2096 &mut self.workspace,
2097 maybe_old_commit,
2098 new_commit,
2099 )
2100 .await?;
2101 self.print_updated_working_copy_stats(ui, maybe_old_commit, new_commit, &stats)
2102 }
2103
2104 fn print_updated_working_copy_stats(
2105 &self,
2106 ui: &Ui,
2107 maybe_old_commit: Option<&Commit>,
2108 new_commit: &Commit,
2109 stats: &CheckoutStats,
2110 ) -> Result<(), CommandError> {
2111 if Some(new_commit) != maybe_old_commit
2112 && let Some(mut formatter) = ui.status_formatter()
2113 {
2114 let template = self.commit_summary_template();
2115 write!(formatter, "Working copy (@) now at: ")?;
2116 template.format(new_commit, formatter.as_mut())?;
2117 writeln!(formatter)?;
2118 for parent in new_commit.parents().block_on()? {
2119 write!(formatter, "Parent commit (@-) : ")?;
2121 template.format(&parent, formatter.as_mut())?;
2122 writeln!(formatter)?;
2123 }
2124 }
2125 print_checkout_stats(ui, stats, new_commit)?;
2126 if Some(new_commit) != maybe_old_commit
2127 && let Some(mut formatter) = ui.status_formatter()
2128 && new_commit.has_conflict()
2129 {
2130 let conflicts = new_commit.tree().conflicts().collect_vec();
2131 writeln!(
2132 formatter.labeled("warning").with_heading("Warning: "),
2133 "There are unresolved conflicts at these paths:"
2134 )?;
2135 print_conflicted_paths(conflicts, formatter.as_mut(), self)?;
2136 }
2137 Ok(())
2138 }
2139
2140 pub fn start_transaction(&mut self) -> WorkspaceCommandTransaction<'_> {
2141 let tx = start_repo_transaction(
2142 self.repo(),
2143 self.workspace_name(),
2144 self.env.command.string_args(),
2145 );
2146 let id_prefix_context = mem::take(&mut self.user_repo.id_prefix_context);
2147 WorkspaceCommandTransaction {
2148 helper: self,
2149 tx,
2150 id_prefix_context,
2151 }
2152 }
2153
2154 async fn finish_transaction(
2155 &mut self,
2156 ui: &Ui,
2157 mut tx: Transaction,
2158 description: impl Into<String>,
2159 _git_import_export_lock: &GitImportExportLock,
2160 ) -> Result<(), CommandError> {
2161 let num_rebased = tx.repo_mut().rebase_descendants().await?;
2162 if num_rebased > 0 {
2163 writeln!(ui.status(), "Rebased {num_rebased} descendant commits")?;
2164 }
2165
2166 for (name, wc_commit_id) in &tx.repo().view().wc_commit_ids().clone() {
2167 let wc_expr = RevsetExpression::commit(wc_commit_id.clone());
2171 let is_immutable = match self.env.find_immutable_commit(tx.repo(), &wc_expr).await {
2172 Ok(commit_id) => commit_id.is_some(),
2173 Err(CommandError { error, .. }) => {
2174 writeln!(
2175 ui.warning_default(),
2176 "Failed to check mutability of the new working-copy revision."
2177 )?;
2178 print_error_sources(ui, Some(&error))?;
2179 break;
2181 }
2182 };
2183 if is_immutable {
2184 let wc_commit = tx.repo().store().get_commit_async(wc_commit_id).await?;
2185 tx.repo_mut().check_out(name.clone(), &wc_commit).await?;
2186 writeln!(
2187 ui.warning_default(),
2188 "The working-copy commit in workspace '{name}' became immutable, so a new \
2189 commit has been created on top of it.",
2190 name = name.as_symbol()
2191 )?;
2192 }
2193 }
2194 if let Err(err) =
2195 revset_util::try_resolve_trunk_alias(tx.repo(), &self.env.revset_parse_context())
2196 {
2197 if tx.repo().view().wc_commit_ids().is_empty() {
2199 writeln!(
2200 ui.warning_default(),
2201 "Failed to resolve `revset-aliases.trunk()`: {err}"
2202 )?;
2203 }
2204 writeln!(
2205 ui.hint_default(),
2206 "Use `jj config edit --repo` to adjust the `trunk()` alias."
2207 )?;
2208 }
2209
2210 let old_repo = tx.base_repo().clone();
2211
2212 let maybe_old_wc_commit = old_repo
2213 .view()
2214 .get_wc_commit_id(self.workspace_name())
2215 .map(|commit_id| tx.base_repo().store().get_commit(commit_id))
2216 .transpose()?;
2217 let maybe_new_wc_commit = tx
2218 .repo()
2219 .view()
2220 .get_wc_commit_id(self.workspace_name())
2221 .map(|commit_id| tx.repo().store().get_commit(commit_id))
2222 .transpose()?;
2223
2224 #[cfg(feature = "git")]
2225 if self.working_copy_shared_with_git {
2226 use std::error::Error as _;
2227 if let Some(wc_commit) = &maybe_new_wc_commit {
2228 match jj_lib::git::reset_head(tx.repo_mut(), wc_commit).await {
2235 Ok(()) => {}
2236 Err(err @ jj_lib::git::GitResetHeadError::UpdateHeadRef(_)) => {
2237 writeln!(ui.warning_default(), "{err}")?;
2238 print_error_sources(ui, err.source())?;
2239 }
2240 Err(err) => return Err(err.into()),
2241 }
2242 }
2243 let stats = jj_lib::git::export_refs(tx.repo_mut())?;
2244 crate::git_util::print_git_export_stats(ui, &stats)?;
2245 }
2246
2247 self.user_repo = ReadonlyUserRepo::new(tx.commit(description).await?);
2248
2249 if self.may_update_working_copy {
2253 if let Some(new_commit) = &maybe_new_wc_commit {
2254 self.update_working_copy(ui, maybe_old_wc_commit.as_ref(), new_commit)
2255 .await?;
2256 } else {
2257 }
2260 }
2261
2262 self.report_repo_changes(ui, &old_repo).await?;
2263
2264 let settings = self.settings();
2265 let missing_user_name = settings.user_name().is_empty();
2266 let missing_user_mail = settings.user_email().is_empty();
2267 if missing_user_name || missing_user_mail {
2268 let not_configured_msg = match (missing_user_name, missing_user_mail) {
2269 (true, true) => "Name and email not configured.",
2270 (true, false) => "Name not configured.",
2271 (false, true) => "Email not configured.",
2272 _ => unreachable!(),
2273 };
2274 writeln!(
2275 ui.warning_default(),
2276 "{not_configured_msg} Until configured, your commits will be created with the \
2277 empty identity, and can't be pushed to remotes."
2278 )?;
2279 writeln!(ui.hint_default(), "To configure, run:")?;
2280 if missing_user_name {
2281 writeln!(
2282 ui.hint_no_heading(),
2283 r#" jj config set --user user.name "Some One""#
2284 )?;
2285 }
2286 if missing_user_mail {
2287 writeln!(
2288 ui.hint_no_heading(),
2289 r#" jj config set --user user.email "someone@example.com""#
2290 )?;
2291 }
2292 }
2293 Ok(())
2294 }
2295
2296 async fn report_repo_changes(
2299 &self,
2300 ui: &Ui,
2301 old_repo: &Arc<ReadonlyRepo>,
2302 ) -> Result<(), CommandError> {
2303 let Some(mut fmt) = ui.status_formatter() else {
2304 return Ok(());
2305 };
2306 let old_view = old_repo.view();
2307 let new_repo = self.repo().as_ref();
2308 let new_view = new_repo.view();
2309
2310 let workspace_name = self.workspace_name();
2311 if old_view.wc_commit_ids().contains_key(workspace_name)
2312 && !new_view.wc_commit_ids().contains_key(workspace_name)
2313 {
2314 writeln!(
2315 fmt.labeled("warning").with_heading("Warning: "),
2316 "The current workspace '{}' no longer exists after this operation. The working \
2317 copy was left untouched.",
2318 workspace_name.as_symbol(),
2319 )?;
2320 writeln!(
2321 fmt.labeled("hint").with_heading("Hint: "),
2322 "Restore to an operation that contains the workspace (e.g. `jj undo` or `jj \
2323 redo`).",
2324 )?;
2325 }
2326
2327 let old_heads = RevsetExpression::commits(old_view.heads().iter().cloned().collect());
2328 let new_heads = RevsetExpression::commits(new_view.heads().iter().cloned().collect());
2329 let conflicts = RevsetExpression::filter(RevsetFilterPredicate::HasConflict)
2335 .filtered(RevsetFilterPredicate::File(FilesetExpression::all()));
2336 let removed_conflicts_expr = new_heads.range(&old_heads).intersection(&conflicts);
2337 let added_conflicts_expr = old_heads.range(&new_heads).intersection(&conflicts);
2338
2339 let get_commits =
2340 async |expr: Arc<ResolvedRevsetExpression>| -> Result<Vec<Commit>, CommandError> {
2341 let commits = expr
2342 .evaluate(new_repo)?
2343 .stream()
2344 .commits(new_repo.store())
2345 .try_collect()
2346 .await?;
2347 Ok(commits)
2348 };
2349 let removed_conflict_commits = get_commits(removed_conflicts_expr).await?;
2350 let added_conflict_commits = get_commits(added_conflicts_expr).await?;
2351
2352 fn commits_by_change_id(commits: &[Commit]) -> IndexMap<&ChangeId, Vec<&Commit>> {
2353 let mut result: IndexMap<&ChangeId, Vec<&Commit>> = IndexMap::new();
2354 for commit in commits {
2355 result.entry(commit.change_id()).or_default().push(commit);
2356 }
2357 result
2358 }
2359 let removed_conflicts_by_change_id = commits_by_change_id(&removed_conflict_commits);
2360 let added_conflicts_by_change_id = commits_by_change_id(&added_conflict_commits);
2361 let mut resolved_conflicts_by_change_id = removed_conflicts_by_change_id.clone();
2362 resolved_conflicts_by_change_id
2363 .retain(|change_id, _commits| !added_conflicts_by_change_id.contains_key(change_id));
2364 let mut new_conflicts_by_change_id = added_conflicts_by_change_id.clone();
2365 new_conflicts_by_change_id
2366 .retain(|change_id, _commits| !removed_conflicts_by_change_id.contains_key(change_id));
2367
2368 if !resolved_conflicts_by_change_id.is_empty() {
2370 let num_resolved: usize = resolved_conflicts_by_change_id
2374 .values()
2375 .map(|commits| commits.len())
2376 .sum();
2377 writeln!(
2378 fmt,
2379 "Existing conflicts were resolved or abandoned from {num_resolved} commits."
2380 )?;
2381 }
2382 if !new_conflicts_by_change_id.is_empty() {
2383 let num_conflicted: usize = new_conflicts_by_change_id
2384 .values()
2385 .map(|commits| commits.len())
2386 .sum();
2387 writeln!(fmt, "New conflicts appeared in {num_conflicted} commits:")?;
2388 print_updated_commits(
2389 fmt.as_mut(),
2390 &self.commit_summary_template(),
2391 new_conflicts_by_change_id.values().flatten().copied(),
2392 )?;
2393 }
2394
2395 if !(added_conflict_commits.is_empty()
2399 || resolved_conflicts_by_change_id.is_empty() && new_conflicts_by_change_id.is_empty())
2400 {
2401 if new_conflicts_by_change_id.is_empty() {
2406 writeln!(
2407 fmt,
2408 "There are still unresolved conflicts in rebased descendants.",
2409 )?;
2410 }
2411
2412 self.report_repo_conflicts(
2413 fmt.as_mut(),
2414 new_repo,
2415 added_conflict_commits
2416 .iter()
2417 .map(|commit| commit.id().clone())
2418 .collect(),
2419 )
2420 .await?;
2421 }
2422
2423 Ok(())
2424 }
2425
2426 pub async fn report_repo_conflicts(
2427 &self,
2428 fmt: &mut dyn Formatter,
2429 repo: &ReadonlyRepo,
2430 conflicted_commits: Vec<CommitId>,
2431 ) -> Result<(), CommandError> {
2432 if !self.settings().get_bool("hints.resolving-conflicts")? || conflicted_commits.is_empty()
2433 {
2434 return Ok(());
2435 }
2436
2437 let only_one_conflicted_commit = conflicted_commits.len() == 1;
2438 let root_conflicts_revset = RevsetExpression::commits(conflicted_commits)
2439 .roots()
2440 .evaluate(repo)?;
2441
2442 let root_conflict_commits: Vec<_> = root_conflicts_revset
2443 .stream()
2444 .commits(repo.store())
2445 .try_collect()
2446 .await?;
2447
2448 let instruction = if only_one_conflicted_commit {
2450 indoc! {"
2451 To resolve the conflicts, start by creating a commit on top of
2452 the conflicted commit:
2453 "}
2454 } else if root_conflict_commits.len() == 1 {
2455 indoc! {"
2456 To resolve the conflicts, start by creating a commit on top of
2457 the first conflicted commit:
2458 "}
2459 } else {
2460 indoc! {"
2461 To resolve the conflicts, start by creating a commit on top of
2462 one of the first conflicted commits:
2463 "}
2464 };
2465 write!(fmt.labeled("hint").with_heading("Hint: "), "{instruction}")?;
2466 let format_short_change_id = self.short_change_id_template();
2467 {
2468 let mut fmt = fmt.labeled("hint");
2469 for commit in &root_conflict_commits {
2470 write!(fmt, " jj new ")?;
2471 format_short_change_id.format(commit, *fmt)?;
2472 writeln!(fmt)?;
2473 }
2474 }
2475 writedoc!(
2476 fmt.labeled("hint"),
2477 "
2478 Then use `jj resolve`, or edit the conflict markers in the file directly.
2479 Once the conflicts are resolved, you can inspect the result with `jj diff`.
2480 Then run `jj squash` to move the resolution into the conflicted commit.
2481 ",
2482 )?;
2483 Ok(())
2484 }
2485
2486 pub fn get_advanceable_bookmarks<'a>(
2501 &self,
2502 ui: &Ui,
2503 from: impl IntoIterator<Item = &'a CommitId>,
2504 ) -> Result<Vec<AdvanceableBookmark>, CommandError> {
2505 let Some(ab_matcher) = load_advance_bookmarks_matcher(ui, self.settings())? else {
2506 return Ok(Vec::new());
2508 };
2509
2510 let mut advanceable_bookmarks = Vec::new();
2511 for from_commit in from {
2512 for (name, _) in self.repo().view().local_bookmarks_for_commit(from_commit) {
2513 if ab_matcher.is_match(name.as_str()) {
2514 advanceable_bookmarks.push(AdvanceableBookmark {
2515 name: name.to_owned(),
2516 old_commit_id: from_commit.clone(),
2517 });
2518 }
2519 }
2520 }
2521
2522 Ok(advanceable_bookmarks)
2523 }
2524}
2525
2526#[cfg(feature = "git")]
2527pub async fn export_working_copy_changes_to_git(
2528 ui: &Ui,
2529 mut_repo: &mut MutableRepo,
2530 old_tree: &MergedTree,
2531 new_tree: &MergedTree,
2532) -> Result<(), CommandError> {
2533 let repo = mut_repo.base_repo().as_ref();
2534 jj_lib::git::update_intent_to_add(repo, old_tree, new_tree).await?;
2535 let stats = jj_lib::git::export_refs(mut_repo)?;
2536 crate::git_util::print_git_export_stats(ui, &stats)?;
2537 Ok(())
2538}
2539#[cfg(not(feature = "git"))]
2540pub async fn export_working_copy_changes_to_git(
2541 _ui: &Ui,
2542 _mut_repo: &mut MutableRepo,
2543 _old_tree: &MergedTree,
2544 _new_tree: &MergedTree,
2545) -> Result<(), CommandError> {
2546 Ok(())
2547}
2548
2549#[must_use]
2557pub struct WorkspaceCommandTransaction<'a> {
2558 helper: &'a mut WorkspaceCommandHelper,
2559 tx: Transaction,
2560 id_prefix_context: OnceCell<IdPrefixContext>,
2562}
2563
2564impl WorkspaceCommandTransaction<'_> {
2565 pub fn base_workspace_helper(&self) -> &WorkspaceCommandHelper {
2567 self.helper
2568 }
2569
2570 pub fn settings(&self) -> &UserSettings {
2572 self.helper.settings()
2573 }
2574
2575 pub fn base_repo(&self) -> &Arc<ReadonlyRepo> {
2576 self.tx.base_repo()
2577 }
2578
2579 pub fn repo(&self) -> &MutableRepo {
2580 self.tx.repo()
2581 }
2582
2583 pub fn repo_mut(&mut self) -> &mut MutableRepo {
2584 self.id_prefix_context.take(); self.tx.repo_mut()
2586 }
2587
2588 pub fn check_out(&mut self, commit: &Commit) -> Result<Commit, CheckOutCommitError> {
2589 let name = self.helper.workspace_name().to_owned();
2590 self.id_prefix_context.take(); self.tx.repo_mut().check_out(name, commit).block_on()
2592 }
2593
2594 pub fn edit(&mut self, commit: &Commit) -> Result<(), EditCommitError> {
2595 let name = self.helper.workspace_name().to_owned();
2596 self.id_prefix_context.take(); self.tx.repo_mut().edit(name, commit).block_on()
2598 }
2599
2600 pub fn format_commit_summary(&self, commit: &Commit) -> String {
2601 let output = self.commit_summary_template().format_plain_text(commit);
2602 output.into_string_lossy()
2603 }
2604
2605 pub fn write_commit_summary(
2606 &self,
2607 formatter: &mut dyn Formatter,
2608 commit: &Commit,
2609 ) -> std::io::Result<()> {
2610 self.commit_summary_template().format(commit, formatter)
2611 }
2612
2613 pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
2615 let language = self.commit_template_language();
2616 self.helper
2617 .reparse_valid_template(&language, &self.helper.commit_summary_template_text)
2618 .labeled(["commit"])
2619 }
2620
2621 pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
2624 let id_prefix_context = self
2625 .id_prefix_context
2626 .get_or_init(|| self.helper.env.new_id_prefix_context());
2627 self.helper
2628 .env
2629 .commit_template_language(self.tx.repo(), id_prefix_context)
2630 }
2631
2632 pub fn parse_commit_template(
2634 &self,
2635 ui: &Ui,
2636 template_text: &str,
2637 ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
2638 let language = self.commit_template_language();
2639 self.helper.env.parse_template(ui, &language, template_text)
2640 }
2641
2642 pub async fn finish(self, ui: &Ui, description: impl Into<String>) -> Result<(), CommandError> {
2643 if !self.tx.repo().has_changes() {
2644 writeln!(ui.status(), "Nothing changed.")?;
2645 return Ok(());
2646 }
2647 let git_import_export_lock = self.helper.lock_git_import_export()?;
2650 self.helper
2651 .finish_transaction(ui, self.tx, description, &git_import_export_lock)
2652 .await
2653 }
2654
2655 pub fn into_inner(self) -> Transaction {
2660 self.tx
2661 }
2662
2663 pub fn advance_bookmarks(
2669 &mut self,
2670 bookmarks: Vec<AdvanceableBookmark>,
2671 move_to: &CommitId,
2672 ) -> Result<(), CommandError> {
2673 for bookmark in bookmarks {
2674 self.repo_mut().merge_local_bookmark(
2677 &bookmark.name,
2678 &RefTarget::normal(bookmark.old_commit_id),
2679 &RefTarget::normal(move_to.clone()),
2680 )?;
2681 }
2682 Ok(())
2683 }
2684}
2685
2686pub fn find_workspace_dir(cwd: &Path) -> &Path {
2687 cwd.ancestors()
2688 .find(|path| path.join(".jj").is_dir())
2689 .unwrap_or(cwd)
2690}
2691
2692fn map_workspace_load_error(err: WorkspaceLoadError, user_wc_path: Option<&str>) -> CommandError {
2693 match err {
2694 WorkspaceLoadError::NoWorkspaceHere(wc_path) => {
2695 let short_wc_path = user_wc_path.map_or(wc_path.as_ref(), Path::new);
2697 let message = format!(r#"There is no jj repo in "{}""#, short_wc_path.display());
2698 let git_dir = wc_path.join(".git");
2699 if git_dir.is_dir() {
2700 user_error(message).hinted(
2701 "It looks like this is a git repo. You can create a jj repo backed by it by \
2702 running this:
2703jj git init",
2704 )
2705 } else {
2706 user_error(message)
2707 }
2708 }
2709 WorkspaceLoadError::RepoDoesNotExist(repo_dir) => user_error(format!(
2710 "The repository directory at {} is missing. Was it moved?",
2711 repo_dir.display(),
2712 )),
2713 WorkspaceLoadError::StoreLoadError(err @ StoreLoadError::UnsupportedType { .. }) => {
2714 internal_error_with_message(
2715 "This version of the jj binary doesn't support this type of repo",
2716 err,
2717 )
2718 }
2719 WorkspaceLoadError::StoreLoadError(
2720 err @ (StoreLoadError::ReadError { .. } | StoreLoadError::Backend(_)),
2721 ) => internal_error_with_message("The repository appears broken or inaccessible", err),
2722 WorkspaceLoadError::StoreLoadError(StoreLoadError::Signing(err)) => user_error(err),
2723 WorkspaceLoadError::WorkingCopyState(err) => internal_error(err),
2724 WorkspaceLoadError::DecodeRepoPath(_) | WorkspaceLoadError::Path(_) => user_error(err),
2725 }
2726}
2727
2728pub fn start_repo_transaction(
2729 repo: &Arc<ReadonlyRepo>,
2730 workspace_name: &WorkspaceName,
2731 string_args: &[String],
2732) -> Transaction {
2733 let mut tx = repo.start_transaction();
2734 tx.set_workspace_name(workspace_name);
2735 let shell_escape = |arg: &String| {
2738 if arg.as_bytes().iter().all(|b| {
2739 matches!(b,
2740 b'A'..=b'Z'
2741 | b'a'..=b'z'
2742 | b'0'..=b'9'
2743 | b','
2744 | b'-'
2745 | b'.'
2746 | b'/'
2747 | b':'
2748 | b'@'
2749 | b'_'
2750 )
2751 }) {
2752 arg.clone()
2753 } else {
2754 format!("'{}'", arg.replace('\'', "\\'"))
2755 }
2756 };
2757 let mut quoted_strings = vec!["jj".to_string()];
2758 quoted_strings.extend(string_args.iter().skip(1).map(shell_escape));
2759 tx.set_tag("args".to_string(), quoted_strings.join(" "));
2760 tx
2761}
2762
2763async fn handle_stale_working_copy(
2769 locked_wc: &mut dyn LockedWorkingCopy,
2770 repo: Arc<ReadonlyRepo>,
2771 workspace_name: &WorkspaceName,
2772) -> Result<Option<(Arc<ReadonlyRepo>, Commit)>, SnapshotWorkingCopyError> {
2773 let get_wc_commit = |repo: &ReadonlyRepo| -> Result<Option<_>, _> {
2774 repo.view()
2775 .get_wc_commit_id(workspace_name)
2776 .map(|id| repo.store().get_commit(id))
2777 .transpose()
2778 .map_err(snapshot_command_error)
2779 };
2780 let Some(wc_commit) = get_wc_commit(&repo)? else {
2781 return Ok(None);
2782 };
2783 let old_op_id = locked_wc.old_operation_id().clone();
2784 match WorkingCopyFreshness::check_stale(locked_wc, &wc_commit, &repo).await {
2785 Ok(WorkingCopyFreshness::Fresh) => Ok(Some((repo, wc_commit))),
2786 Ok(WorkingCopyFreshness::Updated(wc_operation)) => {
2787 let repo = repo
2788 .reload_at(&wc_operation)
2789 .await
2790 .map_err(snapshot_command_error)?;
2791 if let Some(wc_commit) = get_wc_commit(&repo)? {
2792 Ok(Some((repo, wc_commit)))
2793 } else {
2794 Ok(None)
2795 }
2796 }
2797 Ok(WorkingCopyFreshness::WorkingCopyStale) => {
2798 Err(SnapshotWorkingCopyError::StaleWorkingCopy(
2799 user_error(format!(
2800 "The working copy is stale (not updated since operation {}).",
2801 short_operation_hash(&old_op_id)
2802 ))
2803 .hinted(
2804 "Run `jj workspace update-stale` to update it.
2805See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy \
2806 for more information.",
2807 ),
2808 ))
2809 }
2810 Ok(WorkingCopyFreshness::SiblingOperation) => {
2811 Err(SnapshotWorkingCopyError::StaleWorkingCopy(
2812 internal_error(format!(
2813 "The repo was loaded at operation {}, which seems to be a sibling of the \
2814 working copy's operation {}",
2815 short_operation_hash(repo.op_id()),
2816 short_operation_hash(&old_op_id)
2817 ))
2818 .hinted(format!(
2819 "Run `jj op integrate {}` to add the working copy's operation to the \
2820 operation log.",
2821 short_operation_hash(&old_op_id)
2822 )),
2823 ))
2824 }
2825 Err(OpStoreError::ObjectNotFound { .. }) => {
2826 Err(SnapshotWorkingCopyError::StaleWorkingCopy(
2827 user_error("Could not read working copy's operation.").hinted(
2828 "Run `jj workspace update-stale` to recover.
2829See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy \
2830 for more information.",
2831 ),
2832 ))
2833 }
2834 Err(e) => Err(snapshot_command_error(e)),
2835 }
2836}
2837
2838async fn update_stale_working_copy(
2839 mut locked_ws: LockedWorkspace<'_>,
2840 op_id: OperationId,
2841 stale_commit: &Commit,
2842 new_commit: &Commit,
2843) -> Result<CheckoutStats, CommandError> {
2844 if stale_commit.tree().tree_ids_and_labels()
2847 != locked_ws.locked_wc().old_tree().tree_ids_and_labels()
2848 {
2849 return Err(user_error("Concurrent working copy operation. Try again."));
2850 }
2851 let stats = locked_ws
2852 .locked_wc()
2853 .check_out(new_commit)
2854 .await
2855 .map_err(|err| {
2856 internal_error_with_message(
2857 format!("Failed to check out commit {}", new_commit.id().hex()),
2858 err,
2859 )
2860 })?;
2861 locked_ws.finish(op_id).await?;
2862
2863 Ok(stats)
2864}
2865
2866pub fn print_updated_commits<'a>(
2869 formatter: &mut dyn Formatter,
2870 template: &TemplateRenderer<Commit>,
2871 commits: impl IntoIterator<Item = &'a Commit>,
2872) -> io::Result<()> {
2873 let mut commits = commits.into_iter().fuse();
2874 for commit in commits.by_ref().take(10) {
2875 write!(formatter, " ")?;
2876 template.format(commit, formatter)?;
2877 writeln!(formatter)?;
2878 }
2879 if commits.next().is_some() {
2880 writeln!(formatter, " ...")?;
2881 }
2882 Ok(())
2883}
2884
2885#[instrument(skip_all)]
2886pub fn print_conflicted_paths(
2887 conflicts: Vec<(RepoPathBuf, BackendResult<MergedTreeValue>)>,
2888 formatter: &mut dyn Formatter,
2889 workspace_command: &WorkspaceCommandHelper,
2890) -> Result<(), CommandError> {
2891 let formatted_paths = conflicts
2892 .iter()
2893 .map(|(path, _conflict)| workspace_command.format_file_path(path))
2894 .collect_vec();
2895 let max_path_len = formatted_paths.iter().map(|p| p.len()).max().unwrap_or(0);
2896 let formatted_paths = formatted_paths
2897 .into_iter()
2898 .map(|p| format!("{:width$}", p, width = max_path_len.min(32) + 3));
2899
2900 for ((_, conflict), formatted_path) in std::iter::zip(conflicts, formatted_paths) {
2901 let conflict = conflict?.simplify();
2904 let sides = conflict.num_sides();
2905 let n_adds = conflict.adds().flatten().count();
2906 let deletions = sides - n_adds;
2907
2908 let mut seen_objects = BTreeMap::new(); if deletions > 0 {
2910 seen_objects.insert(
2911 format!(
2912 "{deletions} deletion{}",
2914 if deletions > 1 { "s" } else { "" }
2915 ),
2916 "normal", );
2918 }
2919 for term in itertools::chain(conflict.removes(), conflict.adds()).flatten() {
2923 seen_objects.insert(
2924 match term {
2925 TreeValue::File {
2926 executable: false, ..
2927 } => continue,
2928 TreeValue::File {
2929 executable: true, ..
2930 } => "an executable",
2931 TreeValue::Symlink(_) => "a symlink",
2932 TreeValue::Tree(_) => "a directory",
2933 TreeValue::GitSubmodule(_) => "a git submodule",
2934 }
2935 .to_string(),
2936 "difficult",
2937 );
2938 }
2939
2940 write!(formatter, "{formatted_path} ")?;
2941 {
2942 let mut formatter = formatter.labeled("conflict_description");
2943 let print_pair = |formatter: &mut dyn Formatter, (text, label): &(String, &str)| {
2944 write!(formatter.labeled(label), "{text}")
2945 };
2946 print_pair(
2947 *formatter,
2948 &(
2949 format!("{sides}-sided"),
2950 if sides > 2 { "difficult" } else { "normal" },
2951 ),
2952 )?;
2953 write!(formatter, " conflict")?;
2954
2955 if !seen_objects.is_empty() {
2956 write!(formatter, " including ")?;
2957 let seen_objects = seen_objects.into_iter().collect_vec();
2958 match &seen_objects[..] {
2959 [] => unreachable!(),
2960 [only] => print_pair(*formatter, only)?,
2961 [first, middle @ .., last] => {
2962 print_pair(*formatter, first)?;
2963 for pair in middle {
2964 write!(formatter, ", ")?;
2965 print_pair(*formatter, pair)?;
2966 }
2967 write!(formatter, " and ")?;
2968 print_pair(*formatter, last)?;
2969 }
2970 }
2971 }
2972 }
2973 writeln!(formatter)?;
2974 }
2975 Ok(())
2976}
2977
2978fn build_untracked_reason_message(reason: &UntrackedReason) -> Option<String> {
2980 match reason {
2981 UntrackedReason::FileTooLarge { size, max_size } => {
2982 let size_approx = HumanByteSize(*size);
2985 let max_size_approx = HumanByteSize(*max_size);
2986 Some(format!(
2987 "{size_approx} ({size} bytes); the maximum size allowed is {max_size_approx} \
2988 ({max_size} bytes)",
2989 ))
2990 }
2991 UntrackedReason::FileNotAutoTracked => None,
2995 }
2996}
2997
2998pub fn print_untracked_files(
3000 ui: &Ui,
3001 untracked_paths: &BTreeMap<RepoPathBuf, UntrackedReason>,
3002 path_converter: &RepoPathUiConverter,
3003) -> io::Result<()> {
3004 let mut untracked_paths = untracked_paths
3005 .iter()
3006 .filter_map(|(path, reason)| build_untracked_reason_message(reason).map(|m| (path, m)))
3007 .peekable();
3008
3009 if untracked_paths.peek().is_some() {
3010 writeln!(ui.warning_default(), "Refused to snapshot some files:")?;
3011 let mut formatter = ui.stderr_formatter();
3012 for (path, message) in untracked_paths {
3013 let ui_path = path_converter.format_file_path(path);
3014 writeln!(formatter, " {ui_path}: {message}")?;
3015 }
3016 }
3017
3018 Ok(())
3019}
3020
3021pub fn print_snapshot_stats(
3022 ui: &Ui,
3023 stats: &SnapshotStats,
3024 path_converter: &RepoPathUiConverter,
3025) -> io::Result<()> {
3026 print_untracked_files(ui, &stats.untracked_paths, path_converter)?;
3027
3028 let large_files_sizes = stats
3029 .untracked_paths
3030 .values()
3031 .filter_map(|reason| match reason {
3032 UntrackedReason::FileTooLarge { size, .. } => Some(size),
3033 UntrackedReason::FileNotAutoTracked => None,
3034 });
3035 if let Some(size) = large_files_sizes.max() {
3036 print_large_file_hint(ui, *size, None)?;
3037 }
3038 Ok(())
3039}
3040
3041pub fn print_large_file_hint(
3048 ui: &Ui,
3049 max_size: u64,
3050 large_files: Option<&[String]>,
3051) -> io::Result<()> {
3052 let (command, extra) = large_files
3053 .map(|files| {
3054 let files_list = files
3057 .iter()
3058 .map(|s| shlex::try_quote(s).unwrap_or(s.into()))
3059 .join(" ");
3060 let command = format!("file track {files_list}");
3061 let extra = format!(
3062 r"
3063 * Run `jj file track --include-ignored {files_list}`
3064 This will track the file(s) regardless of size."
3065 );
3066 (command, extra)
3067 })
3068 .unwrap_or(("status".to_string(), String::new()));
3069
3070 writedoc!(
3071 ui.hint_default(),
3072 r"
3073 This is to prevent large files from being added by accident. To fix this:
3074 * Add the file(s) to `.gitignore`
3075 * Run `jj config set --repo snapshot.max-new-file-size {max_size}`
3076 This will increase the maximum file size allowed for new files, in this repository only.
3077 * Run `jj --config snapshot.max-new-file-size={max_size} {command}`
3078 This will increase the maximum file size allowed for new files, for this command only.{extra}
3079 "
3080 )?;
3081 Ok(())
3082}
3083
3084pub fn print_checkout_stats(
3085 ui: &Ui,
3086 stats: &CheckoutStats,
3087 new_commit: &Commit,
3088) -> Result<(), std::io::Error> {
3089 if stats.added_files > 0 || stats.updated_files > 0 || stats.removed_files > 0 {
3090 writeln!(
3091 ui.status(),
3092 "Added {} files, modified {} files, removed {} files",
3093 stats.added_files,
3094 stats.updated_files,
3095 stats.removed_files
3096 )?;
3097 }
3098 if stats.skipped_files != 0 {
3099 writeln!(
3100 ui.warning_default(),
3101 "{} of those updates were skipped because there were conflicting changes in the \
3102 working copy.",
3103 stats.skipped_files
3104 )?;
3105 writeln!(
3106 ui.hint_default(),
3107 "Inspect the changes compared to the intended target with `jj diff --from {}`.
3108Discard the conflicting changes with `jj restore --from {}`.",
3109 short_commit_hash(new_commit.id()),
3110 short_commit_hash(new_commit.id())
3111 )?;
3112 }
3113 Ok(())
3114}
3115
3116pub fn print_unmatched_explicit_paths<'a>(
3119 ui: &Ui,
3120 workspace_command: &WorkspaceCommandHelper,
3121 expression: &FilesetExpression,
3122 trees: impl IntoIterator<Item = &'a MergedTree>,
3123) -> io::Result<()> {
3124 let mut explicit_paths = expression.explicit_paths().collect_vec();
3125 for tree in trees {
3126 explicit_paths.retain(|&path| tree.path_value(path).block_on().unwrap().is_absent());
3128 }
3129
3130 if !explicit_paths.is_empty() {
3131 let ui_paths = explicit_paths
3132 .iter()
3133 .map(|&path| workspace_command.format_file_path(path))
3134 .join(", ");
3135 writeln!(
3136 ui.warning_default(),
3137 "No matching entries for paths: {ui_paths}"
3138 )?;
3139 }
3140
3141 Ok(())
3142}
3143
3144pub async fn update_working_copy(
3145 repo: &Arc<ReadonlyRepo>,
3146 workspace: &mut Workspace,
3147 old_commit: Option<&Commit>,
3148 new_commit: &Commit,
3149) -> Result<CheckoutStats, CommandError> {
3150 let old_tree = old_commit.map(|commit| commit.tree());
3151 let stats = workspace
3154 .check_out(repo.op_id().clone(), old_tree.as_ref(), new_commit)
3155 .await
3156 .map_err(|err| {
3157 internal_error_with_message(
3158 format!("Failed to check out commit {}", new_commit.id().hex()),
3159 err,
3160 )
3161 })?;
3162 Ok(stats)
3163}
3164
3165#[cfg_attr(not(feature = "git"), expect(unused_variables))]
3167pub fn default_ignored_remote_name(store: &Store) -> Option<&'static RemoteName> {
3168 #[cfg(feature = "git")]
3169 {
3170 use jj_lib::git;
3171 if git::get_git_backend(store).is_ok() {
3172 return Some(git::REMOTE_NAME_FOR_LOCAL_GIT_REPO);
3173 }
3174 }
3175 None
3176}
3177
3178pub fn has_tracked_remote_bookmarks(repo: &dyn Repo, bookmark: &RefName) -> bool {
3181 let remote_matcher = match default_ignored_remote_name(repo.store()) {
3182 Some(remote) => StringExpression::exact(remote).negated().to_matcher(),
3183 None => StringMatcher::all(),
3184 };
3185 repo.view()
3186 .remote_bookmarks_matching(&StringMatcher::exact(bookmark), &remote_matcher)
3187 .any(|(_, remote_ref)| remote_ref.is_tracked())
3188}
3189
3190pub fn load_fileset_aliases(
3191 ui: &Ui,
3192 config: &StackedConfig,
3193) -> Result<FilesetAliasesMap, CommandError> {
3194 let table_name = ConfigNamePathBuf::from_iter(["fileset-aliases"]);
3195 load_aliases_map(ui, config, &table_name)
3196}
3197
3198pub fn load_revset_aliases(
3199 ui: &Ui,
3200 config: &StackedConfig,
3201) -> Result<RevsetAliasesMap, CommandError> {
3202 let table_name = ConfigNamePathBuf::from_iter(["revset-aliases"]);
3203 let aliases_map = load_aliases_map(ui, config, &table_name)?;
3204 revset_util::warn_user_redefined_builtin(ui, config, &table_name)?;
3205 Ok(aliases_map)
3206}
3207
3208pub fn load_template_aliases(
3209 ui: &Ui,
3210 config: &StackedConfig,
3211) -> Result<TemplateAliasesMap, CommandError> {
3212 let table_name = ConfigNamePathBuf::from_iter(["template-aliases"]);
3213 load_aliases_map(ui, config, &table_name)
3214}
3215
3216#[derive(Clone, Debug)]
3218pub struct LogContentFormat {
3219 width: usize,
3220 word_wrap: bool,
3221}
3222
3223impl LogContentFormat {
3224 pub fn new(ui: &Ui, settings: &UserSettings) -> Result<Self, ConfigGetError> {
3226 Ok(Self {
3227 width: ui.term_width(),
3228 word_wrap: settings.get_bool("ui.log-word-wrap")?,
3229 })
3230 }
3231
3232 #[must_use]
3234 pub fn sub_width(&self, width: usize) -> Self {
3235 Self {
3236 width: self.width.saturating_sub(width),
3237 word_wrap: self.word_wrap,
3238 }
3239 }
3240
3241 pub fn width(&self) -> usize {
3243 self.width
3244 }
3245
3246 pub fn write<E: From<io::Error>>(
3248 &self,
3249 formatter: &mut dyn Formatter,
3250 content_fn: impl FnOnce(&mut dyn Formatter) -> Result<(), E>,
3251 ) -> Result<(), E> {
3252 if self.word_wrap {
3253 let mut recorder = FormatRecorder::new(formatter.maybe_color());
3254 content_fn(&mut recorder)?;
3255 text_util::write_wrapped(formatter, &recorder, self.width)?;
3256 } else {
3257 content_fn(formatter)?;
3258 }
3259 Ok(())
3260 }
3261}
3262
3263pub fn short_commit_hash(commit_id: &CommitId) -> String {
3264 format!("{commit_id:.12}")
3265}
3266
3267pub fn short_change_hash(change_id: &ChangeId) -> String {
3268 format!("{change_id:.12}")
3269}
3270
3271pub fn short_operation_hash(operation_id: &OperationId) -> String {
3272 format!("{operation_id:.12}")
3273}
3274
3275#[derive(Clone, Debug)]
3277pub enum DiffSelector {
3278 NonInteractive,
3279 Interactive(DiffEditor),
3280}
3281
3282impl DiffSelector {
3283 pub fn is_interactive(&self) -> bool {
3284 matches!(self, Self::Interactive(_))
3285 }
3286
3287 pub async fn select(
3292 &self,
3293 ui: &Ui,
3294 trees: Diff<&MergedTree>,
3295 tree_labels: Diff<String>,
3296 matcher: &dyn Matcher,
3297 format_instructions: impl FnOnce() -> String,
3298 ) -> Result<MergedTree, CommandError> {
3299 let selected_tree = restore_tree(
3300 trees.after,
3301 trees.before,
3302 tree_labels.after,
3303 tree_labels.before,
3304 matcher,
3305 )
3306 .await?;
3307 match self {
3308 Self::NonInteractive => Ok(selected_tree),
3309 Self::Interactive(editor) => {
3310 if selected_tree.tree_ids() == trees.before.tree_ids() {
3311 writeln!(ui.warning_default(), "Empty diff - won't run diff editor.")?;
3312 Ok(selected_tree)
3313 } else {
3314 Ok(editor
3318 .edit(
3319 Diff::new(trees.before, &selected_tree),
3320 matcher,
3321 format_instructions,
3322 )
3323 .await?)
3324 }
3325 }
3326 }
3327 }
3328}
3329
3330#[derive(Clone, Debug)]
3332pub(crate) struct RemoteBookmarkNamePattern {
3333 pub bookmark: StringPattern,
3334 pub remote: StringPattern,
3335}
3336
3337impl FromStr for RemoteBookmarkNamePattern {
3338 type Err = String;
3339
3340 fn from_str(src: &str) -> Result<Self, Self::Err> {
3341 let (maybe_kind, pat) = src
3346 .split_once(':')
3347 .map_or((None, src), |(kind, pat)| (Some(kind), pat));
3348 let to_pattern = |pat: &str| {
3349 if let Some(kind) = maybe_kind {
3350 StringPattern::from_str_kind(pat, kind).map_err(|err| err.to_string())
3351 } else {
3352 StringPattern::glob(pat).map_err(|err| err.to_string())
3353 }
3354 };
3355 let (bookmark, remote) = pat.rsplit_once('@').ok_or_else(|| {
3357 "remote bookmark must be specified in bookmark@remote form".to_owned()
3358 })?;
3359 Ok(Self {
3360 bookmark: to_pattern(bookmark)?,
3361 remote: to_pattern(remote)?,
3362 })
3363 }
3364}
3365
3366impl RemoteBookmarkNamePattern {
3367 pub fn as_exact(&self) -> Option<RemoteRefSymbol<'_>> {
3368 let bookmark = RefName::new(self.bookmark.as_exact()?);
3369 let remote = RemoteName::new(self.remote.as_exact()?);
3370 Some(bookmark.to_remote_symbol(remote))
3371 }
3372}
3373
3374impl fmt::Display for RemoteBookmarkNamePattern {
3375 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3376 let Self { bookmark, remote } = self;
3379 write!(f, "{bookmark}@{remote}")
3380 }
3381}
3382
3383pub async fn compute_commit_location(
3388 ui: &Ui,
3389 workspace_command: &WorkspaceCommandHelper,
3390 destination: Option<&[RevisionArg]>,
3391 insert_after: Option<&[RevisionArg]>,
3392 insert_before: Option<&[RevisionArg]>,
3393 commit_type: &str,
3394) -> Result<(Vec<CommitId>, Vec<CommitId>), CommandError> {
3395 let resolve_revisions =
3396 async |revisions: Option<&[RevisionArg]>| -> Result<Option<Vec<CommitId>>, CommandError> {
3397 if let Some(revisions) = revisions {
3398 Ok(Some(
3399 workspace_command
3400 .resolve_revsets_ordered(ui, revisions)
3401 .await?
3402 .into_iter()
3403 .collect_vec(),
3404 ))
3405 } else {
3406 Ok(None)
3407 }
3408 };
3409 let destination_commit_ids = resolve_revisions(destination).await?;
3410 let after_commit_ids = resolve_revisions(insert_after).await?;
3411 let before_commit_ids = resolve_revisions(insert_before).await?;
3412
3413 let (new_parent_ids, new_child_ids) =
3414 match (destination_commit_ids, after_commit_ids, before_commit_ids) {
3415 (Some(destination_commit_ids), None, None) => (destination_commit_ids, vec![]),
3416 (None, Some(after_commit_ids), Some(before_commit_ids)) => {
3417 (after_commit_ids, before_commit_ids)
3418 }
3419 (None, Some(after_commit_ids), None) => {
3420 let new_child_ids: Vec<_> = RevsetExpression::commits(after_commit_ids.clone())
3421 .children()
3422 .evaluate(workspace_command.repo().as_ref())?
3423 .stream()
3424 .try_collect()
3425 .await?;
3426
3427 (after_commit_ids, new_child_ids)
3428 }
3429 (None, None, Some(before_commit_ids)) => {
3430 let before_commits: Vec<_> = before_commit_ids
3431 .iter()
3432 .map(|id| workspace_command.repo().store().get_commit(id))
3433 .try_collect()?;
3434 let new_parent_ids = before_commits
3437 .iter()
3438 .flat_map(|commit| commit.parent_ids())
3439 .unique()
3440 .cloned()
3441 .collect_vec();
3442
3443 (new_parent_ids, before_commit_ids)
3444 }
3445 (Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
3446 panic!("destination cannot be used with insert_after/insert_before")
3447 }
3448 (None, None, None) => {
3449 panic!("expected at least one of destination or insert_after/insert_before")
3450 }
3451 };
3452
3453 if !new_child_ids.is_empty() {
3454 workspace_command
3455 .check_rewritable(new_child_ids.iter())
3456 .await?;
3457 ensure_no_commit_loop(
3458 workspace_command.repo().as_ref(),
3459 &RevsetExpression::commits(new_child_ids.clone()),
3460 &RevsetExpression::commits(new_parent_ids.clone()),
3461 commit_type,
3462 )
3463 .await?;
3464 }
3465
3466 if new_parent_ids.is_empty() {
3467 return Err(user_error("No revisions found to use as parent"));
3468 }
3469
3470 Ok((new_parent_ids, new_child_ids))
3471}
3472
3473async fn ensure_no_commit_loop(
3476 repo: &ReadonlyRepo,
3477 children_expression: &Arc<ResolvedRevsetExpression>,
3478 parents_expression: &Arc<ResolvedRevsetExpression>,
3479 commit_type: &str,
3480) -> Result<(), CommandError> {
3481 if let Some(commit_id) = children_expression
3482 .dag_range_to(parents_expression)
3483 .evaluate(repo)?
3484 .stream()
3485 .try_next()
3486 .await?
3487 {
3488 return Err(user_error(format!(
3489 "Refusing to create a loop: commit {} would be both an ancestor and a descendant of \
3490 the {commit_type}",
3491 short_commit_hash(&commit_id),
3492 )));
3493 }
3494 Ok(())
3495}
3496
3497#[derive(clap::Parser, Clone, Debug)]
3504#[command(name = "jj")]
3505pub struct Args {
3506 #[command(flatten)]
3507 pub global_args: GlobalArgs,
3508}
3509
3510#[derive(clap::Args, Clone, Debug)]
3511#[command(next_help_heading = "Global Options")]
3512pub struct GlobalArgs {
3513 #[arg(long, short = 'R', global = true, value_hint = clap::ValueHint::DirPath)]
3518 pub repository: Option<String>,
3519
3520 #[arg(long, global = true)]
3533 pub ignore_working_copy: bool,
3534
3535 #[arg(long, global = true)]
3544 pub ignore_immutable: bool,
3545
3546 #[arg(long, visible_alias = "at-op", global = true)]
3569 #[arg(add = ArgValueCandidates::new(complete::operations))]
3570 pub at_operation: Option<String>,
3571
3572 #[arg(long, global = true)]
3574 pub debug: bool,
3575
3576 #[command(flatten)]
3577 pub early_args: EarlyArgs,
3578}
3579
3580#[derive(clap::Args, Clone, Debug)]
3581pub struct EarlyArgs {
3582 #[arg(long, value_name = "WHEN", global = true)]
3584 pub color: Option<ColorChoice>,
3585
3586 #[arg(long, global = true, action = ArgAction::SetTrue)]
3593 pub quiet: Option<bool>,
3596
3597 #[arg(long, global = true, action = ArgAction::SetTrue)]
3599 pub no_pager: Option<bool>,
3602
3603 #[arg(long, value_name = "NAME=VALUE", global = true)]
3609 #[arg(add = ArgValueCompleter::new(complete::leaf_config_key_value))]
3610 pub config: Vec<String>,
3611
3612 #[arg(long, value_name = "PATH", global = true, value_hint = clap::ValueHint::FilePath)]
3614 pub config_file: Vec<String>,
3615}
3616
3617impl EarlyArgs {
3618 pub(crate) fn merged_config_args(&self, matches: &ArgMatches) -> Vec<(ConfigArgKind, &str)> {
3619 merge_args_with(
3620 matches,
3621 &[("config", &self.config), ("config_file", &self.config_file)],
3622 |id, value| match id {
3623 "config" => (ConfigArgKind::Item, value.as_ref()),
3624 "config_file" => (ConfigArgKind::File, value.as_ref()),
3625 _ => unreachable!("unexpected id {id:?}"),
3626 },
3627 )
3628 }
3629
3630 fn has_config_args(&self) -> bool {
3631 !self.config.is_empty() || !self.config_file.is_empty()
3632 }
3633}
3634
3635#[derive(Clone, Debug)]
3641pub struct RevisionArg(Cow<'static, str>);
3642
3643impl RevisionArg {
3644 pub const AT: Self = Self(Cow::Borrowed("@"));
3646}
3647
3648impl From<String> for RevisionArg {
3649 fn from(s: String) -> Self {
3650 Self(s.into())
3651 }
3652}
3653
3654impl AsRef<str> for RevisionArg {
3655 fn as_ref(&self) -> &str {
3656 &self.0
3657 }
3658}
3659
3660impl fmt::Display for RevisionArg {
3661 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3662 write!(f, "{}", self.0)
3663 }
3664}
3665
3666impl ValueParserFactory for RevisionArg {
3667 type Parser = MapValueParser<NonEmptyStringValueParser, fn(String) -> Self>;
3668
3669 fn value_parser() -> Self::Parser {
3670 NonEmptyStringValueParser::new().map(Self::from)
3671 }
3672}
3673
3674pub fn merge_args_with<'k, 'v, T, U>(
3682 matches: &ArgMatches,
3683 id_values: &[(&'k str, &'v [T])],
3684 mut convert: impl FnMut(&'k str, &'v T) -> U,
3685) -> Vec<U> {
3686 let mut pos_values: Vec<(usize, U)> = Vec::new();
3687 for (id, values) in id_values {
3688 pos_values.extend(itertools::zip_eq(
3689 matches.indices_of(id).into_iter().flatten(),
3690 values.iter().map(|v| convert(id, v)),
3691 ));
3692 }
3693 pos_values.sort_unstable_by_key(|&(pos, _)| pos);
3694 pos_values.into_iter().map(|(_, value)| value).collect()
3695}
3696
3697fn resolve_default_command(
3698 ui: &Ui,
3699 config: &StackedConfig,
3700 app: &Command,
3701 mut string_args: Vec<String>,
3702) -> Result<Vec<String>, CommandError> {
3703 const PRIORITY_FLAGS: &[&str] = &["--help", "-h", "--version", "-V"];
3704
3705 let has_priority_flag = string_args
3706 .iter()
3707 .any(|arg| PRIORITY_FLAGS.contains(&arg.as_str()));
3708 if has_priority_flag {
3709 return Ok(string_args);
3710 }
3711
3712 let app_clone = app
3713 .clone()
3714 .allow_external_subcommands(true)
3715 .ignore_errors(true);
3716 let matches = app_clone.try_get_matches_from(&string_args).ok();
3717
3718 if let Some(matches) = matches
3719 && matches.subcommand_name().is_none()
3720 {
3721 let default_command = if let Ok(string) = config.get::<String>("ui.default-command") {
3726 if string.contains(' ') {
3729 let elements: ConfigValue = string.split_whitespace().collect();
3730 writeln!(
3731 ui.warning_default(),
3732 "To include flags/arguments in `ui.default-command`, use an array instead of \
3733 a string: `ui.default-command = {elements}`"
3734 )?;
3735 }
3736
3737 vec![string]
3738 } else if let Some(array) = config.get::<Vec<String>>("ui.default-command").optional()? {
3739 array
3740 } else {
3741 writeln!(
3742 ui.hint_default(),
3743 "Use `jj -h` for a list of available commands."
3744 )?;
3745 writeln!(
3746 ui.hint_no_heading(),
3747 "Run `jj config set --user ui.default-command log` to disable this message."
3748 )?;
3749
3750 vec!["log".to_string()]
3751 };
3752
3753 string_args.splice(1..1, default_command);
3755 }
3756 Ok(string_args)
3757}
3758
3759fn resolve_aliases(
3760 ui: &Ui,
3761 config: &StackedConfig,
3762 app: &Command,
3763 mut string_args: Vec<String>,
3764) -> Result<Vec<String>, CommandError> {
3765 let defined_aliases: HashSet<_> = config.table_keys("aliases").collect();
3766 let mut resolved_aliases = HashSet::new();
3767 let mut real_commands = HashSet::new();
3768 for command in app.get_subcommands() {
3769 real_commands.insert(command.get_name());
3770 for alias in command.get_all_aliases() {
3771 real_commands.insert(alias);
3772 }
3773 }
3774 for alias in defined_aliases.intersection(&real_commands).sorted() {
3775 writeln!(
3776 ui.warning_default(),
3777 "Cannot define an alias that overrides the built-in command '{alias}'"
3778 )?;
3779 }
3780
3781 loop {
3782 let app_clone = app.clone().allow_external_subcommands(true);
3783 let matches = app_clone.try_get_matches_from(&string_args).ok();
3784 if let Some((command_name, submatches)) = matches.as_ref().and_then(|m| m.subcommand())
3785 && !real_commands.contains(command_name)
3786 {
3787 let alias_name = command_name.to_string();
3788 let alias_args = submatches
3789 .get_many::<OsString>("")
3790 .unwrap_or_default()
3791 .map(|arg| arg.to_str().unwrap().to_string())
3792 .collect_vec();
3793 if resolved_aliases.contains(&*alias_name) {
3794 return Err(user_error(format!(
3795 "Recursive alias definition involving `{alias_name}`"
3796 )));
3797 }
3798 if let Some(&alias_name) = defined_aliases.get(&*alias_name) {
3799 let alias_definition: Vec<String> = config.get(["aliases", alias_name])?;
3800 assert!(string_args.ends_with(&alias_args));
3801 string_args.truncate(string_args.len() - 1 - alias_args.len());
3802 string_args.extend(alias_definition);
3803 string_args.extend_from_slice(&alias_args);
3804 resolved_aliases.insert(alias_name);
3805 continue;
3806 } else {
3807 return Ok(string_args);
3809 }
3810 }
3811 return Ok(string_args);
3813 }
3814}
3815
3816fn parse_early_args(
3818 app: &Command,
3819 args: &[String],
3820) -> Result<(EarlyArgs, Vec<ConfigLayer>), CommandError> {
3821 let early_matches = app
3823 .clone()
3824 .disable_version_flag(true)
3825 .disable_help_flag(true)
3827 .arg(
3829 clap::Arg::new("help")
3830 .short('h')
3831 .long("help")
3832 .global(true)
3833 .action(ArgAction::Count),
3834 )
3835 .ignore_errors(true)
3836 .try_get_matches_from(args)?;
3837 let args = EarlyArgs::from_arg_matches(&early_matches).unwrap();
3838
3839 let mut config_layers = parse_config_args(&args.merged_config_args(&early_matches))?;
3840 let mut layer = ConfigLayer::empty(ConfigSource::CommandArg);
3843 if let Some(choice) = args.color {
3844 layer.set_value("ui.color", choice.to_string()).unwrap();
3845 }
3846 if args.quiet.unwrap_or_default() {
3847 layer.set_value("ui.quiet", true).unwrap();
3848 }
3849 if args.no_pager.unwrap_or_default() {
3850 layer.set_value("ui.paginate", "never").unwrap();
3851 }
3852 if !layer.is_empty() {
3853 config_layers.push(layer);
3854 }
3855 Ok((args, config_layers))
3856}
3857
3858fn handle_shell_completion(
3859 ui: &Ui,
3860 app: &Command,
3861 config: &StackedConfig,
3862 cwd: &Path,
3863) -> Result<(), CommandError> {
3864 let mut orig_args = env::args_os();
3865
3866 let mut args = vec![];
3867 args.extend(orig_args.by_ref().take(2));
3870
3871 if orig_args.len() > 0 {
3875 let complete_index: Option<usize> = env::var("_CLAP_COMPLETE_INDEX")
3876 .ok()
3877 .and_then(|s| s.parse().ok());
3878 let resolved_aliases = if let Some(index) = complete_index {
3879 let pad_len = usize::saturating_sub(index + 1, orig_args.len());
3884 let padded_args = orig_args
3885 .by_ref()
3886 .chain(std::iter::repeat_n(OsString::new(), pad_len));
3887
3888 let mut expanded_args =
3890 expand_args_for_completion(ui, app, padded_args.take(index + 1), config)?;
3891
3892 unsafe {
3896 env::set_var(
3897 "_CLAP_COMPLETE_INDEX",
3898 (expanded_args.len() - 1).to_string(),
3899 );
3900 }
3901
3902 let split_off_padding = expanded_args.split_off(expanded_args.len() - pad_len);
3905 assert!(
3906 split_off_padding.iter().all(|s| s.is_empty()),
3907 "split-off padding should only consist of empty strings but was \
3908 {split_off_padding:?}",
3909 );
3910
3911 expanded_args.extend(to_string_args(orig_args)?);
3913 expanded_args
3914 } else {
3915 expand_args_for_completion(ui, app, orig_args, config)?
3916 };
3917 args.extend(resolved_aliases.into_iter().map(OsString::from));
3918 }
3919 let ran_completion = clap_complete::CompleteEnv::with_factory(|| {
3920 let mut app = app.clone();
3921 hide_short_subcommand_aliases(&mut app);
3924 app.allow_external_subcommands(true)
3926 })
3927 .try_complete(args.iter(), Some(cwd))?;
3928 assert!(
3929 ran_completion,
3930 "This function should not be called without the COMPLETE variable set."
3931 );
3932 Ok(())
3933}
3934
3935fn hide_short_subcommand_aliases(cmd: &mut Command) {
3938 for cmd in cmd.get_subcommands_mut() {
3939 hide_short_subcommand_aliases(cmd);
3940 }
3941 let (short_aliases, new_visible_aliases) = cmd
3942 .get_visible_aliases()
3943 .map(|name| name.to_owned())
3944 .partition::<Vec<_>, _>(|name| cmd.get_name().starts_with(name));
3945 if short_aliases.is_empty() {
3946 return;
3947 }
3948 *cmd = mem::take(cmd)
3949 .visible_alias(None)
3951 .visible_aliases(new_visible_aliases)
3952 .aliases(short_aliases);
3954}
3955
3956pub fn expand_args(
3957 ui: &Ui,
3958 app: &Command,
3959 args_os: impl IntoIterator<Item = OsString>,
3960 config: &StackedConfig,
3961) -> Result<Vec<String>, CommandError> {
3962 let string_args = to_string_args(args_os)?;
3963 let string_args = resolve_default_command(ui, config, app, string_args)?;
3964 resolve_aliases(ui, config, app, string_args)
3965}
3966
3967fn expand_args_for_completion(
3968 ui: &Ui,
3969 app: &Command,
3970 args_os: impl IntoIterator<Item = OsString>,
3971 config: &StackedConfig,
3972) -> Result<Vec<String>, CommandError> {
3973 let string_args = to_string_args(args_os)?;
3974
3975 let mut string_args = resolve_default_command(ui, config, app, string_args)?;
3979
3980 let cursor_arg = string_args.pop();
3983 let mut resolved_args = resolve_aliases(ui, config, app, string_args)?;
3984 resolved_args.extend(cursor_arg);
3985 Ok(resolved_args)
3986}
3987
3988fn to_string_args(
3989 args_os: impl IntoIterator<Item = OsString>,
3990) -> Result<Vec<String>, CommandError> {
3991 args_os
3992 .into_iter()
3993 .map(|arg_os| {
3994 arg_os
3995 .into_string()
3996 .map_err(|_| cli_error("Non-UTF-8 argument"))
3997 })
3998 .collect()
3999}
4000
4001fn parse_args(app: &Command, string_args: &[String]) -> Result<(ArgMatches, Args), clap::Error> {
4002 let matches = app
4003 .clone()
4004 .arg_required_else_help(true)
4005 .subcommand_required(true)
4006 .try_get_matches_from(string_args)?;
4007 let args = Args::from_arg_matches(&matches).unwrap();
4008 Ok((matches, args))
4009}
4010
4011fn command_name(mut matches: &ArgMatches) -> String {
4012 let mut command = String::new();
4013 while let Some((subcommand, new_matches)) = matches.subcommand() {
4014 if !command.is_empty() {
4015 command.push(' ');
4016 }
4017 command.push_str(subcommand);
4018 matches = new_matches;
4019 }
4020 command
4021}
4022
4023pub fn format_template<C: Clone>(ui: &Ui, arg: &C, template: &TemplateRenderer<C>) -> String {
4024 let mut output = vec![];
4025 template
4026 .format(arg, ui.new_formatter(&mut output).as_mut())
4027 .expect("write() to vec backed formatter should never fail");
4028 output.into_string_lossy()
4030}
4031
4032type BoxedCliDispatchFuture<'a> = Pin<Box<dyn Future<Output = Result<(), CommandError>> + 'a>>;
4034pub type BoxedAsyncCliDispatch<'a> = Box<dyn AsyncCliDispatch + 'a>;
4035type BoxedAsyncCliDispatchHook<'a> = Box<dyn AsyncCliDispatchHook + 'a>;
4036
4037pub trait AsyncCliDispatch {
4039 fn call<'a>(
4040 self: Box<Self>,
4041 ui: &'a mut Ui,
4042 command_helper: &'a CommandHelper,
4043 ) -> BoxedCliDispatchFuture<'a>
4044 where
4045 Self: 'a;
4046}
4047
4048trait AsyncCliDispatchHook {
4050 fn call<'a>(
4051 self: Box<Self>,
4052 ui: &'a mut Ui,
4053 command_helper: &'a CommandHelper,
4054 old_dispatch: BoxedAsyncCliDispatch<'a>,
4055 ) -> BoxedCliDispatchFuture<'a>
4056 where
4057 Self: 'a;
4058}
4059
4060struct AsyncCliDispatchFn<F>(F);
4062
4063impl<F> AsyncCliDispatch for AsyncCliDispatchFn<F>
4064where
4065 F: AsyncFnOnce(&mut Ui, &CommandHelper) -> Result<(), CommandError>,
4066{
4067 fn call<'a>(
4068 self: Box<Self>,
4069 ui: &'a mut Ui,
4070 command_helper: &'a CommandHelper,
4071 ) -> BoxedCliDispatchFuture<'a>
4072 where
4073 Self: 'a,
4074 {
4075 Box::pin((self.0)(ui, command_helper))
4076 }
4077}
4078
4079struct AsyncCliDispatchHookFn<F>(F);
4081
4082impl<F> AsyncCliDispatchHook for AsyncCliDispatchHookFn<F>
4083where
4084 F: AsyncFnOnce(&mut Ui, &CommandHelper, BoxedAsyncCliDispatch<'_>) -> Result<(), CommandError>,
4085{
4086 fn call<'a>(
4087 self: Box<Self>,
4088 ui: &'a mut Ui,
4089 command_helper: &'a CommandHelper,
4090 old_dispatch: BoxedAsyncCliDispatch<'a>,
4091 ) -> BoxedCliDispatchFuture<'a>
4092 where
4093 Self: 'a,
4094 {
4095 Box::pin((self.0)(ui, command_helper, old_dispatch))
4096 }
4097}
4098
4099#[must_use]
4101pub struct CliRunner<'a> {
4102 tracing_subscription: TracingSubscription,
4103 app: Command,
4104 config_layers: Vec<ConfigLayer>,
4105 config_migrations: Vec<ConfigMigrationRule>,
4106 store_factories: StoreFactories,
4107 working_copy_factories: WorkingCopyFactories,
4108 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
4109 revset_extensions: RevsetExtensions,
4110 commit_template_extensions: Vec<Arc<dyn CommitTemplateLanguageExtension>>,
4111 operation_template_extensions: Vec<Arc<dyn OperationTemplateLanguageExtension>>,
4112 dispatch: BoxedAsyncCliDispatch<'a>,
4113 dispatch_hooks: Vec<BoxedAsyncCliDispatchHook<'a>>,
4114 process_global_args_fns: Vec<ProcessGlobalArgsFn<'a>>,
4115}
4116
4117type ProcessGlobalArgsFn<'a> =
4118 Box<dyn FnOnce(&mut Ui, &ArgMatches) -> Result<(), CommandError> + 'a>;
4119
4120impl<'a> CliRunner<'a> {
4121 pub fn init() -> Self {
4124 let tracing_subscription = TracingSubscription::init();
4125 crate::cleanup_guard::init();
4126 Self {
4127 tracing_subscription,
4128 app: crate::commands::default_app(),
4129 config_layers: crate::config::default_config_layers(),
4130 config_migrations: crate::config::default_config_migrations(),
4131 store_factories: StoreFactories::default(),
4132 working_copy_factories: default_working_copy_factories(),
4133 workspace_loader_factory: Box::new(DefaultWorkspaceLoaderFactory),
4134 revset_extensions: Default::default(),
4135 commit_template_extensions: vec![],
4136 operation_template_extensions: vec![],
4137 dispatch: Box::new(AsyncCliDispatchFn(crate::commands::run_command)),
4138 dispatch_hooks: vec![],
4139 process_global_args_fns: vec![],
4140 }
4141 }
4142
4143 pub fn name(mut self, name: &str) -> Self {
4145 self.app = self.app.name(name.to_string());
4146 self
4147 }
4148
4149 pub fn about(mut self, about: &str) -> Self {
4151 self.app = self.app.about(about.to_string());
4152 self
4153 }
4154
4155 pub fn version(mut self, version: &str) -> Self {
4157 self.app = self.app.version(version.to_string());
4158 self
4159 }
4160
4161 pub fn add_extra_config(mut self, layer: ConfigLayer) -> Self {
4166 assert_eq!(layer.source, ConfigSource::Default);
4167 self.config_layers.push(layer);
4168 self
4169 }
4170
4171 pub fn add_extra_config_migration(mut self, rule: ConfigMigrationRule) -> Self {
4173 self.config_migrations.push(rule);
4174 self
4175 }
4176
4177 pub fn add_store_factories(mut self, store_factories: StoreFactories) -> Self {
4179 self.store_factories.merge(store_factories);
4180 self
4181 }
4182
4183 pub fn add_working_copy_factories(
4185 mut self,
4186 working_copy_factories: WorkingCopyFactories,
4187 ) -> Self {
4188 merge_factories_map(&mut self.working_copy_factories, working_copy_factories);
4189 self
4190 }
4191
4192 pub fn set_workspace_loader_factory(
4193 mut self,
4194 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
4195 ) -> Self {
4196 self.workspace_loader_factory = workspace_loader_factory;
4197 self
4198 }
4199
4200 pub fn add_symbol_resolver_extension(
4201 mut self,
4202 symbol_resolver: Box<dyn SymbolResolverExtension>,
4203 ) -> Self {
4204 self.revset_extensions.add_symbol_resolver(symbol_resolver);
4205 self
4206 }
4207
4208 pub fn add_revset_function_extension(
4209 mut self,
4210 name: &'static str,
4211 func: RevsetFunction,
4212 ) -> Self {
4213 self.revset_extensions.add_custom_function(name, func);
4214 self
4215 }
4216
4217 pub fn add_commit_template_extension(
4218 mut self,
4219 commit_template_extension: Box<dyn CommitTemplateLanguageExtension>,
4220 ) -> Self {
4221 self.commit_template_extensions
4222 .push(commit_template_extension.into());
4223 self
4224 }
4225
4226 pub fn add_operation_template_extension(
4227 mut self,
4228 operation_template_extension: Box<dyn OperationTemplateLanguageExtension>,
4229 ) -> Self {
4230 self.operation_template_extensions
4231 .push(operation_template_extension.into());
4232 self
4233 }
4234
4235 pub fn add_dispatch_hook<F>(mut self, dispatch_hook_fn: F) -> Self
4239 where
4240 F: AsyncFnOnce(&mut Ui, &CommandHelper, BoxedAsyncCliDispatch) -> Result<(), CommandError>
4241 + 'a,
4242 {
4243 self.dispatch_hooks
4244 .push(Box::new(AsyncCliDispatchHookFn(dispatch_hook_fn)));
4245 self
4246 }
4247
4248 pub fn add_subcommand<C, F>(mut self, custom_dispatch_fn: F) -> Self
4250 where
4251 C: clap::Subcommand,
4252 F: AsyncFnOnce(&mut Ui, &CommandHelper, C) -> Result<(), CommandError> + 'a,
4253 {
4254 let old_dispatch = self.dispatch;
4255 let new_dispatch_fn =
4256 async move |ui: &mut Ui, command_helper: &CommandHelper| match C::from_arg_matches(
4257 command_helper.matches(),
4258 ) {
4259 Ok(command) => custom_dispatch_fn(ui, command_helper, command).await,
4260 Err(_) => old_dispatch.call(ui, command_helper).await,
4261 };
4262 self.app = C::augment_subcommands(self.app);
4263 self.dispatch = Box::new(AsyncCliDispatchFn(new_dispatch_fn));
4264 self
4265 }
4266
4267 pub fn add_global_args<A, F>(mut self, process_before: F) -> Self
4269 where
4270 A: clap::Args,
4271 F: FnOnce(&mut Ui, A) -> Result<(), CommandError> + 'a,
4272 {
4273 let process_global_args_fn = move |ui: &mut Ui, matches: &ArgMatches| {
4274 let custom_args = A::from_arg_matches(matches).unwrap();
4275 process_before(ui, custom_args)
4276 };
4277 self.app = A::augment_args(self.app);
4278 self.process_global_args_fns
4279 .push(Box::new(process_global_args_fn));
4280 self
4281 }
4282
4283 #[instrument(skip_all)]
4284 async fn run_internal(
4285 self,
4286 ui: &mut Ui,
4287 mut raw_config: RawConfig,
4288 ) -> Result<(), CommandError> {
4289 let cwd = env::current_dir()
4292 .and_then(dunce::canonicalize)
4293 .map_err(|_| {
4294 user_error("Could not determine current directory").hinted(
4295 "Did you update to a commit where the directory doesn't exist or can't be \
4296 accessed?",
4297 )
4298 })?;
4299 let mut config_env = ConfigEnv::from_environment();
4300 let mut last_config_migration_descriptions = Vec::new();
4301 let mut migrate_config = |config: &mut StackedConfig| -> Result<(), CommandError> {
4302 last_config_migration_descriptions =
4303 jj_lib::config::migrate(config, &self.config_migrations)?;
4304 Ok(())
4305 };
4306
4307 let maybe_cwd_workspace_loader = self
4313 .workspace_loader_factory
4314 .create(find_workspace_dir(&cwd))
4315 .map_err(|err| map_workspace_load_error(err, Some(".")));
4316 config_env.reload_user_config(&mut raw_config)?;
4317 if let Ok(loader) = &maybe_cwd_workspace_loader {
4318 config_env.reset_repo_path(loader.repo_path());
4319 config_env.reload_repo_config(ui, &mut raw_config)?;
4320 config_env.reset_workspace_path(loader.workspace_root());
4321 config_env.reload_workspace_config(ui, &mut raw_config)?;
4322 }
4323 let mut config = config_env.resolve_config(&raw_config)?;
4324 migrate_config(&mut config)?;
4325 ui.reset(&config)?;
4326
4327 if env::var_os("COMPLETE").is_some_and(|v| !v.is_empty() && v != "0") {
4328 return handle_shell_completion(&Ui::null(), &self.app, &config, &cwd);
4329 }
4330
4331 let string_args = expand_args(ui, &self.app, env::args_os(), &config)?;
4332 let (args, config_layers) = parse_early_args(&self.app, &string_args)?;
4333 if !config_layers.is_empty() {
4334 raw_config.as_mut().extend_layers(config_layers);
4335 config = config_env.resolve_config(&raw_config)?;
4336 migrate_config(&mut config)?;
4337 ui.reset(&config)?;
4338 }
4339
4340 if args.has_config_args() {
4341 warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
4342 }
4343
4344 let (matches, args) = parse_args(&self.app, &string_args)
4345 .map_err(|err| map_clap_cli_error(err, ui, &config))?;
4346 if args.global_args.debug {
4347 self.tracing_subscription.enable_debug_logging()?;
4349 }
4350 for process_global_args_fn in self.process_global_args_fns {
4351 process_global_args_fn(ui, &matches)?;
4352 }
4353 config_env.set_command_name(command_name(&matches));
4354
4355 let maybe_workspace_loader = if let Some(path) = &args.global_args.repository {
4356 let abs_path = cwd.join(path);
4358 let abs_path = dunce::canonicalize(&abs_path).unwrap_or(abs_path);
4359 let loader = self
4361 .workspace_loader_factory
4362 .create(&abs_path)
4363 .map_err(|err| map_workspace_load_error(err, Some(path)))?;
4364 config_env.reset_repo_path(loader.repo_path());
4365 config_env.reload_repo_config(ui, &mut raw_config)?;
4366 config_env.reset_workspace_path(loader.workspace_root());
4367 config_env.reload_workspace_config(ui, &mut raw_config)?;
4368 Ok(loader)
4369 } else {
4370 maybe_cwd_workspace_loader
4371 };
4372
4373 config = config_env.resolve_config(&raw_config)?;
4375 migrate_config(&mut config)?;
4376 ui.reset(&config)?;
4377
4378 for (source, desc) in &last_config_migration_descriptions {
4380 let source_str = match source {
4381 ConfigSource::Default => "default-provided",
4382 ConfigSource::EnvBase | ConfigSource::EnvOverrides => "environment-provided",
4383 ConfigSource::User => "user-level",
4384 ConfigSource::Repo => "repo-level",
4385 ConfigSource::Workspace => "workspace-level",
4386 ConfigSource::CommandArg => "CLI-provided",
4387 };
4388 writeln!(
4389 ui.warning_default(),
4390 "Deprecated {source_str} config: {desc}"
4391 )?;
4392 }
4393
4394 if args.global_args.repository.is_some() {
4395 warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
4396 }
4397
4398 let settings = UserSettings::from_config(config)?;
4399 let command_helper_data = CommandHelperData {
4400 app: self.app,
4401 cwd,
4402 string_args,
4403 matches,
4404 global_args: args.global_args,
4405 config_env,
4406 config_migrations: self.config_migrations,
4407 raw_config,
4408 settings,
4409 revset_extensions: self.revset_extensions.into(),
4410 commit_template_extensions: self.commit_template_extensions,
4411 operation_template_extensions: self.operation_template_extensions,
4412 maybe_workspace_loader,
4413 store_factories: self.store_factories,
4414 working_copy_factories: self.working_copy_factories,
4415 workspace_loader_factory: self.workspace_loader_factory,
4416 };
4417 let command_helper = CommandHelper {
4418 data: Rc::new(command_helper_data),
4419 };
4420 let dispatch =
4421 self.dispatch_hooks
4422 .into_iter()
4423 .fold(self.dispatch, |old_dispatch, dispatch_hook| {
4424 let f = async move |ui: &mut Ui, command_helper: &CommandHelper| {
4425 dispatch_hook.call(ui, command_helper, old_dispatch).await
4426 };
4427 Box::new(AsyncCliDispatchFn(f))
4428 });
4429 dispatch.call(ui, &command_helper).await
4430 }
4431
4432 #[must_use]
4433 #[instrument(skip(self))]
4434 pub fn run(mut self) -> u8 {
4435 crossterm::style::force_color_output(true);
4437 let config = config_from_environment(self.config_layers.drain(..));
4438 let mut ui = Ui::with_config(config.as_ref())
4441 .expect("default config should be valid, env vars are stringly typed");
4442 let result = self.run_internal(&mut ui, config).block_on();
4443 let exit_code = handle_command_result(&mut ui, result);
4444 ui.finalize_pager();
4445 exit_code
4446 }
4447}
4448
4449fn map_clap_cli_error(err: clap::Error, ui: &Ui, config: &StackedConfig) -> CommandError {
4450 if let Some(ContextValue::String(cmd)) = err.get(ContextKind::InvalidSubcommand) {
4451 let remove_useless_error_context = |mut err: clap::Error| {
4452 err.remove(ContextKind::SuggestedSubcommand);
4455 err.remove(ContextKind::Suggested); err.remove(ContextKind::Usage); err
4458 };
4459 match cmd.as_str() {
4460 "clone" | "init" => {
4463 let cmd = cmd.clone();
4464 return CommandError::from(remove_useless_error_context(err))
4465 .hinted(format!(
4466 "You probably want `jj git {cmd}`. See also `jj help git`."
4467 ))
4468 .hinted(format!(
4469 r#"You can configure `aliases.{cmd} = ["git", "{cmd}"]` if you want `jj {cmd}` to work and always use the Git backend."#
4470 ));
4471 }
4472 "amend" => {
4473 return CommandError::from(remove_useless_error_context(err))
4474 .hinted(
4475 r#"You probably want `jj squash`. You can configure `aliases.amend = ["squash"]` if you want `jj amend` to work."#);
4476 }
4477 _ => {}
4478 }
4479 }
4480 if let (Some(ContextValue::String(arg)), Some(ContextValue::String(value))) = (
4481 err.get(ContextKind::InvalidArg),
4482 err.get(ContextKind::InvalidValue),
4483 ) && arg.as_str() == "--template <TEMPLATE>"
4484 && value.is_empty()
4485 {
4486 if let Ok(template_aliases) = load_template_aliases(ui, config) {
4488 return CommandError::from(err).hinted(format_template_aliases_hint(&template_aliases));
4489 }
4490 }
4491 CommandError::from(err)
4492}
4493
4494fn format_template_aliases_hint(template_aliases: &TemplateAliasesMap) -> String {
4495 let mut hint = String::from("The following template aliases are defined:\n");
4496 hint.push_str(
4497 &template_aliases
4498 .symbol_names()
4499 .sorted_unstable()
4500 .map(|name| format!("- {name}"))
4501 .join("\n"),
4502 );
4503 hint
4504}
4505
4506fn warn_if_args_mismatch(
4508 ui: &Ui,
4509 app: &Command,
4510 config: &StackedConfig,
4511 expected_args: &[String],
4512) -> Result<(), CommandError> {
4513 let new_string_args = expand_args(ui, app, env::args_os(), config).ok();
4514 if new_string_args.as_deref() != Some(expected_args) {
4515 writeln!(
4516 ui.warning_default(),
4517 "Command aliases cannot be loaded from -R/--repository path or --config/--config-file \
4518 arguments."
4519 )?;
4520 }
4521 Ok(())
4522}
4523
4524#[cfg(test)]
4525mod tests {
4526 use clap::CommandFactory as _;
4527
4528 use super::*;
4529
4530 #[derive(clap::Parser, Clone, Debug)]
4531 pub struct TestArgs {
4532 #[arg(long)]
4533 pub foo: Vec<u32>,
4534 #[arg(long)]
4535 pub bar: Vec<u32>,
4536 #[arg(long)]
4537 pub baz: bool,
4538 }
4539
4540 #[test]
4541 fn test_merge_args_with() {
4542 let command = TestArgs::command();
4543 let parse = |args: &[&str]| -> Vec<(&'static str, u32)> {
4544 let matches = command.clone().try_get_matches_from(args).unwrap();
4545 let args = TestArgs::from_arg_matches(&matches).unwrap();
4546 merge_args_with(
4547 &matches,
4548 &[("foo", &args.foo), ("bar", &args.bar)],
4549 |id, value| (id, *value),
4550 )
4551 };
4552
4553 assert_eq!(parse(&["jj"]), vec![]);
4554 assert_eq!(parse(&["jj", "--foo=1"]), vec![("foo", 1)]);
4555 assert_eq!(
4556 parse(&["jj", "--foo=1", "--bar=2"]),
4557 vec![("foo", 1), ("bar", 2)]
4558 );
4559 assert_eq!(
4560 parse(&["jj", "--foo=1", "--baz", "--bar=2", "--foo", "3"]),
4561 vec![("foo", 1), ("bar", 2), ("foo", 3)]
4562 );
4563 }
4564}