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;
32use std::str::FromStr;
33use std::sync::Arc;
34use std::time::SystemTime;
35
36use bstr::ByteVec as _;
37use chrono::TimeZone as _;
38use clap::ArgAction;
39use clap::ArgMatches;
40use clap::Command;
41use clap::FromArgMatches as _;
42use clap::builder::MapValueParser;
43use clap::builder::NonEmptyStringValueParser;
44use clap::builder::TypedValueParser as _;
45use clap::builder::ValueParserFactory;
46use clap::error::ContextKind;
47use clap::error::ContextValue;
48use clap_complete::ArgValueCandidates;
49use clap_complete::ArgValueCompleter;
50use indexmap::IndexMap;
51use indexmap::IndexSet;
52use indoc::indoc;
53use indoc::writedoc;
54use itertools::Itertools as _;
55use jj_lib::backend::BackendResult;
56use jj_lib::backend::ChangeId;
57use jj_lib::backend::CommitId;
58use jj_lib::backend::MergedTreeId;
59use jj_lib::backend::TreeValue;
60use jj_lib::commit::Commit;
61use jj_lib::config::ConfigGetError;
62use jj_lib::config::ConfigGetResultExt as _;
63use jj_lib::config::ConfigLayer;
64use jj_lib::config::ConfigMigrationRule;
65use jj_lib::config::ConfigNamePathBuf;
66use jj_lib::config::ConfigSource;
67use jj_lib::config::StackedConfig;
68use jj_lib::conflicts::ConflictMarkerStyle;
69use jj_lib::fileset;
70use jj_lib::fileset::FilesetDiagnostics;
71use jj_lib::fileset::FilesetExpression;
72use jj_lib::gitignore::GitIgnoreError;
73use jj_lib::gitignore::GitIgnoreFile;
74use jj_lib::id_prefix::IdPrefixContext;
75use jj_lib::matchers::Matcher;
76use jj_lib::merge::MergedTreeValue;
77use jj_lib::merged_tree::MergedTree;
78use jj_lib::object_id::ObjectId as _;
79use jj_lib::op_heads_store;
80use jj_lib::op_store::OpStoreError;
81use jj_lib::op_store::OperationId;
82use jj_lib::op_store::RefTarget;
83use jj_lib::op_walk;
84use jj_lib::op_walk::OpsetEvaluationError;
85use jj_lib::operation::Operation;
86use jj_lib::ref_name::RefName;
87use jj_lib::ref_name::RefNameBuf;
88use jj_lib::ref_name::WorkspaceName;
89use jj_lib::ref_name::WorkspaceNameBuf;
90use jj_lib::repo::CheckOutCommitError;
91use jj_lib::repo::EditCommitError;
92use jj_lib::repo::MutableRepo;
93use jj_lib::repo::ReadonlyRepo;
94use jj_lib::repo::Repo;
95use jj_lib::repo::RepoLoader;
96use jj_lib::repo::StoreFactories;
97use jj_lib::repo::StoreLoadError;
98use jj_lib::repo::merge_factories_map;
99use jj_lib::repo_path::RepoPath;
100use jj_lib::repo_path::RepoPathBuf;
101use jj_lib::repo_path::RepoPathUiConverter;
102use jj_lib::repo_path::UiPathParseError;
103use jj_lib::revset;
104use jj_lib::revset::ResolvedRevsetExpression;
105use jj_lib::revset::RevsetAliasesMap;
106use jj_lib::revset::RevsetDiagnostics;
107use jj_lib::revset::RevsetExpression;
108use jj_lib::revset::RevsetExtensions;
109use jj_lib::revset::RevsetFilterPredicate;
110use jj_lib::revset::RevsetFunction;
111use jj_lib::revset::RevsetIteratorExt as _;
112use jj_lib::revset::RevsetModifier;
113use jj_lib::revset::RevsetParseContext;
114use jj_lib::revset::RevsetWorkspaceContext;
115use jj_lib::revset::SymbolResolverExtension;
116use jj_lib::revset::UserRevsetExpression;
117use jj_lib::rewrite::restore_tree;
118use jj_lib::settings::HumanByteSize;
119use jj_lib::settings::UserSettings;
120use jj_lib::str_util::StringPattern;
121use jj_lib::transaction::Transaction;
122use jj_lib::view::View;
123use jj_lib::working_copy;
124use jj_lib::working_copy::CheckoutStats;
125use jj_lib::working_copy::SnapshotOptions;
126use jj_lib::working_copy::SnapshotStats;
127use jj_lib::working_copy::UntrackedReason;
128use jj_lib::working_copy::WorkingCopy;
129use jj_lib::working_copy::WorkingCopyFactory;
130use jj_lib::working_copy::WorkingCopyFreshness;
131use jj_lib::workspace::DefaultWorkspaceLoaderFactory;
132use jj_lib::workspace::LockedWorkspace;
133use jj_lib::workspace::WorkingCopyFactories;
134use jj_lib::workspace::Workspace;
135use jj_lib::workspace::WorkspaceLoadError;
136use jj_lib::workspace::WorkspaceLoader;
137use jj_lib::workspace::WorkspaceLoaderFactory;
138use jj_lib::workspace::default_working_copy_factories;
139use jj_lib::workspace::get_working_copy_factory;
140use pollster::FutureExt as _;
141use tracing::instrument;
142use tracing_chrome::ChromeLayerBuilder;
143use tracing_subscriber::prelude::*;
144
145use crate::command_error::CommandError;
146use crate::command_error::cli_error;
147use crate::command_error::config_error_with_message;
148use crate::command_error::handle_command_result;
149use crate::command_error::internal_error;
150use crate::command_error::internal_error_with_message;
151use crate::command_error::print_parse_diagnostics;
152use crate::command_error::user_error;
153use crate::command_error::user_error_with_hint;
154use crate::commit_templater::CommitTemplateLanguage;
155use crate::commit_templater::CommitTemplateLanguageExtension;
156use crate::complete;
157use crate::config::ConfigArgKind;
158use crate::config::ConfigEnv;
159use crate::config::RawConfig;
160use crate::config::config_from_environment;
161use crate::config::parse_config_args;
162use crate::description_util::TextEditor;
163use crate::diff_util;
164use crate::diff_util::DiffFormat;
165use crate::diff_util::DiffFormatArgs;
166use crate::diff_util::DiffRenderer;
167use crate::formatter::FormatRecorder;
168use crate::formatter::Formatter;
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: Rc<UserRevsetExpression>,
786 short_prefixes_expression: Option<Rc<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) -> Rc<UserRevsetExpression> {
858 self.immutable_heads_expression.ancestors()
861 }
862
863 pub fn immutable_heads_expression(&self) -> &Rc<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<Rc<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<Rc<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 fsmonitor_settings = self.settings().fsmonitor_settings()?;
1371 let HumanByteSize(mut max_new_file_size) = self
1372 .settings()
1373 .get_value_with("snapshot.max-new-file-size", TryInto::try_into)?;
1374 if max_new_file_size == 0 {
1375 max_new_file_size = u64::MAX;
1376 }
1377 Ok(SnapshotOptions {
1378 base_ignores,
1379 fsmonitor_settings,
1380 progress: None,
1381 start_tracking_matcher,
1382 max_new_file_size,
1383 })
1384 }
1385
1386 pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
1387 self.env.path_converter()
1388 }
1389
1390 #[cfg(not(feature = "git"))]
1391 pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1392 Ok(GitIgnoreFile::empty())
1393 }
1394
1395 #[cfg(feature = "git")]
1396 #[instrument(skip_all)]
1397 pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1398 let get_excludes_file_path = |config: &gix::config::File| -> Option<PathBuf> {
1399 if let Some(value) = config.string("core.excludesFile") {
1402 let path = str::from_utf8(&value)
1403 .ok()
1404 .map(jj_lib::file_util::expand_home_path)?;
1405 Some(self.workspace_root().join(path))
1408 } else {
1409 xdg_config_home().ok().map(|x| x.join("git").join("ignore"))
1410 }
1411 };
1412
1413 fn xdg_config_home() -> Result<PathBuf, std::env::VarError> {
1414 if let Ok(x) = std::env::var("XDG_CONFIG_HOME") {
1415 if !x.is_empty() {
1416 return Ok(PathBuf::from(x));
1417 }
1418 }
1419 std::env::var("HOME").map(|x| Path::new(&x).join(".config"))
1420 }
1421
1422 let mut git_ignores = GitIgnoreFile::empty();
1423 if let Ok(git_backend) = jj_lib::git::get_git_backend(self.repo().store()) {
1424 let git_repo = git_backend.git_repo();
1425 if let Some(excludes_file_path) = get_excludes_file_path(&git_repo.config_snapshot()) {
1426 git_ignores = git_ignores.chain_with_file("", excludes_file_path)?;
1427 }
1428 git_ignores = git_ignores
1429 .chain_with_file("", git_backend.git_repo_path().join("info").join("exclude"))?;
1430 } else if let Ok(git_config) = gix::config::File::from_globals() {
1431 if let Some(excludes_file_path) = get_excludes_file_path(&git_config) {
1432 git_ignores = git_ignores.chain_with_file("", excludes_file_path)?;
1433 }
1434 }
1435 Ok(git_ignores)
1436 }
1437
1438 pub fn diff_renderer(&self, formats: Vec<DiffFormat>) -> DiffRenderer<'_> {
1440 DiffRenderer::new(
1441 self.repo().as_ref(),
1442 self.path_converter(),
1443 self.env.conflict_marker_style(),
1444 formats,
1445 )
1446 }
1447
1448 pub fn diff_renderer_for(
1450 &self,
1451 args: &DiffFormatArgs,
1452 ) -> Result<DiffRenderer<'_>, CommandError> {
1453 let formats = diff_util::diff_formats_for(self.settings(), args)?;
1454 Ok(self.diff_renderer(formats))
1455 }
1456
1457 pub fn diff_renderer_for_log(
1461 &self,
1462 args: &DiffFormatArgs,
1463 patch: bool,
1464 ) -> Result<Option<DiffRenderer<'_>>, CommandError> {
1465 let formats = diff_util::diff_formats_for_log(self.settings(), args, patch)?;
1466 Ok((!formats.is_empty()).then(|| self.diff_renderer(formats)))
1467 }
1468
1469 pub fn diff_editor(
1473 &self,
1474 ui: &Ui,
1475 tool_name: Option<&str>,
1476 ) -> Result<DiffEditor, CommandError> {
1477 let base_ignores = self.base_ignores()?;
1478 let conflict_marker_style = self.env.conflict_marker_style();
1479 if let Some(name) = tool_name {
1480 Ok(DiffEditor::with_name(
1481 name,
1482 self.settings(),
1483 base_ignores,
1484 conflict_marker_style,
1485 )?)
1486 } else {
1487 Ok(DiffEditor::from_settings(
1488 ui,
1489 self.settings(),
1490 base_ignores,
1491 conflict_marker_style,
1492 )?)
1493 }
1494 }
1495
1496 pub fn diff_selector(
1500 &self,
1501 ui: &Ui,
1502 tool_name: Option<&str>,
1503 force_interactive: bool,
1504 ) -> Result<DiffSelector, CommandError> {
1505 if tool_name.is_some() || force_interactive {
1506 Ok(DiffSelector::Interactive(self.diff_editor(ui, tool_name)?))
1507 } else {
1508 Ok(DiffSelector::NonInteractive)
1509 }
1510 }
1511
1512 pub fn merge_editor(
1516 &self,
1517 ui: &Ui,
1518 tool_name: Option<&str>,
1519 ) -> Result<MergeEditor, MergeToolConfigError> {
1520 let conflict_marker_style = self.env.conflict_marker_style();
1521 if let Some(name) = tool_name {
1522 MergeEditor::with_name(
1523 name,
1524 self.settings(),
1525 self.path_converter().clone(),
1526 conflict_marker_style,
1527 )
1528 } else {
1529 MergeEditor::from_settings(
1530 ui,
1531 self.settings(),
1532 self.path_converter().clone(),
1533 conflict_marker_style,
1534 )
1535 }
1536 }
1537
1538 pub fn text_editor(&self) -> Result<TextEditor, ConfigGetError> {
1540 TextEditor::from_settings(self.settings())
1541 }
1542
1543 pub fn resolve_single_op(&self, op_str: &str) -> Result<Operation, OpsetEvaluationError> {
1544 op_walk::resolve_op_with_repo(self.repo(), op_str)
1545 }
1546
1547 pub fn resolve_single_rev(
1550 &self,
1551 ui: &Ui,
1552 revision_arg: &RevisionArg,
1553 ) -> Result<Commit, CommandError> {
1554 let expression = self.parse_revset(ui, revision_arg)?;
1555 revset_util::evaluate_revset_to_single_commit(revision_arg.as_ref(), &expression, || {
1556 self.commit_summary_template()
1557 })
1558 }
1559
1560 pub fn resolve_some_revsets_default_single(
1563 &self,
1564 ui: &Ui,
1565 revision_args: &[RevisionArg],
1566 ) -> Result<IndexSet<CommitId>, CommandError> {
1567 let mut all_commits = IndexSet::new();
1568 for revision_arg in revision_args {
1569 let (expression, modifier) = self.parse_revset_with_modifier(ui, revision_arg)?;
1570 let all = match modifier {
1571 Some(RevsetModifier::All) => true,
1572 None => self.settings().get_bool("ui.always-allow-large-revsets")?,
1573 };
1574 if all {
1575 for commit_id in expression.evaluate_to_commit_ids()? {
1576 all_commits.insert(commit_id?);
1577 }
1578 } else {
1579 let commit = revset_util::evaluate_revset_to_single_commit(
1580 revision_arg.as_ref(),
1581 &expression,
1582 || self.commit_summary_template(),
1583 )?;
1584 if !all_commits.insert(commit.id().clone()) {
1585 let commit_hash = short_commit_hash(commit.id());
1586 return Err(user_error(format!(
1587 r#"More than one revset resolved to revision {commit_hash}"#,
1588 )));
1589 }
1590 }
1591 }
1592 if all_commits.is_empty() {
1593 Err(user_error("Empty revision set"))
1594 } else {
1595 Ok(all_commits)
1596 }
1597 }
1598
1599 pub fn parse_revset(
1600 &self,
1601 ui: &Ui,
1602 revision_arg: &RevisionArg,
1603 ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1604 let (expression, modifier) = self.parse_revset_with_modifier(ui, revision_arg)?;
1605 let (None | Some(RevsetModifier::All)) = modifier;
1608 Ok(expression)
1609 }
1610
1611 fn parse_revset_with_modifier(
1612 &self,
1613 ui: &Ui,
1614 revision_arg: &RevisionArg,
1615 ) -> Result<(RevsetExpressionEvaluator<'_>, Option<RevsetModifier>), CommandError> {
1616 let mut diagnostics = RevsetDiagnostics::new();
1617 let context = self.env.revset_parse_context();
1618 let (expression, modifier) =
1619 revset::parse_with_modifier(&mut diagnostics, revision_arg.as_ref(), &context)?;
1620 print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1621 Ok((self.attach_revset_evaluator(expression), modifier))
1622 }
1623
1624 pub fn parse_union_revsets(
1626 &self,
1627 ui: &Ui,
1628 revision_args: &[RevisionArg],
1629 ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1630 let mut diagnostics = RevsetDiagnostics::new();
1631 let context = self.env.revset_parse_context();
1632 let expressions: Vec<_> = revision_args
1633 .iter()
1634 .map(|arg| revset::parse_with_modifier(&mut diagnostics, arg.as_ref(), &context))
1635 .map_ok(|(expression, None | Some(RevsetModifier::All))| expression)
1636 .try_collect()?;
1637 print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1638 let expression = RevsetExpression::union_all(&expressions);
1639 Ok(self.attach_revset_evaluator(expression))
1640 }
1641
1642 pub fn attach_revset_evaluator(
1643 &self,
1644 expression: Rc<UserRevsetExpression>,
1645 ) -> RevsetExpressionEvaluator<'_> {
1646 RevsetExpressionEvaluator::new(
1647 self.repo().as_ref(),
1648 self.env.command.revset_extensions().clone(),
1649 self.id_prefix_context(),
1650 expression,
1651 )
1652 }
1653
1654 pub fn id_prefix_context(&self) -> &IdPrefixContext {
1655 self.user_repo
1656 .id_prefix_context
1657 .get_or_init(|| self.env.new_id_prefix_context())
1658 }
1659
1660 pub fn parse_template<'a, C, L>(
1662 &self,
1663 ui: &Ui,
1664 language: &L,
1665 template_text: &str,
1666 ) -> Result<TemplateRenderer<'a, C>, CommandError>
1667 where
1668 C: Clone + 'a,
1669 L: TemplateLanguage<'a> + ?Sized,
1670 L::Property: WrapTemplateProperty<'a, C>,
1671 {
1672 self.env.parse_template(ui, language, template_text)
1673 }
1674
1675 fn reparse_valid_template<'a, C, L>(
1677 &self,
1678 language: &L,
1679 template_text: &str,
1680 ) -> TemplateRenderer<'a, C>
1681 where
1682 C: Clone + 'a,
1683 L: TemplateLanguage<'a> + ?Sized,
1684 L::Property: WrapTemplateProperty<'a, C>,
1685 {
1686 template_builder::parse(
1687 language,
1688 &mut TemplateDiagnostics::new(),
1689 template_text,
1690 &self.env.template_aliases_map,
1691 )
1692 .expect("parse error should be confined by WorkspaceCommandHelper::new()")
1693 }
1694
1695 pub fn parse_commit_template(
1697 &self,
1698 ui: &Ui,
1699 template_text: &str,
1700 ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
1701 let language = self.commit_template_language();
1702 self.parse_template(ui, &language, template_text)
1703 }
1704
1705 pub fn parse_operation_template(
1707 &self,
1708 ui: &Ui,
1709 template_text: &str,
1710 ) -> Result<TemplateRenderer<'_, Operation>, CommandError> {
1711 let language = self.operation_template_language();
1712 self.parse_template(ui, &language, template_text)
1713 }
1714
1715 pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
1717 self.env
1718 .commit_template_language(self.repo().as_ref(), self.id_prefix_context())
1719 }
1720
1721 pub fn operation_template_language(&self) -> OperationTemplateLanguage {
1723 OperationTemplateLanguage::new(
1724 self.workspace.repo_loader(),
1725 Some(self.repo().op_id()),
1726 self.env.operation_template_extensions(),
1727 )
1728 }
1729
1730 pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
1732 let language = self.commit_template_language();
1733 self.reparse_valid_template(&language, &self.commit_summary_template_text)
1734 .labeled(["commit"])
1735 }
1736
1737 pub fn operation_summary_template(&self) -> TemplateRenderer<'_, Operation> {
1739 let language = self.operation_template_language();
1740 self.reparse_valid_template(&language, &self.op_summary_template_text)
1741 .labeled(["operation"])
1742 }
1743
1744 pub fn short_change_id_template(&self) -> TemplateRenderer<'_, Commit> {
1745 let language = self.commit_template_language();
1746 self.reparse_valid_template(&language, SHORT_CHANGE_ID_TEMPLATE_TEXT)
1747 .labeled(["commit"])
1748 }
1749
1750 pub fn format_commit_summary(&self, commit: &Commit) -> String {
1755 let output = self.commit_summary_template().format_plain_text(commit);
1756 output.into_string_lossy()
1757 }
1758
1759 #[instrument(skip_all)]
1763 pub fn write_commit_summary(
1764 &self,
1765 formatter: &mut dyn Formatter,
1766 commit: &Commit,
1767 ) -> std::io::Result<()> {
1768 self.commit_summary_template().format(commit, formatter)
1769 }
1770
1771 pub fn check_rewritable<'a>(
1772 &self,
1773 commits: impl IntoIterator<Item = &'a CommitId>,
1774 ) -> Result<(), CommandError> {
1775 let repo = self.repo().as_ref();
1776 let commit_ids = commits.into_iter().cloned().collect_vec();
1777 let Some(commit_id) = self.env.find_immutable_commit(repo, &commit_ids)? else {
1778 return Ok(());
1779 };
1780 let error = if &commit_id == repo.store().root_commit_id() {
1781 user_error(format!("The root commit {commit_id:.12} is immutable"))
1782 } else {
1783 let mut error = user_error(format!("Commit {commit_id:.12} is immutable"));
1784 let commit = repo.store().get_commit(&commit_id)?;
1785 error.add_formatted_hint_with(|formatter| {
1786 write!(formatter, "Could not modify commit: ")?;
1787 self.write_commit_summary(formatter, &commit)?;
1788 Ok(())
1789 });
1790 error.add_hint("Immutable commits are used to protect shared history.");
1791 error.add_hint(indoc::indoc! {"
1792 For more information, see:
1793 - https://jj-vcs.github.io/jj/latest/config/#set-of-immutable-commits
1794 - `jj help -k config`, \"Set of immutable commits\""});
1795
1796 let id_prefix_context =
1799 IdPrefixContext::new(self.env.command.revset_extensions().clone());
1800 let to_rewrite_expr = RevsetExpression::commits(commit_ids);
1801 let (lower_bound, upper_bound) = RevsetExpressionEvaluator::new(
1802 repo,
1803 self.env.command.revset_extensions().clone(),
1804 &id_prefix_context,
1805 self.env
1806 .immutable_expression()
1807 .intersection(&to_rewrite_expr.descendants()),
1808 )
1809 .evaluate()?
1810 .count_estimate()?;
1811 let exact = upper_bound == Some(lower_bound);
1812 let or_more = if exact { "" } else { " or more" };
1813 error.add_hint(format!(
1814 "This operation would rewrite {lower_bound}{or_more} immutable commits."
1815 ));
1816
1817 error
1818 };
1819 Err(error)
1820 }
1821
1822 #[instrument(skip_all)]
1823 fn snapshot_working_copy(
1824 &mut self,
1825 ui: &Ui,
1826 ) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
1827 let workspace_name = self.workspace_name().to_owned();
1828 let get_wc_commit = |repo: &ReadonlyRepo| -> Result<Option<_>, _> {
1829 repo.view()
1830 .get_wc_commit_id(&workspace_name)
1831 .map(|id| repo.store().get_commit(id))
1832 .transpose()
1833 .map_err(snapshot_command_error)
1834 };
1835 let repo = self.repo().clone();
1836 let Some(wc_commit) = get_wc_commit(&repo)? else {
1837 return Ok(SnapshotStats::default());
1840 };
1841 let auto_tracking_matcher = self
1842 .auto_tracking_matcher(ui)
1843 .map_err(snapshot_command_error)?;
1844 let options = self
1845 .snapshot_options_with_start_tracking_matcher(&auto_tracking_matcher)
1846 .map_err(snapshot_command_error)?;
1847
1848 let mut locked_ws = self
1850 .workspace
1851 .start_working_copy_mutation()
1852 .map_err(snapshot_command_error)?;
1853 let old_op_id = locked_ws.locked_wc().old_operation_id().clone();
1854
1855 let (repo, wc_commit) =
1856 match WorkingCopyFreshness::check_stale(locked_ws.locked_wc(), &wc_commit, &repo) {
1857 Ok(WorkingCopyFreshness::Fresh) => (repo, wc_commit),
1858 Ok(WorkingCopyFreshness::Updated(wc_operation)) => {
1859 let repo = repo
1860 .reload_at(&wc_operation)
1861 .map_err(snapshot_command_error)?;
1862 let wc_commit = if let Some(wc_commit) = get_wc_commit(&repo)? {
1863 wc_commit
1864 } else {
1865 return Ok(SnapshotStats::default());
1867 };
1868 (repo, wc_commit)
1869 }
1870 Ok(WorkingCopyFreshness::WorkingCopyStale) => {
1871 return Err(SnapshotWorkingCopyError::StaleWorkingCopy(
1872 user_error_with_hint(
1873 format!(
1874 "The working copy is stale (not updated since operation {}).",
1875 short_operation_hash(&old_op_id)
1876 ),
1877 "Run `jj workspace update-stale` to update it.
1878See https://jj-vcs.github.io/jj/latest/working-copy/#stale-working-copy \
1879 for more information.",
1880 ),
1881 ));
1882 }
1883 Ok(WorkingCopyFreshness::SiblingOperation) => {
1884 return Err(SnapshotWorkingCopyError::StaleWorkingCopy(internal_error(
1885 format!(
1886 "The repo was loaded at operation {}, which seems to be a sibling of \
1887 the working copy's operation {}",
1888 short_operation_hash(repo.op_id()),
1889 short_operation_hash(&old_op_id)
1890 ),
1891 )));
1892 }
1893 Err(OpStoreError::ObjectNotFound { .. }) => {
1894 return Err(SnapshotWorkingCopyError::StaleWorkingCopy(
1895 user_error_with_hint(
1896 "Could not read working copy's operation.",
1897 "Run `jj workspace update-stale` to recover.
1898See https://jj-vcs.github.io/jj/latest/working-copy/#stale-working-copy \
1899 for more information.",
1900 ),
1901 ));
1902 }
1903 Err(e) => return Err(snapshot_command_error(e)),
1904 };
1905 self.user_repo = ReadonlyUserRepo::new(repo);
1906 let (new_tree_id, stats) = {
1907 let mut options = options;
1908 let progress = crate::progress::snapshot_progress(ui);
1909 options.progress = progress.as_ref().map(|x| x as _);
1910 locked_ws
1911 .locked_wc()
1912 .snapshot(&options)
1913 .map_err(snapshot_command_error)?
1914 };
1915 if new_tree_id != *wc_commit.tree_id() {
1916 let mut tx =
1917 start_repo_transaction(&self.user_repo.repo, self.env.command.string_args());
1918 tx.set_is_snapshot(true);
1919 let mut_repo = tx.repo_mut();
1920 let commit = mut_repo
1921 .rewrite_commit(&wc_commit)
1922 .set_tree_id(new_tree_id)
1923 .write()
1924 .map_err(snapshot_command_error)?;
1925 mut_repo
1926 .set_wc_commit(workspace_name, commit.id().clone())
1927 .map_err(snapshot_command_error)?;
1928
1929 let num_rebased = mut_repo
1931 .rebase_descendants()
1932 .map_err(snapshot_command_error)?;
1933 if num_rebased > 0 {
1934 writeln!(
1935 ui.status(),
1936 "Rebased {num_rebased} descendant commits onto updated working copy"
1937 )
1938 .map_err(snapshot_command_error)?;
1939 }
1940
1941 #[cfg(feature = "git")]
1942 if self.working_copy_shared_with_git {
1943 let old_tree = wc_commit.tree().map_err(snapshot_command_error)?;
1944 let new_tree = commit.tree().map_err(snapshot_command_error)?;
1945 jj_lib::git::update_intent_to_add(
1946 self.user_repo.repo.as_ref(),
1947 &old_tree,
1948 &new_tree,
1949 )
1950 .map_err(snapshot_command_error)?;
1951
1952 let stats = jj_lib::git::export_refs(mut_repo).map_err(snapshot_command_error)?;
1953 crate::git_util::print_git_export_stats(ui, &stats)
1954 .map_err(snapshot_command_error)?;
1955 }
1956
1957 let repo = tx
1958 .commit("snapshot working copy")
1959 .map_err(snapshot_command_error)?;
1960 self.user_repo = ReadonlyUserRepo::new(repo);
1961 }
1962 locked_ws
1963 .finish(self.user_repo.repo.op_id().clone())
1964 .map_err(snapshot_command_error)?;
1965 Ok(stats)
1966 }
1967
1968 fn update_working_copy(
1969 &mut self,
1970 ui: &Ui,
1971 maybe_old_commit: Option<&Commit>,
1972 new_commit: &Commit,
1973 ) -> Result<(), CommandError> {
1974 assert!(self.may_update_working_copy);
1975 let stats = update_working_copy(
1976 &self.user_repo.repo,
1977 &mut self.workspace,
1978 maybe_old_commit,
1979 new_commit,
1980 )?;
1981 self.print_updated_working_copy_stats(ui, maybe_old_commit, new_commit, &stats)
1982 }
1983
1984 fn print_updated_working_copy_stats(
1985 &self,
1986 ui: &Ui,
1987 maybe_old_commit: Option<&Commit>,
1988 new_commit: &Commit,
1989 stats: &CheckoutStats,
1990 ) -> Result<(), CommandError> {
1991 if Some(new_commit) != maybe_old_commit {
1992 if let Some(mut formatter) = ui.status_formatter() {
1993 let template = self.commit_summary_template();
1994 write!(formatter, "Working copy (@) now at: ")?;
1995 template.format(new_commit, formatter.as_mut())?;
1996 writeln!(formatter)?;
1997 for parent in new_commit.parents() {
1998 let parent = parent?;
1999 write!(formatter, "Parent commit (@-) : ")?;
2001 template.format(&parent, formatter.as_mut())?;
2002 writeln!(formatter)?;
2003 }
2004 }
2005 }
2006 print_checkout_stats(ui, stats, new_commit)?;
2007 if Some(new_commit) != maybe_old_commit {
2008 if let Some(mut formatter) = ui.status_formatter() {
2009 if new_commit.has_conflict()? {
2010 let conflicts = new_commit.tree()?.conflicts().collect_vec();
2011 writeln!(
2012 formatter.labeled("warning").with_heading("Warning: "),
2013 "There are unresolved conflicts at these paths:"
2014 )?;
2015 print_conflicted_paths(conflicts, formatter.as_mut(), self)?;
2016 }
2017 }
2018 }
2019 Ok(())
2020 }
2021
2022 pub fn start_transaction(&mut self) -> WorkspaceCommandTransaction<'_> {
2023 let tx = start_repo_transaction(self.repo(), self.env.command.string_args());
2024 let id_prefix_context = mem::take(&mut self.user_repo.id_prefix_context);
2025 WorkspaceCommandTransaction {
2026 helper: self,
2027 tx,
2028 id_prefix_context,
2029 }
2030 }
2031
2032 fn finish_transaction(
2033 &mut self,
2034 ui: &Ui,
2035 mut tx: Transaction,
2036 description: impl Into<String>,
2037 ) -> Result<(), CommandError> {
2038 if !tx.repo().has_changes() {
2039 writeln!(ui.status(), "Nothing changed.")?;
2040 return Ok(());
2041 }
2042 let num_rebased = tx.repo_mut().rebase_descendants()?;
2043 if num_rebased > 0 {
2044 writeln!(ui.status(), "Rebased {num_rebased} descendant commits")?;
2045 }
2046
2047 for (name, wc_commit_id) in &tx.repo().view().wc_commit_ids().clone() {
2048 if self
2049 .env
2050 .find_immutable_commit(tx.repo(), slice::from_ref(wc_commit_id))?
2051 .is_some()
2052 {
2053 let wc_commit = tx.repo().store().get_commit(wc_commit_id)?;
2054 tx.repo_mut().check_out(name.clone(), &wc_commit)?;
2055 writeln!(
2056 ui.warning_default(),
2057 "The working-copy commit in workspace '{name}' became immutable, so a new \
2058 commit has been created on top of it.",
2059 name = name.as_symbol()
2060 )?;
2061 }
2062 }
2063
2064 let old_repo = tx.base_repo().clone();
2065
2066 let maybe_old_wc_commit = old_repo
2067 .view()
2068 .get_wc_commit_id(self.workspace_name())
2069 .map(|commit_id| tx.base_repo().store().get_commit(commit_id))
2070 .transpose()?;
2071 let maybe_new_wc_commit = tx
2072 .repo()
2073 .view()
2074 .get_wc_commit_id(self.workspace_name())
2075 .map(|commit_id| tx.repo().store().get_commit(commit_id))
2076 .transpose()?;
2077
2078 #[cfg(feature = "git")]
2079 if self.working_copy_shared_with_git {
2080 use std::error::Error as _;
2081 if let Some(wc_commit) = &maybe_new_wc_commit {
2082 match jj_lib::git::reset_head(tx.repo_mut(), wc_commit) {
2085 Ok(()) => {}
2086 Err(err @ jj_lib::git::GitResetHeadError::UpdateHeadRef(_)) => {
2087 writeln!(ui.warning_default(), "{err}")?;
2088 crate::command_error::print_error_sources(ui, err.source())?;
2089 }
2090 Err(err) => return Err(err.into()),
2091 }
2092 }
2093 let stats = jj_lib::git::export_refs(tx.repo_mut())?;
2094 crate::git_util::print_git_export_stats(ui, &stats)?;
2095 }
2096
2097 self.user_repo = ReadonlyUserRepo::new(tx.commit(description)?);
2098
2099 if self.may_update_working_copy {
2103 if let Some(new_commit) = &maybe_new_wc_commit {
2104 self.update_working_copy(ui, maybe_old_wc_commit.as_ref(), new_commit)?;
2105 } else {
2106 }
2109 }
2110
2111 self.report_repo_changes(ui, &old_repo)?;
2112
2113 let settings = self.settings();
2114 let missing_user_name = settings.user_name().is_empty();
2115 let missing_user_mail = settings.user_email().is_empty();
2116 if missing_user_name || missing_user_mail {
2117 let not_configured_msg = match (missing_user_name, missing_user_mail) {
2118 (true, true) => "Name and email not configured.",
2119 (true, false) => "Name not configured.",
2120 (false, true) => "Email not configured.",
2121 _ => unreachable!(),
2122 };
2123 writeln!(
2124 ui.warning_default(),
2125 "{not_configured_msg} Until configured, your commits will be created with the \
2126 empty identity, and can't be pushed to remotes."
2127 )?;
2128 writeln!(ui.hint_default(), "To configure, run:")?;
2129 if missing_user_name {
2130 writeln!(
2131 ui.hint_no_heading(),
2132 r#" jj config set --user user.name "Some One""#
2133 )?;
2134 }
2135 if missing_user_mail {
2136 writeln!(
2137 ui.hint_no_heading(),
2138 r#" jj config set --user user.email "someone@example.com""#
2139 )?;
2140 }
2141 }
2142 Ok(())
2143 }
2144
2145 fn report_repo_changes(
2148 &self,
2149 ui: &Ui,
2150 old_repo: &Arc<ReadonlyRepo>,
2151 ) -> Result<(), CommandError> {
2152 let Some(mut fmt) = ui.status_formatter() else {
2153 return Ok(());
2154 };
2155 let old_view = old_repo.view();
2156 let new_repo = self.repo().as_ref();
2157 let new_view = new_repo.view();
2158 let old_heads = RevsetExpression::commits(old_view.heads().iter().cloned().collect());
2159 let new_heads = RevsetExpression::commits(new_view.heads().iter().cloned().collect());
2160 let conflicts = RevsetExpression::filter(RevsetFilterPredicate::HasConflict)
2166 .filtered(RevsetFilterPredicate::File(FilesetExpression::all()));
2167 let removed_conflicts_expr = new_heads.range(&old_heads).intersection(&conflicts);
2168 let added_conflicts_expr = old_heads.range(&new_heads).intersection(&conflicts);
2169
2170 let get_commits =
2171 |expr: Rc<ResolvedRevsetExpression>| -> Result<Vec<Commit>, CommandError> {
2172 let commits = expr
2173 .evaluate(new_repo)?
2174 .iter()
2175 .commits(new_repo.store())
2176 .try_collect()?;
2177 Ok(commits)
2178 };
2179 let removed_conflict_commits = get_commits(removed_conflicts_expr)?;
2180 let added_conflict_commits = get_commits(added_conflicts_expr)?;
2181
2182 fn commits_by_change_id(commits: &[Commit]) -> IndexMap<&ChangeId, Vec<&Commit>> {
2183 let mut result: IndexMap<&ChangeId, Vec<&Commit>> = IndexMap::new();
2184 for commit in commits {
2185 result.entry(commit.change_id()).or_default().push(commit);
2186 }
2187 result
2188 }
2189 let removed_conflicts_by_change_id = commits_by_change_id(&removed_conflict_commits);
2190 let added_conflicts_by_change_id = commits_by_change_id(&added_conflict_commits);
2191 let mut resolved_conflicts_by_change_id = removed_conflicts_by_change_id.clone();
2192 resolved_conflicts_by_change_id
2193 .retain(|change_id, _commits| !added_conflicts_by_change_id.contains_key(change_id));
2194 let mut new_conflicts_by_change_id = added_conflicts_by_change_id.clone();
2195 new_conflicts_by_change_id
2196 .retain(|change_id, _commits| !removed_conflicts_by_change_id.contains_key(change_id));
2197
2198 if !resolved_conflicts_by_change_id.is_empty() {
2200 let num_resolved: usize = resolved_conflicts_by_change_id
2204 .values()
2205 .map(|commits| commits.len())
2206 .sum();
2207 writeln!(
2208 fmt,
2209 "Existing conflicts were resolved or abandoned from {num_resolved} commits."
2210 )?;
2211 }
2212 if !new_conflicts_by_change_id.is_empty() {
2213 let num_conflicted: usize = new_conflicts_by_change_id
2214 .values()
2215 .map(|commits| commits.len())
2216 .sum();
2217 writeln!(fmt, "New conflicts appeared in {num_conflicted} commits:")?;
2218 print_updated_commits(
2219 fmt.as_mut(),
2220 &self.commit_summary_template(),
2221 new_conflicts_by_change_id.values().flatten().copied(),
2222 )?;
2223 }
2224
2225 if !(added_conflict_commits.is_empty()
2229 || resolved_conflicts_by_change_id.is_empty() && new_conflicts_by_change_id.is_empty())
2230 {
2231 if new_conflicts_by_change_id.is_empty() {
2236 writeln!(
2237 fmt,
2238 "There are still unresolved conflicts in rebased descendants.",
2239 )?;
2240 }
2241
2242 self.report_repo_conflicts(
2243 fmt.as_mut(),
2244 new_repo,
2245 added_conflict_commits
2246 .iter()
2247 .map(|commit| commit.id().clone())
2248 .collect(),
2249 )?;
2250 }
2251 revset_util::warn_unresolvable_trunk(ui, new_repo, &self.env.revset_parse_context())?;
2252
2253 Ok(())
2254 }
2255
2256 pub fn report_repo_conflicts(
2257 &self,
2258 fmt: &mut dyn Formatter,
2259 repo: &ReadonlyRepo,
2260 conflicted_commits: Vec<CommitId>,
2261 ) -> Result<(), CommandError> {
2262 if !self.settings().get_bool("hints.resolving-conflicts")? || conflicted_commits.is_empty()
2263 {
2264 return Ok(());
2265 }
2266
2267 let only_one_conflicted_commit = conflicted_commits.len() == 1;
2268 let root_conflicts_revset = RevsetExpression::commits(conflicted_commits)
2269 .roots()
2270 .evaluate(repo)?;
2271
2272 let root_conflict_commits: Vec<_> = root_conflicts_revset
2273 .iter()
2274 .commits(repo.store())
2275 .try_collect()?;
2276
2277 let instruction = if only_one_conflicted_commit {
2279 indoc! {"
2280 To resolve the conflicts, start by creating a commit on top of
2281 the conflicted commit:
2282 "}
2283 } else if root_conflict_commits.len() == 1 {
2284 indoc! {"
2285 To resolve the conflicts, start by creating a commit on top of
2286 the first conflicted commit:
2287 "}
2288 } else {
2289 indoc! {"
2290 To resolve the conflicts, start by creating a commit on top of
2291 one of the first conflicted commits:
2292 "}
2293 };
2294 write!(fmt.labeled("hint").with_heading("Hint: "), "{instruction}")?;
2295 let format_short_change_id = self.short_change_id_template();
2296 fmt.with_label("hint", |fmt| {
2297 for commit in &root_conflict_commits {
2298 write!(fmt, " jj new ")?;
2299 format_short_change_id.format(commit, fmt)?;
2300 writeln!(fmt)?;
2301 }
2302 io::Result::Ok(())
2303 })?;
2304 writedoc!(
2305 fmt.labeled("hint"),
2306 "
2307 Then use `jj resolve`, or edit the conflict markers in the file directly.
2308 Once the conflicts are resolved, you can inspect the result with `jj diff`.
2309 Then run `jj squash` to move the resolution into the conflicted commit.
2310 ",
2311 )?;
2312 Ok(())
2313 }
2314
2315 pub fn get_advanceable_bookmarks<'a>(
2330 &self,
2331 from: impl IntoIterator<Item = &'a CommitId>,
2332 ) -> Result<Vec<AdvanceableBookmark>, CommandError> {
2333 let ab_settings = AdvanceBookmarksSettings::from_settings(self.settings())?;
2334 if !ab_settings.feature_enabled() {
2335 return Ok(Vec::new());
2337 }
2338
2339 let mut advanceable_bookmarks = Vec::new();
2340 for from_commit in from {
2341 for (name, _) in self.repo().view().local_bookmarks_for_commit(from_commit) {
2342 if ab_settings.bookmark_is_eligible(name) {
2343 advanceable_bookmarks.push(AdvanceableBookmark {
2344 name: name.to_owned(),
2345 old_commit_id: from_commit.clone(),
2346 });
2347 }
2348 }
2349 }
2350
2351 Ok(advanceable_bookmarks)
2352 }
2353}
2354
2355#[must_use]
2363pub struct WorkspaceCommandTransaction<'a> {
2364 helper: &'a mut WorkspaceCommandHelper,
2365 tx: Transaction,
2366 id_prefix_context: OnceCell<IdPrefixContext>,
2368}
2369
2370impl WorkspaceCommandTransaction<'_> {
2371 pub fn base_workspace_helper(&self) -> &WorkspaceCommandHelper {
2373 self.helper
2374 }
2375
2376 pub fn settings(&self) -> &UserSettings {
2378 self.helper.settings()
2379 }
2380
2381 pub fn base_repo(&self) -> &Arc<ReadonlyRepo> {
2382 self.tx.base_repo()
2383 }
2384
2385 pub fn repo(&self) -> &MutableRepo {
2386 self.tx.repo()
2387 }
2388
2389 pub fn repo_mut(&mut self) -> &mut MutableRepo {
2390 self.id_prefix_context.take(); self.tx.repo_mut()
2392 }
2393
2394 pub fn check_out(&mut self, commit: &Commit) -> Result<Commit, CheckOutCommitError> {
2395 let name = self.helper.workspace_name().to_owned();
2396 self.id_prefix_context.take(); self.tx.repo_mut().check_out(name, commit)
2398 }
2399
2400 pub fn edit(&mut self, commit: &Commit) -> Result<(), EditCommitError> {
2401 let name = self.helper.workspace_name().to_owned();
2402 self.id_prefix_context.take(); self.tx.repo_mut().edit(name, commit)
2404 }
2405
2406 pub fn format_commit_summary(&self, commit: &Commit) -> String {
2407 let output = self.commit_summary_template().format_plain_text(commit);
2408 output.into_string_lossy()
2409 }
2410
2411 pub fn write_commit_summary(
2412 &self,
2413 formatter: &mut dyn Formatter,
2414 commit: &Commit,
2415 ) -> std::io::Result<()> {
2416 self.commit_summary_template().format(commit, formatter)
2417 }
2418
2419 pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
2421 let language = self.commit_template_language();
2422 self.helper
2423 .reparse_valid_template(&language, &self.helper.commit_summary_template_text)
2424 .labeled(["commit"])
2425 }
2426
2427 pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
2430 let id_prefix_context = self
2431 .id_prefix_context
2432 .get_or_init(|| self.helper.env.new_id_prefix_context());
2433 self.helper
2434 .env
2435 .commit_template_language(self.tx.repo(), id_prefix_context)
2436 }
2437
2438 pub fn parse_commit_template(
2440 &self,
2441 ui: &Ui,
2442 template_text: &str,
2443 ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
2444 let language = self.commit_template_language();
2445 self.helper.env.parse_template(ui, &language, template_text)
2446 }
2447
2448 pub fn finish(self, ui: &Ui, description: impl Into<String>) -> Result<(), CommandError> {
2449 self.helper.finish_transaction(ui, self.tx, description)
2450 }
2451
2452 pub fn into_inner(self) -> Transaction {
2457 self.tx
2458 }
2459
2460 pub fn advance_bookmarks(&mut self, bookmarks: Vec<AdvanceableBookmark>, move_to: &CommitId) {
2466 for bookmark in bookmarks {
2467 self.repo_mut().merge_local_bookmark(
2470 &bookmark.name,
2471 &RefTarget::normal(bookmark.old_commit_id),
2472 &RefTarget::normal(move_to.clone()),
2473 );
2474 }
2475 }
2476}
2477
2478pub fn find_workspace_dir(cwd: &Path) -> &Path {
2479 cwd.ancestors()
2480 .find(|path| path.join(".jj").is_dir())
2481 .unwrap_or(cwd)
2482}
2483
2484fn map_workspace_load_error(err: WorkspaceLoadError, user_wc_path: Option<&str>) -> CommandError {
2485 match err {
2486 WorkspaceLoadError::NoWorkspaceHere(wc_path) => {
2487 let short_wc_path = user_wc_path.map_or(wc_path.as_ref(), Path::new);
2489 let message = format!(r#"There is no jj repo in "{}""#, short_wc_path.display());
2490 let git_dir = wc_path.join(".git");
2491 if git_dir.is_dir() {
2492 user_error_with_hint(
2493 message,
2494 "It looks like this is a git repo. You can create a jj repo backed by it by \
2495 running this:
2496jj git init --colocate",
2497 )
2498 } else {
2499 user_error(message)
2500 }
2501 }
2502 WorkspaceLoadError::RepoDoesNotExist(repo_dir) => user_error(format!(
2503 "The repository directory at {} is missing. Was it moved?",
2504 repo_dir.display(),
2505 )),
2506 WorkspaceLoadError::StoreLoadError(err @ StoreLoadError::UnsupportedType { .. }) => {
2507 internal_error_with_message(
2508 "This version of the jj binary doesn't support this type of repo",
2509 err,
2510 )
2511 }
2512 WorkspaceLoadError::StoreLoadError(
2513 err @ (StoreLoadError::ReadError { .. } | StoreLoadError::Backend(_)),
2514 ) => internal_error_with_message("The repository appears broken or inaccessible", err),
2515 WorkspaceLoadError::StoreLoadError(StoreLoadError::Signing(err)) => user_error(err),
2516 WorkspaceLoadError::WorkingCopyState(err) => internal_error(err),
2517 WorkspaceLoadError::DecodeRepoPath(_) | WorkspaceLoadError::Path(_) => user_error(err),
2518 }
2519}
2520
2521pub fn start_repo_transaction(repo: &Arc<ReadonlyRepo>, string_args: &[String]) -> Transaction {
2522 let mut tx = repo.start_transaction();
2523 let shell_escape = |arg: &String| {
2526 if arg.as_bytes().iter().all(|b| {
2527 matches!(b,
2528 b'A'..=b'Z'
2529 | b'a'..=b'z'
2530 | b'0'..=b'9'
2531 | b','
2532 | b'-'
2533 | b'.'
2534 | b'/'
2535 | b':'
2536 | b'@'
2537 | b'_'
2538 )
2539 }) {
2540 arg.clone()
2541 } else {
2542 format!("'{}'", arg.replace('\'', "\\'"))
2543 }
2544 };
2545 let mut quoted_strings = vec!["jj".to_string()];
2546 quoted_strings.extend(string_args.iter().skip(1).map(shell_escape));
2547 tx.set_tag("args".to_string(), quoted_strings.join(" "));
2548 tx
2549}
2550
2551fn update_stale_working_copy(
2552 mut locked_ws: LockedWorkspace,
2553 op_id: OperationId,
2554 stale_commit: &Commit,
2555 new_commit: &Commit,
2556) -> Result<CheckoutStats, CommandError> {
2557 if stale_commit.tree_id() != locked_ws.locked_wc().old_tree_id() {
2560 return Err(user_error("Concurrent working copy operation. Try again."));
2561 }
2562 let stats = locked_ws.locked_wc().check_out(new_commit).map_err(|err| {
2563 internal_error_with_message(
2564 format!("Failed to check out commit {}", new_commit.id().hex()),
2565 err,
2566 )
2567 })?;
2568 locked_ws.finish(op_id)?;
2569
2570 Ok(stats)
2571}
2572
2573pub fn print_updated_commits<'a>(
2576 formatter: &mut dyn Formatter,
2577 template: &TemplateRenderer<Commit>,
2578 commits: impl IntoIterator<Item = &'a Commit>,
2579) -> io::Result<()> {
2580 let mut commits = commits.into_iter().fuse();
2581 for commit in commits.by_ref().take(10) {
2582 write!(formatter, " ")?;
2583 template.format(commit, formatter)?;
2584 writeln!(formatter)?;
2585 }
2586 if commits.next().is_some() {
2587 writeln!(formatter, " ...")?;
2588 }
2589 Ok(())
2590}
2591
2592#[instrument(skip_all)]
2593pub fn print_conflicted_paths(
2594 conflicts: Vec<(RepoPathBuf, BackendResult<MergedTreeValue>)>,
2595 formatter: &mut dyn Formatter,
2596 workspace_command: &WorkspaceCommandHelper,
2597) -> Result<(), CommandError> {
2598 let formatted_paths = conflicts
2599 .iter()
2600 .map(|(path, _conflict)| workspace_command.format_file_path(path))
2601 .collect_vec();
2602 let max_path_len = formatted_paths.iter().map(|p| p.len()).max().unwrap_or(0);
2603 let formatted_paths = formatted_paths
2604 .into_iter()
2605 .map(|p| format!("{:width$}", p, width = max_path_len.min(32) + 3));
2606
2607 for ((_, conflict), formatted_path) in std::iter::zip(conflicts, formatted_paths) {
2608 let conflict = conflict?.simplify();
2611 let sides = conflict.num_sides();
2612 let n_adds = conflict.adds().flatten().count();
2613 let deletions = sides - n_adds;
2614
2615 let mut seen_objects = BTreeMap::new(); if deletions > 0 {
2617 seen_objects.insert(
2618 format!(
2619 "{deletions} deletion{}",
2621 if deletions > 1 { "s" } else { "" }
2622 ),
2623 "normal", );
2625 }
2626 for term in itertools::chain(conflict.removes(), conflict.adds()).flatten() {
2630 seen_objects.insert(
2631 match term {
2632 TreeValue::File {
2633 executable: false, ..
2634 } => continue,
2635 TreeValue::File {
2636 executable: true, ..
2637 } => "an executable",
2638 TreeValue::Symlink(_) => "a symlink",
2639 TreeValue::Tree(_) => "a directory",
2640 TreeValue::GitSubmodule(_) => "a git submodule",
2641 TreeValue::Conflict(_) => "another conflict (you found a bug!)",
2642 }
2643 .to_string(),
2644 "difficult",
2645 );
2646 }
2647
2648 write!(formatter, "{formatted_path} ")?;
2649 formatter.with_label("conflict_description", |formatter| {
2650 let print_pair = |formatter: &mut dyn Formatter, (text, label): &(String, &str)| {
2651 write!(formatter.labeled(label), "{text}")
2652 };
2653 print_pair(
2654 formatter,
2655 &(
2656 format!("{sides}-sided"),
2657 if sides > 2 { "difficult" } else { "normal" },
2658 ),
2659 )?;
2660 write!(formatter, " conflict")?;
2661
2662 if !seen_objects.is_empty() {
2663 write!(formatter, " including ")?;
2664 let seen_objects = seen_objects.into_iter().collect_vec();
2665 match &seen_objects[..] {
2666 [] => unreachable!(),
2667 [only] => print_pair(formatter, only)?,
2668 [first, middle @ .., last] => {
2669 print_pair(formatter, first)?;
2670 for pair in middle {
2671 write!(formatter, ", ")?;
2672 print_pair(formatter, pair)?;
2673 }
2674 write!(formatter, " and ")?;
2675 print_pair(formatter, last)?;
2676 }
2677 };
2678 }
2679 io::Result::Ok(())
2680 })?;
2681 writeln!(formatter)?;
2682 }
2683 Ok(())
2684}
2685
2686fn build_untracked_reason_message(reason: &UntrackedReason) -> Option<String> {
2688 match reason {
2689 UntrackedReason::FileTooLarge { size, max_size } => {
2690 let size_approx = HumanByteSize(*size);
2693 let max_size_approx = HumanByteSize(*max_size);
2694 Some(format!(
2695 "{size_approx} ({size} bytes); the maximum size allowed is {max_size_approx} \
2696 ({max_size} bytes)",
2697 ))
2698 }
2699 UntrackedReason::FileNotAutoTracked => None,
2703 }
2704}
2705
2706pub fn print_untracked_files(
2708 ui: &Ui,
2709 untracked_paths: &BTreeMap<RepoPathBuf, UntrackedReason>,
2710 path_converter: &RepoPathUiConverter,
2711) -> io::Result<()> {
2712 let mut untracked_paths = untracked_paths
2713 .iter()
2714 .filter_map(|(path, reason)| build_untracked_reason_message(reason).map(|m| (path, m)))
2715 .peekable();
2716
2717 if untracked_paths.peek().is_some() {
2718 writeln!(ui.warning_default(), "Refused to snapshot some files:")?;
2719 let mut formatter = ui.stderr_formatter();
2720 for (path, message) in untracked_paths {
2721 let ui_path = path_converter.format_file_path(path);
2722 writeln!(formatter, " {ui_path}: {message}")?;
2723 }
2724 }
2725
2726 Ok(())
2727}
2728
2729pub fn print_snapshot_stats(
2730 ui: &Ui,
2731 stats: &SnapshotStats,
2732 path_converter: &RepoPathUiConverter,
2733) -> io::Result<()> {
2734 print_untracked_files(ui, &stats.untracked_paths, path_converter)?;
2735
2736 let large_files_sizes = stats
2737 .untracked_paths
2738 .values()
2739 .filter_map(|reason| match reason {
2740 UntrackedReason::FileTooLarge { size, .. } => Some(size),
2741 UntrackedReason::FileNotAutoTracked => None,
2742 });
2743 if let Some(size) = large_files_sizes.max() {
2744 writedoc!(
2745 ui.hint_default(),
2746 r"
2747 This is to prevent large files from being added by accident. You can fix this by:
2748 - Adding the file to `.gitignore`
2749 - Run `jj config set --repo snapshot.max-new-file-size {size}`
2750 This will increase the maximum file size allowed for new files, in this repository only.
2751 - Run `jj --config snapshot.max-new-file-size={size} st`
2752 This will increase the maximum file size allowed for new files, for this command only.
2753 "
2754 )?;
2755 }
2756 Ok(())
2757}
2758
2759pub fn print_checkout_stats(
2760 ui: &Ui,
2761 stats: &CheckoutStats,
2762 new_commit: &Commit,
2763) -> Result<(), std::io::Error> {
2764 if stats.added_files > 0 || stats.updated_files > 0 || stats.removed_files > 0 {
2765 writeln!(
2766 ui.status(),
2767 "Added {} files, modified {} files, removed {} files",
2768 stats.added_files,
2769 stats.updated_files,
2770 stats.removed_files
2771 )?;
2772 }
2773 if stats.skipped_files != 0 {
2774 writeln!(
2775 ui.warning_default(),
2776 "{} of those updates were skipped because there were conflicting changes in the \
2777 working copy.",
2778 stats.skipped_files
2779 )?;
2780 writeln!(
2781 ui.hint_default(),
2782 "Inspect the changes compared to the intended target with `jj diff --from {}`.
2783Discard the conflicting changes with `jj restore --from {}`.",
2784 short_commit_hash(new_commit.id()),
2785 short_commit_hash(new_commit.id())
2786 )?;
2787 }
2788 Ok(())
2789}
2790
2791pub fn print_unmatched_explicit_paths<'a>(
2794 ui: &Ui,
2795 workspace_command: &WorkspaceCommandHelper,
2796 expression: &FilesetExpression,
2797 trees: impl IntoIterator<Item = &'a MergedTree>,
2798) -> io::Result<()> {
2799 let mut explicit_paths = expression.explicit_paths().collect_vec();
2800 for tree in trees {
2801 explicit_paths.retain(|&path| tree.path_value(path).unwrap().is_absent());
2803 if explicit_paths.is_empty() {
2804 return Ok(());
2805 }
2806 }
2807 let ui_paths = explicit_paths
2808 .iter()
2809 .map(|&path| workspace_command.format_file_path(path))
2810 .join(", ");
2811 writeln!(
2812 ui.warning_default(),
2813 "No matching entries for paths: {ui_paths}"
2814 )?;
2815 Ok(())
2816}
2817
2818pub fn update_working_copy(
2819 repo: &Arc<ReadonlyRepo>,
2820 workspace: &mut Workspace,
2821 old_commit: Option<&Commit>,
2822 new_commit: &Commit,
2823) -> Result<CheckoutStats, CommandError> {
2824 let old_tree_id = old_commit.map(|commit| commit.tree_id().clone());
2825 let stats = workspace
2828 .check_out(repo.op_id().clone(), old_tree_id.as_ref(), new_commit)
2829 .map_err(|err| {
2830 internal_error_with_message(
2831 format!("Failed to check out commit {}", new_commit.id().hex()),
2832 err,
2833 )
2834 })?;
2835 Ok(stats)
2836}
2837
2838pub fn has_tracked_remote_bookmarks(view: &View, bookmark: &RefName) -> bool {
2841 view.remote_bookmarks_matching(
2842 &StringPattern::exact(bookmark),
2843 &StringPattern::everything(),
2844 )
2845 .filter(|&(symbol, _)| !jj_lib::git::is_special_git_remote(symbol.remote))
2846 .any(|(_, remote_ref)| remote_ref.is_tracked())
2847}
2848
2849pub fn load_template_aliases(
2850 ui: &Ui,
2851 stacked_config: &StackedConfig,
2852) -> Result<TemplateAliasesMap, CommandError> {
2853 let table_name = ConfigNamePathBuf::from_iter(["template-aliases"]);
2854 let mut aliases_map = TemplateAliasesMap::new();
2855 for layer in stacked_config.layers() {
2858 let table = match layer.look_up_table(&table_name) {
2859 Ok(Some(table)) => table,
2860 Ok(None) => continue,
2861 Err(item) => {
2862 return Err(ConfigGetError::Type {
2863 name: table_name.to_string(),
2864 error: format!("Expected a table, but is {}", item.type_name()).into(),
2865 source_path: layer.path.clone(),
2866 }
2867 .into());
2868 }
2869 };
2870 for (decl, item) in table.iter() {
2871 let r = item
2872 .as_str()
2873 .ok_or_else(|| format!("Expected a string, but is {}", item.type_name()))
2874 .and_then(|v| aliases_map.insert(decl, v).map_err(|e| e.to_string()));
2875 if let Err(s) = r {
2876 writeln!(
2877 ui.warning_default(),
2878 "Failed to load `{table_name}.{decl}`: {s}"
2879 )?;
2880 }
2881 }
2882 }
2883 Ok(aliases_map)
2884}
2885
2886#[derive(Clone, Debug)]
2888pub struct LogContentFormat {
2889 width: usize,
2890 word_wrap: bool,
2891}
2892
2893impl LogContentFormat {
2894 pub fn new(ui: &Ui, settings: &UserSettings) -> Result<Self, ConfigGetError> {
2896 Ok(Self {
2897 width: ui.term_width(),
2898 word_wrap: settings.get_bool("ui.log-word-wrap")?,
2899 })
2900 }
2901
2902 #[must_use]
2904 pub fn sub_width(&self, width: usize) -> Self {
2905 Self {
2906 width: self.width.saturating_sub(width),
2907 word_wrap: self.word_wrap,
2908 }
2909 }
2910
2911 pub fn width(&self) -> usize {
2913 self.width
2914 }
2915
2916 pub fn write<E: From<io::Error>>(
2918 &self,
2919 formatter: &mut dyn Formatter,
2920 content_fn: impl FnOnce(&mut dyn Formatter) -> Result<(), E>,
2921 ) -> Result<(), E> {
2922 if self.word_wrap {
2923 let mut recorder = FormatRecorder::new();
2924 content_fn(&mut recorder)?;
2925 text_util::write_wrapped(formatter, &recorder, self.width)?;
2926 } else {
2927 content_fn(formatter)?;
2928 }
2929 Ok(())
2930 }
2931}
2932
2933pub fn short_commit_hash(commit_id: &CommitId) -> String {
2934 format!("{commit_id:.12}")
2935}
2936
2937pub fn short_change_hash(change_id: &ChangeId) -> String {
2938 format!("{change_id:.12}")
2939}
2940
2941pub fn short_operation_hash(operation_id: &OperationId) -> String {
2942 format!("{operation_id:.12}")
2943}
2944
2945#[derive(Clone, Debug)]
2947pub enum DiffSelector {
2948 NonInteractive,
2949 Interactive(DiffEditor),
2950}
2951
2952impl DiffSelector {
2953 pub fn is_interactive(&self) -> bool {
2954 matches!(self, Self::Interactive(_))
2955 }
2956
2957 pub fn select(
2962 &self,
2963 left_tree: &MergedTree,
2964 right_tree: &MergedTree,
2965 matcher: &dyn Matcher,
2966 format_instructions: impl FnOnce() -> String,
2967 ) -> Result<MergedTreeId, CommandError> {
2968 let selected_tree_id = restore_tree(right_tree, left_tree, matcher).block_on()?;
2969 match self {
2970 Self::NonInteractive => Ok(selected_tree_id),
2971 Self::Interactive(editor) => {
2972 let right_tree = right_tree.store().get_root_tree(&selected_tree_id)?;
2976 Ok(editor.edit(left_tree, &right_tree, matcher, format_instructions)?)
2977 }
2978 }
2979 }
2980}
2981
2982#[derive(Clone, Debug)]
2983pub struct RemoteBookmarkNamePattern {
2984 pub bookmark: StringPattern,
2985 pub remote: StringPattern,
2986}
2987
2988impl FromStr for RemoteBookmarkNamePattern {
2989 type Err = String;
2990
2991 fn from_str(src: &str) -> Result<Self, Self::Err> {
2992 let (maybe_kind, pat) = src
2997 .split_once(':')
2998 .map_or((None, src), |(kind, pat)| (Some(kind), pat));
2999 let to_pattern = |pat: &str| {
3000 if let Some(kind) = maybe_kind {
3001 StringPattern::from_str_kind(pat, kind).map_err(|err| err.to_string())
3002 } else {
3003 Ok(StringPattern::exact(pat))
3004 }
3005 };
3006 let (bookmark, remote) = pat.rsplit_once('@').ok_or_else(|| {
3008 "remote bookmark must be specified in bookmark@remote form".to_owned()
3009 })?;
3010 Ok(Self {
3011 bookmark: to_pattern(bookmark)?,
3012 remote: to_pattern(remote)?,
3013 })
3014 }
3015}
3016
3017impl RemoteBookmarkNamePattern {
3018 pub fn is_exact(&self) -> bool {
3019 self.bookmark.is_exact() && self.remote.is_exact()
3020 }
3021}
3022
3023impl fmt::Display for RemoteBookmarkNamePattern {
3024 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3025 let Self { bookmark, remote } = self;
3028 write!(f, "{bookmark}@{remote}")
3029 }
3030}
3031
3032pub fn compute_commit_location(
3037 ui: &Ui,
3038 workspace_command: &WorkspaceCommandHelper,
3039 destination: Option<&[RevisionArg]>,
3040 insert_after: Option<&[RevisionArg]>,
3041 insert_before: Option<&[RevisionArg]>,
3042 commit_type: &str,
3043) -> Result<(Vec<CommitId>, Vec<CommitId>), CommandError> {
3044 let resolve_revisions =
3045 |revisions: Option<&[RevisionArg]>| -> Result<Option<Vec<CommitId>>, CommandError> {
3046 if let Some(revisions) = revisions {
3047 Ok(Some(
3048 workspace_command
3049 .resolve_some_revsets_default_single(ui, revisions)?
3050 .into_iter()
3051 .collect_vec(),
3052 ))
3053 } else {
3054 Ok(None)
3055 }
3056 };
3057 let destination_commit_ids = resolve_revisions(destination)?;
3058 let after_commit_ids = resolve_revisions(insert_after)?;
3059 let before_commit_ids = resolve_revisions(insert_before)?;
3060
3061 let (new_parent_ids, new_child_ids) =
3062 match (destination_commit_ids, after_commit_ids, before_commit_ids) {
3063 (Some(destination_commit_ids), None, None) => (destination_commit_ids, vec![]),
3064 (None, Some(after_commit_ids), Some(before_commit_ids)) => {
3065 (after_commit_ids, before_commit_ids)
3066 }
3067 (None, Some(after_commit_ids), None) => {
3068 let new_child_ids: Vec<_> = RevsetExpression::commits(after_commit_ids.clone())
3069 .children()
3070 .evaluate(workspace_command.repo().as_ref())?
3071 .iter()
3072 .try_collect()?;
3073
3074 (after_commit_ids, new_child_ids)
3075 }
3076 (None, None, Some(before_commit_ids)) => {
3077 let before_commits: Vec<_> = before_commit_ids
3078 .iter()
3079 .map(|id| workspace_command.repo().store().get_commit(id))
3080 .try_collect()?;
3081 let new_parent_ids = before_commits
3084 .iter()
3085 .flat_map(|commit| commit.parent_ids())
3086 .unique()
3087 .cloned()
3088 .collect_vec();
3089
3090 (new_parent_ids, before_commit_ids)
3091 }
3092 (Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
3093 panic!("destination cannot be used with insert_after/insert_before")
3094 }
3095 (None, None, None) => {
3096 panic!("expected at least one of destination or insert_after/insert_before")
3097 }
3098 };
3099
3100 if !new_child_ids.is_empty() {
3101 workspace_command.check_rewritable(new_child_ids.iter())?;
3102 ensure_no_commit_loop(
3103 workspace_command.repo().as_ref(),
3104 &RevsetExpression::commits(new_child_ids.clone()),
3105 &RevsetExpression::commits(new_parent_ids.clone()),
3106 commit_type,
3107 )?;
3108 }
3109
3110 Ok((new_parent_ids, new_child_ids))
3111}
3112
3113fn ensure_no_commit_loop(
3116 repo: &ReadonlyRepo,
3117 children_expression: &Rc<ResolvedRevsetExpression>,
3118 parents_expression: &Rc<ResolvedRevsetExpression>,
3119 commit_type: &str,
3120) -> Result<(), CommandError> {
3121 if let Some(commit_id) = children_expression
3122 .dag_range_to(parents_expression)
3123 .evaluate(repo)?
3124 .iter()
3125 .next()
3126 {
3127 let commit_id = commit_id?;
3128 return Err(user_error(format!(
3129 "Refusing to create a loop: commit {} would be both an ancestor and a descendant of \
3130 the {commit_type}",
3131 short_commit_hash(&commit_id),
3132 )));
3133 }
3134 Ok(())
3135}
3136
3137#[derive(clap::Parser, Clone, Debug)]
3144#[command(name = "jj")]
3145pub struct Args {
3146 #[command(flatten)]
3147 pub global_args: GlobalArgs,
3148}
3149
3150#[derive(clap::Args, Clone, Debug)]
3151#[command(next_help_heading = "Global Options")]
3152pub struct GlobalArgs {
3153 #[arg(long, short = 'R', global = true, value_hint = clap::ValueHint::DirPath)]
3158 pub repository: Option<String>,
3159 #[arg(long, global = true)]
3172 pub ignore_working_copy: bool,
3173 #[arg(long, global = true)]
3182 pub ignore_immutable: bool,
3183 #[arg(
3206 long,
3207 visible_alias = "at-op",
3208 global = true,
3209 add = ArgValueCandidates::new(complete::operations),
3210 )]
3211 pub at_operation: Option<String>,
3212 #[arg(long, global = true)]
3214 pub debug: bool,
3215
3216 #[command(flatten)]
3217 pub early_args: EarlyArgs,
3218}
3219
3220#[derive(clap::Args, Clone, Debug)]
3221pub struct EarlyArgs {
3222 #[arg(long, value_name = "WHEN", global = true)]
3224 pub color: Option<ColorChoice>,
3225 #[arg(long, global = true, action = ArgAction::SetTrue)]
3232 pub quiet: Option<bool>,
3235 #[arg(long, global = true, action = ArgAction::SetTrue)]
3237 pub no_pager: Option<bool>,
3240 #[arg(long, value_name = "NAME=VALUE", global = true, add = ArgValueCompleter::new(complete::leaf_config_key_value))]
3246 pub config: Vec<String>,
3247 #[arg(long, value_name = "PATH", global = true, value_hint = clap::ValueHint::FilePath)]
3249 pub config_file: Vec<String>,
3250}
3251
3252impl EarlyArgs {
3253 pub(crate) fn merged_config_args(&self, matches: &ArgMatches) -> Vec<(ConfigArgKind, &str)> {
3254 merge_args_with(
3255 matches,
3256 &[("config", &self.config), ("config_file", &self.config_file)],
3257 |id, value| match id {
3258 "config" => (ConfigArgKind::Item, value.as_ref()),
3259 "config_file" => (ConfigArgKind::File, value.as_ref()),
3260 _ => unreachable!("unexpected id {id:?}"),
3261 },
3262 )
3263 }
3264
3265 fn has_config_args(&self) -> bool {
3266 !self.config.is_empty() || !self.config_file.is_empty()
3267 }
3268}
3269
3270#[derive(Clone, Debug)]
3276pub struct RevisionArg(Cow<'static, str>);
3277
3278impl RevisionArg {
3279 pub const AT: Self = Self(Cow::Borrowed("@"));
3281}
3282
3283impl From<String> for RevisionArg {
3284 fn from(s: String) -> Self {
3285 Self(s.into())
3286 }
3287}
3288
3289impl AsRef<str> for RevisionArg {
3290 fn as_ref(&self) -> &str {
3291 &self.0
3292 }
3293}
3294
3295impl fmt::Display for RevisionArg {
3296 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3297 write!(f, "{}", self.0)
3298 }
3299}
3300
3301impl ValueParserFactory for RevisionArg {
3302 type Parser = MapValueParser<NonEmptyStringValueParser, fn(String) -> Self>;
3303
3304 fn value_parser() -> Self::Parser {
3305 NonEmptyStringValueParser::new().map(Self::from)
3306 }
3307}
3308
3309pub fn merge_args_with<'k, 'v, T, U>(
3317 matches: &ArgMatches,
3318 id_values: &[(&'k str, &'v [T])],
3319 mut convert: impl FnMut(&'k str, &'v T) -> U,
3320) -> Vec<U> {
3321 let mut pos_values: Vec<(usize, U)> = Vec::new();
3322 for (id, values) in id_values {
3323 pos_values.extend(itertools::zip_eq(
3324 matches.indices_of(id).into_iter().flatten(),
3325 values.iter().map(|v| convert(id, v)),
3326 ));
3327 }
3328 pos_values.sort_unstable_by_key(|&(pos, _)| pos);
3329 pos_values.into_iter().map(|(_, value)| value).collect()
3330}
3331
3332fn get_string_or_array(
3333 config: &StackedConfig,
3334 key: &'static str,
3335) -> Result<Vec<String>, ConfigGetError> {
3336 config
3337 .get(key)
3338 .map(|string| vec![string])
3339 .or_else(|_| config.get::<Vec<String>>(key))
3340}
3341
3342fn resolve_default_command(
3343 ui: &Ui,
3344 config: &StackedConfig,
3345 app: &Command,
3346 mut string_args: Vec<String>,
3347) -> Result<Vec<String>, CommandError> {
3348 const PRIORITY_FLAGS: &[&str] = &["--help", "-h", "--version", "-V"];
3349
3350 let has_priority_flag = string_args
3351 .iter()
3352 .any(|arg| PRIORITY_FLAGS.contains(&arg.as_str()));
3353 if has_priority_flag {
3354 return Ok(string_args);
3355 }
3356
3357 let app_clone = app
3358 .clone()
3359 .allow_external_subcommands(true)
3360 .ignore_errors(true);
3361 let matches = app_clone.try_get_matches_from(&string_args).ok();
3362
3363 if let Some(matches) = matches {
3364 if matches.subcommand_name().is_none() {
3365 let args = get_string_or_array(config, "ui.default-command").optional()?;
3366 if args.is_none() {
3367 writeln!(
3368 ui.hint_default(),
3369 "Use `jj -h` for a list of available commands."
3370 )?;
3371 writeln!(
3372 ui.hint_no_heading(),
3373 "Run `jj config set --user ui.default-command log` to disable this message."
3374 )?;
3375 }
3376 let default_command = args.unwrap_or_else(|| vec!["log".to_string()]);
3377
3378 string_args.splice(1..1, default_command);
3380 }
3381 }
3382 Ok(string_args)
3383}
3384
3385fn resolve_aliases(
3386 ui: &Ui,
3387 config: &StackedConfig,
3388 app: &Command,
3389 mut string_args: Vec<String>,
3390) -> Result<Vec<String>, CommandError> {
3391 let defined_aliases: HashSet<_> = config.table_keys("aliases").collect();
3392 let mut resolved_aliases = HashSet::new();
3393 let mut real_commands = HashSet::new();
3394 for command in app.get_subcommands() {
3395 real_commands.insert(command.get_name());
3396 for alias in command.get_all_aliases() {
3397 real_commands.insert(alias);
3398 }
3399 }
3400 for alias in defined_aliases.intersection(&real_commands).sorted() {
3401 writeln!(
3402 ui.warning_default(),
3403 "Cannot define an alias that overrides the built-in command '{alias}'"
3404 )?;
3405 }
3406
3407 loop {
3408 let app_clone = app.clone().allow_external_subcommands(true);
3409 let matches = app_clone.try_get_matches_from(&string_args).ok();
3410 if let Some((command_name, submatches)) = matches.as_ref().and_then(|m| m.subcommand()) {
3411 if !real_commands.contains(command_name) {
3412 let alias_name = command_name.to_string();
3413 let alias_args = submatches
3414 .get_many::<OsString>("")
3415 .unwrap_or_default()
3416 .map(|arg| arg.to_str().unwrap().to_string())
3417 .collect_vec();
3418 if resolved_aliases.contains(&*alias_name) {
3419 return Err(user_error(format!(
3420 "Recursive alias definition involving `{alias_name}`"
3421 )));
3422 }
3423 if let Some(&alias_name) = defined_aliases.get(&*alias_name) {
3424 let alias_definition: Vec<String> = config.get(["aliases", alias_name])?;
3425 assert!(string_args.ends_with(&alias_args));
3426 string_args.truncate(string_args.len() - 1 - alias_args.len());
3427 string_args.extend(alias_definition);
3428 string_args.extend_from_slice(&alias_args);
3429 resolved_aliases.insert(alias_name);
3430 continue;
3431 } else {
3432 return Ok(string_args);
3434 }
3435 }
3436 }
3437 return Ok(string_args);
3439 }
3440}
3441
3442fn parse_early_args(
3444 app: &Command,
3445 args: &[String],
3446) -> Result<(EarlyArgs, Vec<ConfigLayer>), CommandError> {
3447 let early_matches = app
3449 .clone()
3450 .disable_version_flag(true)
3451 .disable_help_flag(true)
3453 .arg(
3455 clap::Arg::new("help")
3456 .short('h')
3457 .long("help")
3458 .global(true)
3459 .action(ArgAction::Count),
3460 )
3461 .ignore_errors(true)
3462 .try_get_matches_from(args)?;
3463 let args = EarlyArgs::from_arg_matches(&early_matches).unwrap();
3464
3465 let mut config_layers = parse_config_args(&args.merged_config_args(&early_matches))?;
3466 let mut layer = ConfigLayer::empty(ConfigSource::CommandArg);
3469 if let Some(choice) = args.color {
3470 layer.set_value("ui.color", choice.to_string()).unwrap();
3471 }
3472 if args.quiet.unwrap_or_default() {
3473 layer.set_value("ui.quiet", true).unwrap();
3474 }
3475 if args.no_pager.unwrap_or_default() {
3476 layer.set_value("ui.paginate", "never").unwrap();
3477 }
3478 if !layer.is_empty() {
3479 config_layers.push(layer);
3480 }
3481 Ok((args, config_layers))
3482}
3483
3484fn handle_shell_completion(
3485 ui: &Ui,
3486 app: &Command,
3487 config: &StackedConfig,
3488 cwd: &Path,
3489) -> Result<(), CommandError> {
3490 let mut orig_args = env::args_os();
3491
3492 let mut args = vec![];
3493 args.extend(orig_args.by_ref().take(2));
3496
3497 if orig_args.len() > 0 {
3501 let complete_index: Option<usize> = env::var("_CLAP_COMPLETE_INDEX")
3502 .ok()
3503 .and_then(|s| s.parse().ok());
3504 let resolved_aliases = if let Some(index) = complete_index {
3505 let pad_len = usize::saturating_sub(index + 1, orig_args.len());
3510 let padded_args = orig_args
3511 .by_ref()
3512 .chain(std::iter::repeat_n(OsString::new(), pad_len));
3513
3514 let mut expanded_args = expand_args(ui, app, padded_args.take(index + 1), config)?;
3516
3517 unsafe {
3521 env::set_var(
3522 "_CLAP_COMPLETE_INDEX",
3523 (expanded_args.len() - 1).to_string(),
3524 );
3525 }
3526
3527 let split_off_padding = expanded_args.split_off(expanded_args.len() - pad_len);
3530 assert!(
3531 split_off_padding.iter().all(|s| s.is_empty()),
3532 "split-off padding should only consist of empty strings but was \
3533 {split_off_padding:?}",
3534 );
3535
3536 expanded_args.extend(to_string_args(orig_args)?);
3538 expanded_args
3539 } else {
3540 expand_args(ui, app, orig_args, config)?
3541 };
3542 args.extend(resolved_aliases.into_iter().map(OsString::from));
3543 }
3544 let ran_completion = clap_complete::CompleteEnv::with_factory(|| {
3545 app.clone()
3546 .allow_external_subcommands(true)
3548 })
3549 .try_complete(args.iter(), Some(cwd))?;
3550 assert!(
3551 ran_completion,
3552 "This function should not be called without the COMPLETE variable set."
3553 );
3554 Ok(())
3555}
3556
3557pub fn expand_args(
3558 ui: &Ui,
3559 app: &Command,
3560 args_os: impl IntoIterator<Item = OsString>,
3561 config: &StackedConfig,
3562) -> Result<Vec<String>, CommandError> {
3563 let string_args = to_string_args(args_os)?;
3564 let string_args = resolve_default_command(ui, config, app, string_args)?;
3565 resolve_aliases(ui, config, app, string_args)
3566}
3567
3568fn to_string_args(
3569 args_os: impl IntoIterator<Item = OsString>,
3570) -> Result<Vec<String>, CommandError> {
3571 args_os
3572 .into_iter()
3573 .map(|arg_os| {
3574 arg_os
3575 .into_string()
3576 .map_err(|_| cli_error("Non-UTF-8 argument"))
3577 })
3578 .collect()
3579}
3580
3581fn parse_args(app: &Command, string_args: &[String]) -> Result<(ArgMatches, Args), clap::Error> {
3582 let matches = app
3583 .clone()
3584 .arg_required_else_help(true)
3585 .subcommand_required(true)
3586 .try_get_matches_from(string_args)?;
3587 let args = Args::from_arg_matches(&matches).unwrap();
3588 Ok((matches, args))
3589}
3590
3591fn command_name(mut matches: &ArgMatches) -> String {
3592 let mut command = String::new();
3593 while let Some((subcommand, new_matches)) = matches.subcommand() {
3594 if !command.is_empty() {
3595 command.push(' ');
3596 }
3597 command.push_str(subcommand);
3598 matches = new_matches;
3599 }
3600 command
3601}
3602
3603pub fn format_template<C: Clone>(ui: &Ui, arg: &C, template: &TemplateRenderer<C>) -> String {
3604 let mut output = vec![];
3605 template
3606 .format(arg, ui.new_formatter(&mut output).as_mut())
3607 .expect("write() to vec backed formatter should never fail");
3608 output.into_string_lossy()
3610}
3611
3612#[must_use]
3614pub struct CliRunner<'a> {
3615 tracing_subscription: TracingSubscription,
3616 app: Command,
3617 config_layers: Vec<ConfigLayer>,
3618 config_migrations: Vec<ConfigMigrationRule>,
3619 store_factories: StoreFactories,
3620 working_copy_factories: WorkingCopyFactories,
3621 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
3622 revset_extensions: RevsetExtensions,
3623 commit_template_extensions: Vec<Arc<dyn CommitTemplateLanguageExtension>>,
3624 operation_template_extensions: Vec<Arc<dyn OperationTemplateLanguageExtension>>,
3625 dispatch_fn: CliDispatchFn<'a>,
3626 dispatch_hook_fns: Vec<CliDispatchHookFn<'a>>,
3627 process_global_args_fns: Vec<ProcessGlobalArgsFn<'a>>,
3628}
3629
3630pub type CliDispatchFn<'a> =
3631 Box<dyn FnOnce(&mut Ui, &CommandHelper) -> Result<(), CommandError> + 'a>;
3632
3633type CliDispatchHookFn<'a> =
3634 Box<dyn FnOnce(&mut Ui, &CommandHelper, CliDispatchFn<'a>) -> Result<(), CommandError> + 'a>;
3635
3636type ProcessGlobalArgsFn<'a> =
3637 Box<dyn FnOnce(&mut Ui, &ArgMatches) -> Result<(), CommandError> + 'a>;
3638
3639impl<'a> CliRunner<'a> {
3640 pub fn init() -> Self {
3643 let tracing_subscription = TracingSubscription::init();
3644 crate::cleanup_guard::init();
3645 Self {
3646 tracing_subscription,
3647 app: crate::commands::default_app(),
3648 config_layers: crate::config::default_config_layers(),
3649 config_migrations: crate::config::default_config_migrations(),
3650 store_factories: StoreFactories::default(),
3651 working_copy_factories: default_working_copy_factories(),
3652 workspace_loader_factory: Box::new(DefaultWorkspaceLoaderFactory),
3653 revset_extensions: Default::default(),
3654 commit_template_extensions: vec![],
3655 operation_template_extensions: vec![],
3656 dispatch_fn: Box::new(crate::commands::run_command),
3657 dispatch_hook_fns: vec![],
3658 process_global_args_fns: vec![],
3659 }
3660 }
3661
3662 pub fn name(mut self, name: &str) -> Self {
3664 self.app = self.app.name(name.to_string());
3665 self
3666 }
3667
3668 pub fn about(mut self, about: &str) -> Self {
3670 self.app = self.app.about(about.to_string());
3671 self
3672 }
3673
3674 pub fn version(mut self, version: &str) -> Self {
3676 self.app = self.app.version(version.to_string());
3677 self
3678 }
3679
3680 pub fn add_extra_config(mut self, layer: ConfigLayer) -> Self {
3685 assert_eq!(layer.source, ConfigSource::Default);
3686 self.config_layers.push(layer);
3687 self
3688 }
3689
3690 pub fn add_extra_config_migration(mut self, rule: ConfigMigrationRule) -> Self {
3692 self.config_migrations.push(rule);
3693 self
3694 }
3695
3696 pub fn add_store_factories(mut self, store_factories: StoreFactories) -> Self {
3698 self.store_factories.merge(store_factories);
3699 self
3700 }
3701
3702 pub fn add_working_copy_factories(
3704 mut self,
3705 working_copy_factories: WorkingCopyFactories,
3706 ) -> Self {
3707 merge_factories_map(&mut self.working_copy_factories, working_copy_factories);
3708 self
3709 }
3710
3711 pub fn set_workspace_loader_factory(
3712 mut self,
3713 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
3714 ) -> Self {
3715 self.workspace_loader_factory = workspace_loader_factory;
3716 self
3717 }
3718
3719 pub fn add_symbol_resolver_extension(
3720 mut self,
3721 symbol_resolver: Box<dyn SymbolResolverExtension>,
3722 ) -> Self {
3723 self.revset_extensions.add_symbol_resolver(symbol_resolver);
3724 self
3725 }
3726
3727 pub fn add_revset_function_extension(
3728 mut self,
3729 name: &'static str,
3730 func: RevsetFunction,
3731 ) -> Self {
3732 self.revset_extensions.add_custom_function(name, func);
3733 self
3734 }
3735
3736 pub fn add_commit_template_extension(
3737 mut self,
3738 commit_template_extension: Box<dyn CommitTemplateLanguageExtension>,
3739 ) -> Self {
3740 self.commit_template_extensions
3741 .push(commit_template_extension.into());
3742 self
3743 }
3744
3745 pub fn add_operation_template_extension(
3746 mut self,
3747 operation_template_extension: Box<dyn OperationTemplateLanguageExtension>,
3748 ) -> Self {
3749 self.operation_template_extensions
3750 .push(operation_template_extension.into());
3751 self
3752 }
3753
3754 pub fn add_dispatch_hook<F>(mut self, dispatch_hook_fn: F) -> Self
3758 where
3759 F: FnOnce(&mut Ui, &CommandHelper, CliDispatchFn) -> Result<(), CommandError> + 'a,
3760 {
3761 self.dispatch_hook_fns.push(Box::new(dispatch_hook_fn));
3762 self
3763 }
3764
3765 pub fn add_subcommand<C, F>(mut self, custom_dispatch_fn: F) -> Self
3767 where
3768 C: clap::Subcommand,
3769 F: FnOnce(&mut Ui, &CommandHelper, C) -> Result<(), CommandError> + 'a,
3770 {
3771 let old_dispatch_fn = self.dispatch_fn;
3772 let new_dispatch_fn =
3773 move |ui: &mut Ui, command_helper: &CommandHelper| match C::from_arg_matches(
3774 command_helper.matches(),
3775 ) {
3776 Ok(command) => custom_dispatch_fn(ui, command_helper, command),
3777 Err(_) => old_dispatch_fn(ui, command_helper),
3778 };
3779 self.app = C::augment_subcommands(self.app);
3780 self.dispatch_fn = Box::new(new_dispatch_fn);
3781 self
3782 }
3783
3784 pub fn add_global_args<A, F>(mut self, process_before: F) -> Self
3786 where
3787 A: clap::Args,
3788 F: FnOnce(&mut Ui, A) -> Result<(), CommandError> + 'a,
3789 {
3790 let process_global_args_fn = move |ui: &mut Ui, matches: &ArgMatches| {
3791 let custom_args = A::from_arg_matches(matches).unwrap();
3792 process_before(ui, custom_args)
3793 };
3794 self.app = A::augment_args(self.app);
3795 self.process_global_args_fns
3796 .push(Box::new(process_global_args_fn));
3797 self
3798 }
3799
3800 #[instrument(skip_all)]
3801 fn run_internal(self, ui: &mut Ui, mut raw_config: RawConfig) -> Result<(), CommandError> {
3802 let cwd = env::current_dir()
3805 .and_then(dunce::canonicalize)
3806 .map_err(|_| {
3807 user_error_with_hint(
3808 "Could not determine current directory",
3809 "Did you update to a commit where the directory doesn't exist?",
3810 )
3811 })?;
3812 let mut config_env = ConfigEnv::from_environment(ui);
3813 let mut last_config_migration_descriptions = Vec::new();
3814 let mut migrate_config = |config: &mut StackedConfig| -> Result<(), CommandError> {
3815 last_config_migration_descriptions =
3816 jj_lib::config::migrate(config, &self.config_migrations)?;
3817 Ok(())
3818 };
3819 let maybe_cwd_workspace_loader = self
3823 .workspace_loader_factory
3824 .create(find_workspace_dir(&cwd))
3825 .map_err(|err| map_workspace_load_error(err, Some(".")));
3826 config_env.reload_user_config(&mut raw_config)?;
3827 if let Ok(loader) = &maybe_cwd_workspace_loader {
3828 config_env.reset_repo_path(loader.repo_path());
3829 config_env.reload_repo_config(&mut raw_config)?;
3830 }
3831 let mut config = config_env.resolve_config(&raw_config)?;
3832 migrate_config(&mut config)?;
3833 ui.reset(&config)?;
3834
3835 if env::var_os("COMPLETE").is_some() {
3836 return handle_shell_completion(&Ui::null(), &self.app, &config, &cwd);
3837 }
3838
3839 let string_args = expand_args(ui, &self.app, env::args_os(), &config)?;
3840 let (args, config_layers) = parse_early_args(&self.app, &string_args)?;
3841 if !config_layers.is_empty() {
3842 raw_config.as_mut().extend_layers(config_layers);
3843 config = config_env.resolve_config(&raw_config)?;
3844 migrate_config(&mut config)?;
3845 ui.reset(&config)?;
3846 }
3847
3848 if args.has_config_args() {
3849 warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
3850 }
3851
3852 let (matches, args) = parse_args(&self.app, &string_args)
3853 .map_err(|err| map_clap_cli_error(err, ui, &config))?;
3854 if args.global_args.debug {
3855 self.tracing_subscription.enable_debug_logging()?;
3857 }
3858 for process_global_args_fn in self.process_global_args_fns {
3859 process_global_args_fn(ui, &matches)?;
3860 }
3861 config_env.set_command_name(command_name(&matches));
3862
3863 let maybe_workspace_loader = if let Some(path) = &args.global_args.repository {
3864 let abs_path = cwd.join(path);
3866 let abs_path = dunce::canonicalize(&abs_path).unwrap_or(abs_path);
3867 let loader = self
3869 .workspace_loader_factory
3870 .create(&abs_path)
3871 .map_err(|err| map_workspace_load_error(err, Some(path)))?;
3872 config_env.reset_repo_path(loader.repo_path());
3873 config_env.reload_repo_config(&mut raw_config)?;
3874 Ok(loader)
3875 } else {
3876 maybe_cwd_workspace_loader
3877 };
3878
3879 config = config_env.resolve_config(&raw_config)?;
3881 migrate_config(&mut config)?;
3882 ui.reset(&config)?;
3883
3884 for (source, desc) in &last_config_migration_descriptions {
3886 let source_str = match source {
3887 ConfigSource::Default => "default-provided",
3888 ConfigSource::EnvBase | ConfigSource::EnvOverrides => "environment-provided",
3889 ConfigSource::User => "user-level",
3890 ConfigSource::Repo => "repo-level",
3891 ConfigSource::CommandArg => "CLI-provided",
3892 };
3893 writeln!(
3894 ui.warning_default(),
3895 "Deprecated {source_str} config: {desc}"
3896 )?;
3897 }
3898
3899 if args.global_args.repository.is_some() {
3900 warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
3901 }
3902
3903 let settings = UserSettings::from_config(config)?;
3904 let command_helper_data = CommandHelperData {
3905 app: self.app,
3906 cwd,
3907 string_args,
3908 matches,
3909 global_args: args.global_args,
3910 config_env,
3911 config_migrations: self.config_migrations,
3912 raw_config,
3913 settings,
3914 revset_extensions: self.revset_extensions.into(),
3915 commit_template_extensions: self.commit_template_extensions,
3916 operation_template_extensions: self.operation_template_extensions,
3917 maybe_workspace_loader,
3918 store_factories: self.store_factories,
3919 working_copy_factories: self.working_copy_factories,
3920 workspace_loader_factory: self.workspace_loader_factory,
3921 };
3922 let command_helper = CommandHelper {
3923 data: Rc::new(command_helper_data),
3924 };
3925 let dispatch_fn = self.dispatch_hook_fns.into_iter().fold(
3926 self.dispatch_fn,
3927 |old_dispatch_fn, dispatch_hook_fn| {
3928 Box::new(move |ui: &mut Ui, command_helper: &CommandHelper| {
3929 dispatch_hook_fn(ui, command_helper, old_dispatch_fn)
3930 })
3931 },
3932 );
3933 (dispatch_fn)(ui, &command_helper)
3934 }
3935
3936 #[must_use]
3937 #[instrument(skip(self))]
3938 pub fn run(mut self) -> u8 {
3939 crossterm::style::force_color_output(true);
3941 let config = config_from_environment(self.config_layers.drain(..));
3942 let mut ui = Ui::with_config(config.as_ref())
3945 .expect("default config should be valid, env vars are stringly typed");
3946 let result = self.run_internal(&mut ui, config);
3947 let exit_code = handle_command_result(&mut ui, result);
3948 ui.finalize_pager();
3949 exit_code
3950 }
3951}
3952
3953fn map_clap_cli_error(err: clap::Error, ui: &Ui, config: &StackedConfig) -> CommandError {
3954 if let Some(ContextValue::String(cmd)) = err.get(ContextKind::InvalidSubcommand) {
3955 let remove_useless_error_context = |mut err: clap::Error| {
3956 err.remove(ContextKind::SuggestedSubcommand);
3959 err.remove(ContextKind::Suggested); err.remove(ContextKind::Usage); err
3962 };
3963 match cmd.as_str() {
3964 "clone" | "init" => {
3967 let cmd = cmd.clone();
3968 return CommandError::from(remove_useless_error_context(err))
3969 .hinted(format!(
3970 "You probably want `jj git {cmd}`. See also `jj help git`."
3971 ))
3972 .hinted(format!(
3973 r#"You can configure `aliases.{cmd} = ["git", "{cmd}"]` if you want `jj {cmd}` to work and always use the Git backend."#
3974 ));
3975 }
3976 "amend" => {
3977 return CommandError::from(remove_useless_error_context(err))
3978 .hinted(
3979 r#"You probably want `jj squash`. You can configure `aliases.amend = ["squash"]` if you want `jj amend` to work."#);
3980 }
3981 _ => {}
3982 }
3983 }
3984 if let (Some(ContextValue::String(arg)), Some(ContextValue::String(value))) = (
3985 err.get(ContextKind::InvalidArg),
3986 err.get(ContextKind::InvalidValue),
3987 ) {
3988 if arg.as_str() == "--template <TEMPLATE>" && value.is_empty() {
3989 if let Ok(template_aliases) = load_template_aliases(ui, config) {
3991 return CommandError::from(err)
3992 .hinted(format_template_aliases_hint(&template_aliases));
3993 }
3994 }
3995 }
3996 CommandError::from(err)
3997}
3998
3999fn format_template_aliases_hint(template_aliases: &TemplateAliasesMap) -> String {
4000 let mut hint = String::from("The following template aliases are defined:\n");
4001 hint.push_str(
4002 &template_aliases
4003 .symbol_names()
4004 .sorted_unstable()
4005 .map(|name| format!("- {name}"))
4006 .join("\n"),
4007 );
4008 hint
4009}
4010
4011fn warn_if_args_mismatch(
4013 ui: &Ui,
4014 app: &Command,
4015 config: &StackedConfig,
4016 expected_args: &[String],
4017) -> Result<(), CommandError> {
4018 let new_string_args = expand_args(ui, app, env::args_os(), config).ok();
4019 if new_string_args.as_deref() != Some(expected_args) {
4020 writeln!(
4021 ui.warning_default(),
4022 "Command aliases cannot be loaded from -R/--repository path or --config/--config-file \
4023 arguments."
4024 )?;
4025 }
4026 Ok(())
4027}
4028
4029#[cfg(test)]
4030mod tests {
4031 use clap::CommandFactory as _;
4032
4033 use super::*;
4034
4035 #[derive(clap::Parser, Clone, Debug)]
4036 pub struct TestArgs {
4037 #[arg(long)]
4038 pub foo: Vec<u32>,
4039 #[arg(long)]
4040 pub bar: Vec<u32>,
4041 #[arg(long)]
4042 pub baz: bool,
4043 }
4044
4045 #[test]
4046 fn test_merge_args_with() {
4047 let command = TestArgs::command();
4048 let parse = |args: &[&str]| -> Vec<(&'static str, u32)> {
4049 let matches = command.clone().try_get_matches_from(args).unwrap();
4050 let args = TestArgs::from_arg_matches(&matches).unwrap();
4051 merge_args_with(
4052 &matches,
4053 &[("foo", &args.foo), ("bar", &args.bar)],
4054 |id, value| (id, *value),
4055 )
4056 };
4057
4058 assert_eq!(parse(&["jj"]), vec![]);
4059 assert_eq!(parse(&["jj", "--foo=1"]), vec![("foo", 1)]);
4060 assert_eq!(
4061 parse(&["jj", "--foo=1", "--bar=2"]),
4062 vec![("foo", 1), ("bar", 2)]
4063 );
4064 assert_eq!(
4065 parse(&["jj", "--foo=1", "--baz", "--bar=2", "--foo", "3"]),
4066 vec![("foo", 1), ("bar", 2), ("foo", 3)]
4067 );
4068 }
4069}