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