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