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::slice;
31use std::str::FromStr;
32use std::sync::Arc;
33use std::time::SystemTime;
34
35use bstr::ByteVec as _;
36use chrono::TimeZone as _;
37use clap::ArgAction;
38use clap::ArgMatches;
39use clap::Command;
40use clap::FromArgMatches as _;
41use clap::builder::MapValueParser;
42use clap::builder::NonEmptyStringValueParser;
43use clap::builder::TypedValueParser as _;
44use clap::builder::ValueParserFactory;
45use clap::error::ContextKind;
46use clap::error::ContextValue;
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::CheckOutCommitError;
90use jj_lib::repo::EditCommitError;
91use jj_lib::repo::MutableRepo;
92use jj_lib::repo::ReadonlyRepo;
93use jj_lib::repo::Repo;
94use jj_lib::repo::RepoLoader;
95use jj_lib::repo::StoreFactories;
96use jj_lib::repo::StoreLoadError;
97use jj_lib::repo::merge_factories_map;
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::CheckoutStats;
124use jj_lib::working_copy::SnapshotOptions;
125use jj_lib::working_copy::SnapshotStats;
126use jj_lib::working_copy::UntrackedReason;
127use jj_lib::working_copy::WorkingCopy;
128use jj_lib::working_copy::WorkingCopyFactory;
129use jj_lib::working_copy::WorkingCopyFreshness;
130use jj_lib::workspace::DefaultWorkspaceLoaderFactory;
131use jj_lib::workspace::LockedWorkspace;
132use jj_lib::workspace::WorkingCopyFactories;
133use jj_lib::workspace::Workspace;
134use jj_lib::workspace::WorkspaceLoadError;
135use jj_lib::workspace::WorkspaceLoader;
136use jj_lib::workspace::WorkspaceLoaderFactory;
137use jj_lib::workspace::default_working_copy_factories;
138use jj_lib::workspace::get_working_copy_factory;
139use pollster::FutureExt as _;
140use tracing::instrument;
141use tracing_chrome::ChromeLayerBuilder;
142use tracing_subscriber::prelude::*;
143
144use crate::command_error::CommandError;
145use crate::command_error::cli_error;
146use crate::command_error::config_error_with_message;
147use crate::command_error::handle_command_result;
148use crate::command_error::internal_error;
149use crate::command_error::internal_error_with_message;
150use crate::command_error::print_parse_diagnostics;
151use crate::command_error::user_error;
152use crate::command_error::user_error_with_hint;
153use crate::commit_templater::CommitTemplateLanguage;
154use crate::commit_templater::CommitTemplateLanguageExtension;
155use crate::complete;
156use crate::config::ConfigArgKind;
157use crate::config::ConfigEnv;
158use crate::config::RawConfig;
159use crate::config::config_from_environment;
160use crate::config::parse_config_args;
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::FormatterExt as _;
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: &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 Self {
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
550 let repo = workspace_command.repo().clone();
551 let (mut locked_ws, desired_wc_commit) =
552 workspace_command.unchecked_start_working_copy_mutation()?;
553 match WorkingCopyFreshness::check_stale(
554 locked_ws.locked_wc(),
555 &desired_wc_commit,
556 &repo,
557 )? {
558 WorkingCopyFreshness::Fresh | WorkingCopyFreshness::Updated(_) => {
559 writeln!(
560 ui.status(),
561 "Attempted recovery, but the working copy is not stale"
562 )?;
563 }
564 WorkingCopyFreshness::WorkingCopyStale
565 | WorkingCopyFreshness::SiblingOperation => {
566 let stats = update_stale_working_copy(
567 locked_ws,
568 repo.op_id().clone(),
569 &stale_wc_commit,
570 &desired_wc_commit,
571 )?;
572 workspace_command.print_updated_working_copy_stats(
573 ui,
574 Some(&stale_wc_commit),
575 &desired_wc_commit,
576 &stats,
577 )?;
578 writeln!(
579 ui.status(),
580 "Updated working copy to fresh commit {}",
581 short_commit_hash(desired_wc_commit.id())
582 )?;
583 }
584 };
585
586 Ok((workspace_command, stats))
587 }
588 Err(e @ OpStoreError::ObjectNotFound { .. }) => {
589 writeln!(
590 ui.status(),
591 "Failed to read working copy's current operation; attempting recovery. Error \
592 message from read attempt: {e}"
593 )?;
594
595 let mut workspace_command = self.workspace_helper_no_snapshot(ui)?;
596 let stats = workspace_command.create_and_check_out_recovery_commit(ui)?;
597 Ok((workspace_command, stats))
598 }
599 Err(e) => Err(e.into()),
600 }
601 }
602
603 pub fn workspace_environment(
605 &self,
606 ui: &Ui,
607 workspace: &Workspace,
608 ) -> Result<WorkspaceCommandEnvironment, CommandError> {
609 WorkspaceCommandEnvironment::new(ui, self, workspace)
610 }
611
612 pub fn is_working_copy_writable(&self) -> bool {
615 self.is_at_head_operation() && !self.data.global_args.ignore_working_copy
616 }
617
618 pub fn is_at_head_operation(&self) -> bool {
620 matches!(
623 self.data.global_args.at_operation.as_deref(),
624 None | Some("@")
625 )
626 }
627
628 #[instrument(skip_all)]
633 pub fn resolve_operation(
634 &self,
635 ui: &Ui,
636 repo_loader: &RepoLoader,
637 ) -> Result<Operation, CommandError> {
638 if let Some(op_str) = &self.data.global_args.at_operation {
639 Ok(op_walk::resolve_op_for_load(repo_loader, op_str)?)
640 } else {
641 op_heads_store::resolve_op_heads(
642 repo_loader.op_heads_store().as_ref(),
643 repo_loader.op_store(),
644 |op_heads| {
645 writeln!(
646 ui.status(),
647 "Concurrent modification detected, resolving automatically.",
648 )?;
649 let base_repo = repo_loader.load_at(&op_heads[0])?;
650 let mut tx = start_repo_transaction(&base_repo, &self.data.string_args);
652 for other_op_head in op_heads.into_iter().skip(1) {
653 tx.merge_operation(other_op_head)?;
654 let num_rebased = tx.repo_mut().rebase_descendants()?;
655 if num_rebased > 0 {
656 writeln!(
657 ui.status(),
658 "Rebased {num_rebased} descendant commits onto commits rewritten \
659 by other operation"
660 )?;
661 }
662 }
663 Ok(tx
664 .write("reconcile divergent operations")?
665 .leave_unpublished()
666 .operation()
667 .clone())
668 },
669 )
670 }
671 }
672
673 #[instrument(skip_all)]
677 pub fn for_workable_repo(
678 &self,
679 ui: &Ui,
680 workspace: Workspace,
681 repo: Arc<ReadonlyRepo>,
682 ) -> Result<WorkspaceCommandHelper, CommandError> {
683 let env = self.workspace_environment(ui, &workspace)?;
684 let loaded_at_head = true;
685 WorkspaceCommandHelper::new(ui, workspace, repo, env, loaded_at_head)
686 }
687}
688
689struct ReadonlyUserRepo {
692 repo: Arc<ReadonlyRepo>,
693 id_prefix_context: OnceCell<IdPrefixContext>,
694}
695
696impl ReadonlyUserRepo {
697 fn new(repo: Arc<ReadonlyRepo>) -> Self {
698 Self {
699 repo,
700 id_prefix_context: OnceCell::new(),
701 }
702 }
703}
704
705pub struct AdvanceableBookmark {
715 name: RefNameBuf,
716 old_commit_id: CommitId,
717}
718
719struct AdvanceBookmarksSettings {
729 enabled_bookmarks: Vec<StringPattern>,
730 disabled_bookmarks: Vec<StringPattern>,
731}
732
733impl AdvanceBookmarksSettings {
734 fn from_settings(settings: &UserSettings) -> Result<Self, CommandError> {
735 let get_setting = |setting_key| {
736 let name = ConfigNamePathBuf::from_iter(["experimental-advance-branches", setting_key]);
737 match settings.get::<Vec<String>>(&name).optional()? {
738 Some(patterns) => patterns
739 .into_iter()
740 .map(|s| {
741 StringPattern::parse(&s).map_err(|e| {
742 config_error_with_message(format!("Error parsing `{s}` for {name}"), e)
743 })
744 })
745 .collect(),
746 None => Ok(Vec::new()),
747 }
748 };
749 Ok(Self {
750 enabled_bookmarks: get_setting("enabled-branches")?,
751 disabled_bookmarks: get_setting("disabled-branches")?,
752 })
753 }
754
755 fn bookmark_is_eligible(&self, bookmark_name: &RefName) -> bool {
758 if self
759 .disabled_bookmarks
760 .iter()
761 .any(|d| d.is_match(bookmark_name.as_str()))
762 {
763 return false;
764 }
765 self.enabled_bookmarks
766 .iter()
767 .any(|e| e.is_match(bookmark_name.as_str()))
768 }
769
770 fn feature_enabled(&self) -> bool {
773 !self.enabled_bookmarks.is_empty()
774 }
775}
776
777pub struct WorkspaceCommandEnvironment {
779 command: CommandHelper,
780 settings: UserSettings,
781 revset_aliases_map: RevsetAliasesMap,
782 template_aliases_map: TemplateAliasesMap,
783 path_converter: RepoPathUiConverter,
784 workspace_name: WorkspaceNameBuf,
785 immutable_heads_expression: Arc<UserRevsetExpression>,
786 short_prefixes_expression: Option<Arc<UserRevsetExpression>>,
787 conflict_marker_style: ConflictMarkerStyle,
788}
789
790impl WorkspaceCommandEnvironment {
791 #[instrument(skip_all)]
792 fn new(ui: &Ui, command: &CommandHelper, workspace: &Workspace) -> Result<Self, CommandError> {
793 let settings = workspace.settings();
794 let revset_aliases_map = revset_util::load_revset_aliases(ui, settings.config())?;
795 let template_aliases_map = load_template_aliases(ui, settings.config())?;
796 let path_converter = RepoPathUiConverter::Fs {
797 cwd: command.cwd().to_owned(),
798 base: workspace.workspace_root().to_owned(),
799 };
800 let mut env = Self {
801 command: command.clone(),
802 settings: settings.clone(),
803 revset_aliases_map,
804 template_aliases_map,
805 path_converter,
806 workspace_name: workspace.workspace_name().to_owned(),
807 immutable_heads_expression: RevsetExpression::root(),
808 short_prefixes_expression: None,
809 conflict_marker_style: settings.get("ui.conflict-marker-style")?,
810 };
811 env.immutable_heads_expression = env.load_immutable_heads_expression(ui)?;
812 env.short_prefixes_expression = env.load_short_prefixes_expression(ui)?;
813 Ok(env)
814 }
815
816 pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
817 &self.path_converter
818 }
819
820 pub fn workspace_name(&self) -> &WorkspaceName {
821 &self.workspace_name
822 }
823
824 pub(crate) fn revset_parse_context(&self) -> RevsetParseContext<'_> {
825 let workspace_context = RevsetWorkspaceContext {
826 path_converter: &self.path_converter,
827 workspace_name: &self.workspace_name,
828 };
829 let now = if let Some(timestamp) = self.settings.commit_timestamp() {
830 chrono::Local
831 .timestamp_millis_opt(timestamp.timestamp.0)
832 .unwrap()
833 } else {
834 chrono::Local::now()
835 };
836 RevsetParseContext {
837 aliases_map: &self.revset_aliases_map,
838 local_variables: HashMap::new(),
839 user_email: self.settings.user_email(),
840 date_pattern_context: now.into(),
841 extensions: self.command.revset_extensions(),
842 workspace: Some(workspace_context),
843 }
844 }
845
846 pub fn new_id_prefix_context(&self) -> IdPrefixContext {
849 let context = IdPrefixContext::new(self.command.revset_extensions().clone());
850 match &self.short_prefixes_expression {
851 None => context,
852 Some(expression) => context.disambiguate_within(expression.clone()),
853 }
854 }
855
856 pub fn immutable_expression(&self) -> Arc<UserRevsetExpression> {
858 self.immutable_heads_expression.ancestors()
861 }
862
863 pub fn immutable_heads_expression(&self) -> &Arc<UserRevsetExpression> {
865 &self.immutable_heads_expression
866 }
867
868 pub fn conflict_marker_style(&self) -> ConflictMarkerStyle {
870 self.conflict_marker_style
871 }
872
873 fn load_immutable_heads_expression(
874 &self,
875 ui: &Ui,
876 ) -> Result<Arc<UserRevsetExpression>, CommandError> {
877 let mut diagnostics = RevsetDiagnostics::new();
878 let expression = revset_util::parse_immutable_heads_expression(
879 &mut diagnostics,
880 &self.revset_parse_context(),
881 )
882 .map_err(|e| config_error_with_message("Invalid `revset-aliases.immutable_heads()`", e))?;
883 print_parse_diagnostics(ui, "In `revset-aliases.immutable_heads()`", &diagnostics)?;
884 Ok(expression)
885 }
886
887 fn load_short_prefixes_expression(
888 &self,
889 ui: &Ui,
890 ) -> Result<Option<Arc<UserRevsetExpression>>, CommandError> {
891 let revset_string = self
892 .settings
893 .get_string("revsets.short-prefixes")
894 .optional()?
895 .map_or_else(|| self.settings.get_string("revsets.log"), Ok)?;
896 if revset_string.is_empty() {
897 Ok(None)
898 } else {
899 let mut diagnostics = RevsetDiagnostics::new();
900 let (expression, modifier) = revset::parse_with_modifier(
901 &mut diagnostics,
902 &revset_string,
903 &self.revset_parse_context(),
904 )
905 .map_err(|err| config_error_with_message("Invalid `revsets.short-prefixes`", err))?;
906 print_parse_diagnostics(ui, "In `revsets.short-prefixes`", &diagnostics)?;
907 let (None | Some(RevsetModifier::All)) = modifier;
908 Ok(Some(expression))
909 }
910 }
911
912 fn find_immutable_commit(
914 &self,
915 repo: &dyn Repo,
916 commit_ids: &[CommitId],
917 ) -> Result<Option<CommitId>, CommandError> {
918 if self.command.global_args().ignore_immutable {
919 let root_id = repo.store().root_commit_id();
920 return Ok(commit_ids.iter().find(|id| *id == root_id).cloned());
921 }
922
923 let id_prefix_context = IdPrefixContext::new(self.command.revset_extensions().clone());
927 let to_rewrite_revset = RevsetExpression::commits(commit_ids.to_vec());
928 let mut expression = RevsetExpressionEvaluator::new(
929 repo,
930 self.command.revset_extensions().clone(),
931 &id_prefix_context,
932 self.immutable_expression(),
933 );
934 expression.intersect_with(&to_rewrite_revset);
935
936 let mut commit_id_iter = expression.evaluate_to_commit_ids().map_err(|e| {
937 config_error_with_message("Invalid `revset-aliases.immutable_heads()`", e)
938 })?;
939
940 Ok(commit_id_iter.next().transpose()?)
941 }
942
943 pub fn template_aliases_map(&self) -> &TemplateAliasesMap {
944 &self.template_aliases_map
945 }
946
947 pub fn parse_template<'a, C, L>(
949 &self,
950 ui: &Ui,
951 language: &L,
952 template_text: &str,
953 ) -> Result<TemplateRenderer<'a, C>, CommandError>
954 where
955 C: Clone + 'a,
956 L: TemplateLanguage<'a> + ?Sized,
957 L::Property: WrapTemplateProperty<'a, C>,
958 {
959 let mut diagnostics = TemplateDiagnostics::new();
960 let template = template_builder::parse(
961 language,
962 &mut diagnostics,
963 template_text,
964 &self.template_aliases_map,
965 )?;
966 print_parse_diagnostics(ui, "In template expression", &diagnostics)?;
967 Ok(template)
968 }
969
970 pub fn commit_template_language<'a>(
973 &'a self,
974 repo: &'a dyn Repo,
975 id_prefix_context: &'a IdPrefixContext,
976 ) -> CommitTemplateLanguage<'a> {
977 CommitTemplateLanguage::new(
978 repo,
979 &self.path_converter,
980 &self.workspace_name,
981 self.revset_parse_context(),
982 id_prefix_context,
983 self.immutable_expression(),
984 self.conflict_marker_style,
985 &self.command.data.commit_template_extensions,
986 )
987 }
988
989 pub fn operation_template_extensions(&self) -> &[Arc<dyn OperationTemplateLanguageExtension>] {
990 &self.command.data.operation_template_extensions
991 }
992}
993
994pub struct WorkspaceCommandHelper {
997 workspace: Workspace,
998 user_repo: ReadonlyUserRepo,
999 env: WorkspaceCommandEnvironment,
1000 commit_summary_template_text: String,
1002 op_summary_template_text: String,
1003 may_update_working_copy: bool,
1004 working_copy_shared_with_git: bool,
1005}
1006
1007enum SnapshotWorkingCopyError {
1008 Command(CommandError),
1009 StaleWorkingCopy(CommandError),
1010}
1011
1012impl SnapshotWorkingCopyError {
1013 fn into_command_error(self) -> CommandError {
1014 match self {
1015 Self::Command(err) => err,
1016 Self::StaleWorkingCopy(err) => err,
1017 }
1018 }
1019}
1020
1021fn snapshot_command_error<E>(err: E) -> SnapshotWorkingCopyError
1022where
1023 E: Into<CommandError>,
1024{
1025 SnapshotWorkingCopyError::Command(err.into())
1026}
1027
1028impl WorkspaceCommandHelper {
1029 #[instrument(skip_all)]
1030 fn new(
1031 ui: &Ui,
1032 workspace: Workspace,
1033 repo: Arc<ReadonlyRepo>,
1034 env: WorkspaceCommandEnvironment,
1035 loaded_at_head: bool,
1036 ) -> Result<Self, CommandError> {
1037 let settings = workspace.settings();
1038 let commit_summary_template_text = settings.get_string("templates.commit_summary")?;
1039 let op_summary_template_text = settings.get_string("templates.op_summary")?;
1040 let may_update_working_copy =
1041 loaded_at_head && !env.command.global_args().ignore_working_copy;
1042 let working_copy_shared_with_git =
1043 crate::git_util::is_colocated_git_workspace(&workspace, &repo);
1044
1045 let helper = Self {
1046 workspace,
1047 user_repo: ReadonlyUserRepo::new(repo),
1048 env,
1049 commit_summary_template_text,
1050 op_summary_template_text,
1051 may_update_working_copy,
1052 working_copy_shared_with_git,
1053 };
1054 helper.parse_operation_template(ui, &helper.op_summary_template_text)?;
1057 helper.parse_commit_template(ui, &helper.commit_summary_template_text)?;
1058 helper.parse_commit_template(ui, SHORT_CHANGE_ID_TEMPLATE_TEXT)?;
1059 Ok(helper)
1060 }
1061
1062 pub fn settings(&self) -> &UserSettings {
1064 self.workspace.settings()
1065 }
1066
1067 pub fn check_working_copy_writable(&self) -> Result<(), CommandError> {
1068 if self.may_update_working_copy {
1069 Ok(())
1070 } else {
1071 let hint = if self.env.command.global_args().ignore_working_copy {
1072 "Don't use --ignore-working-copy."
1073 } else {
1074 "Don't use --at-op."
1075 };
1076 Err(user_error_with_hint(
1077 "This command must be able to update the working copy.",
1078 hint,
1079 ))
1080 }
1081 }
1082
1083 #[instrument(skip_all)]
1087 fn maybe_snapshot_impl(&mut self, ui: &Ui) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
1088 if !self.may_update_working_copy {
1089 return Ok(SnapshotStats::default());
1090 }
1091
1092 #[cfg(feature = "git")]
1093 if self.working_copy_shared_with_git {
1094 self.import_git_head(ui).map_err(snapshot_command_error)?;
1095 }
1096 let stats = self.snapshot_working_copy(ui)?;
1101
1102 #[cfg(feature = "git")]
1104 if self.working_copy_shared_with_git {
1105 self.import_git_refs(ui).map_err(snapshot_command_error)?;
1106 }
1107 Ok(stats)
1108 }
1109
1110 #[instrument(skip_all)]
1113 pub fn maybe_snapshot(&mut self, ui: &Ui) -> Result<(), CommandError> {
1114 let stats = self
1115 .maybe_snapshot_impl(ui)
1116 .map_err(|err| err.into_command_error())?;
1117 print_snapshot_stats(ui, &stats, self.env().path_converter())?;
1118 Ok(())
1119 }
1120
1121 #[cfg(feature = "git")]
1128 #[instrument(skip_all)]
1129 fn import_git_head(&mut self, ui: &Ui) -> Result<(), CommandError> {
1130 assert!(self.may_update_working_copy);
1131 let mut tx = self.start_transaction();
1132 jj_lib::git::import_head(tx.repo_mut())?;
1133 if !tx.repo().has_changes() {
1134 return Ok(());
1135 }
1136
1137 let mut tx = tx.into_inner();
1148 let old_git_head = self.repo().view().git_head().clone();
1149 let new_git_head = tx.repo().view().git_head().clone();
1150 if let Some(new_git_head_id) = new_git_head.as_normal() {
1151 let workspace_name = self.workspace_name().to_owned();
1152 let new_git_head_commit = tx.repo().store().get_commit(new_git_head_id)?;
1153 tx.repo_mut()
1154 .check_out(workspace_name, &new_git_head_commit)?;
1155 let mut locked_ws = self.workspace.start_working_copy_mutation()?;
1156 locked_ws.locked_wc().reset(&new_git_head_commit)?;
1160 tx.repo_mut().rebase_descendants()?;
1161 self.user_repo = ReadonlyUserRepo::new(tx.commit("import git head")?);
1162 locked_ws.finish(self.user_repo.repo.op_id().clone())?;
1163 if old_git_head.is_present() {
1164 writeln!(
1165 ui.status(),
1166 "Reset the working copy parent to the new Git HEAD."
1167 )?;
1168 } else {
1169 }
1171 } else {
1172 self.finish_transaction(ui, tx, "import git head")?;
1174 }
1175 Ok(())
1176 }
1177
1178 #[cfg(feature = "git")]
1187 #[instrument(skip_all)]
1188 fn import_git_refs(&mut self, ui: &Ui) -> Result<(), CommandError> {
1189 let git_settings = self.settings().git_settings()?;
1190 let mut tx = self.start_transaction();
1191 let stats = jj_lib::git::import_refs(tx.repo_mut(), &git_settings)?;
1192 crate::git_util::print_git_import_stats(ui, tx.repo(), &stats, false)?;
1193 if !tx.repo().has_changes() {
1194 return Ok(());
1195 }
1196
1197 let mut tx = tx.into_inner();
1198 let num_rebased = tx.repo_mut().rebase_descendants()?;
1200 if num_rebased > 0 {
1201 writeln!(
1202 ui.status(),
1203 "Rebased {num_rebased} descendant commits off of commits rewritten from git"
1204 )?;
1205 }
1206 self.finish_transaction(ui, tx, "import git refs")?;
1207 writeln!(
1208 ui.status(),
1209 "Done importing changes from the underlying Git repo."
1210 )?;
1211 Ok(())
1212 }
1213
1214 pub fn repo(&self) -> &Arc<ReadonlyRepo> {
1215 &self.user_repo.repo
1216 }
1217
1218 pub fn repo_path(&self) -> &Path {
1219 self.workspace.repo_path()
1220 }
1221
1222 pub fn workspace(&self) -> &Workspace {
1223 &self.workspace
1224 }
1225
1226 pub fn working_copy(&self) -> &dyn WorkingCopy {
1227 self.workspace.working_copy()
1228 }
1229
1230 pub fn env(&self) -> &WorkspaceCommandEnvironment {
1231 &self.env
1232 }
1233
1234 pub fn unchecked_start_working_copy_mutation(
1235 &mut self,
1236 ) -> Result<(LockedWorkspace<'_>, Commit), CommandError> {
1237 self.check_working_copy_writable()?;
1238 let wc_commit = if let Some(wc_commit_id) = self.get_wc_commit_id() {
1239 self.repo().store().get_commit(wc_commit_id)?
1240 } else {
1241 return Err(user_error("Nothing checked out in this workspace"));
1242 };
1243
1244 let locked_ws = self.workspace.start_working_copy_mutation()?;
1245
1246 Ok((locked_ws, wc_commit))
1247 }
1248
1249 pub fn start_working_copy_mutation(
1250 &mut self,
1251 ) -> Result<(LockedWorkspace<'_>, Commit), CommandError> {
1252 let (mut locked_ws, wc_commit) = self.unchecked_start_working_copy_mutation()?;
1253 if wc_commit.tree_id() != locked_ws.locked_wc().old_tree_id() {
1254 return Err(user_error("Concurrent working copy operation. Try again."));
1255 }
1256 Ok((locked_ws, wc_commit))
1257 }
1258
1259 fn create_and_check_out_recovery_commit(
1260 &mut self,
1261 ui: &Ui,
1262 ) -> Result<SnapshotStats, CommandError> {
1263 self.check_working_copy_writable()?;
1264
1265 let workspace_name = self.workspace_name().to_owned();
1266 let mut locked_ws = self.workspace.start_working_copy_mutation()?;
1267 let (repo, new_commit) = working_copy::create_and_check_out_recovery_commit(
1268 locked_ws.locked_wc(),
1269 &self.user_repo.repo,
1270 workspace_name,
1271 "RECOVERY COMMIT FROM `jj workspace update-stale`
1272
1273This commit contains changes that were written to the working copy by an
1274operation that was subsequently lost (or was at least unavailable when you ran
1275`jj workspace update-stale`). Because the operation was lost, we don't know
1276what the parent commits are supposed to be. That means that the diff compared
1277to the current parents may contain changes from multiple commits.
1278",
1279 )?;
1280
1281 writeln!(
1282 ui.status(),
1283 "Created and checked out recovery commit {}",
1284 short_commit_hash(new_commit.id())
1285 )?;
1286 locked_ws.finish(repo.op_id().clone())?;
1287 self.user_repo = ReadonlyUserRepo::new(repo);
1288
1289 self.maybe_snapshot_impl(ui)
1290 .map_err(|err| err.into_command_error())
1291 }
1292
1293 pub fn workspace_root(&self) -> &Path {
1294 self.workspace.workspace_root()
1295 }
1296
1297 pub fn workspace_name(&self) -> &WorkspaceName {
1298 self.workspace.workspace_name()
1299 }
1300
1301 pub fn get_wc_commit_id(&self) -> Option<&CommitId> {
1302 self.repo().view().get_wc_commit_id(self.workspace_name())
1303 }
1304
1305 pub fn working_copy_shared_with_git(&self) -> bool {
1306 self.working_copy_shared_with_git
1307 }
1308
1309 pub fn format_file_path(&self, file: &RepoPath) -> String {
1310 self.path_converter().format_file_path(file)
1311 }
1312
1313 pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
1316 self.path_converter().parse_file_path(input)
1317 }
1318
1319 pub fn parse_file_patterns(
1321 &self,
1322 ui: &Ui,
1323 values: &[String],
1324 ) -> Result<FilesetExpression, CommandError> {
1325 if values.is_empty() {
1329 Ok(FilesetExpression::all())
1330 } else {
1331 self.parse_union_filesets(ui, values)
1332 }
1333 }
1334
1335 pub fn parse_union_filesets(
1337 &self,
1338 ui: &Ui,
1339 file_args: &[String], ) -> Result<FilesetExpression, CommandError> {
1341 let mut diagnostics = FilesetDiagnostics::new();
1342 let expressions: Vec<_> = file_args
1343 .iter()
1344 .map(|arg| fileset::parse_maybe_bare(&mut diagnostics, arg, self.path_converter()))
1345 .try_collect()?;
1346 print_parse_diagnostics(ui, "In fileset expression", &diagnostics)?;
1347 Ok(FilesetExpression::union_all(expressions))
1348 }
1349
1350 pub fn auto_tracking_matcher(&self, ui: &Ui) -> Result<Box<dyn Matcher>, CommandError> {
1351 let mut diagnostics = FilesetDiagnostics::new();
1352 let pattern = self.settings().get_string("snapshot.auto-track")?;
1353 let expression = fileset::parse(
1354 &mut diagnostics,
1355 &pattern,
1356 &RepoPathUiConverter::Fs {
1357 cwd: "".into(),
1358 base: "".into(),
1359 },
1360 )?;
1361 print_parse_diagnostics(ui, "In `snapshot.auto-track`", &diagnostics)?;
1362 Ok(expression.to_matcher())
1363 }
1364
1365 pub fn snapshot_options_with_start_tracking_matcher<'a>(
1366 &self,
1367 start_tracking_matcher: &'a dyn Matcher,
1368 ) -> Result<SnapshotOptions<'a>, CommandError> {
1369 let base_ignores = self.base_ignores()?;
1370 let HumanByteSize(mut max_new_file_size) = self
1371 .settings()
1372 .get_value_with("snapshot.max-new-file-size", TryInto::try_into)?;
1373 if max_new_file_size == 0 {
1374 max_new_file_size = u64::MAX;
1375 }
1376 Ok(SnapshotOptions {
1377 base_ignores,
1378 progress: None,
1379 start_tracking_matcher,
1380 max_new_file_size,
1381 })
1382 }
1383
1384 pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
1385 self.env.path_converter()
1386 }
1387
1388 #[cfg(not(feature = "git"))]
1389 pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1390 Ok(GitIgnoreFile::empty())
1391 }
1392
1393 #[cfg(feature = "git")]
1394 #[instrument(skip_all)]
1395 pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1396 let get_excludes_file_path = |config: &gix::config::File| -> Option<PathBuf> {
1397 if let Some(value) = config.string("core.excludesFile") {
1400 let path = str::from_utf8(&value)
1401 .ok()
1402 .map(jj_lib::file_util::expand_home_path)?;
1403 Some(self.workspace_root().join(path))
1406 } else {
1407 xdg_config_home().ok().map(|x| x.join("git").join("ignore"))
1408 }
1409 };
1410
1411 fn xdg_config_home() -> Result<PathBuf, std::env::VarError> {
1412 if let Ok(x) = std::env::var("XDG_CONFIG_HOME")
1413 && !x.is_empty()
1414 {
1415 return Ok(PathBuf::from(x));
1416 }
1417 std::env::var("HOME").map(|x| Path::new(&x).join(".config"))
1418 }
1419
1420 let mut git_ignores = GitIgnoreFile::empty();
1421 if let Ok(git_backend) = jj_lib::git::get_git_backend(self.repo().store()) {
1422 let git_repo = git_backend.git_repo();
1423 if let Some(excludes_file_path) = get_excludes_file_path(&git_repo.config_snapshot()) {
1424 git_ignores = git_ignores.chain_with_file("", excludes_file_path)?;
1425 }
1426 git_ignores = git_ignores
1427 .chain_with_file("", git_backend.git_repo_path().join("info").join("exclude"))?;
1428 } else if let Ok(git_config) = gix::config::File::from_globals()
1429 && let Some(excludes_file_path) = get_excludes_file_path(&git_config)
1430 {
1431 git_ignores = git_ignores.chain_with_file("", excludes_file_path)?;
1432 }
1433 Ok(git_ignores)
1434 }
1435
1436 pub fn diff_renderer(&self, formats: Vec<DiffFormat>) -> DiffRenderer<'_> {
1438 DiffRenderer::new(
1439 self.repo().as_ref(),
1440 self.path_converter(),
1441 self.env.conflict_marker_style(),
1442 formats,
1443 )
1444 }
1445
1446 pub fn diff_renderer_for(
1448 &self,
1449 args: &DiffFormatArgs,
1450 ) -> Result<DiffRenderer<'_>, CommandError> {
1451 let formats = diff_util::diff_formats_for(self.settings(), args)?;
1452 Ok(self.diff_renderer(formats))
1453 }
1454
1455 pub fn diff_renderer_for_log(
1459 &self,
1460 args: &DiffFormatArgs,
1461 patch: bool,
1462 ) -> Result<Option<DiffRenderer<'_>>, CommandError> {
1463 let formats = diff_util::diff_formats_for_log(self.settings(), args, patch)?;
1464 Ok((!formats.is_empty()).then(|| self.diff_renderer(formats)))
1465 }
1466
1467 pub fn diff_editor(
1471 &self,
1472 ui: &Ui,
1473 tool_name: Option<&str>,
1474 ) -> Result<DiffEditor, CommandError> {
1475 let base_ignores = self.base_ignores()?;
1476 let conflict_marker_style = self.env.conflict_marker_style();
1477 if let Some(name) = tool_name {
1478 Ok(DiffEditor::with_name(
1479 name,
1480 self.settings(),
1481 base_ignores,
1482 conflict_marker_style,
1483 )?)
1484 } else {
1485 Ok(DiffEditor::from_settings(
1486 ui,
1487 self.settings(),
1488 base_ignores,
1489 conflict_marker_style,
1490 )?)
1491 }
1492 }
1493
1494 pub fn diff_selector(
1498 &self,
1499 ui: &Ui,
1500 tool_name: Option<&str>,
1501 force_interactive: bool,
1502 ) -> Result<DiffSelector, CommandError> {
1503 if tool_name.is_some() || force_interactive {
1504 Ok(DiffSelector::Interactive(self.diff_editor(ui, tool_name)?))
1505 } else {
1506 Ok(DiffSelector::NonInteractive)
1507 }
1508 }
1509
1510 pub fn merge_editor(
1514 &self,
1515 ui: &Ui,
1516 tool_name: Option<&str>,
1517 ) -> Result<MergeEditor, MergeToolConfigError> {
1518 let conflict_marker_style = self.env.conflict_marker_style();
1519 if let Some(name) = tool_name {
1520 MergeEditor::with_name(
1521 name,
1522 self.settings(),
1523 self.path_converter().clone(),
1524 conflict_marker_style,
1525 )
1526 } else {
1527 MergeEditor::from_settings(
1528 ui,
1529 self.settings(),
1530 self.path_converter().clone(),
1531 conflict_marker_style,
1532 )
1533 }
1534 }
1535
1536 pub fn text_editor(&self) -> Result<TextEditor, ConfigGetError> {
1538 TextEditor::from_settings(self.settings())
1539 }
1540
1541 pub fn resolve_single_op(&self, op_str: &str) -> Result<Operation, OpsetEvaluationError> {
1542 op_walk::resolve_op_with_repo(self.repo(), op_str)
1543 }
1544
1545 pub fn resolve_single_rev(
1548 &self,
1549 ui: &Ui,
1550 revision_arg: &RevisionArg,
1551 ) -> Result<Commit, CommandError> {
1552 let expression = self.parse_revset(ui, revision_arg)?;
1553 revset_util::evaluate_revset_to_single_commit(revision_arg.as_ref(), &expression, || {
1554 self.commit_summary_template()
1555 })
1556 }
1557
1558 pub fn resolve_some_revsets_default_single(
1561 &self,
1562 ui: &Ui,
1563 revision_args: &[RevisionArg],
1564 ) -> Result<IndexSet<CommitId>, CommandError> {
1565 let mut all_commits = IndexSet::new();
1566 for revision_arg in revision_args {
1567 let (expression, modifier) = self.parse_revset_with_modifier(ui, revision_arg)?;
1568 let all = match modifier {
1569 Some(RevsetModifier::All) => true,
1570 None => self.settings().get_bool("ui.always-allow-large-revsets")?,
1571 };
1572 if all {
1573 for commit_id in expression.evaluate_to_commit_ids()? {
1574 all_commits.insert(commit_id?);
1575 }
1576 } else {
1577 let commit = revset_util::evaluate_revset_to_single_commit(
1578 revision_arg.as_ref(),
1579 &expression,
1580 || self.commit_summary_template(),
1581 )?;
1582 if !all_commits.insert(commit.id().clone()) {
1583 let commit_hash = short_commit_hash(commit.id());
1584 return Err(user_error(format!(
1585 r#"More than one revset resolved to revision {commit_hash}"#,
1586 )));
1587 }
1588 }
1589 }
1590 if all_commits.is_empty() {
1591 Err(user_error("Empty revision set"))
1592 } else {
1593 Ok(all_commits)
1594 }
1595 }
1596
1597 pub fn parse_revset(
1598 &self,
1599 ui: &Ui,
1600 revision_arg: &RevisionArg,
1601 ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1602 let (expression, modifier) = self.parse_revset_with_modifier(ui, revision_arg)?;
1603 let (None | Some(RevsetModifier::All)) = modifier;
1606 Ok(expression)
1607 }
1608
1609 fn parse_revset_with_modifier(
1610 &self,
1611 ui: &Ui,
1612 revision_arg: &RevisionArg,
1613 ) -> Result<(RevsetExpressionEvaluator<'_>, Option<RevsetModifier>), CommandError> {
1614 let mut diagnostics = RevsetDiagnostics::new();
1615 let context = self.env.revset_parse_context();
1616 let (expression, modifier) =
1617 revset::parse_with_modifier(&mut diagnostics, revision_arg.as_ref(), &context)?;
1618 print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1619 Ok((self.attach_revset_evaluator(expression), modifier))
1620 }
1621
1622 pub fn parse_union_revsets(
1624 &self,
1625 ui: &Ui,
1626 revision_args: &[RevisionArg],
1627 ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1628 let mut diagnostics = RevsetDiagnostics::new();
1629 let context = self.env.revset_parse_context();
1630 let expressions: Vec<_> = revision_args
1631 .iter()
1632 .map(|arg| revset::parse_with_modifier(&mut diagnostics, arg.as_ref(), &context))
1633 .map_ok(|(expression, None | Some(RevsetModifier::All))| expression)
1634 .try_collect()?;
1635 print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1636 let expression = RevsetExpression::union_all(&expressions);
1637 Ok(self.attach_revset_evaluator(expression))
1638 }
1639
1640 pub fn attach_revset_evaluator(
1641 &self,
1642 expression: Arc<UserRevsetExpression>,
1643 ) -> RevsetExpressionEvaluator<'_> {
1644 RevsetExpressionEvaluator::new(
1645 self.repo().as_ref(),
1646 self.env.command.revset_extensions().clone(),
1647 self.id_prefix_context(),
1648 expression,
1649 )
1650 }
1651
1652 pub fn id_prefix_context(&self) -> &IdPrefixContext {
1653 self.user_repo
1654 .id_prefix_context
1655 .get_or_init(|| self.env.new_id_prefix_context())
1656 }
1657
1658 pub fn parse_template<'a, C, L>(
1660 &self,
1661 ui: &Ui,
1662 language: &L,
1663 template_text: &str,
1664 ) -> Result<TemplateRenderer<'a, C>, CommandError>
1665 where
1666 C: Clone + 'a,
1667 L: TemplateLanguage<'a> + ?Sized,
1668 L::Property: WrapTemplateProperty<'a, C>,
1669 {
1670 self.env.parse_template(ui, language, template_text)
1671 }
1672
1673 fn reparse_valid_template<'a, C, L>(
1675 &self,
1676 language: &L,
1677 template_text: &str,
1678 ) -> TemplateRenderer<'a, C>
1679 where
1680 C: Clone + 'a,
1681 L: TemplateLanguage<'a> + ?Sized,
1682 L::Property: WrapTemplateProperty<'a, C>,
1683 {
1684 template_builder::parse(
1685 language,
1686 &mut TemplateDiagnostics::new(),
1687 template_text,
1688 &self.env.template_aliases_map,
1689 )
1690 .expect("parse error should be confined by WorkspaceCommandHelper::new()")
1691 }
1692
1693 pub fn parse_commit_template(
1695 &self,
1696 ui: &Ui,
1697 template_text: &str,
1698 ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
1699 let language = self.commit_template_language();
1700 self.parse_template(ui, &language, template_text)
1701 }
1702
1703 pub fn parse_operation_template(
1705 &self,
1706 ui: &Ui,
1707 template_text: &str,
1708 ) -> Result<TemplateRenderer<'_, Operation>, CommandError> {
1709 let language = self.operation_template_language();
1710 self.parse_template(ui, &language, template_text)
1711 }
1712
1713 pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
1715 self.env
1716 .commit_template_language(self.repo().as_ref(), self.id_prefix_context())
1717 }
1718
1719 pub fn operation_template_language(&self) -> OperationTemplateLanguage {
1721 OperationTemplateLanguage::new(
1722 self.workspace.repo_loader(),
1723 Some(self.repo().op_id()),
1724 self.env.operation_template_extensions(),
1725 )
1726 }
1727
1728 pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
1730 let language = self.commit_template_language();
1731 self.reparse_valid_template(&language, &self.commit_summary_template_text)
1732 .labeled(["commit"])
1733 }
1734
1735 pub fn operation_summary_template(&self) -> TemplateRenderer<'_, Operation> {
1737 let language = self.operation_template_language();
1738 self.reparse_valid_template(&language, &self.op_summary_template_text)
1739 .labeled(["operation"])
1740 }
1741
1742 pub fn short_change_id_template(&self) -> TemplateRenderer<'_, Commit> {
1743 let language = self.commit_template_language();
1744 self.reparse_valid_template(&language, SHORT_CHANGE_ID_TEMPLATE_TEXT)
1745 .labeled(["commit"])
1746 }
1747
1748 pub fn format_commit_summary(&self, commit: &Commit) -> String {
1753 let output = self.commit_summary_template().format_plain_text(commit);
1754 output.into_string_lossy()
1755 }
1756
1757 #[instrument(skip_all)]
1761 pub fn write_commit_summary(
1762 &self,
1763 formatter: &mut dyn Formatter,
1764 commit: &Commit,
1765 ) -> std::io::Result<()> {
1766 self.commit_summary_template().format(commit, formatter)
1767 }
1768
1769 pub fn check_rewritable<'a>(
1770 &self,
1771 commits: impl IntoIterator<Item = &'a CommitId>,
1772 ) -> Result<(), CommandError> {
1773 let repo = self.repo().as_ref();
1774 let commit_ids = commits.into_iter().cloned().collect_vec();
1775 let Some(commit_id) = self.env.find_immutable_commit(repo, &commit_ids)? else {
1776 return Ok(());
1777 };
1778 let error = if &commit_id == repo.store().root_commit_id() {
1779 user_error(format!("The root commit {commit_id:.12} is immutable"))
1780 } else {
1781 let mut error = user_error(format!("Commit {commit_id:.12} is immutable"));
1782 let commit = repo.store().get_commit(&commit_id)?;
1783 error.add_formatted_hint_with(|formatter| {
1784 write!(formatter, "Could not modify commit: ")?;
1785 self.write_commit_summary(formatter, &commit)?;
1786 Ok(())
1787 });
1788 error.add_hint("Immutable commits are used to protect shared history.");
1789 error.add_hint(indoc::indoc! {"
1790 For more information, see:
1791 - https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
1792 - `jj help -k config`, \"Set of immutable commits\""});
1793
1794 let id_prefix_context =
1797 IdPrefixContext::new(self.env.command.revset_extensions().clone());
1798 let to_rewrite_expr = RevsetExpression::commits(commit_ids);
1799 let (lower_bound, upper_bound) = RevsetExpressionEvaluator::new(
1800 repo,
1801 self.env.command.revset_extensions().clone(),
1802 &id_prefix_context,
1803 self.env
1804 .immutable_expression()
1805 .intersection(&to_rewrite_expr.descendants()),
1806 )
1807 .evaluate()?
1808 .count_estimate()?;
1809 let exact = upper_bound == Some(lower_bound);
1810 let or_more = if exact { "" } else { " or more" };
1811 error.add_hint(format!(
1812 "This operation would rewrite {lower_bound}{or_more} immutable commits."
1813 ));
1814
1815 error
1816 };
1817 Err(error)
1818 }
1819
1820 #[instrument(skip_all)]
1821 fn snapshot_working_copy(
1822 &mut self,
1823 ui: &Ui,
1824 ) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
1825 let workspace_name = self.workspace_name().to_owned();
1826 let get_wc_commit = |repo: &ReadonlyRepo| -> Result<Option<_>, _> {
1827 repo.view()
1828 .get_wc_commit_id(&workspace_name)
1829 .map(|id| repo.store().get_commit(id))
1830 .transpose()
1831 .map_err(snapshot_command_error)
1832 };
1833 let repo = self.repo().clone();
1834 let Some(wc_commit) = get_wc_commit(&repo)? else {
1835 return Ok(SnapshotStats::default());
1838 };
1839 let auto_tracking_matcher = self
1840 .auto_tracking_matcher(ui)
1841 .map_err(snapshot_command_error)?;
1842 let options = self
1843 .snapshot_options_with_start_tracking_matcher(&auto_tracking_matcher)
1844 .map_err(snapshot_command_error)?;
1845
1846 let mut locked_ws = self
1848 .workspace
1849 .start_working_copy_mutation()
1850 .map_err(snapshot_command_error)?;
1851 let old_op_id = locked_ws.locked_wc().old_operation_id().clone();
1852
1853 let (repo, wc_commit) =
1854 match WorkingCopyFreshness::check_stale(locked_ws.locked_wc(), &wc_commit, &repo) {
1855 Ok(WorkingCopyFreshness::Fresh) => (repo, wc_commit),
1856 Ok(WorkingCopyFreshness::Updated(wc_operation)) => {
1857 let repo = repo
1858 .reload_at(&wc_operation)
1859 .map_err(snapshot_command_error)?;
1860 let wc_commit = if let Some(wc_commit) = get_wc_commit(&repo)? {
1861 wc_commit
1862 } else {
1863 return Ok(SnapshotStats::default());
1865 };
1866 (repo, wc_commit)
1867 }
1868 Ok(WorkingCopyFreshness::WorkingCopyStale) => {
1869 return Err(SnapshotWorkingCopyError::StaleWorkingCopy(
1870 user_error_with_hint(
1871 format!(
1872 "The working copy is stale (not updated since operation {}).",
1873 short_operation_hash(&old_op_id)
1874 ),
1875 "Run `jj workspace update-stale` to update it.
1876See https://jj-vcs.github.io/jj/latest/working-copy/#stale-working-copy \
1877 for more information.",
1878 ),
1879 ));
1880 }
1881 Ok(WorkingCopyFreshness::SiblingOperation) => {
1882 return Err(SnapshotWorkingCopyError::StaleWorkingCopy(internal_error(
1883 format!(
1884 "The repo was loaded at operation {}, which seems to be a sibling of \
1885 the working copy's operation {}",
1886 short_operation_hash(repo.op_id()),
1887 short_operation_hash(&old_op_id)
1888 ),
1889 )));
1890 }
1891 Err(OpStoreError::ObjectNotFound { .. }) => {
1892 return Err(SnapshotWorkingCopyError::StaleWorkingCopy(
1893 user_error_with_hint(
1894 "Could not read working copy's operation.",
1895 "Run `jj workspace update-stale` to recover.
1896See https://jj-vcs.github.io/jj/latest/working-copy/#stale-working-copy \
1897 for more information.",
1898 ),
1899 ));
1900 }
1901 Err(e) => return Err(snapshot_command_error(e)),
1902 };
1903 self.user_repo = ReadonlyUserRepo::new(repo);
1904 let (new_tree_id, stats) = {
1905 let mut options = options;
1906 let progress = crate::progress::snapshot_progress(ui);
1907 options.progress = progress.as_ref().map(|x| x as _);
1908 locked_ws
1909 .locked_wc()
1910 .snapshot(&options)
1911 .map_err(snapshot_command_error)?
1912 };
1913 if new_tree_id != *wc_commit.tree_id() {
1914 let mut tx =
1915 start_repo_transaction(&self.user_repo.repo, self.env.command.string_args());
1916 tx.set_is_snapshot(true);
1917 let mut_repo = tx.repo_mut();
1918 let commit = mut_repo
1919 .rewrite_commit(&wc_commit)
1920 .set_tree_id(new_tree_id)
1921 .write()
1922 .map_err(snapshot_command_error)?;
1923 mut_repo
1924 .set_wc_commit(workspace_name, commit.id().clone())
1925 .map_err(snapshot_command_error)?;
1926
1927 let num_rebased = mut_repo
1929 .rebase_descendants()
1930 .map_err(snapshot_command_error)?;
1931 if num_rebased > 0 {
1932 writeln!(
1933 ui.status(),
1934 "Rebased {num_rebased} descendant commits onto updated working copy"
1935 )
1936 .map_err(snapshot_command_error)?;
1937 }
1938
1939 #[cfg(feature = "git")]
1940 if self.working_copy_shared_with_git {
1941 let old_tree = wc_commit.tree().map_err(snapshot_command_error)?;
1942 let new_tree = commit.tree().map_err(snapshot_command_error)?;
1943 jj_lib::git::update_intent_to_add(
1944 self.user_repo.repo.as_ref(),
1945 &old_tree,
1946 &new_tree,
1947 )
1948 .map_err(snapshot_command_error)?;
1949
1950 let stats = jj_lib::git::export_refs(mut_repo).map_err(snapshot_command_error)?;
1951 crate::git_util::print_git_export_stats(ui, &stats)
1952 .map_err(snapshot_command_error)?;
1953 }
1954
1955 let repo = tx
1956 .commit("snapshot working copy")
1957 .map_err(snapshot_command_error)?;
1958 self.user_repo = ReadonlyUserRepo::new(repo);
1959 }
1960 locked_ws
1961 .finish(self.user_repo.repo.op_id().clone())
1962 .map_err(snapshot_command_error)?;
1963 Ok(stats)
1964 }
1965
1966 fn update_working_copy(
1967 &mut self,
1968 ui: &Ui,
1969 maybe_old_commit: Option<&Commit>,
1970 new_commit: &Commit,
1971 ) -> Result<(), CommandError> {
1972 assert!(self.may_update_working_copy);
1973 let stats = update_working_copy(
1974 &self.user_repo.repo,
1975 &mut self.workspace,
1976 maybe_old_commit,
1977 new_commit,
1978 )?;
1979 self.print_updated_working_copy_stats(ui, maybe_old_commit, new_commit, &stats)
1980 }
1981
1982 fn print_updated_working_copy_stats(
1983 &self,
1984 ui: &Ui,
1985 maybe_old_commit: Option<&Commit>,
1986 new_commit: &Commit,
1987 stats: &CheckoutStats,
1988 ) -> Result<(), CommandError> {
1989 if Some(new_commit) != maybe_old_commit
1990 && let Some(mut formatter) = ui.status_formatter()
1991 {
1992 let template = self.commit_summary_template();
1993 write!(formatter, "Working copy (@) now at: ")?;
1994 template.format(new_commit, formatter.as_mut())?;
1995 writeln!(formatter)?;
1996 for parent in new_commit.parents() {
1997 let parent = parent?;
1998 write!(formatter, "Parent commit (@-) : ")?;
2000 template.format(&parent, formatter.as_mut())?;
2001 writeln!(formatter)?;
2002 }
2003 }
2004 print_checkout_stats(ui, stats, new_commit)?;
2005 if Some(new_commit) != maybe_old_commit
2006 && let Some(mut formatter) = ui.status_formatter()
2007 && new_commit.has_conflict()?
2008 {
2009 let conflicts = new_commit.tree()?.conflicts().collect_vec();
2010 writeln!(
2011 formatter.labeled("warning").with_heading("Warning: "),
2012 "There are unresolved conflicts at these paths:"
2013 )?;
2014 print_conflicted_paths(conflicts, formatter.as_mut(), self)?;
2015 }
2016 Ok(())
2017 }
2018
2019 pub fn start_transaction(&mut self) -> WorkspaceCommandTransaction<'_> {
2020 let tx = start_repo_transaction(self.repo(), self.env.command.string_args());
2021 let id_prefix_context = mem::take(&mut self.user_repo.id_prefix_context);
2022 WorkspaceCommandTransaction {
2023 helper: self,
2024 tx,
2025 id_prefix_context,
2026 }
2027 }
2028
2029 fn finish_transaction(
2030 &mut self,
2031 ui: &Ui,
2032 mut tx: Transaction,
2033 description: impl Into<String>,
2034 ) -> Result<(), CommandError> {
2035 if !tx.repo().has_changes() {
2036 writeln!(ui.status(), "Nothing changed.")?;
2037 return Ok(());
2038 }
2039 let num_rebased = tx.repo_mut().rebase_descendants()?;
2040 if num_rebased > 0 {
2041 writeln!(ui.status(), "Rebased {num_rebased} descendant commits")?;
2042 }
2043
2044 for (name, wc_commit_id) in &tx.repo().view().wc_commit_ids().clone() {
2045 if self
2046 .env
2047 .find_immutable_commit(tx.repo(), slice::from_ref(wc_commit_id))?
2048 .is_some()
2049 {
2050 let wc_commit = tx.repo().store().get_commit(wc_commit_id)?;
2051 tx.repo_mut().check_out(name.clone(), &wc_commit)?;
2052 writeln!(
2053 ui.warning_default(),
2054 "The working-copy commit in workspace '{name}' became immutable, so a new \
2055 commit has been created on top of it.",
2056 name = name.as_symbol()
2057 )?;
2058 }
2059 }
2060
2061 let old_repo = tx.base_repo().clone();
2062
2063 let maybe_old_wc_commit = old_repo
2064 .view()
2065 .get_wc_commit_id(self.workspace_name())
2066 .map(|commit_id| tx.base_repo().store().get_commit(commit_id))
2067 .transpose()?;
2068 let maybe_new_wc_commit = tx
2069 .repo()
2070 .view()
2071 .get_wc_commit_id(self.workspace_name())
2072 .map(|commit_id| tx.repo().store().get_commit(commit_id))
2073 .transpose()?;
2074
2075 #[cfg(feature = "git")]
2076 if self.working_copy_shared_with_git {
2077 use std::error::Error as _;
2078 if let Some(wc_commit) = &maybe_new_wc_commit {
2079 match jj_lib::git::reset_head(tx.repo_mut(), wc_commit) {
2082 Ok(()) => {}
2083 Err(err @ jj_lib::git::GitResetHeadError::UpdateHeadRef(_)) => {
2084 writeln!(ui.warning_default(), "{err}")?;
2085 crate::command_error::print_error_sources(ui, err.source())?;
2086 }
2087 Err(err) => return Err(err.into()),
2088 }
2089 }
2090 let stats = jj_lib::git::export_refs(tx.repo_mut())?;
2091 crate::git_util::print_git_export_stats(ui, &stats)?;
2092 }
2093
2094 self.user_repo = ReadonlyUserRepo::new(tx.commit(description)?);
2095
2096 if self.may_update_working_copy {
2100 if let Some(new_commit) = &maybe_new_wc_commit {
2101 self.update_working_copy(ui, maybe_old_wc_commit.as_ref(), new_commit)?;
2102 } else {
2103 }
2106 }
2107
2108 self.report_repo_changes(ui, &old_repo)?;
2109
2110 let settings = self.settings();
2111 let missing_user_name = settings.user_name().is_empty();
2112 let missing_user_mail = settings.user_email().is_empty();
2113 if missing_user_name || missing_user_mail {
2114 let not_configured_msg = match (missing_user_name, missing_user_mail) {
2115 (true, true) => "Name and email not configured.",
2116 (true, false) => "Name not configured.",
2117 (false, true) => "Email not configured.",
2118 _ => unreachable!(),
2119 };
2120 writeln!(
2121 ui.warning_default(),
2122 "{not_configured_msg} Until configured, your commits will be created with the \
2123 empty identity, and can't be pushed to remotes."
2124 )?;
2125 writeln!(ui.hint_default(), "To configure, run:")?;
2126 if missing_user_name {
2127 writeln!(
2128 ui.hint_no_heading(),
2129 r#" jj config set --user user.name "Some One""#
2130 )?;
2131 }
2132 if missing_user_mail {
2133 writeln!(
2134 ui.hint_no_heading(),
2135 r#" jj config set --user user.email "someone@example.com""#
2136 )?;
2137 }
2138 }
2139 Ok(())
2140 }
2141
2142 fn report_repo_changes(
2145 &self,
2146 ui: &Ui,
2147 old_repo: &Arc<ReadonlyRepo>,
2148 ) -> Result<(), CommandError> {
2149 let Some(mut fmt) = ui.status_formatter() else {
2150 return Ok(());
2151 };
2152 let old_view = old_repo.view();
2153 let new_repo = self.repo().as_ref();
2154 let new_view = new_repo.view();
2155 let old_heads = RevsetExpression::commits(old_view.heads().iter().cloned().collect());
2156 let new_heads = RevsetExpression::commits(new_view.heads().iter().cloned().collect());
2157 let conflicts = RevsetExpression::filter(RevsetFilterPredicate::HasConflict)
2163 .filtered(RevsetFilterPredicate::File(FilesetExpression::all()));
2164 let removed_conflicts_expr = new_heads.range(&old_heads).intersection(&conflicts);
2165 let added_conflicts_expr = old_heads.range(&new_heads).intersection(&conflicts);
2166
2167 let get_commits =
2168 |expr: Arc<ResolvedRevsetExpression>| -> Result<Vec<Commit>, CommandError> {
2169 let commits = expr
2170 .evaluate(new_repo)?
2171 .iter()
2172 .commits(new_repo.store())
2173 .try_collect()?;
2174 Ok(commits)
2175 };
2176 let removed_conflict_commits = get_commits(removed_conflicts_expr)?;
2177 let added_conflict_commits = get_commits(added_conflicts_expr)?;
2178
2179 fn commits_by_change_id(commits: &[Commit]) -> IndexMap<&ChangeId, Vec<&Commit>> {
2180 let mut result: IndexMap<&ChangeId, Vec<&Commit>> = IndexMap::new();
2181 for commit in commits {
2182 result.entry(commit.change_id()).or_default().push(commit);
2183 }
2184 result
2185 }
2186 let removed_conflicts_by_change_id = commits_by_change_id(&removed_conflict_commits);
2187 let added_conflicts_by_change_id = commits_by_change_id(&added_conflict_commits);
2188 let mut resolved_conflicts_by_change_id = removed_conflicts_by_change_id.clone();
2189 resolved_conflicts_by_change_id
2190 .retain(|change_id, _commits| !added_conflicts_by_change_id.contains_key(change_id));
2191 let mut new_conflicts_by_change_id = added_conflicts_by_change_id.clone();
2192 new_conflicts_by_change_id
2193 .retain(|change_id, _commits| !removed_conflicts_by_change_id.contains_key(change_id));
2194
2195 if !resolved_conflicts_by_change_id.is_empty() {
2197 let num_resolved: usize = resolved_conflicts_by_change_id
2201 .values()
2202 .map(|commits| commits.len())
2203 .sum();
2204 writeln!(
2205 fmt,
2206 "Existing conflicts were resolved or abandoned from {num_resolved} commits."
2207 )?;
2208 }
2209 if !new_conflicts_by_change_id.is_empty() {
2210 let num_conflicted: usize = new_conflicts_by_change_id
2211 .values()
2212 .map(|commits| commits.len())
2213 .sum();
2214 writeln!(fmt, "New conflicts appeared in {num_conflicted} commits:")?;
2215 print_updated_commits(
2216 fmt.as_mut(),
2217 &self.commit_summary_template(),
2218 new_conflicts_by_change_id.values().flatten().copied(),
2219 )?;
2220 }
2221
2222 if !(added_conflict_commits.is_empty()
2226 || resolved_conflicts_by_change_id.is_empty() && new_conflicts_by_change_id.is_empty())
2227 {
2228 if new_conflicts_by_change_id.is_empty() {
2233 writeln!(
2234 fmt,
2235 "There are still unresolved conflicts in rebased descendants.",
2236 )?;
2237 }
2238
2239 self.report_repo_conflicts(
2240 fmt.as_mut(),
2241 new_repo,
2242 added_conflict_commits
2243 .iter()
2244 .map(|commit| commit.id().clone())
2245 .collect(),
2246 )?;
2247 }
2248 revset_util::warn_unresolvable_trunk(ui, new_repo, &self.env.revset_parse_context())?;
2249
2250 Ok(())
2251 }
2252
2253 pub fn report_repo_conflicts(
2254 &self,
2255 fmt: &mut dyn Formatter,
2256 repo: &ReadonlyRepo,
2257 conflicted_commits: Vec<CommitId>,
2258 ) -> Result<(), CommandError> {
2259 if !self.settings().get_bool("hints.resolving-conflicts")? || conflicted_commits.is_empty()
2260 {
2261 return Ok(());
2262 }
2263
2264 let only_one_conflicted_commit = conflicted_commits.len() == 1;
2265 let root_conflicts_revset = RevsetExpression::commits(conflicted_commits)
2266 .roots()
2267 .evaluate(repo)?;
2268
2269 let root_conflict_commits: Vec<_> = root_conflicts_revset
2270 .iter()
2271 .commits(repo.store())
2272 .try_collect()?;
2273
2274 let instruction = if only_one_conflicted_commit {
2276 indoc! {"
2277 To resolve the conflicts, start by creating a commit on top of
2278 the conflicted commit:
2279 "}
2280 } else if root_conflict_commits.len() == 1 {
2281 indoc! {"
2282 To resolve the conflicts, start by creating a commit on top of
2283 the first conflicted commit:
2284 "}
2285 } else {
2286 indoc! {"
2287 To resolve the conflicts, start by creating a commit on top of
2288 one of the first conflicted commits:
2289 "}
2290 };
2291 write!(fmt.labeled("hint").with_heading("Hint: "), "{instruction}")?;
2292 let format_short_change_id = self.short_change_id_template();
2293 {
2294 let mut fmt = fmt.labeled("hint");
2295 for commit in &root_conflict_commits {
2296 write!(fmt, " jj new ")?;
2297 format_short_change_id.format(commit, *fmt)?;
2298 writeln!(fmt)?;
2299 }
2300 }
2301 writedoc!(
2302 fmt.labeled("hint"),
2303 "
2304 Then use `jj resolve`, or edit the conflict markers in the file directly.
2305 Once the conflicts are resolved, you can inspect the result with `jj diff`.
2306 Then run `jj squash` to move the resolution into the conflicted commit.
2307 ",
2308 )?;
2309 Ok(())
2310 }
2311
2312 pub fn get_advanceable_bookmarks<'a>(
2327 &self,
2328 from: impl IntoIterator<Item = &'a CommitId>,
2329 ) -> Result<Vec<AdvanceableBookmark>, CommandError> {
2330 let ab_settings = AdvanceBookmarksSettings::from_settings(self.settings())?;
2331 if !ab_settings.feature_enabled() {
2332 return Ok(Vec::new());
2334 }
2335
2336 let mut advanceable_bookmarks = Vec::new();
2337 for from_commit in from {
2338 for (name, _) in self.repo().view().local_bookmarks_for_commit(from_commit) {
2339 if ab_settings.bookmark_is_eligible(name) {
2340 advanceable_bookmarks.push(AdvanceableBookmark {
2341 name: name.to_owned(),
2342 old_commit_id: from_commit.clone(),
2343 });
2344 }
2345 }
2346 }
2347
2348 Ok(advanceable_bookmarks)
2349 }
2350}
2351
2352#[must_use]
2360pub struct WorkspaceCommandTransaction<'a> {
2361 helper: &'a mut WorkspaceCommandHelper,
2362 tx: Transaction,
2363 id_prefix_context: OnceCell<IdPrefixContext>,
2365}
2366
2367impl WorkspaceCommandTransaction<'_> {
2368 pub fn base_workspace_helper(&self) -> &WorkspaceCommandHelper {
2370 self.helper
2371 }
2372
2373 pub fn settings(&self) -> &UserSettings {
2375 self.helper.settings()
2376 }
2377
2378 pub fn base_repo(&self) -> &Arc<ReadonlyRepo> {
2379 self.tx.base_repo()
2380 }
2381
2382 pub fn repo(&self) -> &MutableRepo {
2383 self.tx.repo()
2384 }
2385
2386 pub fn repo_mut(&mut self) -> &mut MutableRepo {
2387 self.id_prefix_context.take(); self.tx.repo_mut()
2389 }
2390
2391 pub fn check_out(&mut self, commit: &Commit) -> Result<Commit, CheckOutCommitError> {
2392 let name = self.helper.workspace_name().to_owned();
2393 self.id_prefix_context.take(); self.tx.repo_mut().check_out(name, commit)
2395 }
2396
2397 pub fn edit(&mut self, commit: &Commit) -> Result<(), EditCommitError> {
2398 let name = self.helper.workspace_name().to_owned();
2399 self.id_prefix_context.take(); self.tx.repo_mut().edit(name, commit)
2401 }
2402
2403 pub fn format_commit_summary(&self, commit: &Commit) -> String {
2404 let output = self.commit_summary_template().format_plain_text(commit);
2405 output.into_string_lossy()
2406 }
2407
2408 pub fn write_commit_summary(
2409 &self,
2410 formatter: &mut dyn Formatter,
2411 commit: &Commit,
2412 ) -> std::io::Result<()> {
2413 self.commit_summary_template().format(commit, formatter)
2414 }
2415
2416 pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
2418 let language = self.commit_template_language();
2419 self.helper
2420 .reparse_valid_template(&language, &self.helper.commit_summary_template_text)
2421 .labeled(["commit"])
2422 }
2423
2424 pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
2427 let id_prefix_context = self
2428 .id_prefix_context
2429 .get_or_init(|| self.helper.env.new_id_prefix_context());
2430 self.helper
2431 .env
2432 .commit_template_language(self.tx.repo(), id_prefix_context)
2433 }
2434
2435 pub fn parse_commit_template(
2437 &self,
2438 ui: &Ui,
2439 template_text: &str,
2440 ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
2441 let language = self.commit_template_language();
2442 self.helper.env.parse_template(ui, &language, template_text)
2443 }
2444
2445 pub fn finish(self, ui: &Ui, description: impl Into<String>) -> Result<(), CommandError> {
2446 self.helper.finish_transaction(ui, self.tx, description)
2447 }
2448
2449 pub fn into_inner(self) -> Transaction {
2454 self.tx
2455 }
2456
2457 pub fn advance_bookmarks(&mut self, bookmarks: Vec<AdvanceableBookmark>, move_to: &CommitId) {
2463 for bookmark in bookmarks {
2464 self.repo_mut().merge_local_bookmark(
2467 &bookmark.name,
2468 &RefTarget::normal(bookmark.old_commit_id),
2469 &RefTarget::normal(move_to.clone()),
2470 );
2471 }
2472 }
2473}
2474
2475pub fn find_workspace_dir(cwd: &Path) -> &Path {
2476 cwd.ancestors()
2477 .find(|path| path.join(".jj").is_dir())
2478 .unwrap_or(cwd)
2479}
2480
2481fn map_workspace_load_error(err: WorkspaceLoadError, user_wc_path: Option<&str>) -> CommandError {
2482 match err {
2483 WorkspaceLoadError::NoWorkspaceHere(wc_path) => {
2484 let short_wc_path = user_wc_path.map_or(wc_path.as_ref(), Path::new);
2486 let message = format!(r#"There is no jj repo in "{}""#, short_wc_path.display());
2487 let git_dir = wc_path.join(".git");
2488 if git_dir.is_dir() {
2489 user_error_with_hint(
2490 message,
2491 "It looks like this is a git repo. You can create a jj repo backed by it by \
2492 running this:
2493jj git init",
2494 )
2495 } else {
2496 user_error(message)
2497 }
2498 }
2499 WorkspaceLoadError::RepoDoesNotExist(repo_dir) => user_error(format!(
2500 "The repository directory at {} is missing. Was it moved?",
2501 repo_dir.display(),
2502 )),
2503 WorkspaceLoadError::StoreLoadError(err @ StoreLoadError::UnsupportedType { .. }) => {
2504 internal_error_with_message(
2505 "This version of the jj binary doesn't support this type of repo",
2506 err,
2507 )
2508 }
2509 WorkspaceLoadError::StoreLoadError(
2510 err @ (StoreLoadError::ReadError { .. } | StoreLoadError::Backend(_)),
2511 ) => internal_error_with_message("The repository appears broken or inaccessible", err),
2512 WorkspaceLoadError::StoreLoadError(StoreLoadError::Signing(err)) => user_error(err),
2513 WorkspaceLoadError::WorkingCopyState(err) => internal_error(err),
2514 WorkspaceLoadError::DecodeRepoPath(_) | WorkspaceLoadError::Path(_) => user_error(err),
2515 }
2516}
2517
2518pub fn start_repo_transaction(repo: &Arc<ReadonlyRepo>, string_args: &[String]) -> Transaction {
2519 let mut tx = repo.start_transaction();
2520 let shell_escape = |arg: &String| {
2523 if arg.as_bytes().iter().all(|b| {
2524 matches!(b,
2525 b'A'..=b'Z'
2526 | b'a'..=b'z'
2527 | b'0'..=b'9'
2528 | b','
2529 | b'-'
2530 | b'.'
2531 | b'/'
2532 | b':'
2533 | b'@'
2534 | b'_'
2535 )
2536 }) {
2537 arg.clone()
2538 } else {
2539 format!("'{}'", arg.replace('\'', "\\'"))
2540 }
2541 };
2542 let mut quoted_strings = vec!["jj".to_string()];
2543 quoted_strings.extend(string_args.iter().skip(1).map(shell_escape));
2544 tx.set_tag("args".to_string(), quoted_strings.join(" "));
2545 tx
2546}
2547
2548fn update_stale_working_copy(
2549 mut locked_ws: LockedWorkspace,
2550 op_id: OperationId,
2551 stale_commit: &Commit,
2552 new_commit: &Commit,
2553) -> Result<CheckoutStats, CommandError> {
2554 if stale_commit.tree_id() != locked_ws.locked_wc().old_tree_id() {
2557 return Err(user_error("Concurrent working copy operation. Try again."));
2558 }
2559 let stats = locked_ws.locked_wc().check_out(new_commit).map_err(|err| {
2560 internal_error_with_message(
2561 format!("Failed to check out commit {}", new_commit.id().hex()),
2562 err,
2563 )
2564 })?;
2565 locked_ws.finish(op_id)?;
2566
2567 Ok(stats)
2568}
2569
2570pub fn print_updated_commits<'a>(
2573 formatter: &mut dyn Formatter,
2574 template: &TemplateRenderer<Commit>,
2575 commits: impl IntoIterator<Item = &'a Commit>,
2576) -> io::Result<()> {
2577 let mut commits = commits.into_iter().fuse();
2578 for commit in commits.by_ref().take(10) {
2579 write!(formatter, " ")?;
2580 template.format(commit, formatter)?;
2581 writeln!(formatter)?;
2582 }
2583 if commits.next().is_some() {
2584 writeln!(formatter, " ...")?;
2585 }
2586 Ok(())
2587}
2588
2589#[instrument(skip_all)]
2590pub fn print_conflicted_paths(
2591 conflicts: Vec<(RepoPathBuf, BackendResult<MergedTreeValue>)>,
2592 formatter: &mut dyn Formatter,
2593 workspace_command: &WorkspaceCommandHelper,
2594) -> Result<(), CommandError> {
2595 let formatted_paths = conflicts
2596 .iter()
2597 .map(|(path, _conflict)| workspace_command.format_file_path(path))
2598 .collect_vec();
2599 let max_path_len = formatted_paths.iter().map(|p| p.len()).max().unwrap_or(0);
2600 let formatted_paths = formatted_paths
2601 .into_iter()
2602 .map(|p| format!("{:width$}", p, width = max_path_len.min(32) + 3));
2603
2604 for ((_, conflict), formatted_path) in std::iter::zip(conflicts, formatted_paths) {
2605 let conflict = conflict?.simplify();
2608 let sides = conflict.num_sides();
2609 let n_adds = conflict.adds().flatten().count();
2610 let deletions = sides - n_adds;
2611
2612 let mut seen_objects = BTreeMap::new(); if deletions > 0 {
2614 seen_objects.insert(
2615 format!(
2616 "{deletions} deletion{}",
2618 if deletions > 1 { "s" } else { "" }
2619 ),
2620 "normal", );
2622 }
2623 for term in itertools::chain(conflict.removes(), conflict.adds()).flatten() {
2627 seen_objects.insert(
2628 match term {
2629 TreeValue::File {
2630 executable: false, ..
2631 } => continue,
2632 TreeValue::File {
2633 executable: true, ..
2634 } => "an executable",
2635 TreeValue::Symlink(_) => "a symlink",
2636 TreeValue::Tree(_) => "a directory",
2637 TreeValue::GitSubmodule(_) => "a git submodule",
2638 }
2639 .to_string(),
2640 "difficult",
2641 );
2642 }
2643
2644 write!(formatter, "{formatted_path} ")?;
2645 {
2646 let mut formatter = formatter.labeled("conflict_description");
2647 let print_pair = |formatter: &mut dyn Formatter, (text, label): &(String, &str)| {
2648 write!(formatter.labeled(label), "{text}")
2649 };
2650 print_pair(
2651 *formatter,
2652 &(
2653 format!("{sides}-sided"),
2654 if sides > 2 { "difficult" } else { "normal" },
2655 ),
2656 )?;
2657 write!(formatter, " conflict")?;
2658
2659 if !seen_objects.is_empty() {
2660 write!(formatter, " including ")?;
2661 let seen_objects = seen_objects.into_iter().collect_vec();
2662 match &seen_objects[..] {
2663 [] => unreachable!(),
2664 [only] => print_pair(*formatter, only)?,
2665 [first, middle @ .., last] => {
2666 print_pair(*formatter, first)?;
2667 for pair in middle {
2668 write!(formatter, ", ")?;
2669 print_pair(*formatter, pair)?;
2670 }
2671 write!(formatter, " and ")?;
2672 print_pair(*formatter, last)?;
2673 }
2674 }
2675 }
2676 }
2677 writeln!(formatter)?;
2678 }
2679 Ok(())
2680}
2681
2682fn build_untracked_reason_message(reason: &UntrackedReason) -> Option<String> {
2684 match reason {
2685 UntrackedReason::FileTooLarge { size, max_size } => {
2686 let size_approx = HumanByteSize(*size);
2689 let max_size_approx = HumanByteSize(*max_size);
2690 Some(format!(
2691 "{size_approx} ({size} bytes); the maximum size allowed is {max_size_approx} \
2692 ({max_size} bytes)",
2693 ))
2694 }
2695 UntrackedReason::FileNotAutoTracked => None,
2699 }
2700}
2701
2702pub fn print_untracked_files(
2704 ui: &Ui,
2705 untracked_paths: &BTreeMap<RepoPathBuf, UntrackedReason>,
2706 path_converter: &RepoPathUiConverter,
2707) -> io::Result<()> {
2708 let mut untracked_paths = untracked_paths
2709 .iter()
2710 .filter_map(|(path, reason)| build_untracked_reason_message(reason).map(|m| (path, m)))
2711 .peekable();
2712
2713 if untracked_paths.peek().is_some() {
2714 writeln!(ui.warning_default(), "Refused to snapshot some files:")?;
2715 let mut formatter = ui.stderr_formatter();
2716 for (path, message) in untracked_paths {
2717 let ui_path = path_converter.format_file_path(path);
2718 writeln!(formatter, " {ui_path}: {message}")?;
2719 }
2720 }
2721
2722 Ok(())
2723}
2724
2725pub fn print_snapshot_stats(
2726 ui: &Ui,
2727 stats: &SnapshotStats,
2728 path_converter: &RepoPathUiConverter,
2729) -> io::Result<()> {
2730 print_untracked_files(ui, &stats.untracked_paths, path_converter)?;
2731
2732 let large_files_sizes = stats
2733 .untracked_paths
2734 .values()
2735 .filter_map(|reason| match reason {
2736 UntrackedReason::FileTooLarge { size, .. } => Some(size),
2737 UntrackedReason::FileNotAutoTracked => None,
2738 });
2739 if let Some(size) = large_files_sizes.max() {
2740 writedoc!(
2741 ui.hint_default(),
2742 r"
2743 This is to prevent large files from being added by accident. You can fix this by:
2744 - Adding the file to `.gitignore`
2745 - Run `jj config set --repo snapshot.max-new-file-size {size}`
2746 This will increase the maximum file size allowed for new files, in this repository only.
2747 - Run `jj --config snapshot.max-new-file-size={size} st`
2748 This will increase the maximum file size allowed for new files, for this command only.
2749 "
2750 )?;
2751 }
2752 Ok(())
2753}
2754
2755pub fn print_checkout_stats(
2756 ui: &Ui,
2757 stats: &CheckoutStats,
2758 new_commit: &Commit,
2759) -> Result<(), std::io::Error> {
2760 if stats.added_files > 0 || stats.updated_files > 0 || stats.removed_files > 0 {
2761 writeln!(
2762 ui.status(),
2763 "Added {} files, modified {} files, removed {} files",
2764 stats.added_files,
2765 stats.updated_files,
2766 stats.removed_files
2767 )?;
2768 }
2769 if stats.skipped_files != 0 {
2770 writeln!(
2771 ui.warning_default(),
2772 "{} of those updates were skipped because there were conflicting changes in the \
2773 working copy.",
2774 stats.skipped_files
2775 )?;
2776 writeln!(
2777 ui.hint_default(),
2778 "Inspect the changes compared to the intended target with `jj diff --from {}`.
2779Discard the conflicting changes with `jj restore --from {}`.",
2780 short_commit_hash(new_commit.id()),
2781 short_commit_hash(new_commit.id())
2782 )?;
2783 }
2784 Ok(())
2785}
2786
2787pub fn print_unmatched_explicit_paths<'a>(
2790 ui: &Ui,
2791 workspace_command: &WorkspaceCommandHelper,
2792 expression: &FilesetExpression,
2793 trees: impl IntoIterator<Item = &'a MergedTree>,
2794) -> io::Result<()> {
2795 let mut explicit_paths = expression.explicit_paths().collect_vec();
2796 for tree in trees {
2797 explicit_paths.retain(|&path| tree.path_value(path).unwrap().is_absent());
2799 if explicit_paths.is_empty() {
2800 return Ok(());
2801 }
2802 }
2803 let ui_paths = explicit_paths
2804 .iter()
2805 .map(|&path| workspace_command.format_file_path(path))
2806 .join(", ");
2807 writeln!(
2808 ui.warning_default(),
2809 "No matching entries for paths: {ui_paths}"
2810 )?;
2811 Ok(())
2812}
2813
2814pub fn update_working_copy(
2815 repo: &Arc<ReadonlyRepo>,
2816 workspace: &mut Workspace,
2817 old_commit: Option<&Commit>,
2818 new_commit: &Commit,
2819) -> Result<CheckoutStats, CommandError> {
2820 let old_tree_id = old_commit.map(|commit| commit.tree_id().clone());
2821 let stats = workspace
2824 .check_out(repo.op_id().clone(), old_tree_id.as_ref(), new_commit)
2825 .map_err(|err| {
2826 internal_error_with_message(
2827 format!("Failed to check out commit {}", new_commit.id().hex()),
2828 err,
2829 )
2830 })?;
2831 Ok(stats)
2832}
2833
2834pub fn has_tracked_remote_bookmarks(view: &View, bookmark: &RefName) -> bool {
2837 view.remote_bookmarks_matching(
2838 &StringPattern::exact(bookmark),
2839 &StringPattern::everything(),
2840 )
2841 .filter(|&(symbol, _)| !jj_lib::git::is_special_git_remote(symbol.remote))
2842 .any(|(_, remote_ref)| remote_ref.is_tracked())
2843}
2844
2845pub fn load_template_aliases(
2846 ui: &Ui,
2847 stacked_config: &StackedConfig,
2848) -> Result<TemplateAliasesMap, CommandError> {
2849 let table_name = ConfigNamePathBuf::from_iter(["template-aliases"]);
2850 let mut aliases_map = TemplateAliasesMap::new();
2851 for layer in stacked_config.layers() {
2854 let table = match layer.look_up_table(&table_name) {
2855 Ok(Some(table)) => table,
2856 Ok(None) => continue,
2857 Err(item) => {
2858 return Err(ConfigGetError::Type {
2859 name: table_name.to_string(),
2860 error: format!("Expected a table, but is {}", item.type_name()).into(),
2861 source_path: layer.path.clone(),
2862 }
2863 .into());
2864 }
2865 };
2866 for (decl, item) in table.iter() {
2867 let r = item
2868 .as_str()
2869 .ok_or_else(|| format!("Expected a string, but is {}", item.type_name()))
2870 .and_then(|v| aliases_map.insert(decl, v).map_err(|e| e.to_string()));
2871 if let Err(s) = r {
2872 writeln!(
2873 ui.warning_default(),
2874 "Failed to load `{table_name}.{decl}`: {s}"
2875 )?;
2876 }
2877 }
2878 }
2879 Ok(aliases_map)
2880}
2881
2882#[derive(Clone, Debug)]
2884pub struct LogContentFormat {
2885 width: usize,
2886 word_wrap: bool,
2887}
2888
2889impl LogContentFormat {
2890 pub fn new(ui: &Ui, settings: &UserSettings) -> Result<Self, ConfigGetError> {
2892 Ok(Self {
2893 width: ui.term_width(),
2894 word_wrap: settings.get_bool("ui.log-word-wrap")?,
2895 })
2896 }
2897
2898 #[must_use]
2900 pub fn sub_width(&self, width: usize) -> Self {
2901 Self {
2902 width: self.width.saturating_sub(width),
2903 word_wrap: self.word_wrap,
2904 }
2905 }
2906
2907 pub fn width(&self) -> usize {
2909 self.width
2910 }
2911
2912 pub fn write<E: From<io::Error>>(
2914 &self,
2915 formatter: &mut dyn Formatter,
2916 content_fn: impl FnOnce(&mut dyn Formatter) -> Result<(), E>,
2917 ) -> Result<(), E> {
2918 if self.word_wrap {
2919 let mut recorder = FormatRecorder::new();
2920 content_fn(&mut recorder)?;
2921 text_util::write_wrapped(formatter, &recorder, self.width)?;
2922 } else {
2923 content_fn(formatter)?;
2924 }
2925 Ok(())
2926 }
2927}
2928
2929pub fn short_commit_hash(commit_id: &CommitId) -> String {
2930 format!("{commit_id:.12}")
2931}
2932
2933pub fn short_change_hash(change_id: &ChangeId) -> String {
2934 format!("{change_id:.12}")
2935}
2936
2937pub fn short_operation_hash(operation_id: &OperationId) -> String {
2938 format!("{operation_id:.12}")
2939}
2940
2941#[derive(Clone, Debug)]
2943pub enum DiffSelector {
2944 NonInteractive,
2945 Interactive(DiffEditor),
2946}
2947
2948impl DiffSelector {
2949 pub fn is_interactive(&self) -> bool {
2950 matches!(self, Self::Interactive(_))
2951 }
2952
2953 pub fn select(
2958 &self,
2959 [left_tree, right_tree]: [&MergedTree; 2],
2960 matcher: &dyn Matcher,
2961 format_instructions: impl FnOnce() -> String,
2962 ) -> Result<MergedTreeId, CommandError> {
2963 let selected_tree_id = restore_tree(right_tree, left_tree, matcher).block_on()?;
2964 match self {
2965 Self::NonInteractive => Ok(selected_tree_id),
2966 Self::Interactive(editor) => {
2967 let right_tree = right_tree.store().get_root_tree(&selected_tree_id)?;
2971 Ok(editor.edit([left_tree, &right_tree], matcher, format_instructions)?)
2972 }
2973 }
2974 }
2975}
2976
2977#[derive(Clone, Debug)]
2978pub struct RemoteBookmarkNamePattern {
2979 pub bookmark: StringPattern,
2980 pub remote: StringPattern,
2981}
2982
2983impl FromStr for RemoteBookmarkNamePattern {
2984 type Err = String;
2985
2986 fn from_str(src: &str) -> Result<Self, Self::Err> {
2987 let (maybe_kind, pat) = src
2992 .split_once(':')
2993 .map_or((None, src), |(kind, pat)| (Some(kind), pat));
2994 let to_pattern = |pat: &str| {
2995 if let Some(kind) = maybe_kind {
2996 StringPattern::from_str_kind(pat, kind).map_err(|err| err.to_string())
2997 } else {
2998 Ok(StringPattern::exact(pat))
2999 }
3000 };
3001 let (bookmark, remote) = pat.rsplit_once('@').ok_or_else(|| {
3003 "remote bookmark must be specified in bookmark@remote form".to_owned()
3004 })?;
3005 Ok(Self {
3006 bookmark: to_pattern(bookmark)?,
3007 remote: to_pattern(remote)?,
3008 })
3009 }
3010}
3011
3012impl RemoteBookmarkNamePattern {
3013 pub fn is_exact(&self) -> bool {
3014 self.bookmark.is_exact() && self.remote.is_exact()
3015 }
3016}
3017
3018impl fmt::Display for RemoteBookmarkNamePattern {
3019 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3020 let Self { bookmark, remote } = self;
3023 write!(f, "{bookmark}@{remote}")
3024 }
3025}
3026
3027pub fn compute_commit_location(
3032 ui: &Ui,
3033 workspace_command: &WorkspaceCommandHelper,
3034 destination: Option<&[RevisionArg]>,
3035 insert_after: Option<&[RevisionArg]>,
3036 insert_before: Option<&[RevisionArg]>,
3037 commit_type: &str,
3038) -> Result<(Vec<CommitId>, Vec<CommitId>), CommandError> {
3039 let resolve_revisions =
3040 |revisions: Option<&[RevisionArg]>| -> Result<Option<Vec<CommitId>>, CommandError> {
3041 if let Some(revisions) = revisions {
3042 Ok(Some(
3043 workspace_command
3044 .resolve_some_revsets_default_single(ui, revisions)?
3045 .into_iter()
3046 .collect_vec(),
3047 ))
3048 } else {
3049 Ok(None)
3050 }
3051 };
3052 let destination_commit_ids = resolve_revisions(destination)?;
3053 let after_commit_ids = resolve_revisions(insert_after)?;
3054 let before_commit_ids = resolve_revisions(insert_before)?;
3055
3056 let (new_parent_ids, new_child_ids) =
3057 match (destination_commit_ids, after_commit_ids, before_commit_ids) {
3058 (Some(destination_commit_ids), None, None) => (destination_commit_ids, vec![]),
3059 (None, Some(after_commit_ids), Some(before_commit_ids)) => {
3060 (after_commit_ids, before_commit_ids)
3061 }
3062 (None, Some(after_commit_ids), None) => {
3063 let new_child_ids: Vec<_> = RevsetExpression::commits(after_commit_ids.clone())
3064 .children()
3065 .evaluate(workspace_command.repo().as_ref())?
3066 .iter()
3067 .try_collect()?;
3068
3069 (after_commit_ids, new_child_ids)
3070 }
3071 (None, None, Some(before_commit_ids)) => {
3072 let before_commits: Vec<_> = before_commit_ids
3073 .iter()
3074 .map(|id| workspace_command.repo().store().get_commit(id))
3075 .try_collect()?;
3076 let new_parent_ids = before_commits
3079 .iter()
3080 .flat_map(|commit| commit.parent_ids())
3081 .unique()
3082 .cloned()
3083 .collect_vec();
3084
3085 (new_parent_ids, before_commit_ids)
3086 }
3087 (Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
3088 panic!("destination cannot be used with insert_after/insert_before")
3089 }
3090 (None, None, None) => {
3091 panic!("expected at least one of destination or insert_after/insert_before")
3092 }
3093 };
3094
3095 if !new_child_ids.is_empty() {
3096 workspace_command.check_rewritable(new_child_ids.iter())?;
3097 ensure_no_commit_loop(
3098 workspace_command.repo().as_ref(),
3099 &RevsetExpression::commits(new_child_ids.clone()),
3100 &RevsetExpression::commits(new_parent_ids.clone()),
3101 commit_type,
3102 )?;
3103 }
3104
3105 Ok((new_parent_ids, new_child_ids))
3106}
3107
3108fn ensure_no_commit_loop(
3111 repo: &ReadonlyRepo,
3112 children_expression: &Arc<ResolvedRevsetExpression>,
3113 parents_expression: &Arc<ResolvedRevsetExpression>,
3114 commit_type: &str,
3115) -> Result<(), CommandError> {
3116 if let Some(commit_id) = children_expression
3117 .dag_range_to(parents_expression)
3118 .evaluate(repo)?
3119 .iter()
3120 .next()
3121 {
3122 let commit_id = commit_id?;
3123 return Err(user_error(format!(
3124 "Refusing to create a loop: commit {} would be both an ancestor and a descendant of \
3125 the {commit_type}",
3126 short_commit_hash(&commit_id),
3127 )));
3128 }
3129 Ok(())
3130}
3131
3132#[derive(clap::Parser, Clone, Debug)]
3139#[command(name = "jj")]
3140pub struct Args {
3141 #[command(flatten)]
3142 pub global_args: GlobalArgs,
3143}
3144
3145#[derive(clap::Args, Clone, Debug)]
3146#[command(next_help_heading = "Global Options")]
3147pub struct GlobalArgs {
3148 #[arg(long, short = 'R', global = true, value_hint = clap::ValueHint::DirPath)]
3153 pub repository: Option<String>,
3154 #[arg(long, global = true)]
3167 pub ignore_working_copy: bool,
3168 #[arg(long, global = true)]
3177 pub ignore_immutable: bool,
3178 #[arg(
3201 long,
3202 visible_alias = "at-op",
3203 global = true,
3204 add = ArgValueCandidates::new(complete::operations),
3205 )]
3206 pub at_operation: Option<String>,
3207 #[arg(long, global = true)]
3209 pub debug: bool,
3210
3211 #[command(flatten)]
3212 pub early_args: EarlyArgs,
3213}
3214
3215#[derive(clap::Args, Clone, Debug)]
3216pub struct EarlyArgs {
3217 #[arg(long, value_name = "WHEN", global = true)]
3219 pub color: Option<ColorChoice>,
3220 #[arg(long, global = true, action = ArgAction::SetTrue)]
3227 pub quiet: Option<bool>,
3230 #[arg(long, global = true, action = ArgAction::SetTrue)]
3232 pub no_pager: Option<bool>,
3235 #[arg(long, value_name = "NAME=VALUE", global = true, add = ArgValueCompleter::new(complete::leaf_config_key_value))]
3241 pub config: Vec<String>,
3242 #[arg(long, value_name = "PATH", global = true, value_hint = clap::ValueHint::FilePath)]
3244 pub config_file: Vec<String>,
3245}
3246
3247impl EarlyArgs {
3248 pub(crate) fn merged_config_args(&self, matches: &ArgMatches) -> Vec<(ConfigArgKind, &str)> {
3249 merge_args_with(
3250 matches,
3251 &[("config", &self.config), ("config_file", &self.config_file)],
3252 |id, value| match id {
3253 "config" => (ConfigArgKind::Item, value.as_ref()),
3254 "config_file" => (ConfigArgKind::File, value.as_ref()),
3255 _ => unreachable!("unexpected id {id:?}"),
3256 },
3257 )
3258 }
3259
3260 fn has_config_args(&self) -> bool {
3261 !self.config.is_empty() || !self.config_file.is_empty()
3262 }
3263}
3264
3265#[derive(Clone, Debug)]
3271pub struct RevisionArg(Cow<'static, str>);
3272
3273impl RevisionArg {
3274 pub const AT: Self = Self(Cow::Borrowed("@"));
3276}
3277
3278impl From<String> for RevisionArg {
3279 fn from(s: String) -> Self {
3280 Self(s.into())
3281 }
3282}
3283
3284impl AsRef<str> for RevisionArg {
3285 fn as_ref(&self) -> &str {
3286 &self.0
3287 }
3288}
3289
3290impl fmt::Display for RevisionArg {
3291 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3292 write!(f, "{}", self.0)
3293 }
3294}
3295
3296impl ValueParserFactory for RevisionArg {
3297 type Parser = MapValueParser<NonEmptyStringValueParser, fn(String) -> Self>;
3298
3299 fn value_parser() -> Self::Parser {
3300 NonEmptyStringValueParser::new().map(Self::from)
3301 }
3302}
3303
3304pub fn merge_args_with<'k, 'v, T, U>(
3312 matches: &ArgMatches,
3313 id_values: &[(&'k str, &'v [T])],
3314 mut convert: impl FnMut(&'k str, &'v T) -> U,
3315) -> Vec<U> {
3316 let mut pos_values: Vec<(usize, U)> = Vec::new();
3317 for (id, values) in id_values {
3318 pos_values.extend(itertools::zip_eq(
3319 matches.indices_of(id).into_iter().flatten(),
3320 values.iter().map(|v| convert(id, v)),
3321 ));
3322 }
3323 pos_values.sort_unstable_by_key(|&(pos, _)| pos);
3324 pos_values.into_iter().map(|(_, value)| value).collect()
3325}
3326
3327fn get_string_or_array(
3328 config: &StackedConfig,
3329 key: &'static str,
3330) -> Result<Vec<String>, ConfigGetError> {
3331 config
3332 .get(key)
3333 .map(|string| vec![string])
3334 .or_else(|_| config.get::<Vec<String>>(key))
3335}
3336
3337fn resolve_default_command(
3338 ui: &Ui,
3339 config: &StackedConfig,
3340 app: &Command,
3341 mut string_args: Vec<String>,
3342) -> Result<Vec<String>, CommandError> {
3343 const PRIORITY_FLAGS: &[&str] = &["--help", "-h", "--version", "-V"];
3344
3345 let has_priority_flag = string_args
3346 .iter()
3347 .any(|arg| PRIORITY_FLAGS.contains(&arg.as_str()));
3348 if has_priority_flag {
3349 return Ok(string_args);
3350 }
3351
3352 let app_clone = app
3353 .clone()
3354 .allow_external_subcommands(true)
3355 .ignore_errors(true);
3356 let matches = app_clone.try_get_matches_from(&string_args).ok();
3357
3358 if let Some(matches) = matches
3359 && matches.subcommand_name().is_none()
3360 {
3361 let args = get_string_or_array(config, "ui.default-command").optional()?;
3362 if args.is_none() {
3363 writeln!(
3364 ui.hint_default(),
3365 "Use `jj -h` for a list of available commands."
3366 )?;
3367 writeln!(
3368 ui.hint_no_heading(),
3369 "Run `jj config set --user ui.default-command log` to disable this message."
3370 )?;
3371 }
3372 let default_command = args.unwrap_or_else(|| vec!["log".to_string()]);
3373
3374 string_args.splice(1..1, default_command);
3376 }
3377 Ok(string_args)
3378}
3379
3380fn resolve_aliases(
3381 ui: &Ui,
3382 config: &StackedConfig,
3383 app: &Command,
3384 mut string_args: Vec<String>,
3385) -> Result<Vec<String>, CommandError> {
3386 let defined_aliases: HashSet<_> = config.table_keys("aliases").collect();
3387 let mut resolved_aliases = HashSet::new();
3388 let mut real_commands = HashSet::new();
3389 for command in app.get_subcommands() {
3390 real_commands.insert(command.get_name());
3391 for alias in command.get_all_aliases() {
3392 real_commands.insert(alias);
3393 }
3394 }
3395 for alias in defined_aliases.intersection(&real_commands).sorted() {
3396 writeln!(
3397 ui.warning_default(),
3398 "Cannot define an alias that overrides the built-in command '{alias}'"
3399 )?;
3400 }
3401
3402 loop {
3403 let app_clone = app.clone().allow_external_subcommands(true);
3404 let matches = app_clone.try_get_matches_from(&string_args).ok();
3405 if let Some((command_name, submatches)) = matches.as_ref().and_then(|m| m.subcommand())
3406 && !real_commands.contains(command_name)
3407 {
3408 let alias_name = command_name.to_string();
3409 let alias_args = submatches
3410 .get_many::<OsString>("")
3411 .unwrap_or_default()
3412 .map(|arg| arg.to_str().unwrap().to_string())
3413 .collect_vec();
3414 if resolved_aliases.contains(&*alias_name) {
3415 return Err(user_error(format!(
3416 "Recursive alias definition involving `{alias_name}`"
3417 )));
3418 }
3419 if let Some(&alias_name) = defined_aliases.get(&*alias_name) {
3420 let alias_definition: Vec<String> = config.get(["aliases", alias_name])?;
3421 assert!(string_args.ends_with(&alias_args));
3422 string_args.truncate(string_args.len() - 1 - alias_args.len());
3423 string_args.extend(alias_definition);
3424 string_args.extend_from_slice(&alias_args);
3425 resolved_aliases.insert(alias_name);
3426 continue;
3427 } else {
3428 return Ok(string_args);
3430 }
3431 }
3432 return Ok(string_args);
3434 }
3435}
3436
3437fn parse_early_args(
3439 app: &Command,
3440 args: &[String],
3441) -> Result<(EarlyArgs, Vec<ConfigLayer>), CommandError> {
3442 let early_matches = app
3444 .clone()
3445 .disable_version_flag(true)
3446 .disable_help_flag(true)
3448 .arg(
3450 clap::Arg::new("help")
3451 .short('h')
3452 .long("help")
3453 .global(true)
3454 .action(ArgAction::Count),
3455 )
3456 .ignore_errors(true)
3457 .try_get_matches_from(args)?;
3458 let args = EarlyArgs::from_arg_matches(&early_matches).unwrap();
3459
3460 let mut config_layers = parse_config_args(&args.merged_config_args(&early_matches))?;
3461 let mut layer = ConfigLayer::empty(ConfigSource::CommandArg);
3464 if let Some(choice) = args.color {
3465 layer.set_value("ui.color", choice.to_string()).unwrap();
3466 }
3467 if args.quiet.unwrap_or_default() {
3468 layer.set_value("ui.quiet", true).unwrap();
3469 }
3470 if args.no_pager.unwrap_or_default() {
3471 layer.set_value("ui.paginate", "never").unwrap();
3472 }
3473 if !layer.is_empty() {
3474 config_layers.push(layer);
3475 }
3476 Ok((args, config_layers))
3477}
3478
3479fn handle_shell_completion(
3480 ui: &Ui,
3481 app: &Command,
3482 config: &StackedConfig,
3483 cwd: &Path,
3484) -> Result<(), CommandError> {
3485 let mut orig_args = env::args_os();
3486
3487 let mut args = vec![];
3488 args.extend(orig_args.by_ref().take(2));
3491
3492 if orig_args.len() > 0 {
3496 let complete_index: Option<usize> = env::var("_CLAP_COMPLETE_INDEX")
3497 .ok()
3498 .and_then(|s| s.parse().ok());
3499 let resolved_aliases = if let Some(index) = complete_index {
3500 let pad_len = usize::saturating_sub(index + 1, orig_args.len());
3505 let padded_args = orig_args
3506 .by_ref()
3507 .chain(std::iter::repeat_n(OsString::new(), pad_len));
3508
3509 let mut expanded_args = expand_args(ui, app, padded_args.take(index + 1), config)?;
3511
3512 unsafe {
3516 env::set_var(
3517 "_CLAP_COMPLETE_INDEX",
3518 (expanded_args.len() - 1).to_string(),
3519 );
3520 }
3521
3522 let split_off_padding = expanded_args.split_off(expanded_args.len() - pad_len);
3525 assert!(
3526 split_off_padding.iter().all(|s| s.is_empty()),
3527 "split-off padding should only consist of empty strings but was \
3528 {split_off_padding:?}",
3529 );
3530
3531 expanded_args.extend(to_string_args(orig_args)?);
3533 expanded_args
3534 } else {
3535 expand_args(ui, app, orig_args, config)?
3536 };
3537 args.extend(resolved_aliases.into_iter().map(OsString::from));
3538 }
3539 let ran_completion = clap_complete::CompleteEnv::with_factory(|| {
3540 app.clone()
3541 .allow_external_subcommands(true)
3543 })
3544 .try_complete(args.iter(), Some(cwd))?;
3545 assert!(
3546 ran_completion,
3547 "This function should not be called without the COMPLETE variable set."
3548 );
3549 Ok(())
3550}
3551
3552pub fn expand_args(
3553 ui: &Ui,
3554 app: &Command,
3555 args_os: impl IntoIterator<Item = OsString>,
3556 config: &StackedConfig,
3557) -> Result<Vec<String>, CommandError> {
3558 let string_args = to_string_args(args_os)?;
3559 let string_args = resolve_default_command(ui, config, app, string_args)?;
3560 resolve_aliases(ui, config, app, string_args)
3561}
3562
3563fn to_string_args(
3564 args_os: impl IntoIterator<Item = OsString>,
3565) -> Result<Vec<String>, CommandError> {
3566 args_os
3567 .into_iter()
3568 .map(|arg_os| {
3569 arg_os
3570 .into_string()
3571 .map_err(|_| cli_error("Non-UTF-8 argument"))
3572 })
3573 .collect()
3574}
3575
3576fn parse_args(app: &Command, string_args: &[String]) -> Result<(ArgMatches, Args), clap::Error> {
3577 let matches = app
3578 .clone()
3579 .arg_required_else_help(true)
3580 .subcommand_required(true)
3581 .try_get_matches_from(string_args)?;
3582 let args = Args::from_arg_matches(&matches).unwrap();
3583 Ok((matches, args))
3584}
3585
3586fn command_name(mut matches: &ArgMatches) -> String {
3587 let mut command = String::new();
3588 while let Some((subcommand, new_matches)) = matches.subcommand() {
3589 if !command.is_empty() {
3590 command.push(' ');
3591 }
3592 command.push_str(subcommand);
3593 matches = new_matches;
3594 }
3595 command
3596}
3597
3598pub fn format_template<C: Clone>(ui: &Ui, arg: &C, template: &TemplateRenderer<C>) -> String {
3599 let mut output = vec![];
3600 template
3601 .format(arg, ui.new_formatter(&mut output).as_mut())
3602 .expect("write() to vec backed formatter should never fail");
3603 output.into_string_lossy()
3605}
3606
3607#[must_use]
3609pub struct CliRunner<'a> {
3610 tracing_subscription: TracingSubscription,
3611 app: Command,
3612 config_layers: Vec<ConfigLayer>,
3613 config_migrations: Vec<ConfigMigrationRule>,
3614 store_factories: StoreFactories,
3615 working_copy_factories: WorkingCopyFactories,
3616 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
3617 revset_extensions: RevsetExtensions,
3618 commit_template_extensions: Vec<Arc<dyn CommitTemplateLanguageExtension>>,
3619 operation_template_extensions: Vec<Arc<dyn OperationTemplateLanguageExtension>>,
3620 dispatch_fn: CliDispatchFn<'a>,
3621 dispatch_hook_fns: Vec<CliDispatchHookFn<'a>>,
3622 process_global_args_fns: Vec<ProcessGlobalArgsFn<'a>>,
3623}
3624
3625pub type CliDispatchFn<'a> =
3626 Box<dyn FnOnce(&mut Ui, &CommandHelper) -> Result<(), CommandError> + 'a>;
3627
3628type CliDispatchHookFn<'a> =
3629 Box<dyn FnOnce(&mut Ui, &CommandHelper, CliDispatchFn<'a>) -> Result<(), CommandError> + 'a>;
3630
3631type ProcessGlobalArgsFn<'a> =
3632 Box<dyn FnOnce(&mut Ui, &ArgMatches) -> Result<(), CommandError> + 'a>;
3633
3634impl<'a> CliRunner<'a> {
3635 pub fn init() -> Self {
3638 let tracing_subscription = TracingSubscription::init();
3639 crate::cleanup_guard::init();
3640 Self {
3641 tracing_subscription,
3642 app: crate::commands::default_app(),
3643 config_layers: crate::config::default_config_layers(),
3644 config_migrations: crate::config::default_config_migrations(),
3645 store_factories: StoreFactories::default(),
3646 working_copy_factories: default_working_copy_factories(),
3647 workspace_loader_factory: Box::new(DefaultWorkspaceLoaderFactory),
3648 revset_extensions: Default::default(),
3649 commit_template_extensions: vec![],
3650 operation_template_extensions: vec![],
3651 dispatch_fn: Box::new(crate::commands::run_command),
3652 dispatch_hook_fns: vec![],
3653 process_global_args_fns: vec![],
3654 }
3655 }
3656
3657 pub fn name(mut self, name: &str) -> Self {
3659 self.app = self.app.name(name.to_string());
3660 self
3661 }
3662
3663 pub fn about(mut self, about: &str) -> Self {
3665 self.app = self.app.about(about.to_string());
3666 self
3667 }
3668
3669 pub fn version(mut self, version: &str) -> Self {
3671 self.app = self.app.version(version.to_string());
3672 self
3673 }
3674
3675 pub fn add_extra_config(mut self, layer: ConfigLayer) -> Self {
3680 assert_eq!(layer.source, ConfigSource::Default);
3681 self.config_layers.push(layer);
3682 self
3683 }
3684
3685 pub fn add_extra_config_migration(mut self, rule: ConfigMigrationRule) -> Self {
3687 self.config_migrations.push(rule);
3688 self
3689 }
3690
3691 pub fn add_store_factories(mut self, store_factories: StoreFactories) -> Self {
3693 self.store_factories.merge(store_factories);
3694 self
3695 }
3696
3697 pub fn add_working_copy_factories(
3699 mut self,
3700 working_copy_factories: WorkingCopyFactories,
3701 ) -> Self {
3702 merge_factories_map(&mut self.working_copy_factories, working_copy_factories);
3703 self
3704 }
3705
3706 pub fn set_workspace_loader_factory(
3707 mut self,
3708 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
3709 ) -> Self {
3710 self.workspace_loader_factory = workspace_loader_factory;
3711 self
3712 }
3713
3714 pub fn add_symbol_resolver_extension(
3715 mut self,
3716 symbol_resolver: Box<dyn SymbolResolverExtension>,
3717 ) -> Self {
3718 self.revset_extensions.add_symbol_resolver(symbol_resolver);
3719 self
3720 }
3721
3722 pub fn add_revset_function_extension(
3723 mut self,
3724 name: &'static str,
3725 func: RevsetFunction,
3726 ) -> Self {
3727 self.revset_extensions.add_custom_function(name, func);
3728 self
3729 }
3730
3731 pub fn add_commit_template_extension(
3732 mut self,
3733 commit_template_extension: Box<dyn CommitTemplateLanguageExtension>,
3734 ) -> Self {
3735 self.commit_template_extensions
3736 .push(commit_template_extension.into());
3737 self
3738 }
3739
3740 pub fn add_operation_template_extension(
3741 mut self,
3742 operation_template_extension: Box<dyn OperationTemplateLanguageExtension>,
3743 ) -> Self {
3744 self.operation_template_extensions
3745 .push(operation_template_extension.into());
3746 self
3747 }
3748
3749 pub fn add_dispatch_hook<F>(mut self, dispatch_hook_fn: F) -> Self
3753 where
3754 F: FnOnce(&mut Ui, &CommandHelper, CliDispatchFn) -> Result<(), CommandError> + 'a,
3755 {
3756 self.dispatch_hook_fns.push(Box::new(dispatch_hook_fn));
3757 self
3758 }
3759
3760 pub fn add_subcommand<C, F>(mut self, custom_dispatch_fn: F) -> Self
3762 where
3763 C: clap::Subcommand,
3764 F: FnOnce(&mut Ui, &CommandHelper, C) -> Result<(), CommandError> + 'a,
3765 {
3766 let old_dispatch_fn = self.dispatch_fn;
3767 let new_dispatch_fn =
3768 move |ui: &mut Ui, command_helper: &CommandHelper| match C::from_arg_matches(
3769 command_helper.matches(),
3770 ) {
3771 Ok(command) => custom_dispatch_fn(ui, command_helper, command),
3772 Err(_) => old_dispatch_fn(ui, command_helper),
3773 };
3774 self.app = C::augment_subcommands(self.app);
3775 self.dispatch_fn = Box::new(new_dispatch_fn);
3776 self
3777 }
3778
3779 pub fn add_global_args<A, F>(mut self, process_before: F) -> Self
3781 where
3782 A: clap::Args,
3783 F: FnOnce(&mut Ui, A) -> Result<(), CommandError> + 'a,
3784 {
3785 let process_global_args_fn = move |ui: &mut Ui, matches: &ArgMatches| {
3786 let custom_args = A::from_arg_matches(matches).unwrap();
3787 process_before(ui, custom_args)
3788 };
3789 self.app = A::augment_args(self.app);
3790 self.process_global_args_fns
3791 .push(Box::new(process_global_args_fn));
3792 self
3793 }
3794
3795 #[instrument(skip_all)]
3796 fn run_internal(self, ui: &mut Ui, mut raw_config: RawConfig) -> Result<(), CommandError> {
3797 let cwd = env::current_dir()
3800 .and_then(dunce::canonicalize)
3801 .map_err(|_| {
3802 user_error_with_hint(
3803 "Could not determine current directory",
3804 "Did you update to a commit where the directory doesn't exist?",
3805 )
3806 })?;
3807 let mut config_env = ConfigEnv::from_environment(ui);
3808 let mut last_config_migration_descriptions = Vec::new();
3809 let mut migrate_config = |config: &mut StackedConfig| -> Result<(), CommandError> {
3810 last_config_migration_descriptions =
3811 jj_lib::config::migrate(config, &self.config_migrations)?;
3812 Ok(())
3813 };
3814 let maybe_cwd_workspace_loader = self
3818 .workspace_loader_factory
3819 .create(find_workspace_dir(&cwd))
3820 .map_err(|err| map_workspace_load_error(err, Some(".")));
3821 config_env.reload_user_config(&mut raw_config)?;
3822 if let Ok(loader) = &maybe_cwd_workspace_loader {
3823 config_env.reset_repo_path(loader.repo_path());
3824 config_env.reload_repo_config(&mut raw_config)?;
3825 }
3826 let mut config = config_env.resolve_config(&raw_config)?;
3827 migrate_config(&mut config)?;
3828 ui.reset(&config)?;
3829
3830 if env::var_os("COMPLETE").is_some() {
3831 return handle_shell_completion(&Ui::null(), &self.app, &config, &cwd);
3832 }
3833
3834 let string_args = expand_args(ui, &self.app, env::args_os(), &config)?;
3835 let (args, config_layers) = parse_early_args(&self.app, &string_args)?;
3836 if !config_layers.is_empty() {
3837 raw_config.as_mut().extend_layers(config_layers);
3838 config = config_env.resolve_config(&raw_config)?;
3839 migrate_config(&mut config)?;
3840 ui.reset(&config)?;
3841 }
3842
3843 if args.has_config_args() {
3844 warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
3845 }
3846
3847 let (matches, args) = parse_args(&self.app, &string_args)
3848 .map_err(|err| map_clap_cli_error(err, ui, &config))?;
3849 if args.global_args.debug {
3850 self.tracing_subscription.enable_debug_logging()?;
3852 }
3853 for process_global_args_fn in self.process_global_args_fns {
3854 process_global_args_fn(ui, &matches)?;
3855 }
3856 config_env.set_command_name(command_name(&matches));
3857
3858 let maybe_workspace_loader = if let Some(path) = &args.global_args.repository {
3859 let abs_path = cwd.join(path);
3861 let abs_path = dunce::canonicalize(&abs_path).unwrap_or(abs_path);
3862 let loader = self
3864 .workspace_loader_factory
3865 .create(&abs_path)
3866 .map_err(|err| map_workspace_load_error(err, Some(path)))?;
3867 config_env.reset_repo_path(loader.repo_path());
3868 config_env.reload_repo_config(&mut raw_config)?;
3869 Ok(loader)
3870 } else {
3871 maybe_cwd_workspace_loader
3872 };
3873
3874 config = config_env.resolve_config(&raw_config)?;
3876 migrate_config(&mut config)?;
3877 ui.reset(&config)?;
3878
3879 for (source, desc) in &last_config_migration_descriptions {
3881 let source_str = match source {
3882 ConfigSource::Default => "default-provided",
3883 ConfigSource::EnvBase | ConfigSource::EnvOverrides => "environment-provided",
3884 ConfigSource::User => "user-level",
3885 ConfigSource::Repo => "repo-level",
3886 ConfigSource::CommandArg => "CLI-provided",
3887 };
3888 writeln!(
3889 ui.warning_default(),
3890 "Deprecated {source_str} config: {desc}"
3891 )?;
3892 }
3893
3894 if args.global_args.repository.is_some() {
3895 warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
3896 }
3897
3898 let settings = UserSettings::from_config(config)?;
3899 let command_helper_data = CommandHelperData {
3900 app: self.app,
3901 cwd,
3902 string_args,
3903 matches,
3904 global_args: args.global_args,
3905 config_env,
3906 config_migrations: self.config_migrations,
3907 raw_config,
3908 settings,
3909 revset_extensions: self.revset_extensions.into(),
3910 commit_template_extensions: self.commit_template_extensions,
3911 operation_template_extensions: self.operation_template_extensions,
3912 maybe_workspace_loader,
3913 store_factories: self.store_factories,
3914 working_copy_factories: self.working_copy_factories,
3915 workspace_loader_factory: self.workspace_loader_factory,
3916 };
3917 let command_helper = CommandHelper {
3918 data: Rc::new(command_helper_data),
3919 };
3920 let dispatch_fn = self.dispatch_hook_fns.into_iter().fold(
3921 self.dispatch_fn,
3922 |old_dispatch_fn, dispatch_hook_fn| {
3923 Box::new(move |ui: &mut Ui, command_helper: &CommandHelper| {
3924 dispatch_hook_fn(ui, command_helper, old_dispatch_fn)
3925 })
3926 },
3927 );
3928 (dispatch_fn)(ui, &command_helper)
3929 }
3930
3931 #[must_use]
3932 #[instrument(skip(self))]
3933 pub fn run(mut self) -> u8 {
3934 crossterm::style::force_color_output(true);
3936 let config = config_from_environment(self.config_layers.drain(..));
3937 let mut ui = Ui::with_config(config.as_ref())
3940 .expect("default config should be valid, env vars are stringly typed");
3941 let result = self.run_internal(&mut ui, config);
3942 let exit_code = handle_command_result(&mut ui, result);
3943 ui.finalize_pager();
3944 exit_code
3945 }
3946}
3947
3948fn map_clap_cli_error(err: clap::Error, ui: &Ui, config: &StackedConfig) -> CommandError {
3949 if let Some(ContextValue::String(cmd)) = err.get(ContextKind::InvalidSubcommand) {
3950 let remove_useless_error_context = |mut err: clap::Error| {
3951 err.remove(ContextKind::SuggestedSubcommand);
3954 err.remove(ContextKind::Suggested); err.remove(ContextKind::Usage); err
3957 };
3958 match cmd.as_str() {
3959 "clone" | "init" => {
3962 let cmd = cmd.clone();
3963 return CommandError::from(remove_useless_error_context(err))
3964 .hinted(format!(
3965 "You probably want `jj git {cmd}`. See also `jj help git`."
3966 ))
3967 .hinted(format!(
3968 r#"You can configure `aliases.{cmd} = ["git", "{cmd}"]` if you want `jj {cmd}` to work and always use the Git backend."#
3969 ));
3970 }
3971 "amend" => {
3972 return CommandError::from(remove_useless_error_context(err))
3973 .hinted(
3974 r#"You probably want `jj squash`. You can configure `aliases.amend = ["squash"]` if you want `jj amend` to work."#);
3975 }
3976 _ => {}
3977 }
3978 }
3979 if let (Some(ContextValue::String(arg)), Some(ContextValue::String(value))) = (
3980 err.get(ContextKind::InvalidArg),
3981 err.get(ContextKind::InvalidValue),
3982 ) && arg.as_str() == "--template <TEMPLATE>"
3983 && value.is_empty()
3984 {
3985 if let Ok(template_aliases) = load_template_aliases(ui, config) {
3987 return CommandError::from(err).hinted(format_template_aliases_hint(&template_aliases));
3988 }
3989 }
3990 CommandError::from(err)
3991}
3992
3993fn format_template_aliases_hint(template_aliases: &TemplateAliasesMap) -> String {
3994 let mut hint = String::from("The following template aliases are defined:\n");
3995 hint.push_str(
3996 &template_aliases
3997 .symbol_names()
3998 .sorted_unstable()
3999 .map(|name| format!("- {name}"))
4000 .join("\n"),
4001 );
4002 hint
4003}
4004
4005fn warn_if_args_mismatch(
4007 ui: &Ui,
4008 app: &Command,
4009 config: &StackedConfig,
4010 expected_args: &[String],
4011) -> Result<(), CommandError> {
4012 let new_string_args = expand_args(ui, app, env::args_os(), config).ok();
4013 if new_string_args.as_deref() != Some(expected_args) {
4014 writeln!(
4015 ui.warning_default(),
4016 "Command aliases cannot be loaded from -R/--repository path or --config/--config-file \
4017 arguments."
4018 )?;
4019 }
4020 Ok(())
4021}
4022
4023#[cfg(test)]
4024mod tests {
4025 use clap::CommandFactory as _;
4026
4027 use super::*;
4028
4029 #[derive(clap::Parser, Clone, Debug)]
4030 pub struct TestArgs {
4031 #[arg(long)]
4032 pub foo: Vec<u32>,
4033 #[arg(long)]
4034 pub bar: Vec<u32>,
4035 #[arg(long)]
4036 pub baz: bool,
4037 }
4038
4039 #[test]
4040 fn test_merge_args_with() {
4041 let command = TestArgs::command();
4042 let parse = |args: &[&str]| -> Vec<(&'static str, u32)> {
4043 let matches = command.clone().try_get_matches_from(args).unwrap();
4044 let args = TestArgs::from_arg_matches(&matches).unwrap();
4045 merge_args_with(
4046 &matches,
4047 &[("foo", &args.foo), ("bar", &args.bar)],
4048 |id, value| (id, *value),
4049 )
4050 };
4051
4052 assert_eq!(parse(&["jj"]), vec![]);
4053 assert_eq!(parse(&["jj", "--foo=1"]), vec![("foo", 1)]);
4054 assert_eq!(
4055 parse(&["jj", "--foo=1", "--bar=2"]),
4056 vec![("foo", 1), ("bar", 2)]
4057 );
4058 assert_eq!(
4059 parse(&["jj", "--foo=1", "--baz", "--bar=2", "--foo", "3"]),
4060 vec![("foo", 1), ("bar", 2), ("foo", 3)]
4061 );
4062 }
4063}