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