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::sync::Arc;
32use std::sync::LazyLock;
33use std::time::SystemTime;
34
35use bstr::ByteVec as _;
36use chrono::TimeZone as _;
37use clap::ArgAction;
38use clap::ArgMatches;
39use clap::Command;
40use clap::FromArgMatches as _;
41use clap::builder::MapValueParser;
42use clap::builder::NonEmptyStringValueParser;
43use clap::builder::TypedValueParser as _;
44use clap::builder::ValueParserFactory;
45use clap::error::ContextKind;
46use clap::error::ContextValue;
47use clap_complete::ArgValueCandidates;
48use clap_complete::ArgValueCompleter;
49use futures::TryStreamExt as _;
50use futures::future::try_join_all;
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::WorkspaceName;
96use jj_lib::ref_name::WorkspaceNameBuf;
97use jj_lib::repo::CheckOutCommitError;
98use jj_lib::repo::EditCommitError;
99use jj_lib::repo::MutableRepo;
100use jj_lib::repo::ReadonlyRepo;
101use jj_lib::repo::Repo;
102use jj_lib::repo::RepoLoader;
103use jj_lib::repo::StoreFactories;
104use jj_lib::repo::StoreLoadError;
105use jj_lib::repo::merge_factories_map;
106use jj_lib::repo_path::RepoPath;
107use jj_lib::repo_path::RepoPathBuf;
108use jj_lib::repo_path::RepoPathUiConverter;
109use jj_lib::repo_path::UiPathParseError;
110use jj_lib::revset;
111use jj_lib::revset::ResolvedRevsetExpression;
112use jj_lib::revset::RevsetAliasesMap;
113use jj_lib::revset::RevsetDiagnostics;
114use jj_lib::revset::RevsetExpression;
115use jj_lib::revset::RevsetExtensions;
116use jj_lib::revset::RevsetFilterPredicate;
117use jj_lib::revset::RevsetFunction;
118use jj_lib::revset::RevsetParseContext;
119use jj_lib::revset::RevsetStreamExt as _;
120use jj_lib::revset::RevsetWorkspaceContext;
121use jj_lib::revset::SymbolResolverExtension;
122use jj_lib::revset::UserRevsetExpression;
123use jj_lib::rewrite::RebaseOptions;
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::transaction::Transaction;
131use jj_lib::transaction::TransactionCommitError;
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 should_commit_transaction(&self) -> bool {
421 !self.global_args().no_integrate_operation
422 }
423
424 async fn maybe_commit_transaction(
425 &self,
426 tx: Transaction,
427 description: impl Into<String>,
428 ) -> Result<Arc<ReadonlyRepo>, TransactionCommitError> {
429 let unpublished_op = tx.write(description).await?;
430 if self.should_commit_transaction() {
431 unpublished_op.publish().await
432 } else {
433 Ok(unpublished_op.leave_unpublished())
434 }
435 }
436
437 pub fn workspace_loader(&self) -> Result<&dyn WorkspaceLoader, CommandError> {
438 self.data
439 .maybe_workspace_loader
440 .as_deref()
441 .map_err(Clone::clone)
442 }
443
444 fn new_workspace_loader_at(
445 &self,
446 workspace_root: &Path,
447 ) -> Result<Box<dyn WorkspaceLoader>, CommandError> {
448 self.data
449 .workspace_loader_factory
450 .create(workspace_root)
451 .map_err(|err| map_workspace_load_error(err, None))
452 }
453
454 #[instrument(skip(self, ui))]
456 pub async fn workspace_helper(&self, ui: &Ui) -> Result<WorkspaceCommandHelper, CommandError> {
457 let (workspace_command, stats) = self.workspace_helper_with_stats(ui).await?;
458 print_snapshot_stats(ui, &stats, workspace_command.env().path_converter())?;
459 Ok(workspace_command)
460 }
461
462 #[instrument(skip(self, ui))]
469 pub async fn workspace_helper_with_stats(
470 &self,
471 ui: &Ui,
472 ) -> Result<(WorkspaceCommandHelper, SnapshotStats), CommandError> {
473 let mut workspace_command = self.workspace_helper_no_snapshot(ui).await?;
474
475 let (workspace_command, stats) = match workspace_command.maybe_snapshot_impl(ui).await {
476 Ok(stats) => (workspace_command, stats),
477 Err(SnapshotWorkingCopyError::Command(err)) => return Err(err),
478 Err(SnapshotWorkingCopyError::StaleWorkingCopy(err)) => {
479 let auto_update_stale = self.settings().get_bool("snapshot.auto-update-stale")?;
480 if !auto_update_stale {
481 return Err(err);
482 }
483
484 self.recover_stale_working_copy(ui).await?
489 }
490 };
491
492 Ok((workspace_command, stats))
493 }
494
495 #[instrument(skip(self, ui))]
498 pub async fn workspace_helper_no_snapshot(
499 &self,
500 ui: &Ui,
501 ) -> Result<WorkspaceCommandHelper, CommandError> {
502 let workspace = self.load_workspace()?;
503 let op_head =
504 self.resolve_operation(ui, workspace.repo_loader(), workspace.workspace_name())?;
505 let repo = workspace.repo_loader().load_at(&op_head).await?;
506 let mut env = self.workspace_environment(ui, &workspace)?;
507 if let Err(err) =
508 revset_util::try_resolve_trunk_alias(repo.as_ref(), &env.revset_parse_context())
509 {
510 let fallback = "root()";
513 writeln!(
514 ui.warning_default(),
515 "Failed to resolve `revset-aliases.trunk()`: {err}"
516 )?;
517 writeln!(
518 ui.warning_no_heading(),
519 "The `trunk()` alias is temporarily set to `{fallback}`."
520 )?;
521 writeln!(
522 ui.hint_default(),
523 "Use `jj config edit --repo` to adjust the `trunk()` alias."
524 )?;
525 env.revset_aliases_map
526 .insert("trunk()", fallback, None)
527 .expect("valid syntax");
528 env.reload_revset_expressions(ui)?;
529 }
530 WorkspaceCommandHelper::new(ui, workspace, repo, env, self.is_at_head_operation())
531 }
532
533 pub fn get_working_copy_factory(&self) -> Result<&dyn WorkingCopyFactory, CommandError> {
534 let loader = self.workspace_loader()?;
535
536 let factory: Result<_, WorkspaceLoadError> =
538 get_working_copy_factory(loader, &self.data.working_copy_factories)
539 .map_err(|e| e.into());
540 let factory = factory.map_err(|err| {
541 map_workspace_load_error(err, self.data.global_args.repository.as_deref())
542 })?;
543 Ok(factory)
544 }
545
546 #[instrument(skip_all)]
548 pub fn load_workspace(&self) -> Result<Workspace, CommandError> {
549 let loader = self.workspace_loader()?;
550 loader
551 .load(
552 &self.data.settings,
553 &self.data.store_factories,
554 &self.data.working_copy_factories,
555 )
556 .map_err(|err| {
557 map_workspace_load_error(err, self.data.global_args.repository.as_deref())
558 })
559 }
560
561 #[instrument(skip(self, settings))]
563 pub fn load_workspace_at(
564 &self,
565 workspace_root: &Path,
566 settings: &UserSettings,
567 ) -> Result<Workspace, CommandError> {
568 let loader = self.new_workspace_loader_at(workspace_root)?;
569 loader
570 .load(
571 settings,
572 &self.data.store_factories,
573 &self.data.working_copy_factories,
574 )
575 .map_err(|err| map_workspace_load_error(err, None))
576 }
577
578 pub async fn recover_stale_working_copy(
582 &self,
583 ui: &Ui,
584 ) -> Result<(WorkspaceCommandHelper, SnapshotStats), CommandError> {
585 let workspace = self.load_workspace()?;
586 let op_id = workspace.working_copy().operation_id();
587
588 match workspace.repo_loader().load_operation(op_id).await {
589 Ok(op) => {
590 let repo = workspace.repo_loader().load_at(&op).await?;
591 let mut workspace_command = self.for_workable_repo(ui, workspace, repo)?;
592 workspace_command.check_working_copy_writable()?;
593
594 let stale_stats = workspace_command
599 .snapshot_working_copy(ui)
600 .await
601 .map_err(|err| err.into_command_error())?;
602
603 let wc_commit_id = workspace_command.get_wc_commit_id().unwrap();
604 let repo = workspace_command.repo().clone();
605 let stale_wc_commit = repo.store().get_commit_async(wc_commit_id).await?;
606
607 let mut workspace_command = self.workspace_helper_no_snapshot(ui).await?;
608
609 let repo = workspace_command.repo().clone();
610 let (mut locked_ws, desired_wc_commit) = workspace_command
611 .unchecked_start_working_copy_mutation()
612 .await?;
613 match WorkingCopyFreshness::check_stale(
614 locked_ws.locked_wc(),
615 &desired_wc_commit,
616 &repo,
617 )
618 .await?
619 {
620 WorkingCopyFreshness::Fresh | WorkingCopyFreshness::Updated(_) => {
621 drop(locked_ws);
622 writeln!(
623 ui.status(),
624 "Attempted recovery, but the working copy is not stale"
625 )?;
626 }
627 WorkingCopyFreshness::WorkingCopyStale
628 | WorkingCopyFreshness::SiblingOperation => {
629 let stats = update_stale_working_copy(
630 locked_ws,
631 repo.op_id().clone(),
632 &stale_wc_commit,
633 &desired_wc_commit,
634 )
635 .await?;
636 workspace_command.print_updated_working_copy_stats(
637 ui,
638 Some(&stale_wc_commit),
639 &desired_wc_commit,
640 &stats,
641 )?;
642 writeln!(
643 ui.status(),
644 "Updated working copy to fresh commit {}",
645 short_commit_hash(desired_wc_commit.id())
646 )?;
647 }
648 }
649
650 let fresh_stats = workspace_command
655 .maybe_snapshot_impl(ui)
656 .await
657 .map_err(|err| err.into_command_error())?;
658 let merged_stats = {
659 let SnapshotStats {
660 mut untracked_paths,
661 } = stale_stats;
662 untracked_paths.extend(fresh_stats.untracked_paths);
663 SnapshotStats { untracked_paths }
664 };
665 Ok((workspace_command, merged_stats))
666 }
667 Err(e @ OpStoreError::ObjectNotFound { .. }) => {
668 writeln!(
669 ui.status(),
670 "Failed to read working copy's current operation; attempting recovery. Error \
671 message from read attempt: {e}"
672 )?;
673
674 let mut workspace_command = self.workspace_helper_no_snapshot(ui).await?;
675 let stats = workspace_command
676 .create_and_check_out_recovery_commit(ui)
677 .await?;
678 Ok((workspace_command, stats))
679 }
680 Err(e) => Err(e.into()),
681 }
682 }
683
684 pub fn workspace_environment(
686 &self,
687 ui: &Ui,
688 workspace: &Workspace,
689 ) -> Result<WorkspaceCommandEnvironment, CommandError> {
690 WorkspaceCommandEnvironment::new(ui, self, workspace)
691 }
692
693 pub fn is_working_copy_writable(&self) -> bool {
696 self.is_at_head_operation() && !self.data.global_args.ignore_working_copy
697 }
698
699 pub fn is_at_head_operation(&self) -> bool {
701 matches!(
704 self.data.global_args.at_operation.as_deref(),
705 None | Some("@")
706 )
707 }
708
709 #[instrument(skip_all)]
714 pub fn resolve_operation(
715 &self,
716 ui: &Ui,
717 repo_loader: &RepoLoader,
718 workspace_name: &WorkspaceName,
719 ) -> Result<Operation, CommandError> {
720 if let Some(op_str) = &self.data.global_args.at_operation {
721 Ok(op_walk::resolve_op_for_load(repo_loader, op_str).block_on()?)
722 } else {
723 op_heads_store::resolve_op_heads(
724 repo_loader.op_heads_store().as_ref(),
725 repo_loader.op_store(),
726 async |op_heads| {
727 writeln!(
728 ui.status(),
729 "Concurrent modification detected, resolving automatically.",
730 )?;
731 let base_repo = repo_loader.load_at(&op_heads[0]).block_on()?;
732 let mut tx =
734 start_repo_transaction(&base_repo, workspace_name, &self.data.string_args);
735 for other_op_head in op_heads.into_iter().skip(1) {
736 tx.merge_operation(other_op_head).await?;
737 let num_rebased = tx.repo_mut().rebase_descendants().await?;
738 if num_rebased > 0 {
739 writeln!(
740 ui.status(),
741 "Rebased {num_rebased} descendant commits onto commits rewritten \
742 by other operation"
743 )?;
744 }
745 }
746 Ok(tx
747 .write("reconcile divergent operations")
748 .await?
749 .leave_unpublished()
750 .operation()
751 .clone())
752 },
753 )
754 .block_on()
755 }
756 }
757
758 #[instrument(skip_all)]
762 pub fn for_workable_repo(
763 &self,
764 ui: &Ui,
765 workspace: Workspace,
766 repo: Arc<ReadonlyRepo>,
767 ) -> Result<WorkspaceCommandHelper, CommandError> {
768 let env = self.workspace_environment(ui, &workspace)?;
769 let loaded_at_head = true;
770 WorkspaceCommandHelper::new(ui, workspace, repo, env, loaded_at_head)
771 }
772}
773
774struct ReadonlyUserRepo {
777 repo: Arc<ReadonlyRepo>,
778 id_prefix_context: OnceCell<IdPrefixContext>,
779}
780
781impl ReadonlyUserRepo {
782 fn new(repo: Arc<ReadonlyRepo>) -> Self {
783 Self {
784 repo,
785 id_prefix_context: OnceCell::new(),
786 }
787 }
788}
789
790pub struct AdvanceableBookmark {
800 name: RefNameBuf,
801 old_commit_id: CommitId,
802}
803
804fn load_advance_bookmarks_matcher(
815 ui: &Ui,
816 settings: &UserSettings,
817) -> Result<Option<StringMatcher>, CommandError> {
818 let get_setting = |setting_key: &str| -> Result<Vec<String>, _> {
819 let name = ConfigNamePathBuf::from_iter(["experimental-advance-branches", setting_key]);
820 settings.get(&name)
821 };
822 let enabled_names = get_setting("enabled-branches")?;
825 let disabled_names = get_setting("disabled-branches")?;
826 let enabled_expr = parse_union_name_patterns(ui, &enabled_names)?;
827 let disabled_expr = parse_union_name_patterns(ui, &disabled_names)?;
828 if enabled_names.is_empty() {
829 Ok(None)
830 } else {
831 let expr = enabled_expr.intersection(disabled_expr.negated());
832 Ok(Some(expr.to_matcher()))
833 }
834}
835
836pub struct WorkspaceCommandEnvironment {
838 command: CommandHelper,
839 settings: UserSettings,
840 fileset_aliases_map: FilesetAliasesMap,
841 revset_aliases_map: RevsetAliasesMap,
842 template_aliases_map: TemplateAliasesMap,
843 default_ignored_remote: Option<&'static RemoteName>,
844 path_converter: RepoPathUiConverter,
845 workspace_name: WorkspaceNameBuf,
846 immutable_heads_expression: Arc<UserRevsetExpression>,
847 short_prefixes_expression: Option<Arc<UserRevsetExpression>>,
848 conflict_marker_style: ConflictMarkerStyle,
849}
850
851impl WorkspaceCommandEnvironment {
852 #[instrument(skip_all)]
853 fn new(ui: &Ui, command: &CommandHelper, workspace: &Workspace) -> Result<Self, CommandError> {
854 let settings = workspace.settings();
855 let fileset_aliases_map = load_fileset_aliases(ui, settings.config())?;
856 let revset_aliases_map = load_revset_aliases(ui, settings.config())?;
857 let template_aliases_map = load_template_aliases(ui, settings.config())?;
858 let default_ignored_remote = default_ignored_remote_name(workspace.repo_loader().store());
859 let path_converter = RepoPathUiConverter::Fs {
860 cwd: command.cwd().to_owned(),
861 base: workspace.workspace_root().to_owned(),
862 };
863 let mut env = Self {
864 command: command.clone(),
865 settings: settings.clone(),
866 fileset_aliases_map,
867 revset_aliases_map,
868 template_aliases_map,
869 default_ignored_remote,
870 path_converter,
871 workspace_name: workspace.workspace_name().to_owned(),
872 immutable_heads_expression: RevsetExpression::root(),
873 short_prefixes_expression: None,
874 conflict_marker_style: settings.get("ui.conflict-marker-style")?,
875 };
876 env.reload_revset_expressions(ui)?;
877 Ok(env)
878 }
879
880 pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
881 &self.path_converter
882 }
883
884 pub fn workspace_name(&self) -> &WorkspaceName {
885 &self.workspace_name
886 }
887
888 pub(crate) fn fileset_parse_context(&self) -> FilesetParseContext<'_> {
890 FilesetParseContext {
891 aliases_map: &self.fileset_aliases_map,
892 path_converter: &self.path_converter,
893 }
894 }
895
896 pub(crate) fn fileset_parse_context_for_config(&self) -> FilesetParseContext<'_> {
898 static ROOT_PATH_CONVERTER: LazyLock<RepoPathUiConverter> =
900 LazyLock::new(|| RepoPathUiConverter::Fs {
901 cwd: PathBuf::new(),
902 base: PathBuf::new(),
903 });
904 FilesetParseContext {
905 aliases_map: &self.fileset_aliases_map,
906 path_converter: &ROOT_PATH_CONVERTER,
907 }
908 }
909
910 pub(crate) fn revset_parse_context(&self) -> RevsetParseContext<'_> {
911 let workspace_context = RevsetWorkspaceContext {
912 path_converter: &self.path_converter,
913 workspace_name: &self.workspace_name,
914 };
915 let now = if let Some(timestamp) = self.settings.commit_timestamp() {
916 chrono::Local
917 .timestamp_millis_opt(timestamp.timestamp.0)
918 .unwrap()
919 } else {
920 chrono::Local::now()
921 };
922 RevsetParseContext {
923 aliases_map: &self.revset_aliases_map,
924 local_variables: HashMap::new(),
925 user_email: self.settings.user_email(),
926 date_pattern_context: now.into(),
927 default_ignored_remote: self.default_ignored_remote,
928 fileset_aliases_map: &self.fileset_aliases_map,
929 extensions: self.command.revset_extensions(),
930 workspace: Some(workspace_context),
931 }
932 }
933
934 pub fn new_id_prefix_context(&self) -> IdPrefixContext {
937 let context = IdPrefixContext::new(self.command.revset_extensions().clone());
938 match &self.short_prefixes_expression {
939 None => context,
940 Some(expression) => context.disambiguate_within(expression.clone()),
941 }
942 }
943
944 fn reload_revset_expressions(&mut self, ui: &Ui) -> Result<(), CommandError> {
946 self.immutable_heads_expression = self.load_immutable_heads_expression(ui)?;
947 self.short_prefixes_expression = self.load_short_prefixes_expression(ui)?;
948 Ok(())
949 }
950
951 pub fn immutable_expression(&self) -> Arc<UserRevsetExpression> {
953 self.immutable_heads_expression.ancestors()
956 }
957
958 pub fn immutable_heads_expression(&self) -> &Arc<UserRevsetExpression> {
960 &self.immutable_heads_expression
961 }
962
963 pub fn conflict_marker_style(&self) -> ConflictMarkerStyle {
965 self.conflict_marker_style
966 }
967
968 fn load_immutable_heads_expression(
969 &self,
970 ui: &Ui,
971 ) -> Result<Arc<UserRevsetExpression>, CommandError> {
972 let mut diagnostics = RevsetDiagnostics::new();
973 let expression = revset_util::parse_immutable_heads_expression(
974 &mut diagnostics,
975 &self.revset_parse_context(),
976 )
977 .map_err(|e| config_error_with_message("Invalid `revset-aliases.immutable_heads()`", e))?;
978 print_parse_diagnostics(ui, "In `revset-aliases.immutable_heads()`", &diagnostics)?;
979 Ok(expression)
980 }
981
982 fn load_short_prefixes_expression(
983 &self,
984 ui: &Ui,
985 ) -> Result<Option<Arc<UserRevsetExpression>>, CommandError> {
986 let revset_string = self
987 .settings
988 .get_string("revsets.short-prefixes")
989 .optional()?
990 .map_or_else(|| self.settings.get_string("revsets.log"), Ok)?;
991 if revset_string.is_empty() {
992 Ok(None)
993 } else {
994 let mut diagnostics = RevsetDiagnostics::new();
995 let expression = revset::parse(
996 &mut diagnostics,
997 &revset_string,
998 &self.revset_parse_context(),
999 )
1000 .map_err(|err| config_error_with_message("Invalid `revsets.short-prefixes`", err))?;
1001 print_parse_diagnostics(ui, "In `revsets.short-prefixes`", &diagnostics)?;
1002 Ok(Some(expression))
1003 }
1004 }
1005
1006 fn resolve_immutable_expression(
1009 &self,
1010 repo: &dyn Repo,
1011 ) -> Result<Arc<ResolvedRevsetExpression>, CommandError> {
1012 let immutable_expression = if self.command.global_args().ignore_immutable {
1013 UserRevsetExpression::root()
1014 } else {
1015 self.immutable_expression()
1016 };
1017
1018 let id_prefix_context = IdPrefixContext::new(self.command.revset_extensions().clone());
1022 RevsetExpressionEvaluator::new(
1023 repo,
1024 self.command.revset_extensions().clone(),
1025 &id_prefix_context,
1026 immutable_expression,
1027 )
1028 .resolve()
1029 .map_err(|e| config_error_with_message("Invalid `revset-aliases.immutable_heads()`", e))
1030 }
1031
1032 pub fn template_aliases_map(&self) -> &TemplateAliasesMap {
1033 &self.template_aliases_map
1034 }
1035
1036 pub fn parse_template<'a, C, L>(
1038 &self,
1039 ui: &Ui,
1040 language: &L,
1041 template_text: &str,
1042 ) -> Result<TemplateRenderer<'a, C>, CommandError>
1043 where
1044 C: Clone + 'a,
1045 L: TemplateLanguage<'a> + ?Sized,
1046 L::Property: WrapTemplateProperty<'a, C>,
1047 {
1048 let mut diagnostics = TemplateDiagnostics::new();
1049 let template = template_builder::parse(
1050 language,
1051 &mut diagnostics,
1052 template_text,
1053 &self.template_aliases_map,
1054 )?;
1055 print_parse_diagnostics(ui, "In template expression", &diagnostics)?;
1056 Ok(template)
1057 }
1058
1059 pub fn commit_template_language<'a>(
1062 &'a self,
1063 repo: &'a dyn Repo,
1064 id_prefix_context: &'a IdPrefixContext,
1065 ) -> CommitTemplateLanguage<'a> {
1066 CommitTemplateLanguage::new(
1067 repo,
1068 &self.path_converter,
1069 &self.workspace_name,
1070 self.revset_parse_context(),
1071 id_prefix_context,
1072 self.immutable_expression(),
1073 self.conflict_marker_style,
1074 &self.command.data.commit_template_extensions,
1075 )
1076 }
1077
1078 pub fn operation_template_extensions(&self) -> &[Arc<dyn OperationTemplateLanguageExtension>] {
1079 &self.command.data.operation_template_extensions
1080 }
1081}
1082
1083pub struct GitImportExportLock {
1087 _lock: Option<FileLock>,
1088}
1089
1090pub struct WorkspaceCommandHelper {
1093 workspace: Workspace,
1094 user_repo: ReadonlyUserRepo,
1095 env: WorkspaceCommandEnvironment,
1096 commit_summary_template_text: String,
1098 op_summary_template_text: String,
1099 may_snapshot_working_copy: bool,
1100 may_update_working_copy: bool,
1101 working_copy_shared_with_git: bool,
1102}
1103
1104enum SnapshotWorkingCopyError {
1105 Command(CommandError),
1106 StaleWorkingCopy(CommandError),
1107}
1108
1109impl SnapshotWorkingCopyError {
1110 fn into_command_error(self) -> CommandError {
1111 match self {
1112 Self::Command(err) => err,
1113 Self::StaleWorkingCopy(err) => err,
1114 }
1115 }
1116}
1117
1118fn snapshot_command_error<E>(err: E) -> SnapshotWorkingCopyError
1119where
1120 E: Into<CommandError>,
1121{
1122 SnapshotWorkingCopyError::Command(err.into())
1123}
1124
1125impl WorkspaceCommandHelper {
1126 #[instrument(skip_all)]
1127 fn new(
1128 ui: &Ui,
1129 workspace: Workspace,
1130 repo: Arc<ReadonlyRepo>,
1131 env: WorkspaceCommandEnvironment,
1132 loaded_at_head: bool,
1133 ) -> Result<Self, CommandError> {
1134 let settings = workspace.settings();
1135 let commit_summary_template_text = settings.get_string("templates.commit_summary")?;
1136 let op_summary_template_text = settings.get_string("templates.op_summary")?;
1137 let may_snapshot_working_copy =
1138 loaded_at_head && !env.command.global_args().ignore_working_copy;
1139 let may_update_working_copy =
1140 may_snapshot_working_copy && env.command.should_commit_transaction();
1141 let working_copy_shared_with_git =
1142 crate::git_util::is_colocated_git_workspace(&workspace, &repo);
1143
1144 let helper = Self {
1145 workspace,
1146 user_repo: ReadonlyUserRepo::new(repo),
1147 env,
1148 commit_summary_template_text,
1149 op_summary_template_text,
1150 may_snapshot_working_copy,
1151 may_update_working_copy,
1152 working_copy_shared_with_git,
1153 };
1154 helper.parse_operation_template(ui, &helper.op_summary_template_text)?;
1157 helper.parse_commit_template(ui, &helper.commit_summary_template_text)?;
1158 helper.parse_commit_template(ui, SHORT_CHANGE_ID_TEMPLATE_TEXT)?;
1159 Ok(helper)
1160 }
1161
1162 pub fn settings(&self) -> &UserSettings {
1164 self.workspace.settings()
1165 }
1166
1167 pub fn check_working_copy_writable(&self) -> Result<(), CommandError> {
1168 if self.may_update_working_copy {
1169 Ok(())
1170 } else {
1171 let hint = if self.env.command.global_args().ignore_working_copy {
1172 "Don't use --ignore-working-copy."
1173 } else if self.env.command.global_args().no_integrate_operation {
1174 "Don't use --no-integrate-operation."
1175 } else {
1176 "Don't use --at-op."
1177 };
1178 Err(user_error("This command must be able to update the working copy.").hinted(hint))
1179 }
1180 }
1181
1182 fn lock_git_import_export(&self) -> Result<GitImportExportLock, CommandError> {
1187 let lock = if self.working_copy_shared_with_git {
1188 let lock_path = self.workspace.repo_path().join("git_import_export.lock");
1189 Some(FileLock::lock(lock_path.clone()).map_err(|err| {
1190 user_error_with_message("Failed to take lock for Git import/export", err)
1191 })?)
1192 } else {
1193 None
1194 };
1195 Ok(GitImportExportLock { _lock: lock })
1196 }
1197
1198 #[instrument(skip_all)]
1202 async fn maybe_snapshot_impl(
1203 &mut self,
1204 ui: &Ui,
1205 ) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
1206 if !self.may_snapshot_working_copy {
1207 return Ok(SnapshotStats::default());
1208 }
1209
1210 #[cfg_attr(not(feature = "git"), allow(unused_variables))]
1214 let git_import_export_lock = self
1215 .lock_git_import_export()
1216 .map_err(snapshot_command_error)?;
1217
1218 if self.working_copy_shared_with_git {
1221 let repo = self.repo().clone();
1222 let op_heads_store = repo.loader().op_heads_store();
1223 let op_heads = op_heads_store
1224 .get_op_heads()
1225 .await
1226 .map_err(snapshot_command_error)?;
1227 if std::slice::from_ref(repo.op_id()) != op_heads {
1228 let op = self
1229 .env
1230 .command
1231 .resolve_operation(ui, repo.loader(), self.workspace_name())
1232 .map_err(snapshot_command_error)?;
1233 let current_repo = repo
1234 .loader()
1235 .load_at(&op)
1236 .await
1237 .map_err(snapshot_command_error)?;
1238 self.user_repo = ReadonlyUserRepo::new(current_repo);
1239 }
1240 }
1241
1242 #[cfg(feature = "git")]
1243 if self.working_copy_shared_with_git {
1244 self.import_git_head(ui, &git_import_export_lock)
1245 .await
1246 .map_err(snapshot_command_error)?;
1247 }
1248 let stats = self.snapshot_working_copy(ui).await?;
1253
1254 #[cfg(feature = "git")]
1256 if self.working_copy_shared_with_git {
1257 self.import_git_refs(ui, &git_import_export_lock)
1258 .await
1259 .map_err(snapshot_command_error)?;
1260 }
1261 Ok(stats)
1262 }
1263
1264 #[instrument(skip_all)]
1269 pub async fn maybe_snapshot(&mut self, ui: &Ui) -> Result<bool, CommandError> {
1270 let op_id_before = self.repo().op_id().clone();
1271 let stats = self
1272 .maybe_snapshot_impl(ui)
1273 .await
1274 .map_err(|err| err.into_command_error())?;
1275 print_snapshot_stats(ui, &stats, self.env().path_converter())?;
1276 let op_id_after = self.repo().op_id();
1277 Ok(op_id_before != *op_id_after)
1278 }
1279
1280 #[cfg(feature = "git")]
1287 #[instrument(skip_all)]
1288 async fn import_git_head(
1289 &mut self,
1290 ui: &Ui,
1291 git_import_export_lock: &GitImportExportLock,
1292 ) -> Result<(), CommandError> {
1293 assert!(self.may_snapshot_working_copy);
1294 let mut tx = self.start_transaction();
1295 jj_lib::git::import_head(tx.repo_mut()).await?;
1296 if !tx.repo().has_changes() {
1297 return Ok(());
1298 }
1299
1300 let mut tx = tx.into_inner();
1301 let old_git_head = self.repo().view().git_head().clone();
1302 let new_git_head = tx.repo().view().git_head().clone();
1303 if let Some(new_git_head_id) = new_git_head.as_normal() {
1304 let workspace_name = self.workspace_name().to_owned();
1305 let new_git_head_commit = tx.repo().store().get_commit_async(new_git_head_id).await?;
1306 let wc_commit = tx
1307 .repo_mut()
1308 .check_out(workspace_name, &new_git_head_commit)
1309 .await?;
1310 let mut locked_ws = self.workspace.start_working_copy_mutation().await?;
1311 locked_ws.locked_wc().reset(&wc_commit).await?;
1315 tx.repo_mut().rebase_descendants().await?;
1316 self.user_repo = ReadonlyUserRepo::new(
1317 self.env
1318 .command
1319 .maybe_commit_transaction(tx, "import git head")
1320 .await?,
1321 );
1322 if self.env.command.should_commit_transaction() {
1323 locked_ws
1324 .finish(self.user_repo.repo.op_id().clone())
1325 .await?;
1326 }
1327 if old_git_head.is_present() {
1328 writeln!(
1329 ui.status(),
1330 "Reset the working copy parent to the new Git HEAD."
1331 )?;
1332 } else {
1333 }
1335 if !self.env.command.should_commit_transaction() {
1336 writeln!(
1337 ui.status(),
1338 "Operation left uncommitted because --no-integrate-operation was requested: {}",
1339 short_operation_hash(self.repo().op_id())
1340 )?;
1341 }
1342 } else {
1343 let num_rebased = tx.repo_mut().rebase_descendants().await?;
1345 if num_rebased > 0 {
1346 writeln!(ui.status(), "Rebased {num_rebased} descendant commits")?;
1347 }
1348 self.finish_transaction(ui, tx, "import git head", git_import_export_lock)
1349 .await?;
1350 }
1351 Ok(())
1352 }
1353
1354 #[cfg(feature = "git")]
1363 #[instrument(skip_all)]
1364 async fn import_git_refs(
1365 &mut self,
1366 ui: &Ui,
1367 git_import_export_lock: &GitImportExportLock,
1368 ) -> Result<(), CommandError> {
1369 use jj_lib::git;
1370 let git_settings = git::GitSettings::from_settings(self.settings())?;
1371 let remote_settings = self.settings().remote_settings()?;
1372 let import_options =
1373 crate::git_util::load_git_import_options(ui, &git_settings, &remote_settings)?;
1374 let mut tx = self.start_transaction();
1375 let stats = git::import_refs(tx.repo_mut(), &import_options).await?;
1376 crate::git_util::print_git_import_stats_summary(ui, &stats)?;
1377 if !tx.repo().has_changes() {
1378 return Ok(());
1379 }
1380
1381 let mut tx = tx.into_inner();
1382 let num_rebased = rebase_mutable_descendants(&self.env, &mut tx).await?;
1383 if num_rebased > 0 {
1384 writeln!(
1385 ui.status(),
1386 "Rebased {num_rebased} descendant commits off of commits rewritten from git"
1387 )?;
1388 }
1389 self.finish_transaction(ui, tx, "import git refs", git_import_export_lock)
1390 .await?;
1391 writeln!(
1392 ui.status(),
1393 "Done importing changes from the underlying Git repo."
1394 )?;
1395 Ok(())
1396 }
1397
1398 pub fn repo(&self) -> &Arc<ReadonlyRepo> {
1399 &self.user_repo.repo
1400 }
1401
1402 pub fn repo_path(&self) -> &Path {
1403 self.workspace.repo_path()
1404 }
1405
1406 pub fn workspace(&self) -> &Workspace {
1407 &self.workspace
1408 }
1409
1410 pub fn working_copy(&self) -> &dyn WorkingCopy {
1411 self.workspace.working_copy()
1412 }
1413
1414 pub fn env(&self) -> &WorkspaceCommandEnvironment {
1415 &self.env
1416 }
1417
1418 pub async fn unchecked_start_working_copy_mutation(
1419 &mut self,
1420 ) -> Result<(LockedWorkspace<'_>, Commit), CommandError> {
1421 self.check_working_copy_writable()?;
1422 let wc_commit = if let Some(wc_commit_id) = self.get_wc_commit_id() {
1423 self.repo().store().get_commit_async(wc_commit_id).await?
1424 } else {
1425 return Err(user_error("Nothing checked out in this workspace"));
1426 };
1427
1428 let locked_ws = self.workspace.start_working_copy_mutation().await?;
1429
1430 Ok((locked_ws, wc_commit))
1431 }
1432
1433 pub async fn start_working_copy_mutation(
1434 &mut self,
1435 ) -> Result<(LockedWorkspace<'_>, Commit), CommandError> {
1436 let (mut locked_ws, wc_commit) = self.unchecked_start_working_copy_mutation().await?;
1437 if wc_commit.tree().tree_ids_and_labels()
1438 != locked_ws.locked_wc().old_tree().tree_ids_and_labels()
1439 {
1440 return Err(user_error("Concurrent working copy operation. Try again."));
1441 }
1442 Ok((locked_ws, wc_commit))
1443 }
1444
1445 async fn create_and_check_out_recovery_commit(
1446 &mut self,
1447 ui: &Ui,
1448 ) -> Result<SnapshotStats, CommandError> {
1449 self.check_working_copy_writable()?;
1450
1451 let workspace_name = self.workspace_name().to_owned();
1452 let mut locked_ws = self.workspace.start_working_copy_mutation().await?;
1453 let (repo, new_commit) = working_copy::create_and_check_out_recovery_commit(
1454 locked_ws.locked_wc(),
1455 &self.user_repo.repo,
1456 workspace_name,
1457 "RECOVERY COMMIT FROM `jj workspace update-stale`
1458
1459This commit contains changes that were written to the working copy by an
1460operation that was subsequently lost (or was at least unavailable when you ran
1461`jj workspace update-stale`). Because the operation was lost, we don't know
1462what the parent commits are supposed to be. That means that the diff compared
1463to the current parents may contain changes from multiple commits.
1464",
1465 )
1466 .await?;
1467
1468 writeln!(
1469 ui.status(),
1470 "Created and checked out recovery commit {}",
1471 short_commit_hash(new_commit.id())
1472 )?;
1473 locked_ws.finish(repo.op_id().clone()).await?;
1474 self.user_repo = ReadonlyUserRepo::new(repo);
1475
1476 self.maybe_snapshot_impl(ui)
1477 .await
1478 .map_err(|err| err.into_command_error())
1479 }
1480
1481 pub fn workspace_root(&self) -> &Path {
1482 self.workspace.workspace_root()
1483 }
1484
1485 pub fn workspace_name(&self) -> &WorkspaceName {
1486 self.workspace.workspace_name()
1487 }
1488
1489 pub fn get_wc_commit_id(&self) -> Option<&CommitId> {
1490 self.repo().view().get_wc_commit_id(self.workspace_name())
1491 }
1492
1493 pub fn working_copy_shared_with_git(&self) -> bool {
1494 self.working_copy_shared_with_git
1495 }
1496
1497 pub fn format_file_path(&self, file: &RepoPath) -> String {
1498 self.path_converter().format_file_path(file)
1499 }
1500
1501 pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
1504 self.path_converter().parse_file_path(input)
1505 }
1506
1507 pub fn parse_file_patterns(
1509 &self,
1510 ui: &Ui,
1511 values: &[String],
1512 ) -> Result<FilesetExpression, CommandError> {
1513 if values.is_empty() {
1517 Ok(FilesetExpression::all())
1518 } else {
1519 self.parse_union_filesets(ui, values)
1520 }
1521 }
1522
1523 pub fn parse_union_filesets(
1525 &self,
1526 ui: &Ui,
1527 file_args: &[String], ) -> Result<FilesetExpression, CommandError> {
1529 let mut diagnostics = FilesetDiagnostics::new();
1530 let context = self.env.fileset_parse_context();
1531 let expressions: Vec<_> = file_args
1532 .iter()
1533 .map(|arg| fileset::parse_maybe_bare(&mut diagnostics, arg, &context))
1534 .try_collect()?;
1535 print_parse_diagnostics(ui, "In fileset expression", &diagnostics)?;
1536 Ok(FilesetExpression::union_all(expressions))
1537 }
1538
1539 pub fn auto_tracking_matcher(&self, ui: &Ui) -> Result<Box<dyn Matcher>, CommandError> {
1540 let mut diagnostics = FilesetDiagnostics::new();
1541 let pattern = self.settings().get_string("snapshot.auto-track")?;
1542 let context = self.env.fileset_parse_context_for_config();
1543 let expression = fileset::parse(&mut diagnostics, &pattern, &context)?;
1544 print_parse_diagnostics(ui, "In `snapshot.auto-track`", &diagnostics)?;
1545 Ok(expression.to_matcher())
1546 }
1547
1548 pub fn snapshot_options_with_start_tracking_matcher<'a>(
1549 &self,
1550 start_tracking_matcher: &'a dyn Matcher,
1551 ) -> Result<SnapshotOptions<'a>, CommandError> {
1552 let base_ignores = self.base_ignores()?;
1553 let HumanByteSize(mut max_new_file_size) = self
1554 .settings()
1555 .get_value_with("snapshot.max-new-file-size", TryInto::try_into)?;
1556 if max_new_file_size == 0 {
1557 max_new_file_size = u64::MAX;
1558 }
1559 Ok(SnapshotOptions {
1560 base_ignores,
1561 progress: None,
1562 start_tracking_matcher,
1563 force_tracking_matcher: &NothingMatcher,
1564 max_new_file_size,
1565 })
1566 }
1567
1568 pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
1569 self.env.path_converter()
1570 }
1571
1572 #[cfg(not(feature = "git"))]
1573 pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1574 Ok(GitIgnoreFile::empty())
1575 }
1576
1577 #[cfg(feature = "git")]
1578 #[instrument(skip_all)]
1579 pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1580 let get_excludes_file_path = |config: &gix::config::File| -> Option<PathBuf> {
1581 if let Some(value) = config.string("core.excludesFile") {
1584 let path = str::from_utf8(&value)
1585 .ok()
1586 .map(jj_lib::file_util::expand_home_path)?;
1587 Some(self.workspace_root().join(path))
1590 } else {
1591 xdg_config_home().map(|x| x.join("git").join("ignore"))
1592 }
1593 };
1594
1595 fn xdg_config_home() -> Option<PathBuf> {
1596 if let Ok(x) = std::env::var("XDG_CONFIG_HOME")
1597 && !x.is_empty()
1598 {
1599 return Some(PathBuf::from(x));
1600 }
1601 etcetera::home_dir().ok().map(|home| home.join(".config"))
1602 }
1603
1604 let mut git_ignores = GitIgnoreFile::empty();
1605 if let Ok(git_backend) = jj_lib::git::get_git_backend(self.repo().store()) {
1606 let git_repo = git_backend.git_repo();
1607 if let Some(excludes_file_path) = get_excludes_file_path(&git_repo.config_snapshot()) {
1608 git_ignores = git_ignores.chain_with_file(RepoPath::root(), excludes_file_path)?;
1609 }
1610 git_ignores = git_ignores.chain_with_file(
1611 RepoPath::root(),
1612 git_backend.git_repo_path().join("info").join("exclude"),
1613 )?;
1614 } else if let Ok(git_config) = gix::config::File::from_globals()
1615 && let Some(excludes_file_path) = get_excludes_file_path(&git_config)
1616 {
1617 git_ignores = git_ignores.chain_with_file(RepoPath::root(), excludes_file_path)?;
1618 }
1619 Ok(git_ignores)
1620 }
1621
1622 pub fn diff_renderer(&self, formats: Vec<DiffFormat>) -> DiffRenderer<'_> {
1624 DiffRenderer::new(
1625 self.repo().as_ref(),
1626 self.path_converter(),
1627 self.env.conflict_marker_style(),
1628 formats,
1629 )
1630 }
1631
1632 pub fn diff_renderer_for(
1634 &self,
1635 args: &DiffFormatArgs,
1636 ) -> Result<DiffRenderer<'_>, CommandError> {
1637 let formats = diff_util::diff_formats_for(self.settings(), args)?;
1638 Ok(self.diff_renderer(formats))
1639 }
1640
1641 pub fn diff_renderer_for_log(
1645 &self,
1646 args: &DiffFormatArgs,
1647 patch: bool,
1648 ) -> Result<Option<DiffRenderer<'_>>, CommandError> {
1649 let formats = diff_util::diff_formats_for_log(self.settings(), args, patch)?;
1650 Ok((!formats.is_empty()).then(|| self.diff_renderer(formats)))
1651 }
1652
1653 pub fn diff_editor(
1657 &self,
1658 ui: &Ui,
1659 tool_name: Option<&str>,
1660 ) -> Result<DiffEditor, CommandError> {
1661 let base_ignores = self.base_ignores()?;
1662 let conflict_marker_style = self.env.conflict_marker_style();
1663 if let Some(name) = tool_name {
1664 Ok(DiffEditor::with_name(
1665 name,
1666 self.settings(),
1667 base_ignores,
1668 conflict_marker_style,
1669 )?)
1670 } else {
1671 Ok(DiffEditor::from_settings(
1672 ui,
1673 self.settings(),
1674 base_ignores,
1675 conflict_marker_style,
1676 )?)
1677 }
1678 }
1679
1680 pub fn diff_selector(
1684 &self,
1685 ui: &Ui,
1686 tool_name: Option<&str>,
1687 force_interactive: bool,
1688 ) -> Result<DiffSelector, CommandError> {
1689 if tool_name.is_some() || force_interactive {
1690 Ok(DiffSelector::Interactive(self.diff_editor(ui, tool_name)?))
1691 } else {
1692 Ok(DiffSelector::NonInteractive)
1693 }
1694 }
1695
1696 pub fn merge_editor(
1700 &self,
1701 ui: &Ui,
1702 tool_name: Option<&str>,
1703 ) -> Result<MergeEditor, MergeToolConfigError> {
1704 let conflict_marker_style = self.env.conflict_marker_style();
1705 if let Some(name) = tool_name {
1706 MergeEditor::with_name(
1707 name,
1708 self.settings(),
1709 self.path_converter().clone(),
1710 conflict_marker_style,
1711 )
1712 } else {
1713 MergeEditor::from_settings(
1714 ui,
1715 self.settings(),
1716 self.path_converter().clone(),
1717 conflict_marker_style,
1718 )
1719 }
1720 }
1721
1722 pub fn text_editor(&self) -> Result<TextEditor, ConfigGetError> {
1724 TextEditor::from_settings(self.settings())
1725 }
1726
1727 pub fn resolve_single_op(&self, op_str: &str) -> Result<Operation, OpsetEvaluationError> {
1728 op_walk::resolve_op_with_repo(self.repo(), op_str).block_on()
1729 }
1730
1731 pub async fn resolve_single_rev(
1734 &self,
1735 ui: &Ui,
1736 revision_arg: &RevisionArg,
1737 ) -> Result<Commit, CommandError> {
1738 let expression = self.parse_revset(ui, revision_arg)?;
1739 revset_util::evaluate_revset_to_single_commit(revision_arg.as_ref(), &expression, || {
1740 self.commit_summary_template()
1741 })
1742 .await
1743 }
1744
1745 pub async fn resolve_revsets_ordered(
1748 &self,
1749 ui: &Ui,
1750 revision_args: &[RevisionArg],
1751 ) -> Result<IndexSet<CommitId>, CommandError> {
1752 let mut all_commits = IndexSet::new();
1753 for revision_arg in revision_args {
1754 let expression = self.parse_revset(ui, revision_arg)?;
1755 let mut stream = expression.evaluate_to_commit_ids()?;
1756 while let Some(commit_id) = stream.try_next().await? {
1757 all_commits.insert(commit_id);
1758 }
1759 }
1760 Ok(all_commits)
1761 }
1762
1763 pub async fn resolve_some_revsets(
1766 &self,
1767 ui: &Ui,
1768 revision_args: &[RevisionArg],
1769 ) -> Result<IndexSet<CommitId>, CommandError> {
1770 let all_commits = self.resolve_revsets_ordered(ui, revision_args).await?;
1771 if all_commits.is_empty() {
1772 Err(user_error("Empty revision set"))
1773 } else {
1774 Ok(all_commits)
1775 }
1776 }
1777
1778 pub fn parse_revset(
1779 &self,
1780 ui: &Ui,
1781 revision_arg: &RevisionArg,
1782 ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1783 let mut diagnostics = RevsetDiagnostics::new();
1784 let context = self.env.revset_parse_context();
1785 let expression = revset::parse(&mut diagnostics, revision_arg.as_ref(), &context)?;
1786 print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1787 Ok(self.attach_revset_evaluator(expression))
1788 }
1789
1790 pub fn parse_union_revsets(
1792 &self,
1793 ui: &Ui,
1794 revision_args: &[RevisionArg],
1795 ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1796 let mut diagnostics = RevsetDiagnostics::new();
1797 let context = self.env.revset_parse_context();
1798 let expressions: Vec<_> = revision_args
1799 .iter()
1800 .map(|arg| revset::parse(&mut diagnostics, arg.as_ref(), &context))
1801 .try_collect()?;
1802 print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1803 let expression = RevsetExpression::union_all(&expressions);
1804 Ok(self.attach_revset_evaluator(expression))
1805 }
1806
1807 pub fn attach_revset_evaluator(
1808 &self,
1809 expression: Arc<UserRevsetExpression>,
1810 ) -> RevsetExpressionEvaluator<'_> {
1811 RevsetExpressionEvaluator::new(
1812 self.repo().as_ref(),
1813 self.env.command.revset_extensions().clone(),
1814 self.id_prefix_context(),
1815 expression,
1816 )
1817 }
1818
1819 pub fn id_prefix_context(&self) -> &IdPrefixContext {
1820 self.user_repo
1821 .id_prefix_context
1822 .get_or_init(|| self.env.new_id_prefix_context())
1823 }
1824
1825 pub fn parse_template<'a, C, L>(
1827 &self,
1828 ui: &Ui,
1829 language: &L,
1830 template_text: &str,
1831 ) -> Result<TemplateRenderer<'a, C>, CommandError>
1832 where
1833 C: Clone + 'a,
1834 L: TemplateLanguage<'a> + ?Sized,
1835 L::Property: WrapTemplateProperty<'a, C>,
1836 {
1837 self.env.parse_template(ui, language, template_text)
1838 }
1839
1840 fn reparse_valid_template<'a, C, L>(
1842 &self,
1843 language: &L,
1844 template_text: &str,
1845 ) -> TemplateRenderer<'a, C>
1846 where
1847 C: Clone + 'a,
1848 L: TemplateLanguage<'a> + ?Sized,
1849 L::Property: WrapTemplateProperty<'a, C>,
1850 {
1851 template_builder::parse(
1852 language,
1853 &mut TemplateDiagnostics::new(),
1854 template_text,
1855 &self.env.template_aliases_map,
1856 )
1857 .expect("parse error should be confined by WorkspaceCommandHelper::new()")
1858 }
1859
1860 pub fn parse_commit_template(
1862 &self,
1863 ui: &Ui,
1864 template_text: &str,
1865 ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
1866 let language = self.commit_template_language();
1867 self.parse_template(ui, &language, template_text)
1868 }
1869
1870 pub fn parse_operation_template(
1872 &self,
1873 ui: &Ui,
1874 template_text: &str,
1875 ) -> Result<TemplateRenderer<'_, Operation>, CommandError> {
1876 let language = self.operation_template_language();
1877 self.parse_template(ui, &language, template_text)
1878 }
1879
1880 pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
1882 self.env
1883 .commit_template_language(self.repo().as_ref(), self.id_prefix_context())
1884 }
1885
1886 pub fn operation_template_language(&self) -> OperationTemplateLanguage {
1888 OperationTemplateLanguage::new(
1889 self.workspace.repo_loader(),
1890 Some(self.repo().op_id()),
1891 self.env.operation_template_extensions(),
1892 )
1893 }
1894
1895 pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
1897 let language = self.commit_template_language();
1898 self.reparse_valid_template(&language, &self.commit_summary_template_text)
1899 .labeled(["commit"])
1900 }
1901
1902 pub fn operation_summary_template(&self) -> TemplateRenderer<'_, Operation> {
1904 let language = self.operation_template_language();
1905 self.reparse_valid_template(&language, &self.op_summary_template_text)
1906 .labeled(["operation"])
1907 }
1908
1909 pub fn short_change_id_template(&self) -> TemplateRenderer<'_, Commit> {
1910 let language = self.commit_template_language();
1911 self.reparse_valid_template(&language, SHORT_CHANGE_ID_TEMPLATE_TEXT)
1912 .labeled(["commit"])
1913 }
1914
1915 pub fn format_commit_summary(&self, commit: &Commit) -> String {
1920 let output = self.commit_summary_template().format_plain_text(commit);
1921 output.into_string_lossy()
1922 }
1923
1924 #[instrument(skip_all)]
1928 pub fn write_commit_summary(
1929 &self,
1930 formatter: &mut dyn Formatter,
1931 commit: &Commit,
1932 ) -> std::io::Result<()> {
1933 self.commit_summary_template().format(commit, formatter)
1934 }
1935
1936 pub async fn check_rewritable<'a>(
1937 &self,
1938 commits: impl IntoIterator<Item = &'a CommitId>,
1939 ) -> Result<(), CommandError> {
1940 let commit_ids = commits.into_iter().cloned().collect_vec();
1941 let to_rewrite_expr = RevsetExpression::commits(commit_ids);
1942 self.check_rewritable_expr(&to_rewrite_expr).await
1943 }
1944
1945 pub async fn check_rewritable_expr(
1946 &self,
1947 to_rewrite_expr: &Arc<ResolvedRevsetExpression>,
1948 ) -> Result<(), CommandError> {
1949 let repo = self.repo().as_ref();
1950 let immutable_expr = self.env.resolve_immutable_expression(repo)?;
1951 let Some(commit_id) = immutable_expr
1952 .intersection(to_rewrite_expr)
1953 .evaluate(repo)?
1954 .stream()
1955 .try_next()
1956 .await?
1957 else {
1958 return Ok(());
1959 };
1960 let error = if &commit_id == repo.store().root_commit_id() {
1961 user_error(format!("The root commit {commit_id:.12} is immutable"))
1962 } else {
1963 let mut error = user_error(format!("Commit {commit_id:.12} is immutable"));
1964 let commit = repo.store().get_commit_async(&commit_id).await?;
1965 error.add_formatted_hint_with(|formatter| {
1966 write!(formatter, "Could not modify commit: ")?;
1967 self.write_commit_summary(formatter, &commit)?;
1968 Ok(())
1969 });
1970 error.add_hint("Immutable commits are used to protect shared history.");
1971 error.add_hint(indoc::indoc! {"
1972 For more information, see:
1973 - https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits
1974 - `jj help -k config`, \"Set of immutable commits\""});
1975
1976 let (lower_bound, upper_bound) = immutable_expr
1977 .intersection(&to_rewrite_expr.descendants())
1978 .evaluate(repo)?
1979 .count_estimate()?;
1980 let exact = upper_bound == Some(lower_bound);
1981 let or_more = if exact { "" } else { " or more" };
1982 error.add_hint(format!(
1983 "This operation would rewrite {lower_bound}{or_more} immutable commits."
1984 ));
1985
1986 error
1987 };
1988 Err(error)
1989 }
1990
1991 #[instrument(skip_all)]
1992 async fn snapshot_working_copy(
1993 &mut self,
1994 ui: &Ui,
1995 ) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
1996 let workspace_name = self.workspace_name().to_owned();
1997 let repo = self.repo().clone();
1998 let auto_tracking_matcher = self
1999 .auto_tracking_matcher(ui)
2000 .map_err(snapshot_command_error)?;
2001 let options = self
2002 .snapshot_options_with_start_tracking_matcher(&auto_tracking_matcher)
2003 .map_err(snapshot_command_error)?;
2004
2005 let mut locked_ws = self
2007 .workspace
2008 .start_working_copy_mutation()
2009 .await
2010 .map_err(snapshot_command_error)?;
2011
2012 let Some((repo, wc_commit)) =
2013 handle_stale_working_copy(locked_ws.locked_wc(), repo, &workspace_name).await?
2014 else {
2015 return Ok(SnapshotStats::default());
2018 };
2019
2020 self.user_repo = ReadonlyUserRepo::new(repo);
2021 let (new_tree, stats) = {
2022 let mut options = options;
2023 let progress = crate::progress::snapshot_progress(ui);
2024 options.progress = progress.as_ref().map(|x| x as _);
2025 locked_ws
2026 .locked_wc()
2027 .snapshot(&options)
2028 .await
2029 .map_err(snapshot_command_error)?
2030 };
2031 if new_tree.tree_ids_and_labels() != wc_commit.tree().tree_ids_and_labels() {
2032 let mut tx = start_repo_transaction(
2033 &self.user_repo.repo,
2034 &workspace_name,
2035 self.env.command.string_args(),
2036 );
2037 tx.set_is_snapshot(true);
2038 let immutable_expr = self
2039 .env
2040 .resolve_immutable_expression(tx.repo())
2041 .map_err(snapshot_command_error)?;
2042 let wc_immutable = immutable_expr
2043 .intersection(&RevsetExpression::commit(wc_commit.id().clone()))
2044 .evaluate(tx.repo())
2045 .map_err(snapshot_command_error)?
2046 .stream()
2047 .try_next()
2048 .await
2049 .map_err(snapshot_command_error)?
2050 .is_some();
2051 let mut_repo = tx.repo_mut();
2052 let new_wc_commit;
2053 if wc_immutable {
2054 new_wc_commit = mut_repo
2055 .new_commit(vec![wc_commit.id().clone()], new_tree.clone())
2056 .write()
2057 .await
2058 .map_err(snapshot_command_error)?;
2059 writeln!(
2060 ui.warning_default(),
2061 "The working-copy commit is immutable; a new commit has been created on top \
2062 of it.",
2063 )
2064 .map_err(snapshot_command_error)?;
2065 } else {
2066 new_wc_commit = mut_repo
2067 .rewrite_commit(&wc_commit)
2068 .set_tree(new_tree.clone())
2069 .write()
2070 .await
2071 .map_err(snapshot_command_error)?;
2072 }
2073 mut_repo
2074 .set_wc_commit(workspace_name, new_wc_commit.id().clone())
2075 .map_err(snapshot_command_error)?;
2076
2077 let num_rebased = mut_repo
2079 .rebase_descendants()
2080 .await
2081 .map_err(snapshot_command_error)?;
2082 if num_rebased > 0 {
2083 writeln!(
2084 ui.status(),
2085 "Rebased {num_rebased} descendant commits onto updated working copy"
2086 )
2087 .map_err(snapshot_command_error)?;
2088 }
2089
2090 #[cfg(feature = "git")]
2091 if self.working_copy_shared_with_git && self.env.command.should_commit_transaction() {
2092 let old_tree = wc_commit.tree();
2093 let new_tree = new_wc_commit.tree();
2094 export_working_copy_changes_to_git(ui, mut_repo, &old_tree, &new_tree)
2095 .await
2096 .map_err(snapshot_command_error)?;
2097 }
2098
2099 let repo = self
2100 .env
2101 .command
2102 .maybe_commit_transaction(tx, "snapshot working copy")
2103 .await
2104 .map_err(snapshot_command_error)?;
2105 self.user_repo = ReadonlyUserRepo::new(repo);
2106 if !self.env.command.should_commit_transaction() {
2107 writeln!(
2108 ui.status(),
2109 "Snapshot operation left uncommitted because --no-integrate-operation was \
2110 requested: {}",
2111 short_operation_hash(self.user_repo.repo.op_id())
2112 )
2113 .map_err(snapshot_command_error)?;
2114 }
2115 }
2116
2117 #[cfg(feature = "git")]
2118 if self.working_copy_shared_with_git
2119 && let Ok(resolved_tree) = new_tree
2120 .trees()
2121 .await
2122 .map_err(snapshot_command_error)?
2123 .into_resolved()
2124 && resolved_tree
2125 .entries_non_recursive()
2126 .any(|entry| entry.name().as_internal_str().starts_with(".jjconflict"))
2127 {
2128 writeln!(
2129 ui.warning_default(),
2130 "The working copy contains '.jjconflict' files. These files are used by `jj` \
2131 internally and should not be present in the working copy."
2132 )
2133 .map_err(snapshot_command_error)?;
2134 writeln!(
2135 ui.hint_default(),
2136 "You may have used a regular `git` command to check out a conflicted commit."
2137 )
2138 .map_err(snapshot_command_error)?;
2139 writeln!(
2140 ui.hint_default(),
2141 "You can use `jj abandon` to discard the working copy changes."
2142 )
2143 .map_err(snapshot_command_error)?;
2144 }
2145
2146 if self.env.command.should_commit_transaction() {
2147 locked_ws
2148 .finish(self.user_repo.repo.op_id().clone())
2149 .await
2150 .map_err(snapshot_command_error)?;
2151 }
2152 Ok(stats)
2153 }
2154
2155 async fn update_working_copy(
2156 &mut self,
2157 ui: &Ui,
2158 maybe_old_commit: Option<&Commit>,
2159 new_commit: &Commit,
2160 ) -> Result<(), CommandError> {
2161 assert!(self.may_update_working_copy);
2162 let stats = update_working_copy(
2163 &self.user_repo.repo,
2164 &mut self.workspace,
2165 maybe_old_commit,
2166 new_commit,
2167 )
2168 .await?;
2169 self.print_updated_working_copy_stats(ui, maybe_old_commit, new_commit, &stats)
2170 }
2171
2172 fn print_updated_working_copy_stats(
2173 &self,
2174 ui: &Ui,
2175 maybe_old_commit: Option<&Commit>,
2176 new_commit: &Commit,
2177 stats: &CheckoutStats,
2178 ) -> Result<(), CommandError> {
2179 if Some(new_commit) != maybe_old_commit
2180 && let Some(mut formatter) = ui.status_formatter()
2181 {
2182 let template = self.commit_summary_template();
2183 write!(formatter, "Working copy (@) now at: ")?;
2184 template.format(new_commit, formatter.as_mut())?;
2185 writeln!(formatter)?;
2186 for parent in new_commit.parents().block_on()? {
2187 write!(formatter, "Parent commit (@-) : ")?;
2189 template.format(&parent, formatter.as_mut())?;
2190 writeln!(formatter)?;
2191 }
2192 }
2193 print_checkout_stats(ui, stats, new_commit)?;
2194 if Some(new_commit) != maybe_old_commit
2195 && let Some(mut formatter) = ui.status_formatter()
2196 && new_commit.has_conflict()
2197 {
2198 let conflicts = new_commit.tree().conflicts().collect_vec();
2199 writeln!(
2200 formatter.labeled("warning").with_heading("Warning: "),
2201 "There are unresolved conflicts at these paths:"
2202 )?;
2203 print_conflicted_paths(conflicts, formatter.as_mut(), self)?;
2204 }
2205 Ok(())
2206 }
2207
2208 pub fn start_transaction(&mut self) -> WorkspaceCommandTransaction<'_> {
2209 let tx = start_repo_transaction(
2210 self.repo(),
2211 self.workspace_name(),
2212 self.env.command.string_args(),
2213 );
2214 let id_prefix_context = mem::take(&mut self.user_repo.id_prefix_context);
2215 WorkspaceCommandTransaction {
2216 helper: self,
2217 tx,
2218 id_prefix_context,
2219 }
2220 }
2221
2222 async fn finish_transaction(
2223 &mut self,
2224 ui: &Ui,
2225 mut tx: Transaction,
2226 description: impl Into<String>,
2227 _git_import_export_lock: &GitImportExportLock,
2228 ) -> Result<(), CommandError> {
2229 if let Err(err) =
2230 revset_util::try_resolve_trunk_alias(tx.repo(), &self.env.revset_parse_context())
2231 {
2232 writeln!(
2233 ui.warning_default(),
2234 "Failed to resolve `revset-aliases.trunk()`: {err}"
2235 )?;
2236 writeln!(
2237 ui.hint_default(),
2238 "Use `jj config edit --repo` to adjust the `trunk()` alias."
2239 )?;
2240 }
2241
2242 let old_repo = tx.base_repo().clone();
2243
2244 let maybe_old_wc_commit = old_repo
2245 .view()
2246 .get_wc_commit_id(self.workspace_name())
2247 .map(|commit_id| tx.base_repo().store().get_commit(commit_id))
2248 .transpose()?;
2249 let maybe_new_wc_commit = tx
2250 .repo()
2251 .view()
2252 .get_wc_commit_id(self.workspace_name())
2253 .map(|commit_id| tx.repo().store().get_commit(commit_id))
2254 .transpose()?;
2255
2256 #[cfg(feature = "git")]
2257 if self.working_copy_shared_with_git && self.env.command.should_commit_transaction() {
2258 use std::error::Error as _;
2259 if let Some(wc_commit) = &maybe_new_wc_commit {
2260 match jj_lib::git::reset_head(tx.repo_mut(), wc_commit).await {
2267 Ok(()) => {}
2268 Err(err @ jj_lib::git::GitResetHeadError::UpdateHeadRef(_)) => {
2269 writeln!(ui.warning_default(), "{err}")?;
2270 print_error_sources(ui, err.source())?;
2271 }
2272 Err(err) => return Err(err.into()),
2273 }
2274 }
2275 let stats = jj_lib::git::export_refs(tx.repo_mut())?;
2276 crate::git_util::print_git_export_stats(ui, &stats)?;
2277 }
2278
2279 self.user_repo = ReadonlyUserRepo::new(
2280 self.env
2281 .command
2282 .maybe_commit_transaction(tx, description)
2283 .await?,
2284 );
2285
2286 if self.may_update_working_copy {
2290 if let Some(new_commit) = &maybe_new_wc_commit {
2291 self.update_working_copy(ui, maybe_old_wc_commit.as_ref(), new_commit)
2292 .await?;
2293 } else {
2294 }
2297 }
2298
2299 self.report_repo_changes(ui, &old_repo).await?;
2300
2301 if !self.env.command.should_commit_transaction() {
2302 writeln!(
2303 ui.status(),
2304 "Operation left uncommitted because --no-integrate-operation was requested: {}",
2305 short_operation_hash(self.repo().op_id())
2306 )?;
2307 }
2308
2309 let settings = self.settings();
2310 let missing_user_name = settings.user_name().is_empty();
2311 let missing_user_mail = settings.user_email().is_empty();
2312 if missing_user_name || missing_user_mail {
2313 let not_configured_msg = match (missing_user_name, missing_user_mail) {
2314 (true, true) => "Name and email not configured.",
2315 (true, false) => "Name not configured.",
2316 (false, true) => "Email not configured.",
2317 _ => unreachable!(),
2318 };
2319 writeln!(
2320 ui.warning_default(),
2321 "{not_configured_msg} Until configured, your commits will be created with the \
2322 empty identity, and can't be pushed to remotes."
2323 )?;
2324 writeln!(ui.hint_default(), "To configure, run:")?;
2325 if missing_user_name {
2326 writeln!(
2327 ui.hint_no_heading(),
2328 r#" jj config set --user user.name "Some One""#
2329 )?;
2330 }
2331 if missing_user_mail {
2332 writeln!(
2333 ui.hint_no_heading(),
2334 r#" jj config set --user user.email "someone@example.com""#
2335 )?;
2336 }
2337 }
2338 Ok(())
2339 }
2340
2341 async fn report_repo_changes(
2344 &self,
2345 ui: &Ui,
2346 old_repo: &Arc<ReadonlyRepo>,
2347 ) -> Result<(), CommandError> {
2348 let Some(mut fmt) = ui.status_formatter() else {
2349 return Ok(());
2350 };
2351 let old_view = old_repo.view();
2352 let new_repo = self.repo().as_ref();
2353 let new_view = new_repo.view();
2354
2355 let workspace_name = self.workspace_name();
2356 if old_view.wc_commit_ids().contains_key(workspace_name)
2357 && !new_view.wc_commit_ids().contains_key(workspace_name)
2358 {
2359 writeln!(
2360 fmt.labeled("warning").with_heading("Warning: "),
2361 "The current workspace '{}' no longer exists after this operation. The working \
2362 copy was left untouched.",
2363 workspace_name.as_symbol(),
2364 )?;
2365 writeln!(
2366 fmt.labeled("hint").with_heading("Hint: "),
2367 "Restore to an operation that contains the workspace (e.g. `jj undo` or `jj \
2368 redo`).",
2369 )?;
2370 }
2371
2372 let old_heads = RevsetExpression::commits(old_view.heads().iter().cloned().collect());
2373 let new_heads = RevsetExpression::commits(new_view.heads().iter().cloned().collect());
2374 let conflicts = RevsetExpression::filter(RevsetFilterPredicate::HasConflict)
2380 .filtered(RevsetFilterPredicate::File(FilesetExpression::all()));
2381 let removed_conflicts_expr = new_heads.range(&old_heads).intersection(&conflicts);
2382 let added_conflicts_expr = old_heads.range(&new_heads).intersection(&conflicts);
2383
2384 let get_commits =
2385 async |expr: Arc<ResolvedRevsetExpression>| -> Result<Vec<Commit>, CommandError> {
2386 let commits = expr
2387 .evaluate(new_repo)?
2388 .stream()
2389 .commits(new_repo.store())
2390 .try_collect()
2391 .await?;
2392 Ok(commits)
2393 };
2394 let removed_conflict_commits = get_commits(removed_conflicts_expr).await?;
2395 let added_conflict_commits = get_commits(added_conflicts_expr).await?;
2396
2397 fn commits_by_change_id(commits: &[Commit]) -> IndexMap<&ChangeId, Vec<&Commit>> {
2398 let mut result: IndexMap<&ChangeId, Vec<&Commit>> = IndexMap::new();
2399 for commit in commits {
2400 result.entry(commit.change_id()).or_default().push(commit);
2401 }
2402 result
2403 }
2404 let removed_conflicts_by_change_id = commits_by_change_id(&removed_conflict_commits);
2405 let added_conflicts_by_change_id = commits_by_change_id(&added_conflict_commits);
2406 let mut resolved_conflicts_by_change_id = removed_conflicts_by_change_id.clone();
2407 resolved_conflicts_by_change_id
2408 .retain(|change_id, _commits| !added_conflicts_by_change_id.contains_key(change_id));
2409 let mut new_conflicts_by_change_id = added_conflicts_by_change_id.clone();
2410 new_conflicts_by_change_id
2411 .retain(|change_id, _commits| !removed_conflicts_by_change_id.contains_key(change_id));
2412
2413 if !resolved_conflicts_by_change_id.is_empty() {
2415 let num_resolved: usize = resolved_conflicts_by_change_id
2419 .values()
2420 .map(|commits| commits.len())
2421 .sum();
2422 writeln!(
2423 fmt,
2424 "Existing conflicts were resolved or abandoned from {num_resolved} commits."
2425 )?;
2426 }
2427 if !new_conflicts_by_change_id.is_empty() {
2428 let num_conflicted: usize = new_conflicts_by_change_id
2429 .values()
2430 .map(|commits| commits.len())
2431 .sum();
2432 writeln!(fmt, "New conflicts appeared in {num_conflicted} commits:")?;
2433 print_updated_commits(
2434 fmt.as_mut(),
2435 &self.commit_summary_template(),
2436 new_conflicts_by_change_id.values().flatten().copied(),
2437 )?;
2438 }
2439
2440 if !(added_conflict_commits.is_empty()
2444 || resolved_conflicts_by_change_id.is_empty() && new_conflicts_by_change_id.is_empty())
2445 {
2446 if new_conflicts_by_change_id.is_empty() {
2451 writeln!(
2452 fmt,
2453 "There are still unresolved conflicts in rebased descendants.",
2454 )?;
2455 }
2456
2457 self.report_repo_conflicts(
2458 fmt.as_mut(),
2459 new_repo,
2460 added_conflict_commits
2461 .iter()
2462 .map(|commit| commit.id().clone())
2463 .collect(),
2464 )
2465 .await?;
2466 }
2467
2468 Ok(())
2469 }
2470
2471 pub async fn report_repo_conflicts(
2472 &self,
2473 fmt: &mut dyn Formatter,
2474 repo: &ReadonlyRepo,
2475 conflicted_commits: Vec<CommitId>,
2476 ) -> Result<(), CommandError> {
2477 if !self.settings().get_bool("hints.resolving-conflicts")? || conflicted_commits.is_empty()
2478 {
2479 return Ok(());
2480 }
2481
2482 let only_one_conflicted_commit = conflicted_commits.len() == 1;
2483 let root_conflicts_revset = RevsetExpression::commits(conflicted_commits)
2484 .roots()
2485 .evaluate(repo)?;
2486
2487 let root_conflict_commits: Vec<_> = root_conflicts_revset
2488 .stream()
2489 .commits(repo.store())
2490 .try_collect()
2491 .await?;
2492
2493 let instruction = if only_one_conflicted_commit {
2495 indoc! {"
2496 To resolve the conflicts, start by creating a commit on top of
2497 the conflicted commit:
2498 "}
2499 } else if root_conflict_commits.len() == 1 {
2500 indoc! {"
2501 To resolve the conflicts, start by creating a commit on top of
2502 the first conflicted commit:
2503 "}
2504 } else {
2505 indoc! {"
2506 To resolve the conflicts, start by creating a commit on top of
2507 one of the first conflicted commits:
2508 "}
2509 };
2510 write!(fmt.labeled("hint").with_heading("Hint: "), "{instruction}")?;
2511 let format_short_change_id = self.short_change_id_template();
2512 {
2513 let mut fmt = fmt.labeled("hint");
2514 for commit in &root_conflict_commits {
2515 write!(fmt, " jj new ")?;
2516 format_short_change_id.format(commit, *fmt)?;
2517 writeln!(fmt)?;
2518 }
2519 }
2520 writedoc!(
2521 fmt.labeled("hint"),
2522 "
2523 Then use `jj resolve`, or edit the conflict markers in the file directly.
2524 Once the conflicts are resolved, you can inspect the result with `jj diff`.
2525 Then run `jj squash` to move the resolution into the conflicted commit.
2526 ",
2527 )?;
2528 Ok(())
2529 }
2530
2531 pub fn get_advanceable_bookmarks<'a>(
2546 &self,
2547 ui: &Ui,
2548 from: impl IntoIterator<Item = &'a CommitId>,
2549 ) -> Result<Vec<AdvanceableBookmark>, CommandError> {
2550 let Some(ab_matcher) = load_advance_bookmarks_matcher(ui, self.settings())? else {
2551 return Ok(Vec::new());
2553 };
2554
2555 let mut advanceable_bookmarks = Vec::new();
2556 for from_commit in from {
2557 for (name, _) in self.repo().view().local_bookmarks_for_commit(from_commit) {
2558 if ab_matcher.is_match(name.as_str()) {
2559 advanceable_bookmarks.push(AdvanceableBookmark {
2560 name: name.to_owned(),
2561 old_commit_id: from_commit.clone(),
2562 });
2563 }
2564 }
2565 }
2566
2567 Ok(advanceable_bookmarks)
2568 }
2569}
2570
2571#[cfg(feature = "git")]
2572pub async fn export_working_copy_changes_to_git(
2573 ui: &Ui,
2574 mut_repo: &mut MutableRepo,
2575 old_tree: &MergedTree,
2576 new_tree: &MergedTree,
2577) -> Result<(), CommandError> {
2578 let repo = mut_repo.base_repo().as_ref();
2579 jj_lib::git::update_intent_to_add(repo, old_tree, new_tree).await?;
2580 let stats = jj_lib::git::export_refs(mut_repo)?;
2581 crate::git_util::print_git_export_stats(ui, &stats)?;
2582 Ok(())
2583}
2584#[cfg(not(feature = "git"))]
2585pub async fn export_working_copy_changes_to_git(
2586 _ui: &Ui,
2587 _mut_repo: &mut MutableRepo,
2588 _old_tree: &MergedTree,
2589 _new_tree: &MergedTree,
2590) -> Result<(), CommandError> {
2591 Ok(())
2592}
2593
2594#[must_use]
2602pub struct WorkspaceCommandTransaction<'a> {
2603 helper: &'a mut WorkspaceCommandHelper,
2604 tx: Transaction,
2605 id_prefix_context: OnceCell<IdPrefixContext>,
2607}
2608
2609impl WorkspaceCommandTransaction<'_> {
2610 pub fn base_workspace_helper(&self) -> &WorkspaceCommandHelper {
2612 self.helper
2613 }
2614
2615 pub fn settings(&self) -> &UserSettings {
2617 self.helper.settings()
2618 }
2619
2620 pub fn base_repo(&self) -> &Arc<ReadonlyRepo> {
2621 self.tx.base_repo()
2622 }
2623
2624 pub fn repo(&self) -> &MutableRepo {
2625 self.tx.repo()
2626 }
2627
2628 pub fn repo_mut(&mut self) -> &mut MutableRepo {
2629 self.id_prefix_context.take(); self.tx.repo_mut()
2631 }
2632
2633 pub fn check_out(&mut self, commit: &Commit) -> Result<Commit, CheckOutCommitError> {
2634 let name = self.helper.workspace_name().to_owned();
2635 self.id_prefix_context.take(); self.tx.repo_mut().check_out(name, commit).block_on()
2637 }
2638
2639 pub fn edit(&mut self, commit: &Commit) -> Result<(), EditCommitError> {
2640 let name = self.helper.workspace_name().to_owned();
2641 self.id_prefix_context.take(); self.tx.repo_mut().edit(name, commit).block_on()
2643 }
2644
2645 pub fn format_commit_summary(&self, commit: &Commit) -> String {
2646 let output = self.commit_summary_template().format_plain_text(commit);
2647 output.into_string_lossy()
2648 }
2649
2650 pub fn write_commit_summary(
2651 &self,
2652 formatter: &mut dyn Formatter,
2653 commit: &Commit,
2654 ) -> std::io::Result<()> {
2655 self.commit_summary_template().format(commit, formatter)
2656 }
2657
2658 pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
2660 let language = self.commit_template_language();
2661 self.helper
2662 .reparse_valid_template(&language, &self.helper.commit_summary_template_text)
2663 .labeled(["commit"])
2664 }
2665
2666 pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
2669 let id_prefix_context = self
2670 .id_prefix_context
2671 .get_or_init(|| self.helper.env.new_id_prefix_context());
2672 self.helper
2673 .env
2674 .commit_template_language(self.tx.repo(), id_prefix_context)
2675 }
2676
2677 pub fn parse_commit_template(
2679 &self,
2680 ui: &Ui,
2681 template_text: &str,
2682 ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
2683 let language = self.commit_template_language();
2684 self.helper.env.parse_template(ui, &language, template_text)
2685 }
2686
2687 pub async fn finish(self, ui: &Ui, description: impl Into<String>) -> Result<(), CommandError> {
2688 let Self { helper, mut tx, .. } = self;
2689 if !tx.repo().has_changes() {
2690 writeln!(ui.status(), "Nothing changed.")?;
2691 return Ok(());
2692 }
2693 let num_rebased = rebase_mutable_descendants(&helper.env, &mut tx).await?;
2694 if num_rebased > 0 {
2695 writeln!(ui.status(), "Rebased {num_rebased} descendant commits")?;
2696 }
2697 let git_import_export_lock = helper.lock_git_import_export()?;
2700 helper
2701 .finish_transaction(ui, tx, description, &git_import_export_lock)
2702 .await
2703 }
2704
2705 pub fn into_inner(self) -> Transaction {
2710 self.tx
2711 }
2712
2713 pub fn advance_bookmarks(
2719 &mut self,
2720 bookmarks: Vec<AdvanceableBookmark>,
2721 move_to: &CommitId,
2722 ) -> Result<(), CommandError> {
2723 for bookmark in bookmarks {
2724 self.repo_mut().merge_local_bookmark(
2727 &bookmark.name,
2728 &RefTarget::normal(bookmark.old_commit_id),
2729 &RefTarget::normal(move_to.clone()),
2730 )?;
2731 }
2732 Ok(())
2733 }
2734}
2735
2736pub fn find_workspace_dir(cwd: &Path) -> &Path {
2737 cwd.ancestors()
2738 .find(|path| path.join(".jj").is_dir())
2739 .unwrap_or(cwd)
2740}
2741
2742fn map_workspace_load_error(err: WorkspaceLoadError, user_wc_path: Option<&str>) -> CommandError {
2743 match err {
2744 WorkspaceLoadError::NoWorkspaceHere(wc_path) => {
2745 let short_wc_path = user_wc_path.map_or(wc_path.as_ref(), Path::new);
2747 let message = format!(r#"There is no jj repo in "{}""#, short_wc_path.display());
2748 let git_dir = wc_path.join(".git");
2749 if git_dir.is_dir() {
2750 user_error(message).hinted(
2751 "It looks like this is a git repo. You can create a jj repo backed by it by \
2752 running this:
2753jj git init",
2754 )
2755 } else {
2756 user_error(message)
2757 }
2758 }
2759 WorkspaceLoadError::RepoDoesNotExist(repo_dir) => user_error(format!(
2760 "The repository directory at {} is missing. Was it moved?",
2761 repo_dir.display(),
2762 )),
2763 WorkspaceLoadError::StoreLoadError(err @ StoreLoadError::UnsupportedType { .. }) => {
2764 internal_error_with_message(
2765 "This version of the jj binary doesn't support this type of repo",
2766 err,
2767 )
2768 }
2769 WorkspaceLoadError::StoreLoadError(
2770 err @ (StoreLoadError::ReadError { .. } | StoreLoadError::Backend(_)),
2771 ) => internal_error_with_message("The repository appears broken or inaccessible", err),
2772 WorkspaceLoadError::StoreLoadError(StoreLoadError::Signing(err)) => user_error(err),
2773 WorkspaceLoadError::WorkingCopyState(err) => internal_error(err),
2774 WorkspaceLoadError::DecodeRepoPath(_) | WorkspaceLoadError::Path(_) => user_error(err),
2775 }
2776}
2777
2778pub fn start_repo_transaction(
2779 repo: &Arc<ReadonlyRepo>,
2780 workspace_name: &WorkspaceName,
2781 string_args: &[String],
2782) -> Transaction {
2783 let mut tx = repo.start_transaction();
2784 tx.set_workspace_name(workspace_name);
2785 let shell_escape = |arg: &String| {
2788 if arg.as_bytes().iter().all(|b| {
2789 matches!(b,
2790 b'A'..=b'Z'
2791 | b'a'..=b'z'
2792 | b'0'..=b'9'
2793 | b','
2794 | b'-'
2795 | b'.'
2796 | b'/'
2797 | b':'
2798 | b'@'
2799 | b'_'
2800 )
2801 }) {
2802 arg.clone()
2803 } else {
2804 format!("'{}'", arg.replace('\'', "\\'"))
2805 }
2806 };
2807 let mut quoted_strings = vec!["jj".to_string()];
2808 quoted_strings.extend(string_args.iter().skip(1).map(shell_escape));
2809 tx.set_attribute("args".to_string(), quoted_strings.join(" "));
2810 tx
2811}
2812
2813async fn rebase_mutable_descendants(
2814 env: &WorkspaceCommandEnvironment,
2815 tx: &mut Transaction,
2816) -> Result<usize, CommandError> {
2817 let mut num_rebased = 0;
2822 let immutable = env.resolve_immutable_expression(tx.base_repo().as_ref())?;
2823 tx.repo_mut()
2824 .rebase_descendants_with_options(
2825 &immutable,
2826 &RebaseOptions::default(),
2827 |_old_commit, _rebased_commit| num_rebased += 1,
2828 )
2829 .await?;
2830 Ok(num_rebased)
2831}
2832
2833async fn handle_stale_working_copy(
2839 locked_wc: &mut dyn LockedWorkingCopy,
2840 repo: Arc<ReadonlyRepo>,
2841 workspace_name: &WorkspaceName,
2842) -> Result<Option<(Arc<ReadonlyRepo>, Commit)>, SnapshotWorkingCopyError> {
2843 let get_wc_commit = |repo: &ReadonlyRepo| -> Result<Option<_>, _> {
2844 repo.view()
2845 .get_wc_commit_id(workspace_name)
2846 .map(|id| repo.store().get_commit(id))
2847 .transpose()
2848 .map_err(snapshot_command_error)
2849 };
2850 let Some(wc_commit) = get_wc_commit(&repo)? else {
2851 return Ok(None);
2852 };
2853 let old_op_id = locked_wc.old_operation_id().clone();
2854 match WorkingCopyFreshness::check_stale(locked_wc, &wc_commit, &repo).await {
2855 Ok(WorkingCopyFreshness::Fresh) => Ok(Some((repo, wc_commit))),
2856 Ok(WorkingCopyFreshness::Updated(wc_operation)) => {
2857 let repo = repo
2858 .reload_at(&wc_operation)
2859 .await
2860 .map_err(snapshot_command_error)?;
2861 if let Some(wc_commit) = get_wc_commit(&repo)? {
2862 Ok(Some((repo, wc_commit)))
2863 } else {
2864 Ok(None)
2865 }
2866 }
2867 Ok(WorkingCopyFreshness::WorkingCopyStale) => {
2868 Err(SnapshotWorkingCopyError::StaleWorkingCopy(
2869 user_error(format!(
2870 "The working copy is stale (not updated since operation {}).",
2871 short_operation_hash(&old_op_id)
2872 ))
2873 .hinted(
2874 "Run `jj workspace update-stale` to update it.
2875See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy \
2876 for more information.",
2877 ),
2878 ))
2879 }
2880 Ok(WorkingCopyFreshness::SiblingOperation) => {
2881 Err(SnapshotWorkingCopyError::StaleWorkingCopy(
2882 internal_error(format!(
2883 "The repo was loaded at operation {}, which seems to be a sibling of the \
2884 working copy's operation {}",
2885 short_operation_hash(repo.op_id()),
2886 short_operation_hash(&old_op_id)
2887 ))
2888 .hinted(format!(
2889 "Run `jj op integrate {}` to add the working copy's operation to the \
2890 operation log.",
2891 short_operation_hash(&old_op_id)
2892 )),
2893 ))
2894 }
2895 Err(OpStoreError::ObjectNotFound { .. }) => {
2896 Err(SnapshotWorkingCopyError::StaleWorkingCopy(
2897 user_error("Could not read working copy's operation.").hinted(
2898 "Run `jj workspace update-stale` to recover.
2899See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy \
2900 for more information.",
2901 ),
2902 ))
2903 }
2904 Err(e) => Err(snapshot_command_error(e)),
2905 }
2906}
2907
2908async fn update_stale_working_copy(
2909 mut locked_ws: LockedWorkspace<'_>,
2910 op_id: OperationId,
2911 stale_commit: &Commit,
2912 new_commit: &Commit,
2913) -> Result<CheckoutStats, CommandError> {
2914 if stale_commit.tree().tree_ids_and_labels()
2917 != locked_ws.locked_wc().old_tree().tree_ids_and_labels()
2918 {
2919 return Err(user_error("Concurrent working copy operation. Try again."));
2920 }
2921 let stats = locked_ws
2922 .locked_wc()
2923 .check_out(new_commit)
2924 .await
2925 .map_err(|err| {
2926 internal_error_with_message(
2927 format!("Failed to check out commit {}", new_commit.id().hex()),
2928 err,
2929 )
2930 })?;
2931 locked_ws.finish(op_id).await?;
2932
2933 Ok(stats)
2934}
2935
2936pub fn print_updated_commits<'a>(
2939 formatter: &mut dyn Formatter,
2940 template: &TemplateRenderer<Commit>,
2941 commits: impl IntoIterator<Item = &'a Commit>,
2942) -> io::Result<()> {
2943 let mut commits = commits.into_iter().fuse();
2944 for commit in commits.by_ref().take(10) {
2945 write!(formatter, " ")?;
2946 template.format(commit, formatter)?;
2947 writeln!(formatter)?;
2948 }
2949 if commits.next().is_some() {
2950 writeln!(formatter, " ...")?;
2951 }
2952 Ok(())
2953}
2954
2955#[instrument(skip_all)]
2956pub fn print_conflicted_paths(
2957 conflicts: Vec<(RepoPathBuf, BackendResult<MergedTreeValue>)>,
2958 formatter: &mut dyn Formatter,
2959 workspace_command: &WorkspaceCommandHelper,
2960) -> Result<(), CommandError> {
2961 let formatted_paths = conflicts
2962 .iter()
2963 .map(|(path, _conflict)| workspace_command.format_file_path(path))
2964 .collect_vec();
2965 let max_path_len = formatted_paths.iter().map(|p| p.len()).max().unwrap_or(0);
2966 let formatted_paths = formatted_paths
2967 .into_iter()
2968 .map(|p| format!("{:width$}", p, width = max_path_len.min(32) + 3));
2969
2970 for ((_, conflict), formatted_path) in std::iter::zip(conflicts, formatted_paths) {
2971 let conflict = conflict?.simplify();
2974 let sides = conflict.num_sides();
2975 let n_adds = conflict.adds().flatten().count();
2976 let deletions = sides - n_adds;
2977
2978 let mut seen_objects = BTreeMap::new(); if deletions > 0 {
2980 seen_objects.insert(
2981 format!(
2982 "{deletions} deletion{}",
2984 if deletions > 1 { "s" } else { "" }
2985 ),
2986 "normal", );
2988 }
2989 for term in itertools::chain(conflict.removes(), conflict.adds()).flatten() {
2993 seen_objects.insert(
2994 match term {
2995 TreeValue::File {
2996 executable: false, ..
2997 } => continue,
2998 TreeValue::File {
2999 executable: true, ..
3000 } => "an executable",
3001 TreeValue::Symlink(_) => "a symlink",
3002 TreeValue::Tree(_) => "a directory",
3003 TreeValue::GitSubmodule(_) => "a git submodule",
3004 }
3005 .to_string(),
3006 "difficult",
3007 );
3008 }
3009
3010 write!(formatter, "{formatted_path} ")?;
3011 {
3012 let mut formatter = formatter.labeled("conflict_description");
3013 let print_pair = |formatter: &mut dyn Formatter, (text, label): &(String, &str)| {
3014 write!(formatter.labeled(label), "{text}")
3015 };
3016 print_pair(
3017 *formatter,
3018 &(
3019 format!("{sides}-sided"),
3020 if sides > 2 { "difficult" } else { "normal" },
3021 ),
3022 )?;
3023 write!(formatter, " conflict")?;
3024
3025 if !seen_objects.is_empty() {
3026 write!(formatter, " including ")?;
3027 let seen_objects = seen_objects.into_iter().collect_vec();
3028 match &seen_objects[..] {
3029 [] => unreachable!(),
3030 [only] => print_pair(*formatter, only)?,
3031 [first, middle @ .., last] => {
3032 print_pair(*formatter, first)?;
3033 for pair in middle {
3034 write!(formatter, ", ")?;
3035 print_pair(*formatter, pair)?;
3036 }
3037 write!(formatter, " and ")?;
3038 print_pair(*formatter, last)?;
3039 }
3040 }
3041 }
3042 }
3043 writeln!(formatter)?;
3044 }
3045 Ok(())
3046}
3047
3048fn build_untracked_reason_message(reason: &UntrackedReason) -> Option<String> {
3050 match reason {
3051 UntrackedReason::FileTooLarge { size, max_size } => {
3052 let size_approx = HumanByteSize(*size);
3055 let max_size_approx = HumanByteSize(*max_size);
3056 Some(format!(
3057 "{size_approx} ({size} bytes); the maximum size allowed is {max_size_approx} \
3058 ({max_size} bytes)",
3059 ))
3060 }
3061 UntrackedReason::FileNotAutoTracked => None,
3065 }
3066}
3067
3068pub fn print_untracked_files(
3070 ui: &Ui,
3071 untracked_paths: &BTreeMap<RepoPathBuf, UntrackedReason>,
3072 path_converter: &RepoPathUiConverter,
3073) -> io::Result<()> {
3074 let mut untracked_paths = untracked_paths
3075 .iter()
3076 .filter_map(|(path, reason)| build_untracked_reason_message(reason).map(|m| (path, m)))
3077 .peekable();
3078
3079 if untracked_paths.peek().is_some() {
3080 writeln!(ui.warning_default(), "Refused to snapshot some files:")?;
3081 let mut formatter = ui.stderr_formatter();
3082 for (path, message) in untracked_paths {
3083 let ui_path = path_converter.format_file_path(path);
3084 writeln!(formatter, " {ui_path}: {message}")?;
3085 }
3086 }
3087
3088 Ok(())
3089}
3090
3091pub fn print_snapshot_stats(
3092 ui: &Ui,
3093 stats: &SnapshotStats,
3094 path_converter: &RepoPathUiConverter,
3095) -> io::Result<()> {
3096 print_untracked_files(ui, &stats.untracked_paths, path_converter)?;
3097
3098 let large_files_sizes = stats
3099 .untracked_paths
3100 .values()
3101 .filter_map(|reason| match reason {
3102 UntrackedReason::FileTooLarge { size, .. } => Some(size),
3103 UntrackedReason::FileNotAutoTracked => None,
3104 });
3105 if let Some(size) = large_files_sizes.max() {
3106 print_large_file_hint(ui, *size, None)?;
3107 }
3108 Ok(())
3109}
3110
3111pub fn print_large_file_hint(
3118 ui: &Ui,
3119 max_size: u64,
3120 large_files: Option<&[String]>,
3121) -> io::Result<()> {
3122 let (command, extra) = large_files
3123 .map(|files| {
3124 let files_list = files
3127 .iter()
3128 .map(|s| shlex::try_quote(s).unwrap_or(s.into()))
3129 .join(" ");
3130 let command = format!("file track {files_list}");
3131 let extra = format!(
3132 r"
3133 * Run `jj file track --include-ignored {files_list}`
3134 This will track the file(s) regardless of size."
3135 );
3136 (command, extra)
3137 })
3138 .unwrap_or(("status".to_string(), String::new()));
3139
3140 writedoc!(
3141 ui.hint_default(),
3142 r"
3143 This is to prevent large files from being added by accident. To fix this:
3144 * Add the file(s) to `.gitignore`
3145 * Run `jj config set --repo snapshot.max-new-file-size {max_size}`
3146 This will increase the maximum file size allowed for new files, in this repository only.
3147 * Run `jj --config snapshot.max-new-file-size={max_size} {command}`
3148 This will increase the maximum file size allowed for new files, for this command only.{extra}
3149 "
3150 )?;
3151 Ok(())
3152}
3153
3154pub fn print_checkout_stats(
3155 ui: &Ui,
3156 stats: &CheckoutStats,
3157 new_commit: &Commit,
3158) -> Result<(), std::io::Error> {
3159 if stats.added_files > 0 || stats.updated_files > 0 || stats.removed_files > 0 {
3160 writeln!(
3161 ui.status(),
3162 "Added {} files, modified {} files, removed {} files",
3163 stats.added_files,
3164 stats.updated_files,
3165 stats.removed_files
3166 )?;
3167 }
3168 if stats.skipped_files != 0 {
3169 writeln!(
3170 ui.warning_default(),
3171 "{} of those updates were skipped because there were conflicting changes in the \
3172 working copy.",
3173 stats.skipped_files
3174 )?;
3175 writeln!(
3176 ui.hint_default(),
3177 "Inspect the changes compared to the intended target with `jj diff --from {}`.
3178Discard the conflicting changes with `jj restore --from {}`.",
3179 short_commit_hash(new_commit.id()),
3180 short_commit_hash(new_commit.id())
3181 )?;
3182 }
3183 Ok(())
3184}
3185
3186pub fn print_unmatched_explicit_paths<'a>(
3189 ui: &Ui,
3190 workspace_command: &WorkspaceCommandHelper,
3191 expression: &FilesetExpression,
3192 trees: impl IntoIterator<Item = &'a MergedTree>,
3193) -> io::Result<()> {
3194 let mut explicit_paths = expression.explicit_paths().collect_vec();
3195 for tree in trees {
3196 explicit_paths.retain(|&path| tree.path_value(path).block_on().unwrap().is_absent());
3198 }
3199
3200 if !explicit_paths.is_empty() {
3201 let ui_paths = explicit_paths
3202 .iter()
3203 .map(|&path| workspace_command.format_file_path(path))
3204 .join(", ");
3205 writeln!(
3206 ui.warning_default(),
3207 "No matching entries for paths: {ui_paths}"
3208 )?;
3209 }
3210
3211 Ok(())
3212}
3213
3214pub async fn update_working_copy(
3215 repo: &Arc<ReadonlyRepo>,
3216 workspace: &mut Workspace,
3217 old_commit: Option<&Commit>,
3218 new_commit: &Commit,
3219) -> Result<CheckoutStats, CommandError> {
3220 let old_tree = old_commit.map(|commit| commit.tree());
3221 let stats = workspace
3224 .check_out(repo.op_id().clone(), old_tree.as_ref(), new_commit)
3225 .await
3226 .map_err(|err| {
3227 internal_error_with_message(
3228 format!("Failed to check out commit {}", new_commit.id().hex()),
3229 err,
3230 )
3231 })?;
3232 Ok(stats)
3233}
3234
3235#[cfg_attr(not(feature = "git"), expect(unused_variables))]
3237pub fn default_ignored_remote_name(store: &Store) -> Option<&'static RemoteName> {
3238 #[cfg(feature = "git")]
3239 {
3240 use jj_lib::git;
3241 if git::get_git_backend(store).is_ok() {
3242 return Some(git::REMOTE_NAME_FOR_LOCAL_GIT_REPO);
3243 }
3244 }
3245 None
3246}
3247
3248pub fn has_tracked_remote_bookmarks(repo: &dyn Repo, bookmark: &RefName) -> bool {
3251 let remote_matcher = match default_ignored_remote_name(repo.store()) {
3252 Some(remote) => StringExpression::exact(remote).negated().to_matcher(),
3253 None => StringMatcher::all(),
3254 };
3255 repo.view()
3256 .remote_bookmarks_matching(&StringMatcher::exact(bookmark), &remote_matcher)
3257 .any(|(_, remote_ref)| remote_ref.is_tracked())
3258}
3259
3260pub fn has_tracked_remote_tags(repo: &dyn Repo, tag: &RefName) -> bool {
3263 let remote_matcher = match default_ignored_remote_name(repo.store()) {
3264 Some(remote) => StringExpression::exact(remote).negated().to_matcher(),
3265 None => StringMatcher::all(),
3266 };
3267 repo.view()
3268 .remote_tags_matching(&StringMatcher::exact(tag), &remote_matcher)
3269 .any(|(_, remote_ref)| remote_ref.is_tracked())
3270}
3271
3272pub fn load_fileset_aliases(
3273 ui: &Ui,
3274 config: &StackedConfig,
3275) -> Result<FilesetAliasesMap, CommandError> {
3276 let table_name = ConfigNamePathBuf::from_iter(["fileset-aliases"]);
3277 load_aliases_map(ui, config, &table_name)
3278}
3279
3280pub fn load_revset_aliases(
3281 ui: &Ui,
3282 config: &StackedConfig,
3283) -> Result<RevsetAliasesMap, CommandError> {
3284 let table_name = ConfigNamePathBuf::from_iter(["revset-aliases"]);
3285 let aliases_map = load_aliases_map(ui, config, &table_name)?;
3286 revset_util::warn_user_redefined_builtin(ui, config, &table_name)?;
3287 Ok(aliases_map)
3288}
3289
3290pub fn load_template_aliases(
3291 ui: &Ui,
3292 config: &StackedConfig,
3293) -> Result<TemplateAliasesMap, CommandError> {
3294 let table_name = ConfigNamePathBuf::from_iter(["template-aliases"]);
3295 load_aliases_map(ui, config, &table_name)
3296}
3297
3298#[derive(Clone, Debug)]
3300pub struct LogContentFormat {
3301 width: usize,
3302 word_wrap: bool,
3303}
3304
3305impl LogContentFormat {
3306 pub fn new(ui: &Ui, settings: &UserSettings) -> Result<Self, ConfigGetError> {
3308 Ok(Self {
3309 width: ui.term_width(),
3310 word_wrap: settings.get_bool("ui.log-word-wrap")?,
3311 })
3312 }
3313
3314 #[must_use]
3316 pub fn sub_width(&self, width: usize) -> Self {
3317 Self {
3318 width: self.width.saturating_sub(width),
3319 word_wrap: self.word_wrap,
3320 }
3321 }
3322
3323 pub fn width(&self) -> usize {
3325 self.width
3326 }
3327
3328 pub async fn write<E: From<io::Error>>(
3330 &self,
3331 formatter: &mut dyn Formatter,
3332 content_fn: impl AsyncFnOnce(&mut dyn Formatter) -> Result<(), E>,
3333 ) -> Result<(), E> {
3334 if self.word_wrap {
3335 let mut recorder = FormatRecorder::new(formatter.maybe_color());
3336 content_fn(&mut recorder).await?;
3337 text_util::write_wrapped(formatter, &recorder, self.width)?;
3338 } else {
3339 content_fn(formatter).await?;
3340 }
3341 Ok(())
3342 }
3343}
3344
3345pub fn short_commit_hash(commit_id: &CommitId) -> String {
3346 format!("{commit_id:.12}")
3347}
3348
3349pub fn short_change_hash(change_id: &ChangeId) -> String {
3350 format!("{change_id:.12}")
3351}
3352
3353pub fn short_operation_hash(operation_id: &OperationId) -> String {
3354 format!("{operation_id:.12}")
3355}
3356
3357#[derive(Clone, Debug)]
3359pub enum DiffSelector {
3360 NonInteractive,
3361 Interactive(DiffEditor),
3362}
3363
3364impl DiffSelector {
3365 pub fn is_interactive(&self) -> bool {
3366 matches!(self, Self::Interactive(_))
3367 }
3368
3369 pub async fn select(
3374 &self,
3375 ui: &Ui,
3376 trees: Diff<&MergedTree>,
3377 tree_labels: Diff<String>,
3378 matcher: &dyn Matcher,
3379 format_instructions: impl FnOnce() -> String,
3380 ) -> Result<MergedTree, CommandError> {
3381 let selected_tree = restore_tree(
3382 trees.after,
3383 trees.before,
3384 tree_labels.after,
3385 tree_labels.before,
3386 matcher,
3387 )
3388 .await?;
3389 match self {
3390 Self::NonInteractive => Ok(selected_tree),
3391 Self::Interactive(editor) => {
3392 if selected_tree.tree_ids() == trees.before.tree_ids() {
3393 writeln!(ui.warning_default(), "Empty diff - won't run diff editor.")?;
3394 Ok(selected_tree)
3395 } else {
3396 Ok(editor
3400 .edit(
3401 Diff::new(trees.before, &selected_tree),
3402 matcher,
3403 format_instructions,
3404 )
3405 .await?)
3406 }
3407 }
3408 }
3409 }
3410}
3411
3412pub async fn compute_commit_location(
3417 ui: &Ui,
3418 workspace_command: &WorkspaceCommandHelper,
3419 destination: Option<&[RevisionArg]>,
3420 insert_after: Option<&[RevisionArg]>,
3421 insert_before: Option<&[RevisionArg]>,
3422 commit_type: &str,
3423) -> Result<(Vec<CommitId>, Vec<CommitId>), CommandError> {
3424 let resolve_revisions =
3425 async |revisions: Option<&[RevisionArg]>| -> Result<Option<Vec<CommitId>>, CommandError> {
3426 if let Some(revisions) = revisions {
3427 Ok(Some(
3428 workspace_command
3429 .resolve_revsets_ordered(ui, revisions)
3430 .await?
3431 .into_iter()
3432 .collect_vec(),
3433 ))
3434 } else {
3435 Ok(None)
3436 }
3437 };
3438 let destination_commit_ids = resolve_revisions(destination).await?;
3439 let after_commit_ids = resolve_revisions(insert_after).await?;
3440 let before_commit_ids = resolve_revisions(insert_before).await?;
3441
3442 let (new_parent_ids, new_child_ids) =
3443 match (destination_commit_ids, after_commit_ids, before_commit_ids) {
3444 (Some(destination_commit_ids), None, None) => (destination_commit_ids, vec![]),
3445 (None, Some(after_commit_ids), Some(before_commit_ids)) => {
3446 (after_commit_ids, before_commit_ids)
3447 }
3448 (None, Some(after_commit_ids), None) => {
3449 let new_child_ids = RevsetExpression::commits(after_commit_ids.clone())
3450 .children()
3451 .evaluate(workspace_command.repo().as_ref())?
3452 .stream()
3453 .try_collect()
3454 .await?;
3455
3456 (after_commit_ids, new_child_ids)
3457 }
3458 (None, None, Some(before_commit_ids)) => {
3459 let before_commits = try_join_all(
3460 before_commit_ids
3461 .iter()
3462 .map(|id| workspace_command.repo().store().get_commit_async(id)),
3463 )
3464 .await?;
3465 let new_parent_ids = before_commits
3468 .iter()
3469 .flat_map(|commit| commit.parent_ids())
3470 .unique()
3471 .cloned()
3472 .collect_vec();
3473
3474 (new_parent_ids, before_commit_ids)
3475 }
3476 (Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
3477 panic!("destination cannot be used with insert_after/insert_before")
3478 }
3479 (None, None, None) => {
3480 panic!("expected at least one of destination or insert_after/insert_before")
3481 }
3482 };
3483
3484 if !new_child_ids.is_empty() {
3485 workspace_command
3486 .check_rewritable(new_child_ids.iter())
3487 .await?;
3488 ensure_no_commit_loop(
3489 workspace_command.repo().as_ref(),
3490 &RevsetExpression::commits(new_child_ids.clone()),
3491 &RevsetExpression::commits(new_parent_ids.clone()),
3492 commit_type,
3493 )
3494 .await?;
3495 }
3496
3497 if new_parent_ids.is_empty() {
3498 return Err(user_error("No revisions found to use as parent"));
3499 }
3500
3501 Ok((new_parent_ids, new_child_ids))
3502}
3503
3504async fn ensure_no_commit_loop(
3507 repo: &ReadonlyRepo,
3508 children_expression: &Arc<ResolvedRevsetExpression>,
3509 parents_expression: &Arc<ResolvedRevsetExpression>,
3510 commit_type: &str,
3511) -> Result<(), CommandError> {
3512 if let Some(commit_id) = children_expression
3513 .dag_range_to(parents_expression)
3514 .evaluate(repo)?
3515 .stream()
3516 .try_next()
3517 .await?
3518 {
3519 return Err(user_error(format!(
3520 "Refusing to create a loop: commit {} would be both an ancestor and a descendant of \
3521 the {commit_type}",
3522 short_commit_hash(&commit_id),
3523 )));
3524 }
3525 Ok(())
3526}
3527
3528#[derive(clap::Parser, Clone, Debug)]
3535#[command(name = "jj")]
3536pub struct Args {
3537 #[command(flatten)]
3538 pub global_args: GlobalArgs,
3539}
3540
3541#[derive(clap::Args, Clone, Debug)]
3542#[command(next_help_heading = "Global Options")]
3543pub struct GlobalArgs {
3544 #[arg(long, short = 'R', global = true, value_hint = clap::ValueHint::DirPath)]
3549 pub repository: Option<String>,
3550
3551 #[arg(long, global = true)]
3564 pub ignore_working_copy: bool,
3565
3566 #[arg(long, global = true)]
3581 pub no_integrate_operation: bool,
3582
3583 #[arg(long, global = true)]
3592 pub ignore_immutable: bool,
3593
3594 #[arg(long, visible_alias = "at-op", global = true)]
3617 #[arg(add = ArgValueCandidates::new(complete::operations))]
3618 pub at_operation: Option<String>,
3619
3620 #[arg(long, global = true)]
3622 pub debug: bool,
3623
3624 #[command(flatten)]
3625 pub early_args: EarlyArgs,
3626}
3627
3628#[derive(clap::Args, Clone, Debug)]
3629pub struct EarlyArgs {
3630 #[arg(long, value_name = "WHEN", global = true)]
3632 pub color: Option<ColorChoice>,
3633
3634 #[arg(long, global = true, action = ArgAction::SetTrue)]
3641 pub quiet: Option<bool>,
3644
3645 #[arg(long, global = true, action = ArgAction::SetTrue)]
3647 pub no_pager: Option<bool>,
3650
3651 #[arg(long, value_name = "NAME=VALUE", global = true)]
3657 #[arg(add = ArgValueCompleter::new(complete::leaf_config_key_value))]
3658 pub config: Vec<String>,
3659
3660 #[arg(long, value_name = "PATH", global = true, value_hint = clap::ValueHint::FilePath)]
3662 pub config_file: Vec<String>,
3663}
3664
3665impl EarlyArgs {
3666 pub(crate) fn merged_config_args(&self, matches: &ArgMatches) -> Vec<(ConfigArgKind, &str)> {
3667 merge_args_with(
3668 matches,
3669 &[("config", &self.config), ("config_file", &self.config_file)],
3670 |id, value| match id {
3671 "config" => (ConfigArgKind::Item, value.as_ref()),
3672 "config_file" => (ConfigArgKind::File, value.as_ref()),
3673 _ => unreachable!("unexpected id {id:?}"),
3674 },
3675 )
3676 }
3677
3678 fn has_config_args(&self) -> bool {
3679 !self.config.is_empty() || !self.config_file.is_empty()
3680 }
3681}
3682
3683#[derive(Clone, Debug)]
3689pub struct RevisionArg(Cow<'static, str>);
3690
3691impl RevisionArg {
3692 pub const AT: Self = Self(Cow::Borrowed("@"));
3694}
3695
3696impl From<String> for RevisionArg {
3697 fn from(s: String) -> Self {
3698 Self(s.into())
3699 }
3700}
3701
3702impl AsRef<str> for RevisionArg {
3703 fn as_ref(&self) -> &str {
3704 &self.0
3705 }
3706}
3707
3708impl fmt::Display for RevisionArg {
3709 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3710 write!(f, "{}", self.0)
3711 }
3712}
3713
3714impl ValueParserFactory for RevisionArg {
3715 type Parser = MapValueParser<NonEmptyStringValueParser, fn(String) -> Self>;
3716
3717 fn value_parser() -> Self::Parser {
3718 NonEmptyStringValueParser::new().map(Self::from)
3719 }
3720}
3721
3722pub fn merge_args_with<'k, 'v, T, U>(
3730 matches: &ArgMatches,
3731 id_values: &[(&'k str, &'v [T])],
3732 mut convert: impl FnMut(&'k str, &'v T) -> U,
3733) -> Vec<U> {
3734 let mut pos_values: Vec<(usize, U)> = Vec::new();
3735 for (id, values) in id_values {
3736 pos_values.extend(itertools::zip_eq(
3737 matches.indices_of(id).into_iter().flatten(),
3738 values.iter().map(|v| convert(id, v)),
3739 ));
3740 }
3741 pos_values.sort_unstable_by_key(|&(pos, _)| pos);
3742 pos_values.into_iter().map(|(_, value)| value).collect()
3743}
3744
3745fn resolve_default_command(
3746 ui: &Ui,
3747 config: &StackedConfig,
3748 app: &Command,
3749 mut string_args: Vec<String>,
3750) -> Result<Vec<String>, CommandError> {
3751 const PRIORITY_FLAGS: &[&str] = &["--help", "-h", "--version", "-V"];
3752
3753 let has_priority_flag = string_args
3754 .iter()
3755 .any(|arg| PRIORITY_FLAGS.contains(&arg.as_str()));
3756 if has_priority_flag {
3757 return Ok(string_args);
3758 }
3759
3760 let app_clone = app
3761 .clone()
3762 .allow_external_subcommands(true)
3763 .ignore_errors(true);
3764 let matches = app_clone.try_get_matches_from(&string_args).ok();
3765
3766 if let Some(matches) = matches
3767 && matches.subcommand_name().is_none()
3768 {
3769 let default_command = if let Ok(string) = config.get::<String>("ui.default-command") {
3774 if string.contains(' ') {
3777 let elements: ConfigValue = string.split_whitespace().collect();
3778 writeln!(
3779 ui.warning_default(),
3780 "To include flags/arguments in `ui.default-command`, use an array instead of \
3781 a string: `ui.default-command = {elements}`"
3782 )?;
3783 }
3784
3785 vec![string]
3786 } else if let Some(array) = config.get::<Vec<String>>("ui.default-command").optional()? {
3787 array
3788 } else {
3789 writeln!(
3790 ui.hint_default(),
3791 "Use `jj -h` for a list of available commands."
3792 )?;
3793 writeln!(
3794 ui.hint_no_heading(),
3795 "Run `jj config set --user ui.default-command log` to disable this message."
3796 )?;
3797
3798 vec!["log".to_string()]
3799 };
3800
3801 string_args.splice(1..1, default_command);
3803 }
3804 Ok(string_args)
3805}
3806
3807fn resolve_aliases(
3808 ui: &Ui,
3809 config: &StackedConfig,
3810 app: &Command,
3811 mut string_args: Vec<String>,
3812) -> Result<Vec<String>, CommandError> {
3813 let defined_aliases: HashSet<_> = config.table_keys("aliases").collect();
3814 let mut resolved_aliases = HashSet::new();
3815 let mut real_commands = HashSet::new();
3816 for command in app.get_subcommands() {
3817 real_commands.insert(command.get_name());
3818 for alias in command.get_all_aliases() {
3819 real_commands.insert(alias);
3820 }
3821 }
3822 for alias in defined_aliases.intersection(&real_commands).sorted() {
3823 writeln!(
3824 ui.warning_default(),
3825 "Cannot define an alias that overrides the built-in command '{alias}'"
3826 )?;
3827 }
3828
3829 loop {
3830 let app_clone = app.clone().allow_external_subcommands(true);
3831 let matches = app_clone.try_get_matches_from(&string_args).ok();
3832 if let Some((command_name, submatches)) = matches.as_ref().and_then(|m| m.subcommand())
3833 && !real_commands.contains(command_name)
3834 {
3835 let alias_name = command_name.to_string();
3836 let alias_args = submatches
3837 .get_many::<OsString>("")
3838 .unwrap_or_default()
3839 .map(|arg| arg.to_str().unwrap().to_string())
3840 .collect_vec();
3841 if resolved_aliases.contains(&*alias_name) {
3842 return Err(user_error(format!(
3843 "Recursive alias definition involving `{alias_name}`"
3844 )));
3845 }
3846 if let Some(&alias_name) = defined_aliases.get(&*alias_name) {
3847 let alias_definition: Vec<String> = match config.get(["aliases", alias_name]) {
3848 Ok(definition) => definition,
3849 Err(original_err) => config
3850 .get(["aliases", alias_name, "definition"])
3851 .map_err(|_| original_err)?,
3852 };
3853 assert!(string_args.ends_with(&alias_args));
3854 string_args.truncate(string_args.len() - 1 - alias_args.len());
3855 string_args.extend(alias_definition);
3856 string_args.extend_from_slice(&alias_args);
3857 resolved_aliases.insert(alias_name);
3858 continue;
3859 } else {
3860 return Ok(string_args);
3862 }
3863 }
3864 return Ok(string_args);
3866 }
3867}
3868
3869fn parse_early_args(
3871 app: &Command,
3872 args: &[String],
3873) -> Result<(EarlyArgs, Vec<ConfigLayer>), CommandError> {
3874 let early_matches = app
3876 .clone()
3877 .disable_version_flag(true)
3878 .disable_help_flag(true)
3880 .arg(
3882 clap::Arg::new("help")
3883 .short('h')
3884 .long("help")
3885 .global(true)
3886 .action(ArgAction::Count),
3887 )
3888 .ignore_errors(true)
3889 .try_get_matches_from(args)?;
3890 let args = EarlyArgs::from_arg_matches(&early_matches).unwrap();
3891
3892 let mut config_layers = parse_config_args(&args.merged_config_args(&early_matches))?;
3893 let mut layer = ConfigLayer::empty(ConfigSource::CommandArg);
3896 if let Some(choice) = args.color {
3897 layer.set_value("ui.color", choice.to_string()).unwrap();
3898 }
3899 if args.quiet.unwrap_or_default() {
3900 layer.set_value("ui.quiet", true).unwrap();
3901 }
3902 if args.no_pager.unwrap_or_default() {
3903 layer.set_value("ui.paginate", "never").unwrap();
3904 }
3905 if !layer.is_empty() {
3906 config_layers.push(layer);
3907 }
3908 Ok((args, config_layers))
3909}
3910
3911fn handle_shell_completion(
3912 ui: &Ui,
3913 app: &Command,
3914 config: &StackedConfig,
3915 cwd: &Path,
3916) -> Result<(), CommandError> {
3917 let mut orig_args = env::args_os();
3918
3919 let mut args = vec![];
3920 args.extend(orig_args.by_ref().take(2));
3923
3924 if orig_args.len() > 0 {
3928 let complete_index: Option<usize> = env::var("_CLAP_COMPLETE_INDEX")
3929 .ok()
3930 .and_then(|s| s.parse().ok());
3931 let resolved_aliases = if let Some(index) = complete_index {
3932 let pad_len = usize::saturating_sub(index + 1, orig_args.len());
3937 let padded_args = orig_args
3938 .by_ref()
3939 .chain(std::iter::repeat_n(OsString::new(), pad_len));
3940
3941 let mut expanded_args =
3943 expand_args_for_completion(ui, app, padded_args.take(index + 1), config)?;
3944
3945 unsafe {
3949 env::set_var(
3950 "_CLAP_COMPLETE_INDEX",
3951 (expanded_args.len() - 1).to_string(),
3952 );
3953 }
3954
3955 let split_off_padding = expanded_args.split_off(expanded_args.len() - pad_len);
3958 assert!(
3959 split_off_padding.iter().all(|s| s.is_empty()),
3960 "split-off padding should only consist of empty strings but was \
3961 {split_off_padding:?}",
3962 );
3963
3964 expanded_args.extend(to_string_args(orig_args)?);
3966 expanded_args
3967 } else {
3968 expand_args_for_completion(ui, app, orig_args, config)?
3969 };
3970 args.extend(resolved_aliases.into_iter().map(OsString::from));
3971 }
3972 let ran_completion = clap_complete::CompleteEnv::with_factory(|| {
3973 let mut app = app.clone();
3974 hide_short_subcommand_aliases(&mut app);
3977 app.allow_external_subcommands(true)
3979 })
3980 .try_complete(args.iter(), Some(cwd))?;
3981 assert!(
3982 ran_completion,
3983 "This function should not be called without the COMPLETE variable set."
3984 );
3985 Ok(())
3986}
3987
3988fn hide_short_subcommand_aliases(cmd: &mut Command) {
3991 for cmd in cmd.get_subcommands_mut() {
3992 hide_short_subcommand_aliases(cmd);
3993 }
3994 let (short_aliases, new_visible_aliases) = cmd
3995 .get_visible_aliases()
3996 .map(|name| name.to_owned())
3997 .partition::<Vec<_>, _>(|name| cmd.get_name().starts_with(name));
3998 if short_aliases.is_empty() {
3999 return;
4000 }
4001 *cmd = mem::take(cmd)
4002 .visible_alias(None)
4004 .visible_aliases(new_visible_aliases)
4005 .aliases(short_aliases);
4007}
4008
4009pub fn expand_args(
4010 ui: &Ui,
4011 app: &Command,
4012 args_os: impl IntoIterator<Item = OsString>,
4013 config: &StackedConfig,
4014) -> Result<Vec<String>, CommandError> {
4015 let string_args = to_string_args(args_os)?;
4016 let string_args = resolve_default_command(ui, config, app, string_args)?;
4017 resolve_aliases(ui, config, app, string_args)
4018}
4019
4020fn expand_args_for_completion(
4021 ui: &Ui,
4022 app: &Command,
4023 args_os: impl IntoIterator<Item = OsString>,
4024 config: &StackedConfig,
4025) -> Result<Vec<String>, CommandError> {
4026 let string_args = to_string_args(args_os)?;
4027
4028 let mut string_args = resolve_default_command(ui, config, app, string_args)?;
4032
4033 let cursor_arg = string_args.pop();
4036 let mut resolved_args = resolve_aliases(ui, config, app, string_args)?;
4037 resolved_args.extend(cursor_arg);
4038 Ok(resolved_args)
4039}
4040
4041fn to_string_args(
4042 args_os: impl IntoIterator<Item = OsString>,
4043) -> Result<Vec<String>, CommandError> {
4044 args_os
4045 .into_iter()
4046 .map(|arg_os| {
4047 arg_os
4048 .into_string()
4049 .map_err(|_| cli_error("Non-UTF-8 argument"))
4050 })
4051 .collect()
4052}
4053
4054fn parse_args(app: &Command, string_args: &[String]) -> Result<(ArgMatches, Args), clap::Error> {
4055 let matches = app
4056 .clone()
4057 .arg_required_else_help(true)
4058 .subcommand_required(true)
4059 .try_get_matches_from(string_args)?;
4060 let args = Args::from_arg_matches(&matches).unwrap();
4061 Ok((matches, args))
4062}
4063
4064fn command_name(mut matches: &ArgMatches) -> String {
4065 let mut command = String::new();
4066 while let Some((subcommand, new_matches)) = matches.subcommand() {
4067 if !command.is_empty() {
4068 command.push(' ');
4069 }
4070 command.push_str(subcommand);
4071 matches = new_matches;
4072 }
4073 command
4074}
4075
4076pub fn format_template<C: Clone>(ui: &Ui, arg: &C, template: &TemplateRenderer<C>) -> String {
4077 let mut output = vec![];
4078 template
4079 .format(arg, ui.new_formatter(&mut output).as_mut())
4080 .expect("write() to vec backed formatter should never fail");
4081 output.into_string_lossy()
4083}
4084
4085type BoxedCliDispatchFuture<'a> = Pin<Box<dyn Future<Output = Result<(), CommandError>> + 'a>>;
4087pub type BoxedAsyncCliDispatch<'a> = Box<dyn AsyncCliDispatch + 'a>;
4088type BoxedAsyncCliDispatchHook<'a> = Box<dyn AsyncCliDispatchHook + 'a>;
4089
4090pub trait AsyncCliDispatch {
4092 fn call<'a>(
4093 self: Box<Self>,
4094 ui: &'a mut Ui,
4095 command_helper: &'a CommandHelper,
4096 ) -> BoxedCliDispatchFuture<'a>
4097 where
4098 Self: 'a;
4099}
4100
4101trait AsyncCliDispatchHook {
4103 fn call<'a>(
4104 self: Box<Self>,
4105 ui: &'a mut Ui,
4106 command_helper: &'a CommandHelper,
4107 old_dispatch: BoxedAsyncCliDispatch<'a>,
4108 ) -> BoxedCliDispatchFuture<'a>
4109 where
4110 Self: 'a;
4111}
4112
4113struct AsyncCliDispatchFn<F>(F);
4115
4116impl<F> AsyncCliDispatch for AsyncCliDispatchFn<F>
4117where
4118 F: AsyncFnOnce(&mut Ui, &CommandHelper) -> Result<(), CommandError>,
4119{
4120 fn call<'a>(
4121 self: Box<Self>,
4122 ui: &'a mut Ui,
4123 command_helper: &'a CommandHelper,
4124 ) -> BoxedCliDispatchFuture<'a>
4125 where
4126 Self: 'a,
4127 {
4128 Box::pin((self.0)(ui, command_helper))
4129 }
4130}
4131
4132struct AsyncCliDispatchHookFn<F>(F);
4134
4135impl<F> AsyncCliDispatchHook for AsyncCliDispatchHookFn<F>
4136where
4137 F: AsyncFnOnce(&mut Ui, &CommandHelper, BoxedAsyncCliDispatch<'_>) -> Result<(), CommandError>,
4138{
4139 fn call<'a>(
4140 self: Box<Self>,
4141 ui: &'a mut Ui,
4142 command_helper: &'a CommandHelper,
4143 old_dispatch: BoxedAsyncCliDispatch<'a>,
4144 ) -> BoxedCliDispatchFuture<'a>
4145 where
4146 Self: 'a,
4147 {
4148 Box::pin((self.0)(ui, command_helper, old_dispatch))
4149 }
4150}
4151
4152#[must_use]
4154pub struct CliRunner<'a> {
4155 tracing_subscription: TracingSubscription,
4156 app: Command,
4157 config_layers: Vec<ConfigLayer>,
4158 config_migrations: Vec<ConfigMigrationRule>,
4159 store_factories: StoreFactories,
4160 working_copy_factories: WorkingCopyFactories,
4161 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
4162 revset_extensions: RevsetExtensions,
4163 commit_template_extensions: Vec<Arc<dyn CommitTemplateLanguageExtension>>,
4164 operation_template_extensions: Vec<Arc<dyn OperationTemplateLanguageExtension>>,
4165 dispatch: BoxedAsyncCliDispatch<'a>,
4166 dispatch_hooks: Vec<BoxedAsyncCliDispatchHook<'a>>,
4167 process_global_args_fns: Vec<ProcessGlobalArgsFn<'a>>,
4168}
4169
4170type ProcessGlobalArgsFn<'a> =
4171 Box<dyn FnOnce(&mut Ui, &ArgMatches) -> Result<(), CommandError> + 'a>;
4172
4173impl<'a> CliRunner<'a> {
4174 pub fn init() -> Self {
4177 let tracing_subscription = TracingSubscription::init();
4178 crate::cleanup_guard::init();
4179 Self {
4180 tracing_subscription,
4181 app: crate::commands::default_app(),
4182 config_layers: crate::config::default_config_layers(),
4183 config_migrations: crate::config::default_config_migrations(),
4184 store_factories: StoreFactories::default(),
4185 working_copy_factories: default_working_copy_factories(),
4186 workspace_loader_factory: Box::new(DefaultWorkspaceLoaderFactory),
4187 revset_extensions: Default::default(),
4188 commit_template_extensions: vec![],
4189 operation_template_extensions: vec![],
4190 dispatch: Box::new(AsyncCliDispatchFn(crate::commands::run_command)),
4191 dispatch_hooks: vec![],
4192 process_global_args_fns: vec![],
4193 }
4194 }
4195
4196 pub fn name(mut self, name: &str) -> Self {
4198 self.app = self.app.name(name.to_string());
4199 self
4200 }
4201
4202 pub fn about(mut self, about: &str) -> Self {
4204 self.app = self.app.about(about.to_string());
4205 self
4206 }
4207
4208 pub fn version(mut self, version: &str) -> Self {
4210 self.app = self.app.version(version.to_string());
4211 self
4212 }
4213
4214 pub fn add_extra_config(mut self, layer: ConfigLayer) -> Self {
4219 assert_eq!(layer.source, ConfigSource::Default);
4220 self.config_layers.push(layer);
4221 self
4222 }
4223
4224 pub fn add_extra_config_migration(mut self, rule: ConfigMigrationRule) -> Self {
4226 self.config_migrations.push(rule);
4227 self
4228 }
4229
4230 pub fn add_store_factories(mut self, store_factories: StoreFactories) -> Self {
4232 self.store_factories.merge(store_factories);
4233 self
4234 }
4235
4236 pub fn add_working_copy_factories(
4238 mut self,
4239 working_copy_factories: WorkingCopyFactories,
4240 ) -> Self {
4241 merge_factories_map(&mut self.working_copy_factories, working_copy_factories);
4242 self
4243 }
4244
4245 pub fn set_workspace_loader_factory(
4246 mut self,
4247 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
4248 ) -> Self {
4249 self.workspace_loader_factory = workspace_loader_factory;
4250 self
4251 }
4252
4253 pub fn add_symbol_resolver_extension(
4254 mut self,
4255 symbol_resolver: Box<dyn SymbolResolverExtension>,
4256 ) -> Self {
4257 self.revset_extensions.add_symbol_resolver(symbol_resolver);
4258 self
4259 }
4260
4261 pub fn add_revset_function_extension(
4262 mut self,
4263 name: &'static str,
4264 func: RevsetFunction,
4265 ) -> Self {
4266 self.revset_extensions.add_custom_function(name, func);
4267 self
4268 }
4269
4270 pub fn add_commit_template_extension(
4271 mut self,
4272 commit_template_extension: Box<dyn CommitTemplateLanguageExtension>,
4273 ) -> Self {
4274 self.commit_template_extensions
4275 .push(commit_template_extension.into());
4276 self
4277 }
4278
4279 pub fn add_operation_template_extension(
4280 mut self,
4281 operation_template_extension: Box<dyn OperationTemplateLanguageExtension>,
4282 ) -> Self {
4283 self.operation_template_extensions
4284 .push(operation_template_extension.into());
4285 self
4286 }
4287
4288 pub fn add_dispatch_hook<F>(mut self, dispatch_hook_fn: F) -> Self
4292 where
4293 F: AsyncFnOnce(&mut Ui, &CommandHelper, BoxedAsyncCliDispatch) -> Result<(), CommandError>
4294 + 'a,
4295 {
4296 self.dispatch_hooks
4297 .push(Box::new(AsyncCliDispatchHookFn(dispatch_hook_fn)));
4298 self
4299 }
4300
4301 pub fn add_subcommand<C, F>(mut self, custom_dispatch_fn: F) -> Self
4303 where
4304 C: clap::Subcommand,
4305 F: AsyncFnOnce(&mut Ui, &CommandHelper, C) -> Result<(), CommandError> + 'a,
4306 {
4307 let old_dispatch = self.dispatch;
4308 let new_dispatch_fn =
4309 async move |ui: &mut Ui, command_helper: &CommandHelper| match C::from_arg_matches(
4310 command_helper.matches(),
4311 ) {
4312 Ok(command) => custom_dispatch_fn(ui, command_helper, command).await,
4313 Err(_) => old_dispatch.call(ui, command_helper).await,
4314 };
4315 self.app = C::augment_subcommands(self.app);
4316 self.dispatch = Box::new(AsyncCliDispatchFn(new_dispatch_fn));
4317 self
4318 }
4319
4320 pub fn add_global_args<A, F>(mut self, process_before: F) -> Self
4322 where
4323 A: clap::Args,
4324 F: FnOnce(&mut Ui, A) -> Result<(), CommandError> + 'a,
4325 {
4326 let process_global_args_fn = move |ui: &mut Ui, matches: &ArgMatches| {
4327 let custom_args = A::from_arg_matches(matches).unwrap();
4328 process_before(ui, custom_args)
4329 };
4330 self.app = A::augment_args(self.app);
4331 self.process_global_args_fns
4332 .push(Box::new(process_global_args_fn));
4333 self
4334 }
4335
4336 #[instrument(skip_all)]
4337 async fn run_internal(
4338 self,
4339 ui: &mut Ui,
4340 mut raw_config: RawConfig,
4341 ) -> Result<(), CommandError> {
4342 let cwd = env::current_dir()
4345 .and_then(dunce::canonicalize)
4346 .map_err(|_| {
4347 user_error("Could not determine current directory").hinted(
4348 "Did you update to a commit where the directory doesn't exist or can't be \
4349 accessed?",
4350 )
4351 })?;
4352 let mut config_env = ConfigEnv::from_environment();
4353 let mut last_config_migration_descriptions = Vec::new();
4354 let mut migrate_config = |config: &mut StackedConfig| -> Result<(), CommandError> {
4355 last_config_migration_descriptions =
4356 jj_lib::config::migrate(config, &self.config_migrations)?;
4357 Ok(())
4358 };
4359
4360 let maybe_cwd_workspace_loader = self
4366 .workspace_loader_factory
4367 .create(find_workspace_dir(&cwd))
4368 .map_err(|err| map_workspace_load_error(err, Some(".")));
4369 config_env.reload_system_config(&mut raw_config)?;
4370 config_env.reload_user_config(&mut raw_config)?;
4371 if let Ok(loader) = &maybe_cwd_workspace_loader {
4372 config_env.reset_repo_path(loader.repo_path());
4373 config_env.reload_repo_config(ui, &mut raw_config)?;
4374 config_env.reset_workspace_path(loader.workspace_root());
4375 config_env.reload_workspace_config(ui, &mut raw_config)?;
4376 }
4377 let mut config = config_env.resolve_config(&raw_config)?;
4378 migrate_config(&mut config)?;
4379 ui.reset(&config)?;
4380
4381 if env::var_os("COMPLETE").is_some_and(|v| !v.is_empty() && v != "0") {
4382 return handle_shell_completion(&Ui::null(), &self.app, &config, &cwd);
4383 }
4384
4385 let string_args = expand_args(ui, &self.app, env::args_os(), &config)?;
4386 let (args, config_layers) = parse_early_args(&self.app, &string_args)?;
4387 if !config_layers.is_empty() {
4388 raw_config.as_mut().extend_layers(config_layers);
4389 config = config_env.resolve_config(&raw_config)?;
4390 migrate_config(&mut config)?;
4391 ui.reset(&config)?;
4392 }
4393
4394 if args.has_config_args() {
4395 warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
4396 }
4397
4398 let (matches, args) = parse_args(&self.app, &string_args)
4399 .map_err(|err| map_clap_cli_error(err, ui, &config))?;
4400 if args.global_args.debug {
4401 self.tracing_subscription.enable_debug_logging()?;
4403 }
4404 for process_global_args_fn in self.process_global_args_fns {
4405 process_global_args_fn(ui, &matches)?;
4406 }
4407 config_env.set_command_name(command_name(&matches));
4408
4409 let maybe_workspace_loader = if let Some(path) = &args.global_args.repository {
4410 let abs_path = cwd.join(path);
4412 let abs_path = dunce::canonicalize(&abs_path).unwrap_or(abs_path);
4413 let loader = self
4415 .workspace_loader_factory
4416 .create(&abs_path)
4417 .map_err(|err| map_workspace_load_error(err, Some(path)))?;
4418 config_env.reset_repo_path(loader.repo_path());
4419 config_env.reload_repo_config(ui, &mut raw_config)?;
4420 config_env.reset_workspace_path(loader.workspace_root());
4421 config_env.reload_workspace_config(ui, &mut raw_config)?;
4422 Ok(loader)
4423 } else {
4424 maybe_cwd_workspace_loader
4425 };
4426
4427 config = config_env.resolve_config(&raw_config)?;
4429 migrate_config(&mut config)?;
4430 ui.reset(&config)?;
4431
4432 for (source, desc) in &last_config_migration_descriptions {
4434 let source_str = match source {
4435 ConfigSource::Default => "default-provided",
4436 ConfigSource::System => "system-level",
4437 ConfigSource::EnvBase | ConfigSource::EnvOverrides => "environment-provided",
4438 ConfigSource::User => "user-level",
4439 ConfigSource::Repo => "repo-level",
4440 ConfigSource::Workspace => "workspace-level",
4441 ConfigSource::CommandArg => "CLI-provided",
4442 };
4443 writeln!(
4444 ui.warning_default(),
4445 "Deprecated {source_str} config: {desc}"
4446 )?;
4447 }
4448
4449 if args.global_args.repository.is_some() {
4450 warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
4451 }
4452
4453 let settings = UserSettings::from_config(config)?;
4454 let command_helper_data = CommandHelperData {
4455 app: self.app,
4456 cwd,
4457 string_args,
4458 matches,
4459 global_args: args.global_args,
4460 config_env,
4461 config_migrations: self.config_migrations,
4462 raw_config,
4463 settings,
4464 revset_extensions: self.revset_extensions.into(),
4465 commit_template_extensions: self.commit_template_extensions,
4466 operation_template_extensions: self.operation_template_extensions,
4467 maybe_workspace_loader,
4468 store_factories: self.store_factories,
4469 working_copy_factories: self.working_copy_factories,
4470 workspace_loader_factory: self.workspace_loader_factory,
4471 };
4472 let command_helper = CommandHelper {
4473 data: Rc::new(command_helper_data),
4474 };
4475 let dispatch =
4476 self.dispatch_hooks
4477 .into_iter()
4478 .fold(self.dispatch, |old_dispatch, dispatch_hook| {
4479 let f = async move |ui: &mut Ui, command_helper: &CommandHelper| {
4480 dispatch_hook.call(ui, command_helper, old_dispatch).await
4481 };
4482 Box::new(AsyncCliDispatchFn(f))
4483 });
4484 dispatch.call(ui, &command_helper).await
4485 }
4486
4487 #[must_use]
4488 #[instrument(skip(self))]
4489 pub fn run(mut self) -> u8 {
4490 crossterm::style::force_color_output(true);
4492 let config = config_from_environment(self.config_layers.drain(..));
4493 let mut ui = Ui::with_config(config.as_ref())
4496 .expect("default config should be valid, env vars are stringly typed");
4497 let result = self.run_internal(&mut ui, config).block_on();
4498 let exit_code = handle_command_result(&mut ui, result);
4499 ui.finalize_pager();
4500 exit_code
4501 }
4502}
4503
4504fn map_clap_cli_error(err: clap::Error, ui: &Ui, config: &StackedConfig) -> CommandError {
4505 if let Some(ContextValue::String(cmd)) = err.get(ContextKind::InvalidSubcommand) {
4506 let remove_useless_error_context = |mut err: clap::Error| {
4507 err.remove(ContextKind::SuggestedSubcommand);
4510 err.remove(ContextKind::Suggested); err.remove(ContextKind::Usage); err
4513 };
4514 match cmd.as_str() {
4515 "clone" | "init" => {
4518 let cmd = cmd.clone();
4519 return CommandError::from(remove_useless_error_context(err))
4520 .hinted(format!(
4521 "You probably want `jj git {cmd}`. See also `jj help git`."
4522 ))
4523 .hinted(format!(
4524 r#"You can configure `aliases.{cmd} = ["git", "{cmd}"]` if you want `jj {cmd}` to work and always use the Git backend."#
4525 ));
4526 }
4527 "amend" => {
4528 return CommandError::from(remove_useless_error_context(err))
4529 .hinted(
4530 r#"You probably want `jj squash`. You can configure `aliases.amend = ["squash"]` if you want `jj amend` to work."#);
4531 }
4532 _ => {}
4533 }
4534 }
4535 if let (Some(ContextValue::String(arg)), Some(ContextValue::String(value))) = (
4536 err.get(ContextKind::InvalidArg),
4537 err.get(ContextKind::InvalidValue),
4538 ) && arg.as_str() == "--template <TEMPLATE>"
4539 && value.is_empty()
4540 {
4541 if let Ok(template_aliases) = load_template_aliases(ui, config) {
4543 return CommandError::from(err).hinted(format_template_aliases_hint(&template_aliases));
4544 }
4545 }
4546 CommandError::from(err)
4547}
4548
4549fn format_template_aliases_hint(template_aliases: &TemplateAliasesMap) -> String {
4550 let mut hint = String::from("The following template aliases are defined:\n");
4551 hint.push_str(
4552 &template_aliases
4553 .symbol_names()
4554 .sorted_unstable()
4555 .map(|name| format!("- {name}"))
4556 .join("\n"),
4557 );
4558 hint
4559}
4560
4561fn warn_if_args_mismatch(
4563 ui: &Ui,
4564 app: &Command,
4565 config: &StackedConfig,
4566 expected_args: &[String],
4567) -> Result<(), CommandError> {
4568 let new_string_args = expand_args(ui, app, env::args_os(), config).ok();
4569 if new_string_args.as_deref() != Some(expected_args) {
4570 writeln!(
4571 ui.warning_default(),
4572 "Command aliases cannot be loaded from -R/--repository path or --config/--config-file \
4573 arguments."
4574 )?;
4575 }
4576 Ok(())
4577}
4578
4579#[cfg(test)]
4580mod tests {
4581 use clap::CommandFactory as _;
4582
4583 use super::*;
4584
4585 #[derive(clap::Parser, Clone, Debug)]
4586 pub struct TestArgs {
4587 #[arg(long)]
4588 pub foo: Vec<u32>,
4589 #[arg(long)]
4590 pub bar: Vec<u32>,
4591 #[arg(long)]
4592 pub baz: bool,
4593 }
4594
4595 #[test]
4596 fn test_merge_args_with() {
4597 let command = TestArgs::command();
4598 let parse = |args: &[&str]| -> Vec<(&'static str, u32)> {
4599 let matches = command.clone().try_get_matches_from(args).unwrap();
4600 let args = TestArgs::from_arg_matches(&matches).unwrap();
4601 merge_args_with(
4602 &matches,
4603 &[("foo", &args.foo), ("bar", &args.bar)],
4604 |id, value| (id, *value),
4605 )
4606 };
4607
4608 assert_eq!(parse(&["jj"]), vec![]);
4609 assert_eq!(parse(&["jj", "--foo=1"]), vec![("foo", 1)]);
4610 assert_eq!(
4611 parse(&["jj", "--foo=1", "--bar=2"]),
4612 vec![("foo", 1), ("bar", 2)]
4613 );
4614 assert_eq!(
4615 parse(&["jj", "--foo=1", "--baz", "--bar=2", "--foo", "3"]),
4616 vec![("foo", 1), ("bar", 2), ("foo", 3)]
4617 );
4618 }
4619}