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