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