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