1use std::borrow::Cow;
16use std::cell::OnceCell;
17use std::collections::BTreeMap;
18use std::collections::HashMap;
19use std::collections::HashSet;
20use std::env;
21use std::ffi::OsString;
22use std::fmt;
23use std::fmt::Debug;
24use std::io;
25use std::io::Write as _;
26use std::mem;
27use std::path::Path;
28use std::path::PathBuf;
29use std::rc::Rc;
30use std::str;
31use std::str::FromStr;
32use std::sync::Arc;
33use std::time::SystemTime;
34
35use bstr::ByteVec as _;
36use chrono::TimeZone as _;
37use clap::builder::MapValueParser;
38use clap::builder::NonEmptyStringValueParser;
39use clap::builder::TypedValueParser as _;
40use clap::builder::ValueParserFactory;
41use clap::error::ContextKind;
42use clap::error::ContextValue;
43use clap::ArgAction;
44use clap::ArgMatches;
45use clap::Command;
46use clap::FromArgMatches as _;
47use clap_complete::ArgValueCandidates;
48use clap_complete::ArgValueCompleter;
49use indexmap::IndexMap;
50use indexmap::IndexSet;
51use indoc::indoc;
52use indoc::writedoc;
53use itertools::Itertools as _;
54use jj_lib::backend::BackendResult;
55use jj_lib::backend::ChangeId;
56use jj_lib::backend::CommitId;
57use jj_lib::backend::MergedTreeId;
58use jj_lib::backend::TreeValue;
59use jj_lib::commit::Commit;
60use jj_lib::config::ConfigGetError;
61use jj_lib::config::ConfigGetResultExt as _;
62use jj_lib::config::ConfigLayer;
63use jj_lib::config::ConfigMigrationRule;
64use jj_lib::config::ConfigNamePathBuf;
65use jj_lib::config::ConfigSource;
66use jj_lib::config::StackedConfig;
67use jj_lib::conflicts::ConflictMarkerStyle;
68use jj_lib::fileset;
69use jj_lib::fileset::FilesetDiagnostics;
70use jj_lib::fileset::FilesetExpression;
71use jj_lib::gitignore::GitIgnoreError;
72use jj_lib::gitignore::GitIgnoreFile;
73use jj_lib::id_prefix::IdPrefixContext;
74use jj_lib::matchers::Matcher;
75use jj_lib::merge::MergedTreeValue;
76use jj_lib::merged_tree::MergedTree;
77use jj_lib::object_id::ObjectId as _;
78use jj_lib::op_heads_store;
79use jj_lib::op_store::OpStoreError;
80use jj_lib::op_store::OperationId;
81use jj_lib::op_store::RefTarget;
82use jj_lib::op_walk;
83use jj_lib::op_walk::OpsetEvaluationError;
84use jj_lib::operation::Operation;
85use jj_lib::ref_name::RefName;
86use jj_lib::ref_name::RefNameBuf;
87use jj_lib::ref_name::WorkspaceName;
88use jj_lib::ref_name::WorkspaceNameBuf;
89use jj_lib::repo::merge_factories_map;
90use jj_lib::repo::CheckOutCommitError;
91use jj_lib::repo::EditCommitError;
92use jj_lib::repo::MutableRepo;
93use jj_lib::repo::ReadonlyRepo;
94use jj_lib::repo::Repo;
95use jj_lib::repo::RepoLoader;
96use jj_lib::repo::StoreFactories;
97use jj_lib::repo::StoreLoadError;
98use jj_lib::repo_path::RepoPath;
99use jj_lib::repo_path::RepoPathBuf;
100use jj_lib::repo_path::RepoPathUiConverter;
101use jj_lib::repo_path::UiPathParseError;
102use jj_lib::revset;
103use jj_lib::revset::ResolvedRevsetExpression;
104use jj_lib::revset::RevsetAliasesMap;
105use jj_lib::revset::RevsetDiagnostics;
106use jj_lib::revset::RevsetExpression;
107use jj_lib::revset::RevsetExtensions;
108use jj_lib::revset::RevsetFilterPredicate;
109use jj_lib::revset::RevsetFunction;
110use jj_lib::revset::RevsetIteratorExt as _;
111use jj_lib::revset::RevsetModifier;
112use jj_lib::revset::RevsetParseContext;
113use jj_lib::revset::RevsetWorkspaceContext;
114use jj_lib::revset::SymbolResolverExtension;
115use jj_lib::revset::UserRevsetExpression;
116use jj_lib::rewrite::restore_tree;
117use jj_lib::settings::HumanByteSize;
118use jj_lib::settings::UserSettings;
119use jj_lib::str_util::StringPattern;
120use jj_lib::transaction::Transaction;
121use jj_lib::view::View;
122use jj_lib::working_copy;
123use jj_lib::working_copy::CheckoutOptions;
124use jj_lib::working_copy::CheckoutStats;
125use jj_lib::working_copy::SnapshotOptions;
126use jj_lib::working_copy::SnapshotStats;
127use jj_lib::working_copy::UntrackedReason;
128use jj_lib::working_copy::WorkingCopy;
129use jj_lib::working_copy::WorkingCopyFactory;
130use jj_lib::working_copy::WorkingCopyFreshness;
131use jj_lib::workspace::default_working_copy_factories;
132use jj_lib::workspace::get_working_copy_factory;
133use jj_lib::workspace::DefaultWorkspaceLoaderFactory;
134use jj_lib::workspace::LockedWorkspace;
135use jj_lib::workspace::WorkingCopyFactories;
136use jj_lib::workspace::Workspace;
137use jj_lib::workspace::WorkspaceLoadError;
138use jj_lib::workspace::WorkspaceLoader;
139use jj_lib::workspace::WorkspaceLoaderFactory;
140use tracing::instrument;
141use tracing_chrome::ChromeLayerBuilder;
142use tracing_subscriber::prelude::*;
143
144use crate::command_error::cli_error;
145use crate::command_error::config_error_with_message;
146use crate::command_error::handle_command_result;
147use crate::command_error::internal_error;
148use crate::command_error::internal_error_with_message;
149use crate::command_error::print_parse_diagnostics;
150use crate::command_error::user_error;
151use crate::command_error::user_error_with_hint;
152use crate::command_error::CommandError;
153use crate::commit_templater::CommitTemplateLanguage;
154use crate::commit_templater::CommitTemplateLanguageExtension;
155use crate::complete;
156use crate::config::config_from_environment;
157use crate::config::parse_config_args;
158use crate::config::ConfigArgKind;
159use crate::config::ConfigEnv;
160use crate::config::RawConfig;
161use crate::description_util::TextEditor;
162use crate::diff_util;
163use crate::diff_util::DiffFormat;
164use crate::diff_util::DiffFormatArgs;
165use crate::diff_util::DiffRenderer;
166use crate::formatter::FormatRecorder;
167use crate::formatter::Formatter;
168use crate::formatter::PlainTextFormatter;
169use crate::merge_tools::DiffEditor;
170use crate::merge_tools::MergeEditor;
171use crate::merge_tools::MergeToolConfigError;
172use crate::operation_templater::OperationTemplateLanguage;
173use crate::operation_templater::OperationTemplateLanguageExtension;
174use crate::revset_util;
175use crate::revset_util::RevsetExpressionEvaluator;
176use crate::template_builder;
177use crate::template_builder::TemplateLanguage;
178use crate::template_parser::TemplateAliasesMap;
179use crate::template_parser::TemplateDiagnostics;
180use crate::templater::TemplateRenderer;
181use crate::templater::WrapTemplateProperty;
182use crate::text_util;
183use crate::ui::ColorChoice;
184use crate::ui::Ui;
185
186const SHORT_CHANGE_ID_TEMPLATE_TEXT: &str = "format_short_change_id(self.change_id())";
187
188#[derive(Clone)]
189struct ChromeTracingFlushGuard {
190 _inner: Option<Rc<tracing_chrome::FlushGuard>>,
191}
192
193impl Debug for ChromeTracingFlushGuard {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 let Self { _inner } = self;
196 f.debug_struct("ChromeTracingFlushGuard")
197 .finish_non_exhaustive()
198 }
199}
200
201#[derive(Clone, Debug)]
203pub struct TracingSubscription {
204 reload_log_filter: tracing_subscriber::reload::Handle<
205 tracing_subscriber::EnvFilter,
206 tracing_subscriber::Registry,
207 >,
208 _chrome_tracing_flush_guard: ChromeTracingFlushGuard,
209}
210
211impl TracingSubscription {
212 const ENV_VAR_NAME: &'static str = "JJ_LOG";
213
214 pub fn init() -> Self {
217 let filter = tracing_subscriber::EnvFilter::builder()
218 .with_default_directive(tracing::metadata::LevelFilter::ERROR.into())
219 .with_env_var(Self::ENV_VAR_NAME)
220 .from_env_lossy();
221 let (filter, reload_log_filter) = tracing_subscriber::reload::Layer::new(filter);
222
223 let (chrome_tracing_layer, chrome_tracing_flush_guard) = match std::env::var("JJ_TRACE") {
224 Ok(filename) => {
225 let filename = if filename.is_empty() {
226 format!(
227 "jj-trace-{}.json",
228 SystemTime::now()
229 .duration_since(SystemTime::UNIX_EPOCH)
230 .unwrap()
231 .as_secs(),
232 )
233 } else {
234 filename
235 };
236 let include_args = std::env::var("JJ_TRACE_INCLUDE_ARGS").is_ok();
237 let (layer, guard) = ChromeLayerBuilder::new()
238 .file(filename)
239 .include_args(include_args)
240 .build();
241 (
242 Some(layer),
243 ChromeTracingFlushGuard {
244 _inner: Some(Rc::new(guard)),
245 },
246 )
247 }
248 Err(_) => (None, ChromeTracingFlushGuard { _inner: None }),
249 };
250
251 tracing_subscriber::registry()
252 .with(
253 tracing_subscriber::fmt::Layer::default()
254 .with_writer(std::io::stderr)
255 .with_filter(filter),
256 )
257 .with(chrome_tracing_layer)
258 .init();
259 TracingSubscription {
260 reload_log_filter,
261 _chrome_tracing_flush_guard: chrome_tracing_flush_guard,
262 }
263 }
264
265 pub fn enable_debug_logging(&self) -> Result<(), CommandError> {
266 self.reload_log_filter
267 .modify(|filter| {
268 *filter = tracing_subscriber::EnvFilter::builder()
272 .with_default_directive(tracing::metadata::LevelFilter::INFO.into())
273 .with_env_var(Self::ENV_VAR_NAME)
274 .from_env_lossy()
275 .add_directive("jj_lib=debug".parse().unwrap())
276 .add_directive("jj_cli=debug".parse().unwrap());
277 })
278 .map_err(|err| internal_error_with_message("failed to enable debug logging", err))?;
279 tracing::info!("debug logging enabled");
280 Ok(())
281 }
282}
283
284#[derive(Clone)]
285pub struct CommandHelper {
286 data: Rc<CommandHelperData>,
287}
288
289struct CommandHelperData {
290 app: Command,
291 cwd: PathBuf,
292 string_args: Vec<String>,
293 matches: ArgMatches,
294 global_args: GlobalArgs,
295 config_env: ConfigEnv,
296 config_migrations: Vec<ConfigMigrationRule>,
297 raw_config: RawConfig,
298 settings: UserSettings,
299 revset_extensions: Arc<RevsetExtensions>,
300 commit_template_extensions: Vec<Arc<dyn CommitTemplateLanguageExtension>>,
301 operation_template_extensions: Vec<Arc<dyn OperationTemplateLanguageExtension>>,
302 maybe_workspace_loader: Result<Box<dyn WorkspaceLoader>, CommandError>,
303 store_factories: StoreFactories,
304 working_copy_factories: WorkingCopyFactories,
305 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
306}
307
308impl CommandHelper {
309 pub fn app(&self) -> &Command {
310 &self.data.app
311 }
312
313 pub fn cwd(&self) -> &Path {
318 &self.data.cwd
319 }
320
321 pub fn string_args(&self) -> &Vec<String> {
322 &self.data.string_args
323 }
324
325 pub fn matches(&self) -> &ArgMatches {
326 &self.data.matches
327 }
328
329 pub fn global_args(&self) -> &GlobalArgs {
330 &self.data.global_args
331 }
332
333 pub fn config_env(&self) -> &ConfigEnv {
334 &self.data.config_env
335 }
336
337 pub fn raw_config(&self) -> &RawConfig {
342 &self.data.raw_config
343 }
344
345 pub fn settings(&self) -> &UserSettings {
351 &self.data.settings
352 }
353
354 pub fn settings_for_new_workspace(
356 &self,
357 workspace_root: &Path,
358 ) -> Result<UserSettings, CommandError> {
359 let mut config_env = self.data.config_env.clone();
360 let mut raw_config = self.data.raw_config.clone();
361 let repo_path = workspace_root.join(".jj").join("repo");
362 config_env.reset_repo_path(&repo_path);
363 config_env.reload_repo_config(&mut raw_config)?;
364 let mut config = config_env.resolve_config(&raw_config)?;
365 jj_lib::config::migrate(&mut config, &self.data.config_migrations)?;
367 Ok(self.data.settings.with_new_config(config)?)
368 }
369
370 pub fn text_editor(&self) -> Result<TextEditor, ConfigGetError> {
372 TextEditor::from_settings(self.settings())
373 }
374
375 pub fn revset_extensions(&self) -> &Arc<RevsetExtensions> {
376 &self.data.revset_extensions
377 }
378
379 pub fn parse_template<'a, C, L>(
385 &self,
386 ui: &Ui,
387 language: &L,
388 template_text: &str,
389 ) -> Result<TemplateRenderer<'a, C>, CommandError>
390 where
391 C: Clone + 'a,
392 L: TemplateLanguage<'a> + ?Sized,
393 L::Property: WrapTemplateProperty<'a, C>,
394 {
395 let mut diagnostics = TemplateDiagnostics::new();
396 let aliases = load_template_aliases(ui, self.settings().config())?;
397 let template =
398 template_builder::parse(language, &mut diagnostics, template_text, &aliases)?;
399 print_parse_diagnostics(ui, "In template expression", &diagnostics)?;
400 Ok(template)
401 }
402
403 pub fn workspace_loader(&self) -> Result<&dyn WorkspaceLoader, CommandError> {
404 self.data
405 .maybe_workspace_loader
406 .as_deref()
407 .map_err(Clone::clone)
408 }
409
410 fn new_workspace_loader_at(
411 &self,
412 workspace_root: &Path,
413 ) -> Result<Box<dyn WorkspaceLoader>, CommandError> {
414 self.data
415 .workspace_loader_factory
416 .create(workspace_root)
417 .map_err(|err| map_workspace_load_error(err, None))
418 }
419
420 #[instrument(skip(self, ui))]
422 pub fn workspace_helper(&self, ui: &Ui) -> Result<WorkspaceCommandHelper, CommandError> {
423 let (workspace_command, stats) = self.workspace_helper_with_stats(ui)?;
424 print_snapshot_stats(ui, &stats, workspace_command.env().path_converter())?;
425 Ok(workspace_command)
426 }
427
428 #[instrument(skip(self, ui))]
435 pub fn workspace_helper_with_stats(
436 &self,
437 ui: &Ui,
438 ) -> Result<(WorkspaceCommandHelper, SnapshotStats), CommandError> {
439 let mut workspace_command = self.workspace_helper_no_snapshot(ui)?;
440
441 let (workspace_command, stats) = match workspace_command.maybe_snapshot_impl(ui) {
442 Ok(stats) => (workspace_command, stats),
443 Err(SnapshotWorkingCopyError::Command(err)) => return Err(err),
444 Err(SnapshotWorkingCopyError::StaleWorkingCopy(err)) => {
445 let auto_update_stale = self.settings().get_bool("snapshot.auto-update-stale")?;
446 if !auto_update_stale {
447 return Err(err);
448 }
449
450 self.recover_stale_working_copy(ui)?
455 }
456 };
457
458 Ok((workspace_command, stats))
459 }
460
461 #[instrument(skip(self, ui))]
464 pub fn workspace_helper_no_snapshot(
465 &self,
466 ui: &Ui,
467 ) -> Result<WorkspaceCommandHelper, CommandError> {
468 let workspace = self.load_workspace()?;
469 let op_head = self.resolve_operation(ui, workspace.repo_loader())?;
470 let repo = workspace.repo_loader().load_at(&op_head)?;
471 let env = self.workspace_environment(ui, &workspace)?;
472 revset_util::warn_unresolvable_trunk(ui, repo.as_ref(), &env.revset_parse_context())?;
473 WorkspaceCommandHelper::new(ui, workspace, repo, env, self.is_at_head_operation())
474 }
475
476 pub fn get_working_copy_factory(&self) -> Result<&dyn WorkingCopyFactory, CommandError> {
477 let loader = self.workspace_loader()?;
478
479 let factory: Result<_, WorkspaceLoadError> =
481 get_working_copy_factory(loader, &self.data.working_copy_factories)
482 .map_err(|e| e.into());
483 let factory = factory.map_err(|err| {
484 map_workspace_load_error(err, self.data.global_args.repository.as_deref())
485 })?;
486 Ok(factory)
487 }
488
489 #[instrument(skip_all)]
491 pub fn load_workspace(&self) -> Result<Workspace, CommandError> {
492 let loader = self.workspace_loader()?;
493 loader
494 .load(
495 &self.data.settings,
496 &self.data.store_factories,
497 &self.data.working_copy_factories,
498 )
499 .map_err(|err| {
500 map_workspace_load_error(err, self.data.global_args.repository.as_deref())
501 })
502 }
503
504 #[instrument(skip(self, settings))]
506 pub fn load_workspace_at(
507 &self,
508 workspace_root: &Path,
509 settings: &UserSettings,
510 ) -> Result<Workspace, CommandError> {
511 let loader = self.new_workspace_loader_at(workspace_root)?;
512 loader
513 .load(
514 settings,
515 &self.data.store_factories,
516 &self.data.working_copy_factories,
517 )
518 .map_err(|err| map_workspace_load_error(err, None))
519 }
520
521 pub fn recover_stale_working_copy(
525 &self,
526 ui: &Ui,
527 ) -> Result<(WorkspaceCommandHelper, SnapshotStats), CommandError> {
528 let workspace = self.load_workspace()?;
529 let op_id = workspace.working_copy().operation_id();
530
531 match workspace.repo_loader().load_operation(op_id) {
532 Ok(op) => {
533 let repo = workspace.repo_loader().load_at(&op)?;
534 let mut workspace_command = self.for_workable_repo(ui, workspace, repo)?;
535
536 let stats = workspace_command
541 .maybe_snapshot_impl(ui)
542 .map_err(|err| err.into_command_error())?;
543
544 let wc_commit_id = workspace_command.get_wc_commit_id().unwrap();
545 let repo = workspace_command.repo().clone();
546 let stale_wc_commit = repo.store().get_commit(wc_commit_id)?;
547
548 let mut workspace_command = self.workspace_helper_no_snapshot(ui)?;
549 let checkout_options = workspace_command.checkout_options();
550
551 let repo = workspace_command.repo().clone();
552 let (mut locked_ws, desired_wc_commit) =
553 workspace_command.unchecked_start_working_copy_mutation()?;
554 match WorkingCopyFreshness::check_stale(
555 locked_ws.locked_wc(),
556 &desired_wc_commit,
557 &repo,
558 )? {
559 WorkingCopyFreshness::Fresh | WorkingCopyFreshness::Updated(_) => {
560 writeln!(
561 ui.status(),
562 "Attempted recovery, but the working copy is not stale"
563 )?;
564 }
565 WorkingCopyFreshness::WorkingCopyStale
566 | WorkingCopyFreshness::SiblingOperation => {
567 let stats = update_stale_working_copy(
568 locked_ws,
569 repo.op_id().clone(),
570 &stale_wc_commit,
571 &desired_wc_commit,
572 &checkout_options,
573 )?;
574 workspace_command.print_updated_working_copy_stats(
575 ui,
576 Some(&stale_wc_commit),
577 &desired_wc_commit,
578 &stats,
579 )?;
580 writeln!(
581 ui.status(),
582 "Updated working copy to fresh commit {}",
583 short_commit_hash(desired_wc_commit.id())
584 )?;
585 }
586 };
587
588 Ok((workspace_command, stats))
589 }
590 Err(e @ OpStoreError::ObjectNotFound { .. }) => {
591 writeln!(
592 ui.status(),
593 "Failed to read working copy's current operation; attempting recovery. Error \
594 message from read attempt: {e}"
595 )?;
596
597 let mut workspace_command = self.workspace_helper_no_snapshot(ui)?;
598 let stats = workspace_command.create_and_check_out_recovery_commit(ui)?;
599 Ok((workspace_command, stats))
600 }
601 Err(e) => Err(e.into()),
602 }
603 }
604
605 pub fn workspace_environment(
607 &self,
608 ui: &Ui,
609 workspace: &Workspace,
610 ) -> Result<WorkspaceCommandEnvironment, CommandError> {
611 WorkspaceCommandEnvironment::new(ui, self, workspace)
612 }
613
614 pub fn is_working_copy_writable(&self) -> bool {
617 self.is_at_head_operation() && !self.data.global_args.ignore_working_copy
618 }
619
620 pub fn is_at_head_operation(&self) -> bool {
622 matches!(
625 self.data.global_args.at_operation.as_deref(),
626 None | Some("@")
627 )
628 }
629
630 #[instrument(skip_all)]
635 pub fn resolve_operation(
636 &self,
637 ui: &Ui,
638 repo_loader: &RepoLoader,
639 ) -> Result<Operation, CommandError> {
640 if let Some(op_str) = &self.data.global_args.at_operation {
641 Ok(op_walk::resolve_op_for_load(repo_loader, op_str)?)
642 } else {
643 op_heads_store::resolve_op_heads(
644 repo_loader.op_heads_store().as_ref(),
645 repo_loader.op_store(),
646 |op_heads| {
647 writeln!(
648 ui.status(),
649 "Concurrent modification detected, resolving automatically.",
650 )?;
651 let base_repo = repo_loader.load_at(&op_heads[0])?;
652 let mut tx = start_repo_transaction(&base_repo, &self.data.string_args);
654 for other_op_head in op_heads.into_iter().skip(1) {
655 tx.merge_operation(other_op_head)?;
656 let num_rebased = tx.repo_mut().rebase_descendants()?;
657 if num_rebased > 0 {
658 writeln!(
659 ui.status(),
660 "Rebased {num_rebased} descendant commits onto commits rewritten \
661 by other operation"
662 )?;
663 }
664 }
665 Ok(tx
666 .write("reconcile divergent operations")?
667 .leave_unpublished()
668 .operation()
669 .clone())
670 },
671 )
672 }
673 }
674
675 #[instrument(skip_all)]
679 pub fn for_workable_repo(
680 &self,
681 ui: &Ui,
682 workspace: Workspace,
683 repo: Arc<ReadonlyRepo>,
684 ) -> Result<WorkspaceCommandHelper, CommandError> {
685 let env = self.workspace_environment(ui, &workspace)?;
686 let loaded_at_head = true;
687 WorkspaceCommandHelper::new(ui, workspace, repo, env, loaded_at_head)
688 }
689}
690
691struct ReadonlyUserRepo {
694 repo: Arc<ReadonlyRepo>,
695 id_prefix_context: OnceCell<IdPrefixContext>,
696}
697
698impl ReadonlyUserRepo {
699 fn new(repo: Arc<ReadonlyRepo>) -> Self {
700 Self {
701 repo,
702 id_prefix_context: OnceCell::new(),
703 }
704 }
705}
706
707pub struct AdvanceableBookmark {
717 name: RefNameBuf,
718 old_commit_id: CommitId,
719}
720
721struct AdvanceBookmarksSettings {
731 enabled_bookmarks: Vec<StringPattern>,
732 disabled_bookmarks: Vec<StringPattern>,
733}
734
735impl AdvanceBookmarksSettings {
736 fn from_settings(settings: &UserSettings) -> Result<Self, CommandError> {
737 let get_setting = |setting_key| {
738 let name = ConfigNamePathBuf::from_iter(["experimental-advance-branches", setting_key]);
739 match settings.get::<Vec<String>>(&name).optional()? {
740 Some(patterns) => patterns
741 .into_iter()
742 .map(|s| {
743 StringPattern::parse(&s).map_err(|e| {
744 config_error_with_message(format!("Error parsing `{s}` for {name}"), e)
745 })
746 })
747 .collect(),
748 None => Ok(Vec::new()),
749 }
750 };
751 Ok(Self {
752 enabled_bookmarks: get_setting("enabled-branches")?,
753 disabled_bookmarks: get_setting("disabled-branches")?,
754 })
755 }
756
757 fn bookmark_is_eligible(&self, bookmark_name: &RefName) -> bool {
760 if self
761 .disabled_bookmarks
762 .iter()
763 .any(|d| d.matches(bookmark_name.as_str()))
764 {
765 return false;
766 }
767 self.enabled_bookmarks
768 .iter()
769 .any(|e| e.matches(bookmark_name.as_str()))
770 }
771
772 fn feature_enabled(&self) -> bool {
775 !self.enabled_bookmarks.is_empty()
776 }
777}
778
779pub struct WorkspaceCommandEnvironment {
781 command: CommandHelper,
782 settings: UserSettings,
783 revset_aliases_map: RevsetAliasesMap,
784 template_aliases_map: TemplateAliasesMap,
785 path_converter: RepoPathUiConverter,
786 workspace_name: WorkspaceNameBuf,
787 immutable_heads_expression: Rc<UserRevsetExpression>,
788 short_prefixes_expression: Option<Rc<UserRevsetExpression>>,
789 conflict_marker_style: ConflictMarkerStyle,
790}
791
792impl WorkspaceCommandEnvironment {
793 #[instrument(skip_all)]
794 fn new(ui: &Ui, command: &CommandHelper, workspace: &Workspace) -> Result<Self, CommandError> {
795 let settings = workspace.settings();
796 let revset_aliases_map = revset_util::load_revset_aliases(ui, settings.config())?;
797 let template_aliases_map = load_template_aliases(ui, settings.config())?;
798 let path_converter = RepoPathUiConverter::Fs {
799 cwd: command.cwd().to_owned(),
800 base: workspace.workspace_root().to_owned(),
801 };
802 let mut env = Self {
803 command: command.clone(),
804 settings: settings.clone(),
805 revset_aliases_map,
806 template_aliases_map,
807 path_converter,
808 workspace_name: workspace.workspace_name().to_owned(),
809 immutable_heads_expression: RevsetExpression::root(),
810 short_prefixes_expression: None,
811 conflict_marker_style: settings.get("ui.conflict-marker-style")?,
812 };
813 env.immutable_heads_expression = env.load_immutable_heads_expression(ui)?;
814 env.short_prefixes_expression = env.load_short_prefixes_expression(ui)?;
815 Ok(env)
816 }
817
818 pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
819 &self.path_converter
820 }
821
822 pub fn workspace_name(&self) -> &WorkspaceName {
823 &self.workspace_name
824 }
825
826 pub(crate) fn revset_parse_context(&self) -> RevsetParseContext {
827 let workspace_context = RevsetWorkspaceContext {
828 path_converter: &self.path_converter,
829 workspace_name: &self.workspace_name,
830 };
831 let now = if let Some(timestamp) = self.settings.commit_timestamp() {
832 chrono::Local
833 .timestamp_millis_opt(timestamp.timestamp.0)
834 .unwrap()
835 } else {
836 chrono::Local::now()
837 };
838 RevsetParseContext {
839 aliases_map: &self.revset_aliases_map,
840 local_variables: HashMap::new(),
841 user_email: self.settings.user_email(),
842 date_pattern_context: now.into(),
843 extensions: self.command.revset_extensions(),
844 workspace: Some(workspace_context),
845 }
846 }
847
848 pub fn new_id_prefix_context(&self) -> IdPrefixContext {
851 let context = IdPrefixContext::new(self.command.revset_extensions().clone());
852 match &self.short_prefixes_expression {
853 None => context,
854 Some(expression) => context.disambiguate_within(expression.clone()),
855 }
856 }
857
858 pub fn immutable_expression(&self) -> Rc<UserRevsetExpression> {
860 self.immutable_heads_expression.ancestors()
863 }
864
865 pub fn immutable_heads_expression(&self) -> &Rc<UserRevsetExpression> {
867 &self.immutable_heads_expression
868 }
869
870 pub fn conflict_marker_style(&self) -> ConflictMarkerStyle {
872 self.conflict_marker_style
873 }
874
875 fn load_immutable_heads_expression(
876 &self,
877 ui: &Ui,
878 ) -> Result<Rc<UserRevsetExpression>, CommandError> {
879 let mut diagnostics = RevsetDiagnostics::new();
880 let expression = revset_util::parse_immutable_heads_expression(
881 &mut diagnostics,
882 &self.revset_parse_context(),
883 )
884 .map_err(|e| config_error_with_message("Invalid `revset-aliases.immutable_heads()`", e))?;
885 print_parse_diagnostics(ui, "In `revset-aliases.immutable_heads()`", &diagnostics)?;
886 Ok(expression)
887 }
888
889 fn load_short_prefixes_expression(
890 &self,
891 ui: &Ui,
892 ) -> Result<Option<Rc<UserRevsetExpression>>, CommandError> {
893 let revset_string = self
894 .settings
895 .get_string("revsets.short-prefixes")
896 .optional()?
897 .map_or_else(|| self.settings.get_string("revsets.log"), Ok)?;
898 if revset_string.is_empty() {
899 Ok(None)
900 } else {
901 let mut diagnostics = RevsetDiagnostics::new();
902 let (expression, modifier) = revset::parse_with_modifier(
903 &mut diagnostics,
904 &revset_string,
905 &self.revset_parse_context(),
906 )
907 .map_err(|err| config_error_with_message("Invalid `revsets.short-prefixes`", err))?;
908 print_parse_diagnostics(ui, "In `revsets.short-prefixes`", &diagnostics)?;
909 let (None | Some(RevsetModifier::All)) = modifier;
910 Ok(Some(expression))
911 }
912 }
913
914 fn find_immutable_commit<'a>(
917 &self,
918 repo: &dyn Repo,
919 commits: impl IntoIterator<Item = &'a CommitId>,
920 ) -> Result<Option<(CommitId, usize, Option<usize>)>, CommandError> {
921 if self.command.global_args().ignore_immutable {
922 let root_id = repo.store().root_commit_id();
923 return Ok(commits
924 .into_iter()
925 .find(|id| *id == root_id)
926 .map(|root| (root.clone(), 1, None)));
927 }
928
929 let id_prefix_context = IdPrefixContext::new(self.command.revset_extensions().clone());
933 let to_rewrite_revset =
934 RevsetExpression::commits(commits.into_iter().cloned().collect_vec());
935 let mut expression = RevsetExpressionEvaluator::new(
936 repo,
937 self.command.revset_extensions().clone(),
938 &id_prefix_context,
939 self.immutable_expression(),
940 );
941 expression.intersect_with(&to_rewrite_revset);
942
943 let mut commit_id_iter = expression.evaluate_to_commit_ids().map_err(|e| {
944 config_error_with_message("Invalid `revset-aliases.immutable_heads()`", e)
945 })?;
946
947 let Some(first_immutable) = commit_id_iter.next().transpose()? else {
948 return Ok(None);
949 };
950
951 let mut bounds = RevsetExpressionEvaluator::new(
952 repo,
953 self.command.revset_extensions().clone(),
954 &id_prefix_context,
955 self.immutable_expression(),
956 );
957 bounds.intersect_with(&to_rewrite_revset.descendants());
958 let (lower, upper) = bounds.evaluate()?.count_estimate()?;
959
960 Ok(Some((first_immutable, lower, upper)))
961 }
962
963 pub fn template_aliases_map(&self) -> &TemplateAliasesMap {
964 &self.template_aliases_map
965 }
966
967 pub fn parse_template<'a, C, L>(
969 &self,
970 ui: &Ui,
971 language: &L,
972 template_text: &str,
973 ) -> Result<TemplateRenderer<'a, C>, CommandError>
974 where
975 C: Clone + 'a,
976 L: TemplateLanguage<'a> + ?Sized,
977 L::Property: WrapTemplateProperty<'a, C>,
978 {
979 let mut diagnostics = TemplateDiagnostics::new();
980 let template = template_builder::parse(
981 language,
982 &mut diagnostics,
983 template_text,
984 &self.template_aliases_map,
985 )?;
986 print_parse_diagnostics(ui, "In template expression", &diagnostics)?;
987 Ok(template)
988 }
989
990 pub fn commit_template_language<'a>(
993 &'a self,
994 repo: &'a dyn Repo,
995 id_prefix_context: &'a IdPrefixContext,
996 ) -> CommitTemplateLanguage<'a> {
997 CommitTemplateLanguage::new(
998 repo,
999 &self.path_converter,
1000 &self.workspace_name,
1001 self.revset_parse_context(),
1002 id_prefix_context,
1003 self.immutable_expression(),
1004 self.conflict_marker_style,
1005 &self.command.data.commit_template_extensions,
1006 )
1007 }
1008
1009 pub fn operation_template_extensions(&self) -> &[Arc<dyn OperationTemplateLanguageExtension>] {
1010 &self.command.data.operation_template_extensions
1011 }
1012}
1013
1014pub struct WorkspaceCommandHelper {
1017 workspace: Workspace,
1018 user_repo: ReadonlyUserRepo,
1019 env: WorkspaceCommandEnvironment,
1020 commit_summary_template_text: String,
1022 op_summary_template_text: String,
1023 may_update_working_copy: bool,
1024 working_copy_shared_with_git: bool,
1025}
1026
1027enum SnapshotWorkingCopyError {
1028 Command(CommandError),
1029 StaleWorkingCopy(CommandError),
1030}
1031
1032impl SnapshotWorkingCopyError {
1033 fn into_command_error(self) -> CommandError {
1034 match self {
1035 Self::Command(err) => err,
1036 Self::StaleWorkingCopy(err) => err,
1037 }
1038 }
1039}
1040
1041fn snapshot_command_error<E>(err: E) -> SnapshotWorkingCopyError
1042where
1043 E: Into<CommandError>,
1044{
1045 SnapshotWorkingCopyError::Command(err.into())
1046}
1047
1048impl WorkspaceCommandHelper {
1049 #[instrument(skip_all)]
1050 fn new(
1051 ui: &Ui,
1052 workspace: Workspace,
1053 repo: Arc<ReadonlyRepo>,
1054 env: WorkspaceCommandEnvironment,
1055 loaded_at_head: bool,
1056 ) -> Result<Self, CommandError> {
1057 let settings = workspace.settings();
1058 let commit_summary_template_text = settings.get_string("templates.commit_summary")?;
1059 let op_summary_template_text = settings.get_string("templates.op_summary")?;
1060 let may_update_working_copy =
1061 loaded_at_head && !env.command.global_args().ignore_working_copy;
1062 let working_copy_shared_with_git =
1063 crate::git_util::is_colocated_git_workspace(&workspace, &repo);
1064
1065 let helper = Self {
1066 workspace,
1067 user_repo: ReadonlyUserRepo::new(repo),
1068 env,
1069 commit_summary_template_text,
1070 op_summary_template_text,
1071 may_update_working_copy,
1072 working_copy_shared_with_git,
1073 };
1074 helper.parse_operation_template(ui, &helper.op_summary_template_text)?;
1077 helper.parse_commit_template(ui, &helper.commit_summary_template_text)?;
1078 helper.parse_commit_template(ui, SHORT_CHANGE_ID_TEMPLATE_TEXT)?;
1079 Ok(helper)
1080 }
1081
1082 pub fn settings(&self) -> &UserSettings {
1084 self.workspace.settings()
1085 }
1086
1087 pub fn check_working_copy_writable(&self) -> Result<(), CommandError> {
1088 if self.may_update_working_copy {
1089 Ok(())
1090 } else {
1091 let hint = if self.env.command.global_args().ignore_working_copy {
1092 "Don't use --ignore-working-copy."
1093 } else {
1094 "Don't use --at-op."
1095 };
1096 Err(user_error_with_hint(
1097 "This command must be able to update the working copy.",
1098 hint,
1099 ))
1100 }
1101 }
1102
1103 #[instrument(skip_all)]
1107 fn maybe_snapshot_impl(&mut self, ui: &Ui) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
1108 if !self.may_update_working_copy {
1109 return Ok(SnapshotStats::default());
1110 }
1111
1112 #[cfg(feature = "git")]
1113 if self.working_copy_shared_with_git {
1114 self.import_git_head(ui).map_err(snapshot_command_error)?;
1115 }
1116 let stats = self.snapshot_working_copy(ui)?;
1121
1122 #[cfg(feature = "git")]
1124 if self.working_copy_shared_with_git {
1125 self.import_git_refs(ui).map_err(snapshot_command_error)?;
1126 }
1127 Ok(stats)
1128 }
1129
1130 #[instrument(skip_all)]
1133 pub fn maybe_snapshot(&mut self, ui: &Ui) -> Result<(), CommandError> {
1134 let stats = self
1135 .maybe_snapshot_impl(ui)
1136 .map_err(|err| err.into_command_error())?;
1137 print_snapshot_stats(ui, &stats, self.env().path_converter())?;
1138 Ok(())
1139 }
1140
1141 #[cfg(feature = "git")]
1148 #[instrument(skip_all)]
1149 fn import_git_head(&mut self, ui: &Ui) -> Result<(), CommandError> {
1150 assert!(self.may_update_working_copy);
1151 let mut tx = self.start_transaction();
1152 jj_lib::git::import_head(tx.repo_mut())?;
1153 if !tx.repo().has_changes() {
1154 return Ok(());
1155 }
1156
1157 let mut tx = tx.into_inner();
1168 let old_git_head = self.repo().view().git_head().clone();
1169 let new_git_head = tx.repo().view().git_head().clone();
1170 if let Some(new_git_head_id) = new_git_head.as_normal() {
1171 let workspace_name = self.workspace_name().to_owned();
1172 let new_git_head_commit = tx.repo().store().get_commit(new_git_head_id)?;
1173 tx.repo_mut()
1174 .check_out(workspace_name, &new_git_head_commit)?;
1175 let mut locked_ws = self.workspace.start_working_copy_mutation()?;
1176 locked_ws.locked_wc().reset(&new_git_head_commit)?;
1180 tx.repo_mut().rebase_descendants()?;
1181 self.user_repo = ReadonlyUserRepo::new(tx.commit("import git head")?);
1182 locked_ws.finish(self.user_repo.repo.op_id().clone())?;
1183 if old_git_head.is_present() {
1184 writeln!(
1185 ui.status(),
1186 "Reset the working copy parent to the new Git HEAD."
1187 )?;
1188 } else {
1189 }
1191 } else {
1192 self.finish_transaction(ui, tx, "import git head")?;
1194 }
1195 Ok(())
1196 }
1197
1198 #[cfg(feature = "git")]
1207 #[instrument(skip_all)]
1208 fn import_git_refs(&mut self, ui: &Ui) -> Result<(), CommandError> {
1209 let git_settings = self.settings().git_settings()?;
1210 let mut tx = self.start_transaction();
1211 let stats = jj_lib::git::import_refs(tx.repo_mut(), &git_settings)?;
1212 crate::git_util::print_git_import_stats(ui, tx.repo(), &stats, false)?;
1213 if !tx.repo().has_changes() {
1214 return Ok(());
1215 }
1216
1217 let mut tx = tx.into_inner();
1218 let num_rebased = tx.repo_mut().rebase_descendants()?;
1220 if num_rebased > 0 {
1221 writeln!(
1222 ui.status(),
1223 "Rebased {num_rebased} descendant commits off of commits rewritten from git"
1224 )?;
1225 }
1226 self.finish_transaction(ui, tx, "import git refs")?;
1227 writeln!(
1228 ui.status(),
1229 "Done importing changes from the underlying Git repo."
1230 )?;
1231 Ok(())
1232 }
1233
1234 pub fn repo(&self) -> &Arc<ReadonlyRepo> {
1235 &self.user_repo.repo
1236 }
1237
1238 pub fn repo_path(&self) -> &Path {
1239 self.workspace.repo_path()
1240 }
1241
1242 pub fn workspace(&self) -> &Workspace {
1243 &self.workspace
1244 }
1245
1246 pub fn working_copy(&self) -> &dyn WorkingCopy {
1247 self.workspace.working_copy()
1248 }
1249
1250 pub fn env(&self) -> &WorkspaceCommandEnvironment {
1251 &self.env
1252 }
1253
1254 pub fn checkout_options(&self) -> CheckoutOptions {
1255 CheckoutOptions {
1256 conflict_marker_style: self.env.conflict_marker_style(),
1257 }
1258 }
1259
1260 pub fn unchecked_start_working_copy_mutation(
1261 &mut self,
1262 ) -> Result<(LockedWorkspace, Commit), CommandError> {
1263 self.check_working_copy_writable()?;
1264 let wc_commit = if let Some(wc_commit_id) = self.get_wc_commit_id() {
1265 self.repo().store().get_commit(wc_commit_id)?
1266 } else {
1267 return Err(user_error("Nothing checked out in this workspace"));
1268 };
1269
1270 let locked_ws = self.workspace.start_working_copy_mutation()?;
1271
1272 Ok((locked_ws, wc_commit))
1273 }
1274
1275 pub fn start_working_copy_mutation(
1276 &mut self,
1277 ) -> Result<(LockedWorkspace, Commit), CommandError> {
1278 let (mut locked_ws, wc_commit) = self.unchecked_start_working_copy_mutation()?;
1279 if wc_commit.tree_id() != locked_ws.locked_wc().old_tree_id() {
1280 return Err(user_error("Concurrent working copy operation. Try again."));
1281 }
1282 Ok((locked_ws, wc_commit))
1283 }
1284
1285 fn create_and_check_out_recovery_commit(
1286 &mut self,
1287 ui: &Ui,
1288 ) -> Result<SnapshotStats, CommandError> {
1289 self.check_working_copy_writable()?;
1290
1291 let workspace_name = self.workspace_name().to_owned();
1292 let mut locked_ws = self.workspace.start_working_copy_mutation()?;
1293 let (repo, new_commit) = working_copy::create_and_check_out_recovery_commit(
1294 locked_ws.locked_wc(),
1295 &self.user_repo.repo,
1296 workspace_name,
1297 "RECOVERY COMMIT FROM `jj workspace update-stale`
1298
1299This commit contains changes that were written to the working copy by an
1300operation that was subsequently lost (or was at least unavailable when you ran
1301`jj workspace update-stale`). Because the operation was lost, we don't know
1302what the parent commits are supposed to be. That means that the diff compared
1303to the current parents may contain changes from multiple commits.
1304",
1305 )?;
1306
1307 writeln!(
1308 ui.status(),
1309 "Created and checked out recovery commit {}",
1310 short_commit_hash(new_commit.id())
1311 )?;
1312 locked_ws.finish(repo.op_id().clone())?;
1313 self.user_repo = ReadonlyUserRepo::new(repo);
1314
1315 self.maybe_snapshot_impl(ui)
1316 .map_err(|err| err.into_command_error())
1317 }
1318
1319 pub fn workspace_root(&self) -> &Path {
1320 self.workspace.workspace_root()
1321 }
1322
1323 pub fn workspace_name(&self) -> &WorkspaceName {
1324 self.workspace.workspace_name()
1325 }
1326
1327 pub fn get_wc_commit_id(&self) -> Option<&CommitId> {
1328 self.repo().view().get_wc_commit_id(self.workspace_name())
1329 }
1330
1331 pub fn working_copy_shared_with_git(&self) -> bool {
1332 self.working_copy_shared_with_git
1333 }
1334
1335 pub fn format_file_path(&self, file: &RepoPath) -> String {
1336 self.path_converter().format_file_path(file)
1337 }
1338
1339 pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
1342 self.path_converter().parse_file_path(input)
1343 }
1344
1345 pub fn parse_file_patterns(
1347 &self,
1348 ui: &Ui,
1349 values: &[String],
1350 ) -> Result<FilesetExpression, CommandError> {
1351 if values.is_empty() {
1355 Ok(FilesetExpression::all())
1356 } else {
1357 self.parse_union_filesets(ui, values)
1358 }
1359 }
1360
1361 pub fn parse_union_filesets(
1363 &self,
1364 ui: &Ui,
1365 file_args: &[String], ) -> Result<FilesetExpression, CommandError> {
1367 let mut diagnostics = FilesetDiagnostics::new();
1368 let expressions: Vec<_> = file_args
1369 .iter()
1370 .map(|arg| fileset::parse_maybe_bare(&mut diagnostics, arg, self.path_converter()))
1371 .try_collect()?;
1372 print_parse_diagnostics(ui, "In fileset expression", &diagnostics)?;
1373 Ok(FilesetExpression::union_all(expressions))
1374 }
1375
1376 pub fn auto_tracking_matcher(&self, ui: &Ui) -> Result<Box<dyn Matcher>, CommandError> {
1377 let mut diagnostics = FilesetDiagnostics::new();
1378 let pattern = self.settings().get_string("snapshot.auto-track")?;
1379 let expression = fileset::parse(
1380 &mut diagnostics,
1381 &pattern,
1382 &RepoPathUiConverter::Fs {
1383 cwd: "".into(),
1384 base: "".into(),
1385 },
1386 )?;
1387 print_parse_diagnostics(ui, "In `snapshot.auto-track`", &diagnostics)?;
1388 Ok(expression.to_matcher())
1389 }
1390
1391 pub fn snapshot_options_with_start_tracking_matcher<'a>(
1392 &self,
1393 start_tracking_matcher: &'a dyn Matcher,
1394 ) -> Result<SnapshotOptions<'a>, CommandError> {
1395 let base_ignores = self.base_ignores()?;
1396 let fsmonitor_settings = self.settings().fsmonitor_settings()?;
1397 let HumanByteSize(mut max_new_file_size) = self
1398 .settings()
1399 .get_value_with("snapshot.max-new-file-size", TryInto::try_into)?;
1400 if max_new_file_size == 0 {
1401 max_new_file_size = u64::MAX;
1402 }
1403 let conflict_marker_style = self.env.conflict_marker_style();
1404 Ok(SnapshotOptions {
1405 base_ignores,
1406 fsmonitor_settings,
1407 progress: None,
1408 start_tracking_matcher,
1409 max_new_file_size,
1410 conflict_marker_style,
1411 })
1412 }
1413
1414 pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
1415 self.env.path_converter()
1416 }
1417
1418 #[cfg(not(feature = "git"))]
1419 pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1420 Ok(GitIgnoreFile::empty())
1421 }
1422
1423 #[cfg(feature = "git")]
1424 #[instrument(skip_all)]
1425 pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1426 let get_excludes_file_path = |config: &gix::config::File| -> Option<PathBuf> {
1427 if let Some(value) = config.string("core.excludesFile") {
1430 let path = str::from_utf8(&value)
1431 .ok()
1432 .map(jj_lib::file_util::expand_home_path)?;
1433 Some(self.workspace_root().join(path))
1436 } else {
1437 xdg_config_home().ok().map(|x| x.join("git").join("ignore"))
1438 }
1439 };
1440
1441 fn xdg_config_home() -> Result<PathBuf, std::env::VarError> {
1442 if let Ok(x) = std::env::var("XDG_CONFIG_HOME") {
1443 if !x.is_empty() {
1444 return Ok(PathBuf::from(x));
1445 }
1446 }
1447 std::env::var("HOME").map(|x| Path::new(&x).join(".config"))
1448 }
1449
1450 let mut git_ignores = GitIgnoreFile::empty();
1451 if let Ok(git_backend) = jj_lib::git::get_git_backend(self.repo().store()) {
1452 let git_repo = git_backend.git_repo();
1453 if let Some(excludes_file_path) = get_excludes_file_path(&git_repo.config_snapshot()) {
1454 git_ignores = git_ignores.chain_with_file("", excludes_file_path)?;
1455 }
1456 git_ignores = git_ignores
1457 .chain_with_file("", git_backend.git_repo_path().join("info").join("exclude"))?;
1458 } else if let Ok(git_config) = gix::config::File::from_globals() {
1459 if let Some(excludes_file_path) = get_excludes_file_path(&git_config) {
1460 git_ignores = git_ignores.chain_with_file("", excludes_file_path)?;
1461 }
1462 }
1463 Ok(git_ignores)
1464 }
1465
1466 pub fn diff_renderer(&self, formats: Vec<DiffFormat>) -> DiffRenderer<'_> {
1468 DiffRenderer::new(
1469 self.repo().as_ref(),
1470 self.path_converter(),
1471 self.env.conflict_marker_style(),
1472 formats,
1473 )
1474 }
1475
1476 pub fn diff_renderer_for(
1478 &self,
1479 args: &DiffFormatArgs,
1480 ) -> Result<DiffRenderer<'_>, CommandError> {
1481 let formats = diff_util::diff_formats_for(self.settings(), args)?;
1482 Ok(self.diff_renderer(formats))
1483 }
1484
1485 pub fn diff_renderer_for_log(
1489 &self,
1490 args: &DiffFormatArgs,
1491 patch: bool,
1492 ) -> Result<Option<DiffRenderer<'_>>, CommandError> {
1493 let formats = diff_util::diff_formats_for_log(self.settings(), args, patch)?;
1494 Ok((!formats.is_empty()).then(|| self.diff_renderer(formats)))
1495 }
1496
1497 pub fn diff_editor(
1501 &self,
1502 ui: &Ui,
1503 tool_name: Option<&str>,
1504 ) -> Result<DiffEditor, CommandError> {
1505 let base_ignores = self.base_ignores()?;
1506 let conflict_marker_style = self.env.conflict_marker_style();
1507 if let Some(name) = tool_name {
1508 Ok(DiffEditor::with_name(
1509 name,
1510 self.settings(),
1511 base_ignores,
1512 conflict_marker_style,
1513 )?)
1514 } else {
1515 Ok(DiffEditor::from_settings(
1516 ui,
1517 self.settings(),
1518 base_ignores,
1519 conflict_marker_style,
1520 )?)
1521 }
1522 }
1523
1524 pub fn diff_selector(
1528 &self,
1529 ui: &Ui,
1530 tool_name: Option<&str>,
1531 force_interactive: bool,
1532 ) -> Result<DiffSelector, CommandError> {
1533 if tool_name.is_some() || force_interactive {
1534 Ok(DiffSelector::Interactive(self.diff_editor(ui, tool_name)?))
1535 } else {
1536 Ok(DiffSelector::NonInteractive)
1537 }
1538 }
1539
1540 pub fn merge_editor(
1544 &self,
1545 ui: &Ui,
1546 tool_name: Option<&str>,
1547 ) -> Result<MergeEditor, MergeToolConfigError> {
1548 let conflict_marker_style = self.env.conflict_marker_style();
1549 if let Some(name) = tool_name {
1550 MergeEditor::with_name(
1551 name,
1552 self.settings(),
1553 self.path_converter().clone(),
1554 conflict_marker_style,
1555 )
1556 } else {
1557 MergeEditor::from_settings(
1558 ui,
1559 self.settings(),
1560 self.path_converter().clone(),
1561 conflict_marker_style,
1562 )
1563 }
1564 }
1565
1566 pub fn text_editor(&self) -> Result<TextEditor, ConfigGetError> {
1568 TextEditor::from_settings(self.settings())
1569 }
1570
1571 pub fn resolve_single_op(&self, op_str: &str) -> Result<Operation, OpsetEvaluationError> {
1572 op_walk::resolve_op_with_repo(self.repo(), op_str)
1573 }
1574
1575 pub fn resolve_single_rev(
1578 &self,
1579 ui: &Ui,
1580 revision_arg: &RevisionArg,
1581 ) -> Result<Commit, CommandError> {
1582 let expression = self.parse_revset(ui, revision_arg)?;
1583 let should_hint_about_all_prefix = false;
1584 revset_util::evaluate_revset_to_single_commit(
1585 revision_arg.as_ref(),
1586 &expression,
1587 || self.commit_summary_template(),
1588 should_hint_about_all_prefix,
1589 )
1590 }
1591
1592 pub fn resolve_some_revsets_default_single(
1598 &self,
1599 ui: &Ui,
1600 revision_args: &[RevisionArg],
1601 ) -> Result<IndexSet<CommitId>, CommandError> {
1602 let mut all_commits = IndexSet::new();
1603 for revision_arg in revision_args {
1604 let (expression, modifier) = self.parse_revset_with_modifier(ui, revision_arg)?;
1605 let all = match modifier {
1606 Some(RevsetModifier::All) => true,
1607 None => self.settings().get_bool("ui.always-allow-large-revsets")?,
1608 };
1609 if all {
1610 for commit_id in expression.evaluate_to_commit_ids()? {
1611 all_commits.insert(commit_id?);
1612 }
1613 } else {
1614 let should_hint_about_all_prefix = true;
1615 let commit = revset_util::evaluate_revset_to_single_commit(
1616 revision_arg.as_ref(),
1617 &expression,
1618 || self.commit_summary_template(),
1619 should_hint_about_all_prefix,
1620 )?;
1621 if !all_commits.insert(commit.id().clone()) {
1622 let commit_hash = short_commit_hash(commit.id());
1623 return Err(user_error(format!(
1624 r#"More than one revset resolved to revision {commit_hash}"#,
1625 )));
1626 }
1627 }
1628 }
1629 if all_commits.is_empty() {
1630 Err(user_error("Empty revision set"))
1631 } else {
1632 Ok(all_commits)
1633 }
1634 }
1635
1636 pub fn parse_revset(
1637 &self,
1638 ui: &Ui,
1639 revision_arg: &RevisionArg,
1640 ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1641 let (expression, modifier) = self.parse_revset_with_modifier(ui, revision_arg)?;
1642 let (None | Some(RevsetModifier::All)) = modifier;
1645 Ok(expression)
1646 }
1647
1648 fn parse_revset_with_modifier(
1649 &self,
1650 ui: &Ui,
1651 revision_arg: &RevisionArg,
1652 ) -> Result<(RevsetExpressionEvaluator<'_>, Option<RevsetModifier>), CommandError> {
1653 let mut diagnostics = RevsetDiagnostics::new();
1654 let context = self.env.revset_parse_context();
1655 let (expression, modifier) =
1656 revset::parse_with_modifier(&mut diagnostics, revision_arg.as_ref(), &context)?;
1657 print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1658 Ok((self.attach_revset_evaluator(expression), modifier))
1659 }
1660
1661 pub fn parse_union_revsets(
1663 &self,
1664 ui: &Ui,
1665 revision_args: &[RevisionArg],
1666 ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1667 let mut diagnostics = RevsetDiagnostics::new();
1668 let context = self.env.revset_parse_context();
1669 let expressions: Vec<_> = revision_args
1670 .iter()
1671 .map(|arg| revset::parse_with_modifier(&mut diagnostics, arg.as_ref(), &context))
1672 .map_ok(|(expression, None | Some(RevsetModifier::All))| expression)
1673 .try_collect()?;
1674 print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1675 let expression = RevsetExpression::union_all(&expressions);
1676 Ok(self.attach_revset_evaluator(expression))
1677 }
1678
1679 pub fn attach_revset_evaluator(
1680 &self,
1681 expression: Rc<UserRevsetExpression>,
1682 ) -> RevsetExpressionEvaluator<'_> {
1683 RevsetExpressionEvaluator::new(
1684 self.repo().as_ref(),
1685 self.env.command.revset_extensions().clone(),
1686 self.id_prefix_context(),
1687 expression,
1688 )
1689 }
1690
1691 pub fn id_prefix_context(&self) -> &IdPrefixContext {
1692 self.user_repo
1693 .id_prefix_context
1694 .get_or_init(|| self.env.new_id_prefix_context())
1695 }
1696
1697 pub fn parse_template<'a, C, L>(
1699 &self,
1700 ui: &Ui,
1701 language: &L,
1702 template_text: &str,
1703 ) -> Result<TemplateRenderer<'a, C>, CommandError>
1704 where
1705 C: Clone + 'a,
1706 L: TemplateLanguage<'a> + ?Sized,
1707 L::Property: WrapTemplateProperty<'a, C>,
1708 {
1709 self.env.parse_template(ui, language, template_text)
1710 }
1711
1712 fn reparse_valid_template<'a, C, L>(
1714 &self,
1715 language: &L,
1716 template_text: &str,
1717 ) -> TemplateRenderer<'a, C>
1718 where
1719 C: Clone + 'a,
1720 L: TemplateLanguage<'a> + ?Sized,
1721 L::Property: WrapTemplateProperty<'a, C>,
1722 {
1723 template_builder::parse(
1724 language,
1725 &mut TemplateDiagnostics::new(),
1726 template_text,
1727 &self.env.template_aliases_map,
1728 )
1729 .expect("parse error should be confined by WorkspaceCommandHelper::new()")
1730 }
1731
1732 pub fn parse_commit_template(
1734 &self,
1735 ui: &Ui,
1736 template_text: &str,
1737 ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
1738 let language = self.commit_template_language();
1739 self.parse_template(ui, &language, template_text)
1740 }
1741
1742 pub fn parse_operation_template(
1744 &self,
1745 ui: &Ui,
1746 template_text: &str,
1747 ) -> Result<TemplateRenderer<'_, Operation>, CommandError> {
1748 let language = self.operation_template_language();
1749 self.parse_template(ui, &language, template_text)
1750 }
1751
1752 pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
1754 self.env
1755 .commit_template_language(self.repo().as_ref(), self.id_prefix_context())
1756 }
1757
1758 pub fn operation_template_language(&self) -> OperationTemplateLanguage {
1760 OperationTemplateLanguage::new(
1761 self.workspace.repo_loader(),
1762 Some(self.repo().op_id()),
1763 self.env.operation_template_extensions(),
1764 )
1765 }
1766
1767 pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
1769 let language = self.commit_template_language();
1770 self.reparse_valid_template(&language, &self.commit_summary_template_text)
1771 .labeled(["commit"])
1772 }
1773
1774 pub fn operation_summary_template(&self) -> TemplateRenderer<'_, Operation> {
1776 let language = self.operation_template_language();
1777 self.reparse_valid_template(&language, &self.op_summary_template_text)
1778 .labeled(["operation"])
1779 }
1780
1781 pub fn short_change_id_template(&self) -> TemplateRenderer<'_, Commit> {
1782 let language = self.commit_template_language();
1783 self.reparse_valid_template(&language, SHORT_CHANGE_ID_TEMPLATE_TEXT)
1784 .labeled(["commit"])
1785 }
1786
1787 pub fn format_commit_summary(&self, commit: &Commit) -> String {
1792 let mut output = Vec::new();
1793 self.write_commit_summary(&mut PlainTextFormatter::new(&mut output), commit)
1794 .expect("write() to PlainTextFormatter should never fail");
1795 output.into_string_lossy()
1797 }
1798
1799 #[instrument(skip_all)]
1803 pub fn write_commit_summary(
1804 &self,
1805 formatter: &mut dyn Formatter,
1806 commit: &Commit,
1807 ) -> std::io::Result<()> {
1808 self.commit_summary_template().format(commit, formatter)
1809 }
1810
1811 pub fn check_rewritable<'a>(
1812 &self,
1813 commits: impl IntoIterator<Item = &'a CommitId>,
1814 ) -> Result<(), CommandError> {
1815 let Some((commit_id, lower_bound, upper_bound)) = self
1816 .env
1817 .find_immutable_commit(self.repo().as_ref(), commits)?
1818 else {
1819 return Ok(());
1820 };
1821 let error = if &commit_id == self.repo().store().root_commit_id() {
1822 user_error(format!("The root commit {commit_id:.12} is immutable"))
1823 } else {
1824 let mut error = user_error(format!("Commit {commit_id:.12} is immutable"));
1825 let commit = self.repo().store().get_commit(&commit_id)?;
1826 error.add_formatted_hint_with(|formatter| {
1827 write!(formatter, "Could not modify commit: ")?;
1828 self.write_commit_summary(formatter, &commit)?;
1829 Ok(())
1830 });
1831 error.add_hint("Immutable commits are used to protect shared history.");
1832 error.add_hint(indoc::indoc! {"
1833 For more information, see:
1834 - https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
1835 - `jj help -k config`, \"Set of immutable commits\""});
1836
1837 let exact = upper_bound == Some(lower_bound);
1838 let or_more = if exact { "" } else { " or more" };
1839 error.add_hint(format!(
1840 "This operation would rewrite {lower_bound}{or_more} immutable commits."
1841 ));
1842
1843 error
1844 };
1845 Err(error)
1846 }
1847
1848 #[instrument(skip_all)]
1849 fn snapshot_working_copy(
1850 &mut self,
1851 ui: &Ui,
1852 ) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
1853 let workspace_name = self.workspace_name().to_owned();
1854 let get_wc_commit = |repo: &ReadonlyRepo| -> Result<Option<_>, _> {
1855 repo.view()
1856 .get_wc_commit_id(&workspace_name)
1857 .map(|id| repo.store().get_commit(id))
1858 .transpose()
1859 .map_err(snapshot_command_error)
1860 };
1861 let repo = self.repo().clone();
1862 let Some(wc_commit) = get_wc_commit(&repo)? else {
1863 return Ok(SnapshotStats::default());
1866 };
1867 let auto_tracking_matcher = self
1868 .auto_tracking_matcher(ui)
1869 .map_err(snapshot_command_error)?;
1870 let options = self
1871 .snapshot_options_with_start_tracking_matcher(&auto_tracking_matcher)
1872 .map_err(snapshot_command_error)?;
1873
1874 let mut locked_ws = self
1876 .workspace
1877 .start_working_copy_mutation()
1878 .map_err(snapshot_command_error)?;
1879 let old_op_id = locked_ws.locked_wc().old_operation_id().clone();
1880
1881 let (repo, wc_commit) =
1882 match WorkingCopyFreshness::check_stale(locked_ws.locked_wc(), &wc_commit, &repo) {
1883 Ok(WorkingCopyFreshness::Fresh) => (repo, wc_commit),
1884 Ok(WorkingCopyFreshness::Updated(wc_operation)) => {
1885 let repo = repo
1886 .reload_at(&wc_operation)
1887 .map_err(snapshot_command_error)?;
1888 let wc_commit = if let Some(wc_commit) = get_wc_commit(&repo)? {
1889 wc_commit
1890 } else {
1891 return Ok(SnapshotStats::default());
1893 };
1894 (repo, wc_commit)
1895 }
1896 Ok(WorkingCopyFreshness::WorkingCopyStale) => {
1897 return Err(SnapshotWorkingCopyError::StaleWorkingCopy(
1898 user_error_with_hint(
1899 format!(
1900 "The working copy is stale (not updated since operation {}).",
1901 short_operation_hash(&old_op_id)
1902 ),
1903 "Run `jj workspace update-stale` to update it.
1904See https://jj-vcs.github.io/jj/latest/working-copy/#stale-working-copy \
1905 for more information.",
1906 ),
1907 ));
1908 }
1909 Ok(WorkingCopyFreshness::SiblingOperation) => {
1910 return Err(SnapshotWorkingCopyError::StaleWorkingCopy(internal_error(
1911 format!(
1912 "The repo was loaded at operation {}, which seems to be a sibling of \
1913 the working copy's operation {}",
1914 short_operation_hash(repo.op_id()),
1915 short_operation_hash(&old_op_id)
1916 ),
1917 )));
1918 }
1919 Err(OpStoreError::ObjectNotFound { .. }) => {
1920 return Err(SnapshotWorkingCopyError::StaleWorkingCopy(
1921 user_error_with_hint(
1922 "Could not read working copy's operation.",
1923 "Run `jj workspace update-stale` to recover.
1924See https://jj-vcs.github.io/jj/latest/working-copy/#stale-working-copy \
1925 for more information.",
1926 ),
1927 ));
1928 }
1929 Err(e) => return Err(snapshot_command_error(e)),
1930 };
1931 self.user_repo = ReadonlyUserRepo::new(repo);
1932 let (new_tree_id, stats) = {
1933 let mut options = options;
1934 let progress = crate::progress::snapshot_progress(ui);
1935 options.progress = progress.as_ref().map(|x| x as _);
1936 locked_ws
1937 .locked_wc()
1938 .snapshot(&options)
1939 .map_err(snapshot_command_error)?
1940 };
1941 if new_tree_id != *wc_commit.tree_id() {
1942 let mut tx =
1943 start_repo_transaction(&self.user_repo.repo, self.env.command.string_args());
1944 tx.set_is_snapshot(true);
1945 let mut_repo = tx.repo_mut();
1946 let commit = mut_repo
1947 .rewrite_commit(&wc_commit)
1948 .set_tree_id(new_tree_id)
1949 .write()
1950 .map_err(snapshot_command_error)?;
1951 mut_repo
1952 .set_wc_commit(workspace_name, commit.id().clone())
1953 .map_err(snapshot_command_error)?;
1954
1955 let num_rebased = mut_repo
1957 .rebase_descendants()
1958 .map_err(snapshot_command_error)?;
1959 if num_rebased > 0 {
1960 writeln!(
1961 ui.status(),
1962 "Rebased {num_rebased} descendant commits onto updated working copy"
1963 )
1964 .map_err(snapshot_command_error)?;
1965 }
1966
1967 #[cfg(feature = "git")]
1968 if self.working_copy_shared_with_git {
1969 let old_tree = wc_commit.tree().map_err(snapshot_command_error)?;
1970 let new_tree = commit.tree().map_err(snapshot_command_error)?;
1971 jj_lib::git::update_intent_to_add(
1972 self.user_repo.repo.as_ref(),
1973 &old_tree,
1974 &new_tree,
1975 )
1976 .map_err(snapshot_command_error)?;
1977
1978 let stats = jj_lib::git::export_refs(mut_repo).map_err(snapshot_command_error)?;
1979 crate::git_util::print_git_export_stats(ui, &stats)
1980 .map_err(snapshot_command_error)?;
1981 }
1982
1983 let repo = tx
1984 .commit("snapshot working copy")
1985 .map_err(snapshot_command_error)?;
1986 self.user_repo = ReadonlyUserRepo::new(repo);
1987 }
1988 locked_ws
1989 .finish(self.user_repo.repo.op_id().clone())
1990 .map_err(snapshot_command_error)?;
1991 Ok(stats)
1992 }
1993
1994 fn update_working_copy(
1995 &mut self,
1996 ui: &Ui,
1997 maybe_old_commit: Option<&Commit>,
1998 new_commit: &Commit,
1999 ) -> Result<(), CommandError> {
2000 assert!(self.may_update_working_copy);
2001 let checkout_options = self.checkout_options();
2002 let stats = update_working_copy(
2003 &self.user_repo.repo,
2004 &mut self.workspace,
2005 maybe_old_commit,
2006 new_commit,
2007 &checkout_options,
2008 )?;
2009 self.print_updated_working_copy_stats(ui, maybe_old_commit, new_commit, &stats)
2010 }
2011
2012 fn print_updated_working_copy_stats(
2013 &self,
2014 ui: &Ui,
2015 maybe_old_commit: Option<&Commit>,
2016 new_commit: &Commit,
2017 stats: &CheckoutStats,
2018 ) -> Result<(), CommandError> {
2019 if Some(new_commit) != maybe_old_commit {
2020 if let Some(mut formatter) = ui.status_formatter() {
2021 let template = self.commit_summary_template();
2022 write!(formatter, "Working copy (@) now at: ")?;
2023 template.format(new_commit, formatter.as_mut())?;
2024 writeln!(formatter)?;
2025 for parent in new_commit.parents() {
2026 let parent = parent?;
2027 write!(formatter, "Parent commit (@-) : ")?;
2029 template.format(&parent, formatter.as_mut())?;
2030 writeln!(formatter)?;
2031 }
2032 }
2033 }
2034 print_checkout_stats(ui, stats, new_commit)?;
2035 if Some(new_commit) != maybe_old_commit {
2036 if let Some(mut formatter) = ui.status_formatter() {
2037 if new_commit.has_conflict()? {
2038 let conflicts = new_commit.tree()?.conflicts().collect_vec();
2039 writeln!(
2040 formatter.labeled("warning").with_heading("Warning: "),
2041 "There are unresolved conflicts at these paths:"
2042 )?;
2043 print_conflicted_paths(conflicts, formatter.as_mut(), self)?;
2044 }
2045 }
2046 }
2047 Ok(())
2048 }
2049
2050 pub fn start_transaction(&mut self) -> WorkspaceCommandTransaction {
2051 let tx = start_repo_transaction(self.repo(), self.env.command.string_args());
2052 let id_prefix_context = mem::take(&mut self.user_repo.id_prefix_context);
2053 WorkspaceCommandTransaction {
2054 helper: self,
2055 tx,
2056 id_prefix_context,
2057 }
2058 }
2059
2060 fn finish_transaction(
2061 &mut self,
2062 ui: &Ui,
2063 mut tx: Transaction,
2064 description: impl Into<String>,
2065 ) -> Result<(), CommandError> {
2066 if !tx.repo().has_changes() {
2067 writeln!(ui.status(), "Nothing changed.")?;
2068 return Ok(());
2069 }
2070 let num_rebased = tx.repo_mut().rebase_descendants()?;
2071 if num_rebased > 0 {
2072 writeln!(ui.status(), "Rebased {num_rebased} descendant commits")?;
2073 }
2074
2075 for (name, wc_commit_id) in &tx.repo().view().wc_commit_ids().clone() {
2076 if self
2077 .env
2078 .find_immutable_commit(tx.repo(), [wc_commit_id])?
2079 .is_some()
2080 {
2081 let wc_commit = tx.repo().store().get_commit(wc_commit_id)?;
2082 tx.repo_mut().check_out(name.clone(), &wc_commit)?;
2083 writeln!(
2084 ui.warning_default(),
2085 "The working-copy commit in workspace '{name}' became immutable, so a new \
2086 commit has been created on top of it.",
2087 name = name.as_symbol()
2088 )?;
2089 }
2090 }
2091
2092 let old_repo = tx.base_repo().clone();
2093
2094 let maybe_old_wc_commit = old_repo
2095 .view()
2096 .get_wc_commit_id(self.workspace_name())
2097 .map(|commit_id| tx.base_repo().store().get_commit(commit_id))
2098 .transpose()?;
2099 let maybe_new_wc_commit = tx
2100 .repo()
2101 .view()
2102 .get_wc_commit_id(self.workspace_name())
2103 .map(|commit_id| tx.repo().store().get_commit(commit_id))
2104 .transpose()?;
2105
2106 #[cfg(feature = "git")]
2107 if self.working_copy_shared_with_git {
2108 use std::error::Error as _;
2109 if let Some(wc_commit) = &maybe_new_wc_commit {
2110 match jj_lib::git::reset_head(tx.repo_mut(), wc_commit) {
2113 Ok(()) => {}
2114 Err(err @ jj_lib::git::GitResetHeadError::UpdateHeadRef(_)) => {
2115 writeln!(ui.warning_default(), "{err}")?;
2116 crate::command_error::print_error_sources(ui, err.source())?;
2117 }
2118 Err(err) => return Err(err.into()),
2119 }
2120 }
2121 let stats = jj_lib::git::export_refs(tx.repo_mut())?;
2122 crate::git_util::print_git_export_stats(ui, &stats)?;
2123 }
2124
2125 self.user_repo = ReadonlyUserRepo::new(tx.commit(description)?);
2126
2127 if self.may_update_working_copy {
2131 if let Some(new_commit) = &maybe_new_wc_commit {
2132 self.update_working_copy(ui, maybe_old_wc_commit.as_ref(), new_commit)?;
2133 } else {
2134 }
2137 }
2138
2139 self.report_repo_changes(ui, &old_repo)?;
2140
2141 let settings = self.settings();
2142 let missing_user_name = settings.user_name().is_empty();
2143 let missing_user_mail = settings.user_email().is_empty();
2144 if missing_user_name || missing_user_mail {
2145 let not_configured_msg = match (missing_user_name, missing_user_mail) {
2146 (true, true) => "Name and email not configured.",
2147 (true, false) => "Name not configured.",
2148 (false, true) => "Email not configured.",
2149 _ => unreachable!(),
2150 };
2151 writeln!(
2152 ui.warning_default(),
2153 "{not_configured_msg} Until configured, your commits will be created with the \
2154 empty identity, and can't be pushed to remotes."
2155 )?;
2156 writeln!(ui.hint_default(), "To configure, run:")?;
2157 if missing_user_name {
2158 writeln!(
2159 ui.hint_no_heading(),
2160 r#" jj config set --user user.name "Some One""#
2161 )?;
2162 }
2163 if missing_user_mail {
2164 writeln!(
2165 ui.hint_no_heading(),
2166 r#" jj config set --user user.email "someone@example.com""#
2167 )?;
2168 }
2169 }
2170 Ok(())
2171 }
2172
2173 fn report_repo_changes(
2176 &self,
2177 ui: &Ui,
2178 old_repo: &Arc<ReadonlyRepo>,
2179 ) -> Result<(), CommandError> {
2180 let Some(mut fmt) = ui.status_formatter() else {
2181 return Ok(());
2182 };
2183 let old_view = old_repo.view();
2184 let new_repo = self.repo().as_ref();
2185 let new_view = new_repo.view();
2186 let old_heads = RevsetExpression::commits(old_view.heads().iter().cloned().collect());
2187 let new_heads = RevsetExpression::commits(new_view.heads().iter().cloned().collect());
2188 let conflicts = RevsetExpression::filter(RevsetFilterPredicate::HasConflict)
2194 .filtered(RevsetFilterPredicate::File(FilesetExpression::all()));
2195 let removed_conflicts_expr = new_heads.range(&old_heads).intersection(&conflicts);
2196 let added_conflicts_expr = old_heads.range(&new_heads).intersection(&conflicts);
2197
2198 let get_commits =
2199 |expr: Rc<ResolvedRevsetExpression>| -> Result<Vec<Commit>, CommandError> {
2200 let commits = expr
2201 .evaluate(new_repo)?
2202 .iter()
2203 .commits(new_repo.store())
2204 .try_collect()?;
2205 Ok(commits)
2206 };
2207 let removed_conflict_commits = get_commits(removed_conflicts_expr)?;
2208 let added_conflict_commits = get_commits(added_conflicts_expr)?;
2209
2210 fn commits_by_change_id(commits: &[Commit]) -> IndexMap<&ChangeId, Vec<&Commit>> {
2211 let mut result: IndexMap<&ChangeId, Vec<&Commit>> = IndexMap::new();
2212 for commit in commits {
2213 result.entry(commit.change_id()).or_default().push(commit);
2214 }
2215 result
2216 }
2217 let removed_conflicts_by_change_id = commits_by_change_id(&removed_conflict_commits);
2218 let added_conflicts_by_change_id = commits_by_change_id(&added_conflict_commits);
2219 let mut resolved_conflicts_by_change_id = removed_conflicts_by_change_id.clone();
2220 resolved_conflicts_by_change_id
2221 .retain(|change_id, _commits| !added_conflicts_by_change_id.contains_key(change_id));
2222 let mut new_conflicts_by_change_id = added_conflicts_by_change_id.clone();
2223 new_conflicts_by_change_id
2224 .retain(|change_id, _commits| !removed_conflicts_by_change_id.contains_key(change_id));
2225
2226 if !resolved_conflicts_by_change_id.is_empty() {
2228 let num_resolved: usize = resolved_conflicts_by_change_id
2232 .values()
2233 .map(|commits| commits.len())
2234 .sum();
2235 writeln!(
2236 fmt,
2237 "Existing conflicts were resolved or abandoned from {num_resolved} commits."
2238 )?;
2239 }
2240 if !new_conflicts_by_change_id.is_empty() {
2241 let num_conflicted: usize = new_conflicts_by_change_id
2242 .values()
2243 .map(|commits| commits.len())
2244 .sum();
2245 writeln!(fmt, "New conflicts appeared in {num_conflicted} commits:")?;
2246 print_updated_commits(
2247 fmt.as_mut(),
2248 &self.commit_summary_template(),
2249 new_conflicts_by_change_id.values().flatten().copied(),
2250 )?;
2251 }
2252
2253 if !(added_conflict_commits.is_empty()
2257 || resolved_conflicts_by_change_id.is_empty() && new_conflicts_by_change_id.is_empty())
2258 {
2259 if new_conflicts_by_change_id.is_empty() {
2264 writeln!(
2265 fmt,
2266 "There are still unresolved conflicts in rebased descendants.",
2267 )?;
2268 }
2269
2270 self.report_repo_conflicts(
2271 fmt.as_mut(),
2272 new_repo,
2273 added_conflict_commits
2274 .iter()
2275 .map(|commit| commit.id().clone())
2276 .collect(),
2277 )?;
2278 }
2279 revset_util::warn_unresolvable_trunk(ui, new_repo, &self.env.revset_parse_context())?;
2280
2281 Ok(())
2282 }
2283
2284 pub fn report_repo_conflicts(
2285 &self,
2286 fmt: &mut dyn Formatter,
2287 repo: &ReadonlyRepo,
2288 conflicted_commits: Vec<CommitId>,
2289 ) -> Result<(), CommandError> {
2290 if !self.settings().get_bool("hints.resolving-conflicts")? || conflicted_commits.is_empty()
2291 {
2292 return Ok(());
2293 }
2294
2295 let only_one_conflicted_commit = conflicted_commits.len() == 1;
2296 let root_conflicts_revset = RevsetExpression::commits(conflicted_commits)
2297 .roots()
2298 .evaluate(repo)?;
2299
2300 let root_conflict_commits: Vec<_> = root_conflicts_revset
2301 .iter()
2302 .commits(repo.store())
2303 .try_collect()?;
2304
2305 let instruction = if only_one_conflicted_commit {
2307 indoc! {"
2308 To resolve the conflicts, start by creating a commit on top of
2309 the conflicted commit:
2310 "}
2311 } else if root_conflict_commits.len() == 1 {
2312 indoc! {"
2313 To resolve the conflicts, start by creating a commit on top of
2314 the first conflicted commit:
2315 "}
2316 } else {
2317 indoc! {"
2318 To resolve the conflicts, start by creating a commit on top of
2319 one of the first conflicted commits:
2320 "}
2321 };
2322 write!(fmt.labeled("hint").with_heading("Hint: "), "{instruction}")?;
2323 let format_short_change_id = self.short_change_id_template();
2324 fmt.with_label("hint", |fmt| {
2325 for commit in &root_conflict_commits {
2326 write!(fmt, " jj new ")?;
2327 format_short_change_id.format(commit, fmt)?;
2328 writeln!(fmt)?;
2329 }
2330 io::Result::Ok(())
2331 })?;
2332 writedoc!(
2333 fmt.labeled("hint"),
2334 "
2335 Then use `jj resolve`, or edit the conflict markers in the file directly.
2336 Once the conflicts are resolved, you can inspect the result with `jj diff`.
2337 Then run `jj squash` to move the resolution into the conflicted commit.
2338 ",
2339 )?;
2340 Ok(())
2341 }
2342
2343 pub fn get_advanceable_bookmarks<'a>(
2358 &self,
2359 from: impl IntoIterator<Item = &'a CommitId>,
2360 ) -> Result<Vec<AdvanceableBookmark>, CommandError> {
2361 let ab_settings = AdvanceBookmarksSettings::from_settings(self.settings())?;
2362 if !ab_settings.feature_enabled() {
2363 return Ok(Vec::new());
2365 }
2366
2367 let mut advanceable_bookmarks = Vec::new();
2368 for from_commit in from {
2369 for (name, _) in self.repo().view().local_bookmarks_for_commit(from_commit) {
2370 if ab_settings.bookmark_is_eligible(name) {
2371 advanceable_bookmarks.push(AdvanceableBookmark {
2372 name: name.to_owned(),
2373 old_commit_id: from_commit.clone(),
2374 });
2375 }
2376 }
2377 }
2378
2379 Ok(advanceable_bookmarks)
2380 }
2381}
2382
2383#[must_use]
2391pub struct WorkspaceCommandTransaction<'a> {
2392 helper: &'a mut WorkspaceCommandHelper,
2393 tx: Transaction,
2394 id_prefix_context: OnceCell<IdPrefixContext>,
2396}
2397
2398impl WorkspaceCommandTransaction<'_> {
2399 pub fn base_workspace_helper(&self) -> &WorkspaceCommandHelper {
2401 self.helper
2402 }
2403
2404 pub fn settings(&self) -> &UserSettings {
2406 self.helper.settings()
2407 }
2408
2409 pub fn base_repo(&self) -> &Arc<ReadonlyRepo> {
2410 self.tx.base_repo()
2411 }
2412
2413 pub fn repo(&self) -> &MutableRepo {
2414 self.tx.repo()
2415 }
2416
2417 pub fn repo_mut(&mut self) -> &mut MutableRepo {
2418 self.id_prefix_context.take(); self.tx.repo_mut()
2420 }
2421
2422 pub fn check_out(&mut self, commit: &Commit) -> Result<Commit, CheckOutCommitError> {
2423 let name = self.helper.workspace_name().to_owned();
2424 self.id_prefix_context.take(); self.tx.repo_mut().check_out(name, commit)
2426 }
2427
2428 pub fn edit(&mut self, commit: &Commit) -> Result<(), EditCommitError> {
2429 let name = self.helper.workspace_name().to_owned();
2430 self.id_prefix_context.take(); self.tx.repo_mut().edit(name, commit)
2432 }
2433
2434 pub fn format_commit_summary(&self, commit: &Commit) -> String {
2435 let mut output = Vec::new();
2436 self.write_commit_summary(&mut PlainTextFormatter::new(&mut output), commit)
2437 .expect("write() to PlainTextFormatter should never fail");
2438 output.into_string_lossy()
2440 }
2441
2442 pub fn write_commit_summary(
2443 &self,
2444 formatter: &mut dyn Formatter,
2445 commit: &Commit,
2446 ) -> std::io::Result<()> {
2447 self.commit_summary_template().format(commit, formatter)
2448 }
2449
2450 pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
2452 let language = self.commit_template_language();
2453 self.helper
2454 .reparse_valid_template(&language, &self.helper.commit_summary_template_text)
2455 .labeled(["commit"])
2456 }
2457
2458 pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
2461 let id_prefix_context = self
2462 .id_prefix_context
2463 .get_or_init(|| self.helper.env.new_id_prefix_context());
2464 self.helper
2465 .env
2466 .commit_template_language(self.tx.repo(), id_prefix_context)
2467 }
2468
2469 pub fn parse_commit_template(
2471 &self,
2472 ui: &Ui,
2473 template_text: &str,
2474 ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
2475 let language = self.commit_template_language();
2476 self.helper.env.parse_template(ui, &language, template_text)
2477 }
2478
2479 pub fn finish(self, ui: &Ui, description: impl Into<String>) -> Result<(), CommandError> {
2480 self.helper.finish_transaction(ui, self.tx, description)
2481 }
2482
2483 pub fn into_inner(self) -> Transaction {
2488 self.tx
2489 }
2490
2491 pub fn advance_bookmarks(&mut self, bookmarks: Vec<AdvanceableBookmark>, move_to: &CommitId) {
2497 for bookmark in bookmarks {
2498 self.repo_mut().merge_local_bookmark(
2501 &bookmark.name,
2502 &RefTarget::normal(bookmark.old_commit_id),
2503 &RefTarget::normal(move_to.clone()),
2504 );
2505 }
2506 }
2507}
2508
2509pub fn find_workspace_dir(cwd: &Path) -> &Path {
2510 cwd.ancestors()
2511 .find(|path| path.join(".jj").is_dir())
2512 .unwrap_or(cwd)
2513}
2514
2515fn map_workspace_load_error(err: WorkspaceLoadError, user_wc_path: Option<&str>) -> CommandError {
2516 match err {
2517 WorkspaceLoadError::NoWorkspaceHere(wc_path) => {
2518 let short_wc_path = user_wc_path.map_or(wc_path.as_ref(), Path::new);
2520 let message = format!(r#"There is no jj repo in "{}""#, short_wc_path.display());
2521 let git_dir = wc_path.join(".git");
2522 if git_dir.is_dir() {
2523 user_error_with_hint(
2524 message,
2525 "It looks like this is a git repo. You can create a jj repo backed by it by \
2526 running this:
2527jj git init --colocate",
2528 )
2529 } else {
2530 user_error(message)
2531 }
2532 }
2533 WorkspaceLoadError::RepoDoesNotExist(repo_dir) => user_error(format!(
2534 "The repository directory at {} is missing. Was it moved?",
2535 repo_dir.display(),
2536 )),
2537 WorkspaceLoadError::StoreLoadError(err @ StoreLoadError::UnsupportedType { .. }) => {
2538 internal_error_with_message(
2539 "This version of the jj binary doesn't support this type of repo",
2540 err,
2541 )
2542 }
2543 WorkspaceLoadError::StoreLoadError(
2544 err @ (StoreLoadError::ReadError { .. } | StoreLoadError::Backend(_)),
2545 ) => internal_error_with_message("The repository appears broken or inaccessible", err),
2546 WorkspaceLoadError::StoreLoadError(StoreLoadError::Signing(err)) => user_error(err),
2547 WorkspaceLoadError::WorkingCopyState(err) => internal_error(err),
2548 WorkspaceLoadError::NonUnicodePath | WorkspaceLoadError::Path(_) => user_error(err),
2549 }
2550}
2551
2552pub fn start_repo_transaction(repo: &Arc<ReadonlyRepo>, string_args: &[String]) -> Transaction {
2553 let mut tx = repo.start_transaction();
2554 let shell_escape = |arg: &String| {
2557 if arg.as_bytes().iter().all(|b| {
2558 matches!(b,
2559 b'A'..=b'Z'
2560 | b'a'..=b'z'
2561 | b'0'..=b'9'
2562 | b','
2563 | b'-'
2564 | b'.'
2565 | b'/'
2566 | b':'
2567 | b'@'
2568 | b'_'
2569 )
2570 }) {
2571 arg.clone()
2572 } else {
2573 format!("'{}'", arg.replace('\'', "\\'"))
2574 }
2575 };
2576 let mut quoted_strings = vec!["jj".to_string()];
2577 quoted_strings.extend(string_args.iter().skip(1).map(shell_escape));
2578 tx.set_tag("args".to_string(), quoted_strings.join(" "));
2579 tx
2580}
2581
2582fn update_stale_working_copy(
2583 mut locked_ws: LockedWorkspace,
2584 op_id: OperationId,
2585 stale_commit: &Commit,
2586 new_commit: &Commit,
2587 options: &CheckoutOptions,
2588) -> Result<CheckoutStats, CommandError> {
2589 if stale_commit.tree_id() != locked_ws.locked_wc().old_tree_id() {
2592 return Err(user_error("Concurrent working copy operation. Try again."));
2593 }
2594 let stats = locked_ws
2595 .locked_wc()
2596 .check_out(new_commit, options)
2597 .map_err(|err| {
2598 internal_error_with_message(
2599 format!("Failed to check out commit {}", new_commit.id().hex()),
2600 err,
2601 )
2602 })?;
2603 locked_ws.finish(op_id)?;
2604
2605 Ok(stats)
2606}
2607
2608pub fn print_updated_commits<'a>(
2611 formatter: &mut dyn Formatter,
2612 template: &TemplateRenderer<Commit>,
2613 commits: impl IntoIterator<Item = &'a Commit>,
2614) -> io::Result<()> {
2615 let mut commits = commits.into_iter().fuse();
2616 for commit in commits.by_ref().take(10) {
2617 write!(formatter, " ")?;
2618 template.format(commit, formatter)?;
2619 writeln!(formatter)?;
2620 }
2621 if commits.next().is_some() {
2622 writeln!(formatter, " ...")?;
2623 }
2624 Ok(())
2625}
2626
2627#[instrument(skip_all)]
2628pub fn print_conflicted_paths(
2629 conflicts: Vec<(RepoPathBuf, BackendResult<MergedTreeValue>)>,
2630 formatter: &mut dyn Formatter,
2631 workspace_command: &WorkspaceCommandHelper,
2632) -> Result<(), CommandError> {
2633 let formatted_paths = conflicts
2634 .iter()
2635 .map(|(path, _conflict)| workspace_command.format_file_path(path))
2636 .collect_vec();
2637 let max_path_len = formatted_paths.iter().map(|p| p.len()).max().unwrap_or(0);
2638 let formatted_paths = formatted_paths
2639 .into_iter()
2640 .map(|p| format!("{:width$}", p, width = max_path_len.min(32) + 3));
2641
2642 for ((_, conflict), formatted_path) in std::iter::zip(conflicts, formatted_paths) {
2643 let conflict = conflict?.simplify();
2646 let sides = conflict.num_sides();
2647 let n_adds = conflict.adds().flatten().count();
2648 let deletions = sides - n_adds;
2649
2650 let mut seen_objects = BTreeMap::new(); if deletions > 0 {
2652 seen_objects.insert(
2653 format!(
2654 "{deletions} deletion{}",
2656 if deletions > 1 { "s" } else { "" }
2657 ),
2658 "normal", );
2660 }
2661 for term in itertools::chain(conflict.removes(), conflict.adds()).flatten() {
2665 seen_objects.insert(
2666 match term {
2667 TreeValue::File {
2668 executable: false, ..
2669 } => continue,
2670 TreeValue::File {
2671 executable: true, ..
2672 } => "an executable",
2673 TreeValue::Symlink(_) => "a symlink",
2674 TreeValue::Tree(_) => "a directory",
2675 TreeValue::GitSubmodule(_) => "a git submodule",
2676 TreeValue::Conflict(_) => "another conflict (you found a bug!)",
2677 }
2678 .to_string(),
2679 "difficult",
2680 );
2681 }
2682
2683 write!(formatter, "{formatted_path} ")?;
2684 formatter.with_label("conflict_description", |formatter| {
2685 let print_pair = |formatter: &mut dyn Formatter, (text, label): &(String, &str)| {
2686 write!(formatter.labeled(label), "{text}")
2687 };
2688 print_pair(
2689 formatter,
2690 &(
2691 format!("{sides}-sided"),
2692 if sides > 2 { "difficult" } else { "normal" },
2693 ),
2694 )?;
2695 write!(formatter, " conflict")?;
2696
2697 if !seen_objects.is_empty() {
2698 write!(formatter, " including ")?;
2699 let seen_objects = seen_objects.into_iter().collect_vec();
2700 match &seen_objects[..] {
2701 [] => unreachable!(),
2702 [only] => print_pair(formatter, only)?,
2703 [first, middle @ .., last] => {
2704 print_pair(formatter, first)?;
2705 for pair in middle {
2706 write!(formatter, ", ")?;
2707 print_pair(formatter, pair)?;
2708 }
2709 write!(formatter, " and ")?;
2710 print_pair(formatter, last)?;
2711 }
2712 };
2713 }
2714 io::Result::Ok(())
2715 })?;
2716 writeln!(formatter)?;
2717 }
2718 Ok(())
2719}
2720
2721fn build_untracked_reason_message(reason: &UntrackedReason) -> Option<String> {
2723 match reason {
2724 UntrackedReason::FileTooLarge { size, max_size } => {
2725 let size_approx = HumanByteSize(*size);
2728 let max_size_approx = HumanByteSize(*max_size);
2729 Some(format!(
2730 "{size_approx} ({size} bytes); the maximum size allowed is {max_size_approx} \
2731 ({max_size} bytes)",
2732 ))
2733 }
2734 UntrackedReason::FileNotAutoTracked => None,
2738 }
2739}
2740
2741pub fn print_untracked_files(
2743 ui: &Ui,
2744 untracked_paths: &BTreeMap<RepoPathBuf, UntrackedReason>,
2745 path_converter: &RepoPathUiConverter,
2746) -> io::Result<()> {
2747 let mut untracked_paths = untracked_paths
2748 .iter()
2749 .filter_map(|(path, reason)| build_untracked_reason_message(reason).map(|m| (path, m)))
2750 .peekable();
2751
2752 if untracked_paths.peek().is_some() {
2753 writeln!(ui.warning_default(), "Refused to snapshot some files:")?;
2754 let mut formatter = ui.stderr_formatter();
2755 for (path, message) in untracked_paths {
2756 let ui_path = path_converter.format_file_path(path);
2757 writeln!(formatter, " {ui_path}: {message}")?;
2758 }
2759 }
2760
2761 Ok(())
2762}
2763
2764pub fn print_snapshot_stats(
2765 ui: &Ui,
2766 stats: &SnapshotStats,
2767 path_converter: &RepoPathUiConverter,
2768) -> io::Result<()> {
2769 print_untracked_files(ui, &stats.untracked_paths, path_converter)?;
2770
2771 let large_files_sizes = stats
2772 .untracked_paths
2773 .values()
2774 .filter_map(|reason| match reason {
2775 UntrackedReason::FileTooLarge { size, .. } => Some(size),
2776 UntrackedReason::FileNotAutoTracked => None,
2777 });
2778 if let Some(size) = large_files_sizes.max() {
2779 writedoc!(
2780 ui.hint_default(),
2781 r"
2782 This is to prevent large files from being added by accident. You can fix this by:
2783 - Adding the file to `.gitignore`
2784 - Run `jj config set --repo snapshot.max-new-file-size {size}`
2785 This will increase the maximum file size allowed for new files, in this repository only.
2786 - Run `jj --config snapshot.max-new-file-size={size} st`
2787 This will increase the maximum file size allowed for new files, for this command only.
2788 "
2789 )?;
2790 }
2791 Ok(())
2792}
2793
2794pub fn print_checkout_stats(
2795 ui: &Ui,
2796 stats: &CheckoutStats,
2797 new_commit: &Commit,
2798) -> Result<(), std::io::Error> {
2799 if stats.added_files > 0 || stats.updated_files > 0 || stats.removed_files > 0 {
2800 writeln!(
2801 ui.status(),
2802 "Added {} files, modified {} files, removed {} files",
2803 stats.added_files,
2804 stats.updated_files,
2805 stats.removed_files
2806 )?;
2807 }
2808 if stats.skipped_files != 0 {
2809 writeln!(
2810 ui.warning_default(),
2811 "{} of those updates were skipped because there were conflicting changes in the \
2812 working copy.",
2813 stats.skipped_files
2814 )?;
2815 writeln!(
2816 ui.hint_default(),
2817 "Inspect the changes compared to the intended target with `jj diff --from {}`.
2818Discard the conflicting changes with `jj restore --from {}`.",
2819 short_commit_hash(new_commit.id()),
2820 short_commit_hash(new_commit.id())
2821 )?;
2822 }
2823 Ok(())
2824}
2825
2826pub fn print_unmatched_explicit_paths<'a>(
2829 ui: &Ui,
2830 workspace_command: &WorkspaceCommandHelper,
2831 expression: &FilesetExpression,
2832 trees: impl IntoIterator<Item = &'a MergedTree>,
2833) -> io::Result<()> {
2834 let mut explicit_paths = expression.explicit_paths().collect_vec();
2835 for tree in trees {
2836 explicit_paths.retain(|&path| tree.path_value(path).unwrap().is_absent());
2838 if explicit_paths.is_empty() {
2839 return Ok(());
2840 }
2841 }
2842 let ui_paths = explicit_paths
2843 .iter()
2844 .map(|&path| workspace_command.format_file_path(path))
2845 .join(", ");
2846 writeln!(
2847 ui.warning_default(),
2848 "No matching entries for paths: {ui_paths}"
2849 )?;
2850 Ok(())
2851}
2852
2853pub fn update_working_copy(
2854 repo: &Arc<ReadonlyRepo>,
2855 workspace: &mut Workspace,
2856 old_commit: Option<&Commit>,
2857 new_commit: &Commit,
2858 options: &CheckoutOptions,
2859) -> Result<CheckoutStats, CommandError> {
2860 let old_tree_id = old_commit.map(|commit| commit.tree_id().clone());
2861 let stats = workspace
2864 .check_out(
2865 repo.op_id().clone(),
2866 old_tree_id.as_ref(),
2867 new_commit,
2868 options,
2869 )
2870 .map_err(|err| {
2871 internal_error_with_message(
2872 format!("Failed to check out commit {}", new_commit.id().hex()),
2873 err,
2874 )
2875 })?;
2876 Ok(stats)
2877}
2878
2879pub fn has_tracked_remote_bookmarks(view: &View, bookmark: &RefName) -> bool {
2882 view.remote_bookmarks_matching(
2883 &StringPattern::exact(bookmark),
2884 &StringPattern::everything(),
2885 )
2886 .filter(|&(symbol, _)| !jj_lib::git::is_special_git_remote(symbol.remote))
2887 .any(|(_, remote_ref)| remote_ref.is_tracked())
2888}
2889
2890pub fn load_template_aliases(
2891 ui: &Ui,
2892 stacked_config: &StackedConfig,
2893) -> Result<TemplateAliasesMap, CommandError> {
2894 let table_name = ConfigNamePathBuf::from_iter(["template-aliases"]);
2895 let mut aliases_map = TemplateAliasesMap::new();
2896 for layer in stacked_config.layers() {
2899 let table = match layer.look_up_table(&table_name) {
2900 Ok(Some(table)) => table,
2901 Ok(None) => continue,
2902 Err(item) => {
2903 return Err(ConfigGetError::Type {
2904 name: table_name.to_string(),
2905 error: format!("Expected a table, but is {}", item.type_name()).into(),
2906 source_path: layer.path.clone(),
2907 }
2908 .into());
2909 }
2910 };
2911 for (decl, item) in table.iter() {
2912 let r = item
2913 .as_str()
2914 .ok_or_else(|| format!("Expected a string, but is {}", item.type_name()))
2915 .and_then(|v| aliases_map.insert(decl, v).map_err(|e| e.to_string()));
2916 if let Err(s) = r {
2917 writeln!(
2918 ui.warning_default(),
2919 "Failed to load `{table_name}.{decl}`: {s}"
2920 )?;
2921 }
2922 }
2923 }
2924 Ok(aliases_map)
2925}
2926
2927#[derive(Clone, Debug)]
2929pub struct LogContentFormat {
2930 width: usize,
2931 word_wrap: bool,
2932}
2933
2934impl LogContentFormat {
2935 pub fn new(ui: &Ui, settings: &UserSettings) -> Result<Self, ConfigGetError> {
2937 Ok(LogContentFormat {
2938 width: ui.term_width(),
2939 word_wrap: settings.get_bool("ui.log-word-wrap")?,
2940 })
2941 }
2942
2943 #[must_use]
2945 pub fn sub_width(&self, width: usize) -> Self {
2946 LogContentFormat {
2947 width: self.width.saturating_sub(width),
2948 word_wrap: self.word_wrap,
2949 }
2950 }
2951
2952 pub fn width(&self) -> usize {
2954 self.width
2955 }
2956
2957 pub fn write<E: From<io::Error>>(
2959 &self,
2960 formatter: &mut dyn Formatter,
2961 content_fn: impl FnOnce(&mut dyn Formatter) -> Result<(), E>,
2962 ) -> Result<(), E> {
2963 if self.word_wrap {
2964 let mut recorder = FormatRecorder::new();
2965 content_fn(&mut recorder)?;
2966 text_util::write_wrapped(formatter, &recorder, self.width)?;
2967 } else {
2968 content_fn(formatter)?;
2969 }
2970 Ok(())
2971 }
2972}
2973
2974pub fn short_commit_hash(commit_id: &CommitId) -> String {
2975 format!("{commit_id:.12}")
2976}
2977
2978pub fn short_change_hash(change_id: &ChangeId) -> String {
2979 format!("{change_id:.12}")
2980}
2981
2982pub fn short_operation_hash(operation_id: &OperationId) -> String {
2983 format!("{operation_id:.12}")
2984}
2985
2986#[derive(Clone, Debug)]
2988pub enum DiffSelector {
2989 NonInteractive,
2990 Interactive(DiffEditor),
2991}
2992
2993impl DiffSelector {
2994 pub fn is_interactive(&self) -> bool {
2995 matches!(self, DiffSelector::Interactive(_))
2996 }
2997
2998 pub fn select(
3003 &self,
3004 left_tree: &MergedTree,
3005 right_tree: &MergedTree,
3006 matcher: &dyn Matcher,
3007 format_instructions: impl FnOnce() -> String,
3008 ) -> Result<MergedTreeId, CommandError> {
3009 let selected_tree_id = restore_tree(right_tree, left_tree, matcher)?;
3010 match self {
3011 DiffSelector::NonInteractive => Ok(selected_tree_id),
3012 DiffSelector::Interactive(editor) => {
3013 let right_tree = right_tree.store().get_root_tree(&selected_tree_id)?;
3017 Ok(editor.edit(left_tree, &right_tree, matcher, format_instructions)?)
3018 }
3019 }
3020 }
3021}
3022
3023#[derive(Clone, Debug)]
3024pub struct RemoteBookmarkNamePattern {
3025 pub bookmark: StringPattern,
3026 pub remote: StringPattern,
3027}
3028
3029impl FromStr for RemoteBookmarkNamePattern {
3030 type Err = String;
3031
3032 fn from_str(src: &str) -> Result<Self, Self::Err> {
3033 let (maybe_kind, pat) = src
3038 .split_once(':')
3039 .map_or((None, src), |(kind, pat)| (Some(kind), pat));
3040 let to_pattern = |pat: &str| {
3041 if let Some(kind) = maybe_kind {
3042 StringPattern::from_str_kind(pat, kind).map_err(|err| err.to_string())
3043 } else {
3044 Ok(StringPattern::exact(pat))
3045 }
3046 };
3047 let (bookmark, remote) = pat.rsplit_once('@').ok_or_else(|| {
3049 "remote bookmark must be specified in bookmark@remote form".to_owned()
3050 })?;
3051 Ok(RemoteBookmarkNamePattern {
3052 bookmark: to_pattern(bookmark)?,
3053 remote: to_pattern(remote)?,
3054 })
3055 }
3056}
3057
3058impl RemoteBookmarkNamePattern {
3059 pub fn is_exact(&self) -> bool {
3060 self.bookmark.is_exact() && self.remote.is_exact()
3061 }
3062}
3063
3064impl fmt::Display for RemoteBookmarkNamePattern {
3065 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3066 let RemoteBookmarkNamePattern { bookmark, remote } = self;
3069 write!(f, "{bookmark}@{remote}")
3070 }
3071}
3072
3073pub fn compute_commit_location(
3078 ui: &Ui,
3079 workspace_command: &WorkspaceCommandHelper,
3080 destination: Option<&[RevisionArg]>,
3081 insert_after: Option<&[RevisionArg]>,
3082 insert_before: Option<&[RevisionArg]>,
3083 commit_type: &str,
3084) -> Result<(Vec<CommitId>, Vec<CommitId>), CommandError> {
3085 let resolve_revisions =
3086 |revisions: Option<&[RevisionArg]>| -> Result<Option<Vec<CommitId>>, CommandError> {
3087 if let Some(revisions) = revisions {
3088 Ok(Some(
3089 workspace_command
3090 .resolve_some_revsets_default_single(ui, revisions)?
3091 .into_iter()
3092 .collect_vec(),
3093 ))
3094 } else {
3095 Ok(None)
3096 }
3097 };
3098 let destination_commit_ids = resolve_revisions(destination)?;
3099 let after_commit_ids = resolve_revisions(insert_after)?;
3100 let before_commit_ids = resolve_revisions(insert_before)?;
3101
3102 let (new_parent_ids, new_child_ids) =
3103 match (destination_commit_ids, after_commit_ids, before_commit_ids) {
3104 (Some(destination_commit_ids), None, None) => (destination_commit_ids, vec![]),
3105 (None, Some(after_commit_ids), Some(before_commit_ids)) => {
3106 (after_commit_ids, before_commit_ids)
3107 }
3108 (None, Some(after_commit_ids), None) => {
3109 let new_child_ids: Vec<_> = RevsetExpression::commits(after_commit_ids.clone())
3110 .children()
3111 .evaluate(workspace_command.repo().as_ref())?
3112 .iter()
3113 .try_collect()?;
3114
3115 (after_commit_ids, new_child_ids)
3116 }
3117 (None, None, Some(before_commit_ids)) => {
3118 let before_commits: Vec<_> = before_commit_ids
3119 .iter()
3120 .map(|id| workspace_command.repo().store().get_commit(id))
3121 .try_collect()?;
3122 let new_parent_ids = before_commits
3125 .iter()
3126 .flat_map(|commit| commit.parent_ids())
3127 .unique()
3128 .cloned()
3129 .collect_vec();
3130
3131 (new_parent_ids, before_commit_ids)
3132 }
3133 (Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
3134 panic!("destination cannot be used with insert_after/insert_before")
3135 }
3136 (None, None, None) => {
3137 panic!("expected at least one of destination or insert_after/insert_before")
3138 }
3139 };
3140
3141 if !new_child_ids.is_empty() {
3142 workspace_command.check_rewritable(new_child_ids.iter())?;
3143 ensure_no_commit_loop(
3144 workspace_command.repo().as_ref(),
3145 &RevsetExpression::commits(new_child_ids.clone()),
3146 &RevsetExpression::commits(new_parent_ids.clone()),
3147 commit_type,
3148 )?;
3149 }
3150
3151 Ok((new_parent_ids, new_child_ids))
3152}
3153
3154fn ensure_no_commit_loop(
3157 repo: &ReadonlyRepo,
3158 children_expression: &Rc<ResolvedRevsetExpression>,
3159 parents_expression: &Rc<ResolvedRevsetExpression>,
3160 commit_type: &str,
3161) -> Result<(), CommandError> {
3162 if let Some(commit_id) = children_expression
3163 .dag_range_to(parents_expression)
3164 .evaluate(repo)?
3165 .iter()
3166 .next()
3167 {
3168 let commit_id = commit_id?;
3169 return Err(user_error(format!(
3170 "Refusing to create a loop: commit {} would be both an ancestor and a descendant of \
3171 the {commit_type}",
3172 short_commit_hash(&commit_id),
3173 )));
3174 }
3175 Ok(())
3176}
3177
3178#[derive(clap::Parser, Clone, Debug)]
3185#[command(name = "jj")]
3186pub struct Args {
3187 #[command(flatten)]
3188 pub global_args: GlobalArgs,
3189}
3190
3191#[derive(clap::Args, Clone, Debug)]
3192#[command(next_help_heading = "Global Options")]
3193pub struct GlobalArgs {
3194 #[arg(long, short = 'R', global = true, value_hint = clap::ValueHint::DirPath)]
3199 pub repository: Option<String>,
3200 #[arg(long, global = true)]
3213 pub ignore_working_copy: bool,
3214 #[arg(long, global = true)]
3223 pub ignore_immutable: bool,
3224 #[arg(
3247 long,
3248 visible_alias = "at-op",
3249 global = true,
3250 add = ArgValueCandidates::new(complete::operations),
3251 )]
3252 pub at_operation: Option<String>,
3253 #[arg(long, global = true)]
3255 pub debug: bool,
3256
3257 #[command(flatten)]
3258 pub early_args: EarlyArgs,
3259}
3260
3261#[derive(clap::Args, Clone, Debug)]
3262pub struct EarlyArgs {
3263 #[arg(long, value_name = "WHEN", global = true)]
3265 pub color: Option<ColorChoice>,
3266 #[arg(long, global = true, action = ArgAction::SetTrue)]
3273 pub quiet: Option<bool>,
3276 #[arg(long, global = true, action = ArgAction::SetTrue)]
3278 pub no_pager: Option<bool>,
3281 #[arg(long, value_name = "NAME=VALUE", global = true, add = ArgValueCompleter::new(complete::leaf_config_key_value))]
3287 pub config: Vec<String>,
3288 #[arg(long, value_name = "TOML", global = true, hide = true)]
3291 pub config_toml: Vec<String>,
3292 #[arg(long, value_name = "PATH", global = true, value_hint = clap::ValueHint::FilePath)]
3294 pub config_file: Vec<String>,
3295}
3296
3297impl EarlyArgs {
3298 pub(crate) fn merged_config_args(&self, matches: &ArgMatches) -> Vec<(ConfigArgKind, &str)> {
3299 merge_args_with(
3300 matches,
3301 &[
3302 ("config", &self.config),
3303 ("config_toml", &self.config_toml),
3304 ("config_file", &self.config_file),
3305 ],
3306 |id, value| match id {
3307 "config" => (ConfigArgKind::Item, value.as_ref()),
3308 "config_toml" => (ConfigArgKind::Toml, value.as_ref()),
3309 "config_file" => (ConfigArgKind::File, value.as_ref()),
3310 _ => unreachable!("unexpected id {id:?}"),
3311 },
3312 )
3313 }
3314
3315 fn has_config_args(&self) -> bool {
3316 !self.config.is_empty() || !self.config_toml.is_empty() || !self.config_file.is_empty()
3317 }
3318}
3319
3320#[derive(Clone, Debug)]
3326pub struct RevisionArg(Cow<'static, str>);
3327
3328impl RevisionArg {
3329 pub const AT: Self = RevisionArg(Cow::Borrowed("@"));
3331}
3332
3333impl From<String> for RevisionArg {
3334 fn from(s: String) -> Self {
3335 RevisionArg(s.into())
3336 }
3337}
3338
3339impl AsRef<str> for RevisionArg {
3340 fn as_ref(&self) -> &str {
3341 &self.0
3342 }
3343}
3344
3345impl fmt::Display for RevisionArg {
3346 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3347 write!(f, "{}", self.0)
3348 }
3349}
3350
3351impl ValueParserFactory for RevisionArg {
3352 type Parser = MapValueParser<NonEmptyStringValueParser, fn(String) -> RevisionArg>;
3353
3354 fn value_parser() -> Self::Parser {
3355 NonEmptyStringValueParser::new().map(RevisionArg::from)
3356 }
3357}
3358
3359pub fn merge_args_with<'k, 'v, T, U>(
3367 matches: &ArgMatches,
3368 id_values: &[(&'k str, &'v [T])],
3369 mut convert: impl FnMut(&'k str, &'v T) -> U,
3370) -> Vec<U> {
3371 let mut pos_values: Vec<(usize, U)> = Vec::new();
3372 for (id, values) in id_values {
3373 pos_values.extend(itertools::zip_eq(
3374 matches.indices_of(id).into_iter().flatten(),
3375 values.iter().map(|v| convert(id, v)),
3376 ));
3377 }
3378 pos_values.sort_unstable_by_key(|&(pos, _)| pos);
3379 pos_values.into_iter().map(|(_, value)| value).collect()
3380}
3381
3382fn get_string_or_array(
3383 config: &StackedConfig,
3384 key: &'static str,
3385) -> Result<Vec<String>, ConfigGetError> {
3386 config
3387 .get(key)
3388 .map(|string| vec![string])
3389 .or_else(|_| config.get::<Vec<String>>(key))
3390}
3391
3392fn resolve_default_command(
3393 ui: &Ui,
3394 config: &StackedConfig,
3395 app: &Command,
3396 mut string_args: Vec<String>,
3397) -> Result<Vec<String>, CommandError> {
3398 const PRIORITY_FLAGS: &[&str] = &["--help", "-h", "--version", "-V"];
3399
3400 let has_priority_flag = string_args
3401 .iter()
3402 .any(|arg| PRIORITY_FLAGS.contains(&arg.as_str()));
3403 if has_priority_flag {
3404 return Ok(string_args);
3405 }
3406
3407 let app_clone = app
3408 .clone()
3409 .allow_external_subcommands(true)
3410 .ignore_errors(true);
3411 let matches = app_clone.try_get_matches_from(&string_args).ok();
3412
3413 if let Some(matches) = matches {
3414 if matches.subcommand_name().is_none() {
3415 let args = get_string_or_array(config, "ui.default-command").optional()?;
3416 if args.is_none() {
3417 writeln!(
3418 ui.hint_default(),
3419 "Use `jj -h` for a list of available commands."
3420 )?;
3421 writeln!(
3422 ui.hint_no_heading(),
3423 "Run `jj config set --user ui.default-command log` to disable this message."
3424 )?;
3425 }
3426 let default_command = args.unwrap_or_else(|| vec!["log".to_string()]);
3427
3428 string_args.splice(1..1, default_command);
3430 }
3431 }
3432 Ok(string_args)
3433}
3434
3435fn resolve_aliases(
3436 ui: &Ui,
3437 config: &StackedConfig,
3438 app: &Command,
3439 mut string_args: Vec<String>,
3440) -> Result<Vec<String>, CommandError> {
3441 let defined_aliases: HashSet<_> = config.table_keys("aliases").collect();
3442 let mut resolved_aliases = HashSet::new();
3443 let mut real_commands = HashSet::new();
3444 for command in app.get_subcommands() {
3445 real_commands.insert(command.get_name());
3446 for alias in command.get_all_aliases() {
3447 real_commands.insert(alias);
3448 }
3449 }
3450 for alias in defined_aliases.intersection(&real_commands).sorted() {
3451 writeln!(
3452 ui.warning_default(),
3453 "Cannot define an alias that overrides the built-in command '{alias}'"
3454 )?;
3455 }
3456
3457 loop {
3458 let app_clone = app.clone().allow_external_subcommands(true);
3459 let matches = app_clone.try_get_matches_from(&string_args).ok();
3460 if let Some((command_name, submatches)) = matches.as_ref().and_then(|m| m.subcommand()) {
3461 if !real_commands.contains(command_name) {
3462 let alias_name = command_name.to_string();
3463 let alias_args = submatches
3464 .get_many::<OsString>("")
3465 .unwrap_or_default()
3466 .map(|arg| arg.to_str().unwrap().to_string())
3467 .collect_vec();
3468 if resolved_aliases.contains(&*alias_name) {
3469 return Err(user_error(format!(
3470 "Recursive alias definition involving `{alias_name}`"
3471 )));
3472 }
3473 if let Some(&alias_name) = defined_aliases.get(&*alias_name) {
3474 let alias_definition: Vec<String> = config.get(["aliases", alias_name])?;
3475 assert!(string_args.ends_with(&alias_args));
3476 string_args.truncate(string_args.len() - 1 - alias_args.len());
3477 string_args.extend(alias_definition);
3478 string_args.extend_from_slice(&alias_args);
3479 resolved_aliases.insert(alias_name);
3480 continue;
3481 } else {
3482 return Ok(string_args);
3484 }
3485 }
3486 }
3487 return Ok(string_args);
3489 }
3490}
3491
3492fn parse_early_args(
3494 app: &Command,
3495 args: &[String],
3496) -> Result<(EarlyArgs, Vec<ConfigLayer>), CommandError> {
3497 let early_matches = app
3499 .clone()
3500 .disable_version_flag(true)
3501 .disable_help_flag(true)
3503 .arg(
3505 clap::Arg::new("help")
3506 .short('h')
3507 .long("help")
3508 .global(true)
3509 .action(ArgAction::Count),
3510 )
3511 .ignore_errors(true)
3512 .try_get_matches_from(args)?;
3513 let args = EarlyArgs::from_arg_matches(&early_matches).unwrap();
3514
3515 let mut config_layers = parse_config_args(&args.merged_config_args(&early_matches))?;
3516 let mut layer = ConfigLayer::empty(ConfigSource::CommandArg);
3519 if let Some(choice) = args.color {
3520 layer.set_value("ui.color", choice.to_string()).unwrap();
3521 }
3522 if args.quiet.unwrap_or_default() {
3523 layer.set_value("ui.quiet", true).unwrap();
3524 }
3525 if args.no_pager.unwrap_or_default() {
3526 layer.set_value("ui.paginate", "never").unwrap();
3527 }
3528 if !layer.is_empty() {
3529 config_layers.push(layer);
3530 }
3531 Ok((args, config_layers))
3532}
3533
3534fn handle_shell_completion(
3535 ui: &Ui,
3536 app: &Command,
3537 config: &StackedConfig,
3538 cwd: &Path,
3539) -> Result<(), CommandError> {
3540 let mut orig_args = env::args_os();
3541
3542 let mut args = vec![];
3543 args.extend(orig_args.by_ref().take(2));
3546
3547 if orig_args.len() > 0 {
3551 let complete_index: Option<usize> = env::var("_CLAP_COMPLETE_INDEX")
3552 .ok()
3553 .and_then(|s| s.parse().ok());
3554 let resolved_aliases = if let Some(index) = complete_index {
3555 let pad_len = usize::saturating_sub(index + 1, orig_args.len());
3560 let padded_args = orig_args
3561 .by_ref()
3562 .chain(std::iter::repeat_n(OsString::new(), pad_len));
3563
3564 let mut expanded_args = expand_args(ui, app, padded_args.take(index + 1), config)?;
3566
3567 unsafe {
3571 env::set_var(
3572 "_CLAP_COMPLETE_INDEX",
3573 (expanded_args.len() - 1).to_string(),
3574 );
3575 }
3576
3577 let split_off_padding = expanded_args.split_off(expanded_args.len() - pad_len);
3580 assert!(
3581 split_off_padding.iter().all(|s| s.is_empty()),
3582 "split-off padding should only consist of empty strings but was \
3583 {split_off_padding:?}",
3584 );
3585
3586 expanded_args.extend(to_string_args(orig_args)?);
3588 expanded_args
3589 } else {
3590 expand_args(ui, app, orig_args, config)?
3591 };
3592 args.extend(resolved_aliases.into_iter().map(OsString::from));
3593 }
3594 let ran_completion = clap_complete::CompleteEnv::with_factory(|| {
3595 app.clone()
3596 .allow_external_subcommands(true)
3598 })
3599 .try_complete(args.iter(), Some(cwd))?;
3600 assert!(
3601 ran_completion,
3602 "This function should not be called without the COMPLETE variable set."
3603 );
3604 Ok(())
3605}
3606
3607pub fn expand_args(
3608 ui: &Ui,
3609 app: &Command,
3610 args_os: impl IntoIterator<Item = OsString>,
3611 config: &StackedConfig,
3612) -> Result<Vec<String>, CommandError> {
3613 let string_args = to_string_args(args_os)?;
3614 let string_args = resolve_default_command(ui, config, app, string_args)?;
3615 resolve_aliases(ui, config, app, string_args)
3616}
3617
3618fn to_string_args(
3619 args_os: impl IntoIterator<Item = OsString>,
3620) -> Result<Vec<String>, CommandError> {
3621 args_os
3622 .into_iter()
3623 .map(|arg_os| {
3624 arg_os
3625 .into_string()
3626 .map_err(|_| cli_error("Non-UTF-8 argument"))
3627 })
3628 .collect()
3629}
3630
3631fn parse_args(app: &Command, string_args: &[String]) -> Result<(ArgMatches, Args), clap::Error> {
3632 let matches = app
3633 .clone()
3634 .arg_required_else_help(true)
3635 .subcommand_required(true)
3636 .try_get_matches_from(string_args)?;
3637 let args = Args::from_arg_matches(&matches).unwrap();
3638 Ok((matches, args))
3639}
3640
3641fn command_name(mut matches: &ArgMatches) -> String {
3642 let mut command = String::new();
3643 while let Some((subcommand, new_matches)) = matches.subcommand() {
3644 if !command.is_empty() {
3645 command.push(' ');
3646 }
3647 command.push_str(subcommand);
3648 matches = new_matches;
3649 }
3650 command
3651}
3652
3653pub fn format_template<C: Clone>(ui: &Ui, arg: &C, template: &TemplateRenderer<C>) -> String {
3654 let mut output = vec![];
3655 template
3656 .format(arg, ui.new_formatter(&mut output).as_mut())
3657 .expect("write() to vec backed formatter should never fail");
3658 output.into_string_lossy()
3660}
3661
3662#[must_use]
3664pub struct CliRunner<'a> {
3665 tracing_subscription: TracingSubscription,
3666 app: Command,
3667 config_layers: Vec<ConfigLayer>,
3668 config_migrations: Vec<ConfigMigrationRule>,
3669 store_factories: StoreFactories,
3670 working_copy_factories: WorkingCopyFactories,
3671 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
3672 revset_extensions: RevsetExtensions,
3673 commit_template_extensions: Vec<Arc<dyn CommitTemplateLanguageExtension>>,
3674 operation_template_extensions: Vec<Arc<dyn OperationTemplateLanguageExtension>>,
3675 dispatch_fn: CliDispatchFn<'a>,
3676 dispatch_hook_fns: Vec<CliDispatchHookFn<'a>>,
3677 process_global_args_fns: Vec<ProcessGlobalArgsFn<'a>>,
3678}
3679
3680pub type CliDispatchFn<'a> =
3681 Box<dyn FnOnce(&mut Ui, &CommandHelper) -> Result<(), CommandError> + 'a>;
3682
3683type CliDispatchHookFn<'a> =
3684 Box<dyn FnOnce(&mut Ui, &CommandHelper, CliDispatchFn<'a>) -> Result<(), CommandError> + 'a>;
3685
3686type ProcessGlobalArgsFn<'a> =
3687 Box<dyn FnOnce(&mut Ui, &ArgMatches) -> Result<(), CommandError> + 'a>;
3688
3689impl<'a> CliRunner<'a> {
3690 pub fn init() -> Self {
3693 let tracing_subscription = TracingSubscription::init();
3694 crate::cleanup_guard::init();
3695 CliRunner {
3696 tracing_subscription,
3697 app: crate::commands::default_app(),
3698 config_layers: crate::config::default_config_layers(),
3699 config_migrations: crate::config::default_config_migrations(),
3700 store_factories: StoreFactories::default(),
3701 working_copy_factories: default_working_copy_factories(),
3702 workspace_loader_factory: Box::new(DefaultWorkspaceLoaderFactory),
3703 revset_extensions: Default::default(),
3704 commit_template_extensions: vec![],
3705 operation_template_extensions: vec![],
3706 dispatch_fn: Box::new(crate::commands::run_command),
3707 dispatch_hook_fns: vec![],
3708 process_global_args_fns: vec![],
3709 }
3710 }
3711
3712 pub fn name(mut self, name: &str) -> Self {
3714 self.app = self.app.name(name.to_string());
3715 self
3716 }
3717
3718 pub fn about(mut self, about: &str) -> Self {
3720 self.app = self.app.about(about.to_string());
3721 self
3722 }
3723
3724 pub fn version(mut self, version: &str) -> Self {
3726 self.app = self.app.version(version.to_string());
3727 self
3728 }
3729
3730 pub fn add_extra_config(mut self, layer: ConfigLayer) -> Self {
3735 assert_eq!(layer.source, ConfigSource::Default);
3736 self.config_layers.push(layer);
3737 self
3738 }
3739
3740 pub fn add_extra_config_migration(mut self, rule: ConfigMigrationRule) -> Self {
3742 self.config_migrations.push(rule);
3743 self
3744 }
3745
3746 pub fn add_store_factories(mut self, store_factories: StoreFactories) -> Self {
3748 self.store_factories.merge(store_factories);
3749 self
3750 }
3751
3752 pub fn add_working_copy_factories(
3754 mut self,
3755 working_copy_factories: WorkingCopyFactories,
3756 ) -> Self {
3757 merge_factories_map(&mut self.working_copy_factories, working_copy_factories);
3758 self
3759 }
3760
3761 pub fn set_workspace_loader_factory(
3762 mut self,
3763 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
3764 ) -> Self {
3765 self.workspace_loader_factory = workspace_loader_factory;
3766 self
3767 }
3768
3769 pub fn add_symbol_resolver_extension(
3770 mut self,
3771 symbol_resolver: Box<dyn SymbolResolverExtension>,
3772 ) -> Self {
3773 self.revset_extensions.add_symbol_resolver(symbol_resolver);
3774 self
3775 }
3776
3777 pub fn add_revset_function_extension(
3778 mut self,
3779 name: &'static str,
3780 func: RevsetFunction,
3781 ) -> Self {
3782 self.revset_extensions.add_custom_function(name, func);
3783 self
3784 }
3785
3786 pub fn add_commit_template_extension(
3787 mut self,
3788 commit_template_extension: Box<dyn CommitTemplateLanguageExtension>,
3789 ) -> Self {
3790 self.commit_template_extensions
3791 .push(commit_template_extension.into());
3792 self
3793 }
3794
3795 pub fn add_operation_template_extension(
3796 mut self,
3797 operation_template_extension: Box<dyn OperationTemplateLanguageExtension>,
3798 ) -> Self {
3799 self.operation_template_extensions
3800 .push(operation_template_extension.into());
3801 self
3802 }
3803
3804 pub fn add_dispatch_hook<F>(mut self, dispatch_hook_fn: F) -> Self
3808 where
3809 F: FnOnce(&mut Ui, &CommandHelper, CliDispatchFn) -> Result<(), CommandError> + 'a,
3810 {
3811 self.dispatch_hook_fns.push(Box::new(dispatch_hook_fn));
3812 self
3813 }
3814
3815 pub fn add_subcommand<C, F>(mut self, custom_dispatch_fn: F) -> Self
3817 where
3818 C: clap::Subcommand,
3819 F: FnOnce(&mut Ui, &CommandHelper, C) -> Result<(), CommandError> + 'a,
3820 {
3821 let old_dispatch_fn = self.dispatch_fn;
3822 let new_dispatch_fn =
3823 move |ui: &mut Ui, command_helper: &CommandHelper| match C::from_arg_matches(
3824 command_helper.matches(),
3825 ) {
3826 Ok(command) => custom_dispatch_fn(ui, command_helper, command),
3827 Err(_) => old_dispatch_fn(ui, command_helper),
3828 };
3829 self.app = C::augment_subcommands(self.app);
3830 self.dispatch_fn = Box::new(new_dispatch_fn);
3831 self
3832 }
3833
3834 pub fn add_global_args<A, F>(mut self, process_before: F) -> Self
3836 where
3837 A: clap::Args,
3838 F: FnOnce(&mut Ui, A) -> Result<(), CommandError> + 'a,
3839 {
3840 let process_global_args_fn = move |ui: &mut Ui, matches: &ArgMatches| {
3841 let custom_args = A::from_arg_matches(matches).unwrap();
3842 process_before(ui, custom_args)
3843 };
3844 self.app = A::augment_args(self.app);
3845 self.process_global_args_fns
3846 .push(Box::new(process_global_args_fn));
3847 self
3848 }
3849
3850 #[instrument(skip_all)]
3851 fn run_internal(self, ui: &mut Ui, mut raw_config: RawConfig) -> Result<(), CommandError> {
3852 let cwd = env::current_dir()
3855 .and_then(dunce::canonicalize)
3856 .map_err(|_| {
3857 user_error_with_hint(
3858 "Could not determine current directory",
3859 "Did you update to a commit where the directory doesn't exist?",
3860 )
3861 })?;
3862 let mut config_env = ConfigEnv::from_environment(ui);
3863 let mut last_config_migration_descriptions = Vec::new();
3864 let mut migrate_config = |config: &mut StackedConfig| -> Result<(), CommandError> {
3865 last_config_migration_descriptions =
3866 jj_lib::config::migrate(config, &self.config_migrations)?;
3867 Ok(())
3868 };
3869 let maybe_cwd_workspace_loader = self
3873 .workspace_loader_factory
3874 .create(find_workspace_dir(&cwd))
3875 .map_err(|err| map_workspace_load_error(err, Some(".")));
3876 config_env.reload_user_config(&mut raw_config)?;
3877 if let Ok(loader) = &maybe_cwd_workspace_loader {
3878 config_env.reset_repo_path(loader.repo_path());
3879 config_env.reload_repo_config(&mut raw_config)?;
3880 }
3881 let mut config = config_env.resolve_config(&raw_config)?;
3882 migrate_config(&mut config)?;
3883 ui.reset(&config)?;
3884
3885 if env::var_os("COMPLETE").is_some() {
3886 return handle_shell_completion(&Ui::null(), &self.app, &config, &cwd);
3887 }
3888
3889 let string_args = expand_args(ui, &self.app, env::args_os(), &config)?;
3890 let (args, config_layers) = parse_early_args(&self.app, &string_args)?;
3891 if !config_layers.is_empty() {
3892 raw_config.as_mut().extend_layers(config_layers);
3893 config = config_env.resolve_config(&raw_config)?;
3894 migrate_config(&mut config)?;
3895 ui.reset(&config)?;
3896 }
3897 if !args.config_toml.is_empty() {
3898 writeln!(
3899 ui.warning_default(),
3900 "--config-toml is deprecated; use --config or --config-file instead."
3901 )?;
3902 }
3903
3904 if args.has_config_args() {
3905 warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
3906 }
3907
3908 let (matches, args) = parse_args(&self.app, &string_args)
3909 .map_err(|err| map_clap_cli_error(err, ui, &config))?;
3910 if args.global_args.debug {
3911 self.tracing_subscription.enable_debug_logging()?;
3913 }
3914 for process_global_args_fn in self.process_global_args_fns {
3915 process_global_args_fn(ui, &matches)?;
3916 }
3917 config_env.set_command_name(command_name(&matches));
3918
3919 let maybe_workspace_loader = if let Some(path) = &args.global_args.repository {
3920 let abs_path = cwd.join(path);
3922 let abs_path = dunce::canonicalize(&abs_path).unwrap_or(abs_path);
3923 let loader = self
3925 .workspace_loader_factory
3926 .create(&abs_path)
3927 .map_err(|err| map_workspace_load_error(err, Some(path)))?;
3928 config_env.reset_repo_path(loader.repo_path());
3929 config_env.reload_repo_config(&mut raw_config)?;
3930 Ok(loader)
3931 } else {
3932 maybe_cwd_workspace_loader
3933 };
3934
3935 config = config_env.resolve_config(&raw_config)?;
3937 migrate_config(&mut config)?;
3938 ui.reset(&config)?;
3939
3940 for (source, desc) in &last_config_migration_descriptions {
3942 let source_str = match source {
3943 ConfigSource::Default => "default-provided",
3944 ConfigSource::EnvBase | ConfigSource::EnvOverrides => "environment-provided",
3945 ConfigSource::User => "user-level",
3946 ConfigSource::Repo => "repo-level",
3947 ConfigSource::CommandArg => "CLI-provided",
3948 };
3949 writeln!(
3950 ui.warning_default(),
3951 "Deprecated {source_str} config: {desc}"
3952 )?;
3953 }
3954
3955 if args.global_args.repository.is_some() {
3956 warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
3957 }
3958
3959 let settings = UserSettings::from_config(config)?;
3960 let command_helper_data = CommandHelperData {
3961 app: self.app,
3962 cwd,
3963 string_args,
3964 matches,
3965 global_args: args.global_args,
3966 config_env,
3967 config_migrations: self.config_migrations,
3968 raw_config,
3969 settings,
3970 revset_extensions: self.revset_extensions.into(),
3971 commit_template_extensions: self.commit_template_extensions,
3972 operation_template_extensions: self.operation_template_extensions,
3973 maybe_workspace_loader,
3974 store_factories: self.store_factories,
3975 working_copy_factories: self.working_copy_factories,
3976 workspace_loader_factory: self.workspace_loader_factory,
3977 };
3978 let command_helper = CommandHelper {
3979 data: Rc::new(command_helper_data),
3980 };
3981 let dispatch_fn = self.dispatch_hook_fns.into_iter().fold(
3982 self.dispatch_fn,
3983 |old_dispatch_fn, dispatch_hook_fn| {
3984 Box::new(move |ui: &mut Ui, command_helper: &CommandHelper| {
3985 dispatch_hook_fn(ui, command_helper, old_dispatch_fn)
3986 })
3987 },
3988 );
3989 (dispatch_fn)(ui, &command_helper)
3990 }
3991
3992 #[must_use]
3993 #[instrument(skip(self))]
3994 pub fn run(mut self) -> u8 {
3995 crossterm::style::force_color_output(true);
3997 let config = config_from_environment(self.config_layers.drain(..));
3998 let mut ui = Ui::with_config(config.as_ref())
4001 .expect("default config should be valid, env vars are stringly typed");
4002 let result = self.run_internal(&mut ui, config);
4003 let exit_code = handle_command_result(&mut ui, result);
4004 ui.finalize_pager();
4005 exit_code
4006 }
4007}
4008
4009fn map_clap_cli_error(err: clap::Error, ui: &Ui, config: &StackedConfig) -> CommandError {
4010 if let Some(ContextValue::String(cmd)) = err.get(ContextKind::InvalidSubcommand) {
4011 match cmd.as_str() {
4012 "clone" | "init" => {
4015 let cmd = cmd.clone();
4016 let mut err = err;
4017 err.remove(ContextKind::SuggestedSubcommand);
4019 err.remove(ContextKind::Suggested); err.remove(ContextKind::Usage);
4021 return CommandError::from(err)
4022 .hinted(format!(
4023 "You probably want `jj git {cmd}`. See also `jj help git`."
4024 ))
4025 .hinted(format!(
4026 r#"You can configure `aliases.{cmd} = ["git", "{cmd}"]` if you want `jj {cmd}` to work and always use the Git backend."#
4027 ));
4028 }
4029 _ => {}
4030 }
4031 }
4032 if let (Some(ContextValue::String(arg)), Some(ContextValue::String(value))) = (
4033 err.get(ContextKind::InvalidArg),
4034 err.get(ContextKind::InvalidValue),
4035 ) {
4036 if arg.as_str() == "--template <TEMPLATE>" && value.is_empty() {
4037 if let Ok(template_aliases) = load_template_aliases(ui, config) {
4039 return CommandError::from(err)
4040 .hinted(format_template_aliases_hint(&template_aliases));
4041 }
4042 }
4043 }
4044 CommandError::from(err)
4045}
4046
4047fn format_template_aliases_hint(template_aliases: &TemplateAliasesMap) -> String {
4048 let mut hint = String::from("The following template aliases are defined:\n");
4049 hint.push_str(
4050 &template_aliases
4051 .symbol_names()
4052 .sorted_unstable()
4053 .map(|name| format!("- {name}"))
4054 .join("\n"),
4055 );
4056 hint
4057}
4058
4059fn warn_if_args_mismatch(
4061 ui: &Ui,
4062 app: &Command,
4063 config: &StackedConfig,
4064 expected_args: &[String],
4065) -> Result<(), CommandError> {
4066 let new_string_args = expand_args(ui, app, env::args_os(), config).ok();
4067 if new_string_args.as_deref() != Some(expected_args) {
4068 writeln!(
4069 ui.warning_default(),
4070 "Command aliases cannot be loaded from -R/--repository path or --config/--config-file \
4071 arguments."
4072 )?;
4073 }
4074 Ok(())
4075}
4076
4077#[cfg(test)]
4078mod tests {
4079 use clap::CommandFactory as _;
4080
4081 use super::*;
4082
4083 #[derive(clap::Parser, Clone, Debug)]
4084 pub struct TestArgs {
4085 #[arg(long)]
4086 pub foo: Vec<u32>,
4087 #[arg(long)]
4088 pub bar: Vec<u32>,
4089 #[arg(long)]
4090 pub baz: bool,
4091 }
4092
4093 #[test]
4094 fn test_merge_args_with() {
4095 let command = TestArgs::command();
4096 let parse = |args: &[&str]| -> Vec<(&'static str, u32)> {
4097 let matches = command.clone().try_get_matches_from(args).unwrap();
4098 let args = TestArgs::from_arg_matches(&matches).unwrap();
4099 merge_args_with(
4100 &matches,
4101 &[("foo", &args.foo), ("bar", &args.bar)],
4102 |id, value| (id, *value),
4103 )
4104 };
4105
4106 assert_eq!(parse(&["jj"]), vec![]);
4107 assert_eq!(parse(&["jj", "--foo=1"]), vec![("foo", 1)]);
4108 assert_eq!(
4109 parse(&["jj", "--foo=1", "--bar=2"]),
4110 vec![("foo", 1), ("bar", 2)]
4111 );
4112 assert_eq!(
4113 parse(&["jj", "--foo=1", "--baz", "--bar=2", "--foo", "3"]),
4114 vec![("foo", 1), ("bar", 2), ("foo", 3)]
4115 );
4116 }
4117}