1use std::borrow::Cow;
16use std::cell::OnceCell;
17use std::collections::BTreeMap;
18use std::collections::HashMap;
19use std::collections::HashSet;
20use std::env;
21use std::ffi::OsString;
22use std::fmt;
23use std::fmt::Debug;
24use std::io;
25use std::io::Write as _;
26use std::mem;
27use std::path::Path;
28use std::path::PathBuf;
29use std::rc::Rc;
30use std::str::FromStr;
31use std::sync::Arc;
32use std::time::SystemTime;
33
34use bstr::ByteVec as _;
35use chrono::TimeZone as _;
36use clap::ArgAction;
37use clap::ArgMatches;
38use clap::Command;
39use clap::FromArgMatches as _;
40use clap::builder::MapValueParser;
41use clap::builder::NonEmptyStringValueParser;
42use clap::builder::TypedValueParser as _;
43use clap::builder::ValueParserFactory;
44use clap::error::ContextKind;
45use clap::error::ContextValue;
46use clap_complete::ArgValueCandidates;
47use clap_complete::ArgValueCompleter;
48use indexmap::IndexMap;
49use indexmap::IndexSet;
50use indoc::indoc;
51use indoc::writedoc;
52use itertools::Itertools as _;
53use jj_lib::backend::BackendResult;
54use jj_lib::backend::ChangeId;
55use jj_lib::backend::CommitId;
56use jj_lib::backend::TreeValue;
57use jj_lib::commit::Commit;
58use jj_lib::config::ConfigGetError;
59use jj_lib::config::ConfigGetResultExt as _;
60use jj_lib::config::ConfigLayer;
61use jj_lib::config::ConfigMigrationRule;
62use jj_lib::config::ConfigNamePathBuf;
63use jj_lib::config::ConfigSource;
64use jj_lib::config::StackedConfig;
65use jj_lib::conflicts::ConflictMarkerStyle;
66use jj_lib::fileset;
67use jj_lib::fileset::FilesetDiagnostics;
68use jj_lib::fileset::FilesetExpression;
69use jj_lib::gitignore::GitIgnoreError;
70use jj_lib::gitignore::GitIgnoreFile;
71use jj_lib::id_prefix::IdPrefixContext;
72use jj_lib::lock::FileLock;
73use jj_lib::matchers::Matcher;
74use jj_lib::matchers::NothingMatcher;
75use jj_lib::merge::Diff;
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::RemoteName;
89use jj_lib::ref_name::RemoteRefSymbol;
90use jj_lib::ref_name::WorkspaceName;
91use jj_lib::ref_name::WorkspaceNameBuf;
92use jj_lib::repo::CheckOutCommitError;
93use jj_lib::repo::EditCommitError;
94use jj_lib::repo::MutableRepo;
95use jj_lib::repo::ReadonlyRepo;
96use jj_lib::repo::Repo;
97use jj_lib::repo::RepoLoader;
98use jj_lib::repo::StoreFactories;
99use jj_lib::repo::StoreLoadError;
100use jj_lib::repo::merge_factories_map;
101use jj_lib::repo_path::RepoPath;
102use jj_lib::repo_path::RepoPathBuf;
103use jj_lib::repo_path::RepoPathUiConverter;
104use jj_lib::repo_path::UiPathParseError;
105use jj_lib::revset;
106use jj_lib::revset::ResolvedRevsetExpression;
107use jj_lib::revset::RevsetAliasesMap;
108use jj_lib::revset::RevsetDiagnostics;
109use jj_lib::revset::RevsetExpression;
110use jj_lib::revset::RevsetExtensions;
111use jj_lib::revset::RevsetFilterPredicate;
112use jj_lib::revset::RevsetFunction;
113use jj_lib::revset::RevsetIteratorExt as _;
114use jj_lib::revset::RevsetParseContext;
115use jj_lib::revset::RevsetWorkspaceContext;
116use jj_lib::revset::SymbolResolverExtension;
117use jj_lib::revset::UserRevsetExpression;
118use jj_lib::rewrite::restore_tree;
119use jj_lib::settings::HumanByteSize;
120use jj_lib::settings::UserSettings;
121use jj_lib::store::Store;
122use jj_lib::str_util::StringExpression;
123use jj_lib::str_util::StringMatcher;
124use jj_lib::str_util::StringPattern;
125use jj_lib::transaction::Transaction;
126use jj_lib::working_copy;
127use jj_lib::working_copy::CheckoutStats;
128use jj_lib::working_copy::LockedWorkingCopy;
129use jj_lib::working_copy::SnapshotOptions;
130use jj_lib::working_copy::SnapshotStats;
131use jj_lib::working_copy::UntrackedReason;
132use jj_lib::working_copy::WorkingCopy;
133use jj_lib::working_copy::WorkingCopyFactory;
134use jj_lib::working_copy::WorkingCopyFreshness;
135use jj_lib::workspace::DefaultWorkspaceLoaderFactory;
136use jj_lib::workspace::LockedWorkspace;
137use jj_lib::workspace::WorkingCopyFactories;
138use jj_lib::workspace::Workspace;
139use jj_lib::workspace::WorkspaceLoadError;
140use jj_lib::workspace::WorkspaceLoader;
141use jj_lib::workspace::WorkspaceLoaderFactory;
142use jj_lib::workspace::default_working_copy_factories;
143use jj_lib::workspace::get_working_copy_factory;
144use pollster::FutureExt as _;
145use tracing::instrument;
146use tracing_chrome::ChromeLayerBuilder;
147use tracing_subscriber::prelude::*;
148
149use crate::command_error::CommandError;
150use crate::command_error::cli_error;
151use crate::command_error::config_error_with_message;
152use crate::command_error::handle_command_result;
153use crate::command_error::internal_error;
154use crate::command_error::internal_error_with_message;
155use crate::command_error::print_error_sources;
156use crate::command_error::print_parse_diagnostics;
157use crate::command_error::user_error;
158use crate::command_error::user_error_with_message;
159use crate::commit_templater::CommitTemplateLanguage;
160use crate::commit_templater::CommitTemplateLanguageExtension;
161use crate::complete;
162use crate::config::ConfigArgKind;
163use crate::config::ConfigEnv;
164use crate::config::RawConfig;
165use crate::config::config_from_environment;
166use crate::config::parse_config_args;
167use crate::description_util::TextEditor;
168use crate::diff_util;
169use crate::diff_util::DiffFormat;
170use crate::diff_util::DiffFormatArgs;
171use crate::diff_util::DiffRenderer;
172use crate::formatter::FormatRecorder;
173use crate::formatter::Formatter;
174use crate::formatter::FormatterExt as _;
175use crate::merge_tools::DiffEditor;
176use crate::merge_tools::MergeEditor;
177use crate::merge_tools::MergeToolConfigError;
178use crate::operation_templater::OperationTemplateLanguage;
179use crate::operation_templater::OperationTemplateLanguageExtension;
180use crate::revset_util;
181use crate::revset_util::RevsetExpressionEvaluator;
182use crate::revset_util::parse_union_name_patterns;
183use crate::template_builder;
184use crate::template_builder::TemplateLanguage;
185use crate::template_parser::TemplateAliasesMap;
186use crate::template_parser::TemplateDiagnostics;
187use crate::templater::TemplateRenderer;
188use crate::templater::WrapTemplateProperty;
189use crate::text_util;
190use crate::ui::ColorChoice;
191use crate::ui::Ui;
192
193const SHORT_CHANGE_ID_TEMPLATE_TEXT: &str = "format_short_change_id_with_change_offset(self)";
194
195#[derive(Clone)]
196struct ChromeTracingFlushGuard {
197 _inner: Option<Rc<tracing_chrome::FlushGuard>>,
198}
199
200impl Debug for ChromeTracingFlushGuard {
201 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
202 let Self { _inner } = self;
203 f.debug_struct("ChromeTracingFlushGuard")
204 .finish_non_exhaustive()
205 }
206}
207
208#[derive(Clone, Debug)]
210pub struct TracingSubscription {
211 reload_log_filter: tracing_subscriber::reload::Handle<
212 tracing_subscriber::EnvFilter,
213 tracing_subscriber::Registry,
214 >,
215 _chrome_tracing_flush_guard: ChromeTracingFlushGuard,
216}
217
218impl TracingSubscription {
219 const ENV_VAR_NAME: &str = "JJ_LOG";
220
221 pub fn init() -> Self {
224 let filter = tracing_subscriber::EnvFilter::builder()
225 .with_default_directive(tracing::metadata::LevelFilter::ERROR.into())
226 .with_env_var(Self::ENV_VAR_NAME)
227 .from_env_lossy();
228 let (filter, reload_log_filter) = tracing_subscriber::reload::Layer::new(filter);
229
230 let (chrome_tracing_layer, chrome_tracing_flush_guard) = match std::env::var("JJ_TRACE") {
231 Ok(filename) => {
232 let filename = if filename.is_empty() {
233 format!(
234 "jj-trace-{}.json",
235 SystemTime::now()
236 .duration_since(SystemTime::UNIX_EPOCH)
237 .unwrap()
238 .as_secs(),
239 )
240 } else {
241 filename
242 };
243 let include_args = std::env::var("JJ_TRACE_INCLUDE_ARGS").is_ok();
244 let (layer, guard) = ChromeLayerBuilder::new()
245 .file(filename)
246 .include_args(include_args)
247 .build();
248 (
249 Some(layer),
250 ChromeTracingFlushGuard {
251 _inner: Some(Rc::new(guard)),
252 },
253 )
254 }
255 Err(_) => (None, ChromeTracingFlushGuard { _inner: None }),
256 };
257
258 tracing_subscriber::registry()
259 .with(
260 tracing_subscriber::fmt::Layer::default()
261 .with_writer(std::io::stderr)
262 .with_filter(filter),
263 )
264 .with(chrome_tracing_layer)
265 .init();
266 Self {
267 reload_log_filter,
268 _chrome_tracing_flush_guard: chrome_tracing_flush_guard,
269 }
270 }
271
272 pub fn enable_debug_logging(&self) -> Result<(), CommandError> {
273 self.reload_log_filter
274 .modify(|filter| {
275 *filter = tracing_subscriber::EnvFilter::builder()
279 .with_default_directive(tracing::metadata::LevelFilter::INFO.into())
280 .with_env_var(Self::ENV_VAR_NAME)
281 .from_env_lossy()
282 .add_directive("jj_lib=debug".parse().unwrap())
283 .add_directive("jj_cli=debug".parse().unwrap());
284 })
285 .map_err(|err| internal_error_with_message("failed to enable debug logging", err))?;
286 tracing::info!("debug logging enabled");
287 Ok(())
288 }
289}
290
291#[derive(Clone)]
292pub struct CommandHelper {
293 data: Rc<CommandHelperData>,
294}
295
296struct CommandHelperData {
297 app: Command,
298 cwd: PathBuf,
299 string_args: Vec<String>,
300 matches: ArgMatches,
301 global_args: GlobalArgs,
302 config_env: ConfigEnv,
303 config_migrations: Vec<ConfigMigrationRule>,
304 raw_config: RawConfig,
305 settings: UserSettings,
306 revset_extensions: Arc<RevsetExtensions>,
307 commit_template_extensions: Vec<Arc<dyn CommitTemplateLanguageExtension>>,
308 operation_template_extensions: Vec<Arc<dyn OperationTemplateLanguageExtension>>,
309 maybe_workspace_loader: Result<Box<dyn WorkspaceLoader>, CommandError>,
310 store_factories: StoreFactories,
311 working_copy_factories: WorkingCopyFactories,
312 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
313}
314
315impl CommandHelper {
316 pub fn app(&self) -> &Command {
317 &self.data.app
318 }
319
320 pub fn cwd(&self) -> &Path {
325 &self.data.cwd
326 }
327
328 pub fn string_args(&self) -> &Vec<String> {
329 &self.data.string_args
330 }
331
332 pub fn matches(&self) -> &ArgMatches {
333 &self.data.matches
334 }
335
336 pub fn global_args(&self) -> &GlobalArgs {
337 &self.data.global_args
338 }
339
340 pub fn config_env(&self) -> &ConfigEnv {
341 &self.data.config_env
342 }
343
344 pub fn raw_config(&self) -> &RawConfig {
349 &self.data.raw_config
350 }
351
352 pub fn settings(&self) -> &UserSettings {
358 &self.data.settings
359 }
360
361 pub fn settings_for_new_workspace(
363 &self,
364 ui: &Ui,
365 workspace_root: &Path,
366 ) -> Result<(UserSettings, ConfigEnv), CommandError> {
367 let mut config_env = self.data.config_env.clone();
368 let mut raw_config = self.data.raw_config.clone();
369 let repo_path = workspace_root.join(".jj").join("repo");
370 config_env.reset_repo_path(&repo_path);
371 config_env.reload_repo_config(ui, &mut raw_config)?;
372 config_env.reset_workspace_path(workspace_root);
373 config_env.reload_workspace_config(ui, &mut raw_config)?;
374 let mut config = config_env.resolve_config(&raw_config)?;
375 jj_lib::config::migrate(&mut config, &self.data.config_migrations)?;
377 Ok((self.data.settings.with_new_config(config)?, config_env))
378 }
379
380 pub fn text_editor(&self) -> Result<TextEditor, ConfigGetError> {
382 TextEditor::from_settings(self.settings())
383 }
384
385 pub fn revset_extensions(&self) -> &Arc<RevsetExtensions> {
386 &self.data.revset_extensions
387 }
388
389 pub fn parse_template<'a, C, L>(
395 &self,
396 ui: &Ui,
397 language: &L,
398 template_text: &str,
399 ) -> Result<TemplateRenderer<'a, C>, CommandError>
400 where
401 C: Clone + 'a,
402 L: TemplateLanguage<'a> + ?Sized,
403 L::Property: WrapTemplateProperty<'a, C>,
404 {
405 let mut diagnostics = TemplateDiagnostics::new();
406 let aliases = load_template_aliases(ui, self.settings().config())?;
407 let template =
408 template_builder::parse(language, &mut diagnostics, template_text, &aliases)?;
409 print_parse_diagnostics(ui, "In template expression", &diagnostics)?;
410 Ok(template)
411 }
412
413 pub fn workspace_loader(&self) -> Result<&dyn WorkspaceLoader, CommandError> {
414 self.data
415 .maybe_workspace_loader
416 .as_deref()
417 .map_err(Clone::clone)
418 }
419
420 fn new_workspace_loader_at(
421 &self,
422 workspace_root: &Path,
423 ) -> Result<Box<dyn WorkspaceLoader>, CommandError> {
424 self.data
425 .workspace_loader_factory
426 .create(workspace_root)
427 .map_err(|err| map_workspace_load_error(err, None))
428 }
429
430 #[instrument(skip(self, ui))]
432 pub fn workspace_helper(&self, ui: &Ui) -> Result<WorkspaceCommandHelper, CommandError> {
433 let (workspace_command, stats) = self.workspace_helper_with_stats(ui)?;
434 print_snapshot_stats(ui, &stats, workspace_command.env().path_converter())?;
435 Ok(workspace_command)
436 }
437
438 #[instrument(skip(self, ui))]
445 pub fn workspace_helper_with_stats(
446 &self,
447 ui: &Ui,
448 ) -> Result<(WorkspaceCommandHelper, SnapshotStats), CommandError> {
449 let mut workspace_command = self.workspace_helper_no_snapshot(ui)?;
450
451 let (workspace_command, stats) = match workspace_command.maybe_snapshot_impl(ui) {
452 Ok(stats) => (workspace_command, stats),
453 Err(SnapshotWorkingCopyError::Command(err)) => return Err(err),
454 Err(SnapshotWorkingCopyError::StaleWorkingCopy(err)) => {
455 let auto_update_stale = self.settings().get_bool("snapshot.auto-update-stale")?;
456 if !auto_update_stale {
457 return Err(err);
458 }
459
460 self.recover_stale_working_copy(ui)?
465 }
466 };
467
468 Ok((workspace_command, stats))
469 }
470
471 #[instrument(skip(self, ui))]
474 pub fn workspace_helper_no_snapshot(
475 &self,
476 ui: &Ui,
477 ) -> Result<WorkspaceCommandHelper, CommandError> {
478 let workspace = self.load_workspace()?;
479 let op_head = self.resolve_operation(ui, workspace.repo_loader())?;
480 let repo = workspace.repo_loader().load_at(&op_head)?;
481 let mut env = self.workspace_environment(ui, &workspace)?;
482 if let Err(err) =
483 revset_util::try_resolve_trunk_alias(repo.as_ref(), &env.revset_parse_context())
484 {
485 let fallback = "root()";
488 writeln!(
489 ui.warning_default(),
490 "Failed to resolve `revset-aliases.trunk()`: {err}"
491 )?;
492 writeln!(
493 ui.warning_no_heading(),
494 "The `trunk()` alias is temporarily set to `{fallback}`."
495 )?;
496 writeln!(
497 ui.hint_default(),
498 "Use `jj config edit --repo` to adjust the `trunk()` alias."
499 )?;
500 env.revset_aliases_map
501 .insert("trunk()", fallback)
502 .expect("valid syntax");
503 env.reload_revset_expressions(ui)?;
504 }
505 WorkspaceCommandHelper::new(ui, workspace, repo, env, self.is_at_head_operation())
506 }
507
508 pub fn get_working_copy_factory(&self) -> Result<&dyn WorkingCopyFactory, CommandError> {
509 let loader = self.workspace_loader()?;
510
511 let factory: Result<_, WorkspaceLoadError> =
513 get_working_copy_factory(loader, &self.data.working_copy_factories)
514 .map_err(|e| e.into());
515 let factory = factory.map_err(|err| {
516 map_workspace_load_error(err, self.data.global_args.repository.as_deref())
517 })?;
518 Ok(factory)
519 }
520
521 #[instrument(skip_all)]
523 pub fn load_workspace(&self) -> Result<Workspace, CommandError> {
524 let loader = self.workspace_loader()?;
525 loader
526 .load(
527 &self.data.settings,
528 &self.data.store_factories,
529 &self.data.working_copy_factories,
530 )
531 .map_err(|err| {
532 map_workspace_load_error(err, self.data.global_args.repository.as_deref())
533 })
534 }
535
536 #[instrument(skip(self, settings))]
538 pub fn load_workspace_at(
539 &self,
540 workspace_root: &Path,
541 settings: &UserSettings,
542 ) -> Result<Workspace, CommandError> {
543 let loader = self.new_workspace_loader_at(workspace_root)?;
544 loader
545 .load(
546 settings,
547 &self.data.store_factories,
548 &self.data.working_copy_factories,
549 )
550 .map_err(|err| map_workspace_load_error(err, None))
551 }
552
553 pub fn recover_stale_working_copy(
557 &self,
558 ui: &Ui,
559 ) -> Result<(WorkspaceCommandHelper, SnapshotStats), CommandError> {
560 let workspace = self.load_workspace()?;
561 let op_id = workspace.working_copy().operation_id();
562
563 match workspace.repo_loader().load_operation(op_id) {
564 Ok(op) => {
565 let repo = workspace.repo_loader().load_at(&op)?;
566 let mut workspace_command = self.for_workable_repo(ui, workspace, repo)?;
567 workspace_command.check_working_copy_writable()?;
568
569 let stale_stats = workspace_command
574 .snapshot_working_copy(ui)
575 .map_err(|err| err.into_command_error())?;
576
577 let wc_commit_id = workspace_command.get_wc_commit_id().unwrap();
578 let repo = workspace_command.repo().clone();
579 let stale_wc_commit = repo.store().get_commit(wc_commit_id)?;
580
581 let mut workspace_command = self.workspace_helper_no_snapshot(ui)?;
582
583 let repo = workspace_command.repo().clone();
584 let (mut locked_ws, desired_wc_commit) =
585 workspace_command.unchecked_start_working_copy_mutation()?;
586 match WorkingCopyFreshness::check_stale(
587 locked_ws.locked_wc(),
588 &desired_wc_commit,
589 &repo,
590 )? {
591 WorkingCopyFreshness::Fresh | WorkingCopyFreshness::Updated(_) => {
592 drop(locked_ws);
593 writeln!(
594 ui.status(),
595 "Attempted recovery, but the working copy is not stale"
596 )?;
597 }
598 WorkingCopyFreshness::WorkingCopyStale
599 | WorkingCopyFreshness::SiblingOperation => {
600 let stats = update_stale_working_copy(
601 locked_ws,
602 repo.op_id().clone(),
603 &stale_wc_commit,
604 &desired_wc_commit,
605 )?;
606 workspace_command.print_updated_working_copy_stats(
607 ui,
608 Some(&stale_wc_commit),
609 &desired_wc_commit,
610 &stats,
611 )?;
612 writeln!(
613 ui.status(),
614 "Updated working copy to fresh commit {}",
615 short_commit_hash(desired_wc_commit.id())
616 )?;
617 }
618 }
619
620 let fresh_stats = workspace_command
625 .maybe_snapshot_impl(ui)
626 .map_err(|err| err.into_command_error())?;
627 let merged_stats = {
628 let SnapshotStats {
629 mut untracked_paths,
630 } = stale_stats;
631 untracked_paths.extend(fresh_stats.untracked_paths);
632 SnapshotStats { untracked_paths }
633 };
634 Ok((workspace_command, merged_stats))
635 }
636 Err(e @ OpStoreError::ObjectNotFound { .. }) => {
637 writeln!(
638 ui.status(),
639 "Failed to read working copy's current operation; attempting recovery. Error \
640 message from read attempt: {e}"
641 )?;
642
643 let mut workspace_command = self.workspace_helper_no_snapshot(ui)?;
644 let stats = workspace_command.create_and_check_out_recovery_commit(ui)?;
645 Ok((workspace_command, stats))
646 }
647 Err(e) => Err(e.into()),
648 }
649 }
650
651 pub fn workspace_environment(
653 &self,
654 ui: &Ui,
655 workspace: &Workspace,
656 ) -> Result<WorkspaceCommandEnvironment, CommandError> {
657 WorkspaceCommandEnvironment::new(ui, self, workspace)
658 }
659
660 pub fn is_working_copy_writable(&self) -> bool {
663 self.is_at_head_operation() && !self.data.global_args.ignore_working_copy
664 }
665
666 pub fn is_at_head_operation(&self) -> bool {
668 matches!(
671 self.data.global_args.at_operation.as_deref(),
672 None | Some("@")
673 )
674 }
675
676 #[instrument(skip_all)]
681 pub fn resolve_operation(
682 &self,
683 ui: &Ui,
684 repo_loader: &RepoLoader,
685 ) -> Result<Operation, CommandError> {
686 if let Some(op_str) = &self.data.global_args.at_operation {
687 Ok(op_walk::resolve_op_for_load(repo_loader, op_str)?)
688 } else {
689 op_heads_store::resolve_op_heads(
690 repo_loader.op_heads_store().as_ref(),
691 repo_loader.op_store(),
692 |op_heads| {
693 writeln!(
694 ui.status(),
695 "Concurrent modification detected, resolving automatically.",
696 )?;
697 let base_repo = repo_loader.load_at(&op_heads[0])?;
698 let mut tx = start_repo_transaction(&base_repo, &self.data.string_args);
700 for other_op_head in op_heads.into_iter().skip(1) {
701 tx.merge_operation(other_op_head)?;
702 let num_rebased = tx.repo_mut().rebase_descendants()?;
703 if num_rebased > 0 {
704 writeln!(
705 ui.status(),
706 "Rebased {num_rebased} descendant commits onto commits rewritten \
707 by other operation"
708 )?;
709 }
710 }
711 Ok(tx
712 .write("reconcile divergent operations")?
713 .leave_unpublished()
714 .operation()
715 .clone())
716 },
717 )
718 }
719 }
720
721 #[instrument(skip_all)]
725 pub fn for_workable_repo(
726 &self,
727 ui: &Ui,
728 workspace: Workspace,
729 repo: Arc<ReadonlyRepo>,
730 ) -> Result<WorkspaceCommandHelper, CommandError> {
731 let env = self.workspace_environment(ui, &workspace)?;
732 let loaded_at_head = true;
733 WorkspaceCommandHelper::new(ui, workspace, repo, env, loaded_at_head)
734 }
735}
736
737struct ReadonlyUserRepo {
740 repo: Arc<ReadonlyRepo>,
741 id_prefix_context: OnceCell<IdPrefixContext>,
742}
743
744impl ReadonlyUserRepo {
745 fn new(repo: Arc<ReadonlyRepo>) -> Self {
746 Self {
747 repo,
748 id_prefix_context: OnceCell::new(),
749 }
750 }
751}
752
753pub struct AdvanceableBookmark {
763 name: RefNameBuf,
764 old_commit_id: CommitId,
765}
766
767fn load_advance_bookmarks_matcher(
778 ui: &Ui,
779 settings: &UserSettings,
780) -> Result<Option<StringMatcher>, CommandError> {
781 let get_setting = |setting_key: &str| -> Result<Vec<String>, _> {
782 let name = ConfigNamePathBuf::from_iter(["experimental-advance-branches", setting_key]);
783 settings.get(&name)
784 };
785 let enabled_names = get_setting("enabled-branches")?;
788 let disabled_names = get_setting("disabled-branches")?;
789 let enabled_expr = parse_union_name_patterns(ui, &enabled_names)?;
790 let disabled_expr = parse_union_name_patterns(ui, &disabled_names)?;
791 if enabled_names.is_empty() {
792 Ok(None)
793 } else {
794 let expr = enabled_expr.intersection(disabled_expr.negated());
795 Ok(Some(expr.to_matcher()))
796 }
797}
798
799pub struct WorkspaceCommandEnvironment {
801 command: CommandHelper,
802 settings: UserSettings,
803 revset_aliases_map: RevsetAliasesMap,
804 template_aliases_map: TemplateAliasesMap,
805 default_ignored_remote: Option<&'static RemoteName>,
806 revsets_use_glob_by_default: bool,
807 path_converter: RepoPathUiConverter,
808 workspace_name: WorkspaceNameBuf,
809 immutable_heads_expression: Arc<UserRevsetExpression>,
810 short_prefixes_expression: Option<Arc<UserRevsetExpression>>,
811 conflict_marker_style: ConflictMarkerStyle,
812}
813
814impl WorkspaceCommandEnvironment {
815 #[instrument(skip_all)]
816 fn new(ui: &Ui, command: &CommandHelper, workspace: &Workspace) -> Result<Self, CommandError> {
817 let settings = workspace.settings();
818 let revset_aliases_map = revset_util::load_revset_aliases(ui, settings.config())?;
819 let template_aliases_map = load_template_aliases(ui, settings.config())?;
820 let default_ignored_remote = default_ignored_remote_name(workspace.repo_loader().store());
821 let path_converter = RepoPathUiConverter::Fs {
822 cwd: command.cwd().to_owned(),
823 base: workspace.workspace_root().to_owned(),
824 };
825 let mut env = Self {
826 command: command.clone(),
827 settings: settings.clone(),
828 revset_aliases_map,
829 template_aliases_map,
830 default_ignored_remote,
831 revsets_use_glob_by_default: settings.get("ui.revsets-use-glob-by-default")?,
832 path_converter,
833 workspace_name: workspace.workspace_name().to_owned(),
834 immutable_heads_expression: RevsetExpression::root(),
835 short_prefixes_expression: None,
836 conflict_marker_style: settings.get("ui.conflict-marker-style")?,
837 };
838 env.reload_revset_expressions(ui)?;
839 Ok(env)
840 }
841
842 pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
843 &self.path_converter
844 }
845
846 pub fn workspace_name(&self) -> &WorkspaceName {
847 &self.workspace_name
848 }
849
850 pub(crate) fn revset_parse_context(&self) -> RevsetParseContext<'_> {
851 let workspace_context = RevsetWorkspaceContext {
852 path_converter: &self.path_converter,
853 workspace_name: &self.workspace_name,
854 };
855 let now = if let Some(timestamp) = self.settings.commit_timestamp() {
856 chrono::Local
857 .timestamp_millis_opt(timestamp.timestamp.0)
858 .unwrap()
859 } else {
860 chrono::Local::now()
861 };
862 RevsetParseContext {
863 aliases_map: &self.revset_aliases_map,
864 local_variables: HashMap::new(),
865 user_email: self.settings.user_email(),
866 date_pattern_context: now.into(),
867 default_ignored_remote: self.default_ignored_remote,
868 use_glob_by_default: self.revsets_use_glob_by_default,
869 extensions: self.command.revset_extensions(),
870 workspace: Some(workspace_context),
871 }
872 }
873
874 pub fn new_id_prefix_context(&self) -> IdPrefixContext {
877 let context = IdPrefixContext::new(self.command.revset_extensions().clone());
878 match &self.short_prefixes_expression {
879 None => context,
880 Some(expression) => context.disambiguate_within(expression.clone()),
881 }
882 }
883
884 fn reload_revset_expressions(&mut self, ui: &Ui) -> Result<(), CommandError> {
886 self.immutable_heads_expression = self.load_immutable_heads_expression(ui)?;
887 self.short_prefixes_expression = self.load_short_prefixes_expression(ui)?;
888 Ok(())
889 }
890
891 pub fn immutable_expression(&self) -> Arc<UserRevsetExpression> {
893 self.immutable_heads_expression.ancestors()
896 }
897
898 pub fn immutable_heads_expression(&self) -> &Arc<UserRevsetExpression> {
900 &self.immutable_heads_expression
901 }
902
903 pub fn conflict_marker_style(&self) -> ConflictMarkerStyle {
905 self.conflict_marker_style
906 }
907
908 fn load_immutable_heads_expression(
909 &self,
910 ui: &Ui,
911 ) -> Result<Arc<UserRevsetExpression>, CommandError> {
912 let mut diagnostics = RevsetDiagnostics::new();
913 let expression = revset_util::parse_immutable_heads_expression(
914 &mut diagnostics,
915 &self.revset_parse_context(),
916 )
917 .map_err(|e| config_error_with_message("Invalid `revset-aliases.immutable_heads()`", e))?;
918 print_parse_diagnostics(ui, "In `revset-aliases.immutable_heads()`", &diagnostics)?;
919 Ok(expression)
920 }
921
922 fn load_short_prefixes_expression(
923 &self,
924 ui: &Ui,
925 ) -> Result<Option<Arc<UserRevsetExpression>>, CommandError> {
926 let revset_string = self
927 .settings
928 .get_string("revsets.short-prefixes")
929 .optional()?
930 .map_or_else(|| self.settings.get_string("revsets.log"), Ok)?;
931 if revset_string.is_empty() {
932 Ok(None)
933 } else {
934 let mut diagnostics = RevsetDiagnostics::new();
935 let expression = revset::parse(
936 &mut diagnostics,
937 &revset_string,
938 &self.revset_parse_context(),
939 )
940 .map_err(|err| config_error_with_message("Invalid `revsets.short-prefixes`", err))?;
941 print_parse_diagnostics(ui, "In `revsets.short-prefixes`", &diagnostics)?;
942 Ok(Some(expression))
943 }
944 }
945
946 fn find_immutable_commit(
948 &self,
949 repo: &dyn Repo,
950 to_rewrite_expr: &Arc<ResolvedRevsetExpression>,
951 ) -> Result<Option<CommitId>, CommandError> {
952 let immutable_expression = if self.command.global_args().ignore_immutable {
953 UserRevsetExpression::root()
954 } else {
955 self.immutable_expression()
956 };
957
958 let id_prefix_context = IdPrefixContext::new(self.command.revset_extensions().clone());
962 let immutable_expr = RevsetExpressionEvaluator::new(
963 repo,
964 self.command.revset_extensions().clone(),
965 &id_prefix_context,
966 immutable_expression,
967 )
968 .resolve()
969 .map_err(|e| config_error_with_message("Invalid `revset-aliases.immutable_heads()`", e))?;
970
971 let mut commit_id_iter = immutable_expr
972 .intersection(to_rewrite_expr)
973 .evaluate(repo)?
974 .iter();
975 Ok(commit_id_iter.next().transpose()?)
976 }
977
978 pub fn template_aliases_map(&self) -> &TemplateAliasesMap {
979 &self.template_aliases_map
980 }
981
982 pub fn parse_template<'a, C, L>(
984 &self,
985 ui: &Ui,
986 language: &L,
987 template_text: &str,
988 ) -> Result<TemplateRenderer<'a, C>, CommandError>
989 where
990 C: Clone + 'a,
991 L: TemplateLanguage<'a> + ?Sized,
992 L::Property: WrapTemplateProperty<'a, C>,
993 {
994 let mut diagnostics = TemplateDiagnostics::new();
995 let template = template_builder::parse(
996 language,
997 &mut diagnostics,
998 template_text,
999 &self.template_aliases_map,
1000 )?;
1001 print_parse_diagnostics(ui, "In template expression", &diagnostics)?;
1002 Ok(template)
1003 }
1004
1005 pub fn commit_template_language<'a>(
1008 &'a self,
1009 repo: &'a dyn Repo,
1010 id_prefix_context: &'a IdPrefixContext,
1011 ) -> CommitTemplateLanguage<'a> {
1012 CommitTemplateLanguage::new(
1013 repo,
1014 &self.path_converter,
1015 &self.workspace_name,
1016 self.revset_parse_context(),
1017 id_prefix_context,
1018 self.immutable_expression(),
1019 self.conflict_marker_style,
1020 &self.command.data.commit_template_extensions,
1021 )
1022 }
1023
1024 pub fn operation_template_extensions(&self) -> &[Arc<dyn OperationTemplateLanguageExtension>] {
1025 &self.command.data.operation_template_extensions
1026 }
1027}
1028
1029pub struct GitImportExportLock {
1033 _lock: Option<FileLock>,
1034}
1035
1036pub struct WorkspaceCommandHelper {
1039 workspace: Workspace,
1040 user_repo: ReadonlyUserRepo,
1041 env: WorkspaceCommandEnvironment,
1042 commit_summary_template_text: String,
1044 op_summary_template_text: String,
1045 may_update_working_copy: bool,
1046 working_copy_shared_with_git: bool,
1047}
1048
1049enum SnapshotWorkingCopyError {
1050 Command(CommandError),
1051 StaleWorkingCopy(CommandError),
1052}
1053
1054impl SnapshotWorkingCopyError {
1055 fn into_command_error(self) -> CommandError {
1056 match self {
1057 Self::Command(err) => err,
1058 Self::StaleWorkingCopy(err) => err,
1059 }
1060 }
1061}
1062
1063fn snapshot_command_error<E>(err: E) -> SnapshotWorkingCopyError
1064where
1065 E: Into<CommandError>,
1066{
1067 SnapshotWorkingCopyError::Command(err.into())
1068}
1069
1070impl WorkspaceCommandHelper {
1071 #[instrument(skip_all)]
1072 fn new(
1073 ui: &Ui,
1074 workspace: Workspace,
1075 repo: Arc<ReadonlyRepo>,
1076 env: WorkspaceCommandEnvironment,
1077 loaded_at_head: bool,
1078 ) -> Result<Self, CommandError> {
1079 let settings = workspace.settings();
1080 let commit_summary_template_text = settings.get_string("templates.commit_summary")?;
1081 let op_summary_template_text = settings.get_string("templates.op_summary")?;
1082 let may_update_working_copy =
1083 loaded_at_head && !env.command.global_args().ignore_working_copy;
1084 let working_copy_shared_with_git =
1085 crate::git_util::is_colocated_git_workspace(&workspace, &repo);
1086
1087 let helper = Self {
1088 workspace,
1089 user_repo: ReadonlyUserRepo::new(repo),
1090 env,
1091 commit_summary_template_text,
1092 op_summary_template_text,
1093 may_update_working_copy,
1094 working_copy_shared_with_git,
1095 };
1096 helper.parse_operation_template(ui, &helper.op_summary_template_text)?;
1099 helper.parse_commit_template(ui, &helper.commit_summary_template_text)?;
1100 helper.parse_commit_template(ui, SHORT_CHANGE_ID_TEMPLATE_TEXT)?;
1101 Ok(helper)
1102 }
1103
1104 pub fn settings(&self) -> &UserSettings {
1106 self.workspace.settings()
1107 }
1108
1109 pub fn check_working_copy_writable(&self) -> Result<(), CommandError> {
1110 if self.may_update_working_copy {
1111 Ok(())
1112 } else {
1113 let hint = if self.env.command.global_args().ignore_working_copy {
1114 "Don't use --ignore-working-copy."
1115 } else {
1116 "Don't use --at-op."
1117 };
1118 Err(user_error("This command must be able to update the working copy.").hinted(hint))
1119 }
1120 }
1121
1122 fn lock_git_import_export(&self) -> Result<GitImportExportLock, CommandError> {
1127 let lock = if self.working_copy_shared_with_git {
1128 let lock_path = self.workspace.repo_path().join("git_import_export.lock");
1129 Some(FileLock::lock(lock_path.clone()).map_err(|err| {
1130 user_error_with_message("Failed to take lock for Git import/export", err)
1131 })?)
1132 } else {
1133 None
1134 };
1135 Ok(GitImportExportLock { _lock: lock })
1136 }
1137
1138 #[instrument(skip_all)]
1142 fn maybe_snapshot_impl(&mut self, ui: &Ui) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
1143 if !self.may_update_working_copy {
1144 return Ok(SnapshotStats::default());
1145 }
1146
1147 #[cfg_attr(not(feature = "git"), allow(unused_variables))]
1151 let git_import_export_lock = self
1152 .lock_git_import_export()
1153 .map_err(snapshot_command_error)?;
1154
1155 if self.working_copy_shared_with_git {
1158 let repo = self.repo().clone();
1159 let op_heads_store = repo.loader().op_heads_store();
1160 let op_heads = op_heads_store
1161 .get_op_heads()
1162 .block_on()
1163 .map_err(snapshot_command_error)?;
1164 if std::slice::from_ref(repo.op_id()) != op_heads {
1165 let op = self
1166 .env
1167 .command
1168 .resolve_operation(ui, repo.loader())
1169 .map_err(snapshot_command_error)?;
1170 let current_repo = repo.loader().load_at(&op).map_err(snapshot_command_error)?;
1171 self.user_repo = ReadonlyUserRepo::new(current_repo);
1172 }
1173 }
1174
1175 #[cfg(feature = "git")]
1176 if self.working_copy_shared_with_git {
1177 self.import_git_head(ui, &git_import_export_lock)
1178 .map_err(snapshot_command_error)?;
1179 }
1180 let stats = self.snapshot_working_copy(ui)?;
1185
1186 #[cfg(feature = "git")]
1188 if self.working_copy_shared_with_git {
1189 self.import_git_refs(ui, &git_import_export_lock)
1190 .map_err(snapshot_command_error)?;
1191 }
1192 Ok(stats)
1193 }
1194
1195 #[instrument(skip_all)]
1198 pub fn maybe_snapshot(&mut self, ui: &Ui) -> Result<(), CommandError> {
1199 let stats = self
1200 .maybe_snapshot_impl(ui)
1201 .map_err(|err| err.into_command_error())?;
1202 print_snapshot_stats(ui, &stats, self.env().path_converter())?;
1203 Ok(())
1204 }
1205
1206 #[cfg(feature = "git")]
1213 #[instrument(skip_all)]
1214 fn import_git_head(
1215 &mut self,
1216 ui: &Ui,
1217 git_import_export_lock: &GitImportExportLock,
1218 ) -> Result<(), CommandError> {
1219 assert!(self.may_update_working_copy);
1220 let mut tx = self.start_transaction();
1221 jj_lib::git::import_head(tx.repo_mut())?;
1222 if !tx.repo().has_changes() {
1223 return Ok(());
1224 }
1225
1226 let mut tx = tx.into_inner();
1227 let old_git_head = self.repo().view().git_head().clone();
1228 let new_git_head = tx.repo().view().git_head().clone();
1229 if let Some(new_git_head_id) = new_git_head.as_normal() {
1230 let workspace_name = self.workspace_name().to_owned();
1231 let new_git_head_commit = tx.repo().store().get_commit(new_git_head_id)?;
1232 let wc_commit = tx
1233 .repo_mut()
1234 .check_out(workspace_name, &new_git_head_commit)?;
1235 let mut locked_ws = self.workspace.start_working_copy_mutation()?;
1236 locked_ws.locked_wc().reset(&wc_commit).block_on()?;
1240 tx.repo_mut().rebase_descendants()?;
1241 self.user_repo = ReadonlyUserRepo::new(tx.commit("import git head")?);
1242 locked_ws.finish(self.user_repo.repo.op_id().clone())?;
1243 if old_git_head.is_present() {
1244 writeln!(
1245 ui.status(),
1246 "Reset the working copy parent to the new Git HEAD."
1247 )?;
1248 } else {
1249 }
1251 } else {
1252 self.finish_transaction(ui, tx, "import git head", git_import_export_lock)?;
1254 }
1255 Ok(())
1256 }
1257
1258 #[cfg(feature = "git")]
1267 #[instrument(skip_all)]
1268 fn import_git_refs(
1269 &mut self,
1270 ui: &Ui,
1271 git_import_export_lock: &GitImportExportLock,
1272 ) -> Result<(), CommandError> {
1273 use jj_lib::git;
1274 let git_settings = git::GitSettings::from_settings(self.settings())?;
1275 let remote_settings = self.settings().remote_settings()?;
1276 let import_options =
1277 crate::git_util::load_git_import_options(ui, &git_settings, &remote_settings)?;
1278 let mut tx = self.start_transaction();
1279 let stats = git::import_refs(tx.repo_mut(), &import_options)?;
1280 crate::git_util::print_git_import_stats_summary(ui, &stats)?;
1281 if !tx.repo().has_changes() {
1282 return Ok(());
1283 }
1284
1285 let mut tx = tx.into_inner();
1286 let num_rebased = tx.repo_mut().rebase_descendants()?;
1288 if num_rebased > 0 {
1289 writeln!(
1290 ui.status(),
1291 "Rebased {num_rebased} descendant commits off of commits rewritten from git"
1292 )?;
1293 }
1294 self.finish_transaction(ui, tx, "import git refs", git_import_export_lock)?;
1295 writeln!(
1296 ui.status(),
1297 "Done importing changes from the underlying Git repo."
1298 )?;
1299 Ok(())
1300 }
1301
1302 pub fn repo(&self) -> &Arc<ReadonlyRepo> {
1303 &self.user_repo.repo
1304 }
1305
1306 pub fn repo_path(&self) -> &Path {
1307 self.workspace.repo_path()
1308 }
1309
1310 pub fn workspace(&self) -> &Workspace {
1311 &self.workspace
1312 }
1313
1314 pub fn working_copy(&self) -> &dyn WorkingCopy {
1315 self.workspace.working_copy()
1316 }
1317
1318 pub fn env(&self) -> &WorkspaceCommandEnvironment {
1319 &self.env
1320 }
1321
1322 pub fn unchecked_start_working_copy_mutation(
1323 &mut self,
1324 ) -> Result<(LockedWorkspace<'_>, Commit), CommandError> {
1325 self.check_working_copy_writable()?;
1326 let wc_commit = if let Some(wc_commit_id) = self.get_wc_commit_id() {
1327 self.repo().store().get_commit(wc_commit_id)?
1328 } else {
1329 return Err(user_error("Nothing checked out in this workspace"));
1330 };
1331
1332 let locked_ws = self.workspace.start_working_copy_mutation()?;
1333
1334 Ok((locked_ws, wc_commit))
1335 }
1336
1337 pub fn start_working_copy_mutation(
1338 &mut self,
1339 ) -> Result<(LockedWorkspace<'_>, Commit), CommandError> {
1340 let (mut locked_ws, wc_commit) = self.unchecked_start_working_copy_mutation()?;
1341 if wc_commit.tree().tree_ids_and_labels()
1342 != locked_ws.locked_wc().old_tree().tree_ids_and_labels()
1343 {
1344 return Err(user_error("Concurrent working copy operation. Try again."));
1345 }
1346 Ok((locked_ws, wc_commit))
1347 }
1348
1349 fn create_and_check_out_recovery_commit(
1350 &mut self,
1351 ui: &Ui,
1352 ) -> Result<SnapshotStats, CommandError> {
1353 self.check_working_copy_writable()?;
1354
1355 let workspace_name = self.workspace_name().to_owned();
1356 let mut locked_ws = self.workspace.start_working_copy_mutation()?;
1357 let (repo, new_commit) = working_copy::create_and_check_out_recovery_commit(
1358 locked_ws.locked_wc(),
1359 &self.user_repo.repo,
1360 workspace_name,
1361 "RECOVERY COMMIT FROM `jj workspace update-stale`
1362
1363This commit contains changes that were written to the working copy by an
1364operation that was subsequently lost (or was at least unavailable when you ran
1365`jj workspace update-stale`). Because the operation was lost, we don't know
1366what the parent commits are supposed to be. That means that the diff compared
1367to the current parents may contain changes from multiple commits.
1368",
1369 )?;
1370
1371 writeln!(
1372 ui.status(),
1373 "Created and checked out recovery commit {}",
1374 short_commit_hash(new_commit.id())
1375 )?;
1376 locked_ws.finish(repo.op_id().clone())?;
1377 self.user_repo = ReadonlyUserRepo::new(repo);
1378
1379 self.maybe_snapshot_impl(ui)
1380 .map_err(|err| err.into_command_error())
1381 }
1382
1383 pub fn workspace_root(&self) -> &Path {
1384 self.workspace.workspace_root()
1385 }
1386
1387 pub fn workspace_name(&self) -> &WorkspaceName {
1388 self.workspace.workspace_name()
1389 }
1390
1391 pub fn get_wc_commit_id(&self) -> Option<&CommitId> {
1392 self.repo().view().get_wc_commit_id(self.workspace_name())
1393 }
1394
1395 pub fn working_copy_shared_with_git(&self) -> bool {
1396 self.working_copy_shared_with_git
1397 }
1398
1399 pub fn format_file_path(&self, file: &RepoPath) -> String {
1400 self.path_converter().format_file_path(file)
1401 }
1402
1403 pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
1406 self.path_converter().parse_file_path(input)
1407 }
1408
1409 pub fn parse_file_patterns(
1411 &self,
1412 ui: &Ui,
1413 values: &[String],
1414 ) -> Result<FilesetExpression, CommandError> {
1415 if values.is_empty() {
1419 Ok(FilesetExpression::all())
1420 } else {
1421 self.parse_union_filesets(ui, values)
1422 }
1423 }
1424
1425 pub fn parse_union_filesets(
1427 &self,
1428 ui: &Ui,
1429 file_args: &[String], ) -> Result<FilesetExpression, CommandError> {
1431 let mut diagnostics = FilesetDiagnostics::new();
1432 let expressions: Vec<_> = file_args
1433 .iter()
1434 .map(|arg| fileset::parse_maybe_bare(&mut diagnostics, arg, self.path_converter()))
1435 .try_collect()?;
1436 print_parse_diagnostics(ui, "In fileset expression", &diagnostics)?;
1437 Ok(FilesetExpression::union_all(expressions))
1438 }
1439
1440 pub fn auto_tracking_matcher(&self, ui: &Ui) -> Result<Box<dyn Matcher>, CommandError> {
1441 let mut diagnostics = FilesetDiagnostics::new();
1442 let pattern = self.settings().get_string("snapshot.auto-track")?;
1443 let expression = fileset::parse(
1444 &mut diagnostics,
1445 &pattern,
1446 &RepoPathUiConverter::Fs {
1447 cwd: "".into(),
1448 base: "".into(),
1449 },
1450 )?;
1451 print_parse_diagnostics(ui, "In `snapshot.auto-track`", &diagnostics)?;
1452 Ok(expression.to_matcher())
1453 }
1454
1455 pub fn snapshot_options_with_start_tracking_matcher<'a>(
1456 &self,
1457 start_tracking_matcher: &'a dyn Matcher,
1458 ) -> Result<SnapshotOptions<'a>, CommandError> {
1459 let base_ignores = self.base_ignores()?;
1460 let HumanByteSize(mut max_new_file_size) = self
1461 .settings()
1462 .get_value_with("snapshot.max-new-file-size", TryInto::try_into)?;
1463 if max_new_file_size == 0 {
1464 max_new_file_size = u64::MAX;
1465 }
1466 Ok(SnapshotOptions {
1467 base_ignores,
1468 progress: None,
1469 start_tracking_matcher,
1470 force_tracking_matcher: &NothingMatcher,
1471 max_new_file_size,
1472 })
1473 }
1474
1475 pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
1476 self.env.path_converter()
1477 }
1478
1479 #[cfg(not(feature = "git"))]
1480 pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1481 Ok(GitIgnoreFile::empty())
1482 }
1483
1484 #[cfg(feature = "git")]
1485 #[instrument(skip_all)]
1486 pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1487 let get_excludes_file_path = |config: &gix::config::File| -> Option<PathBuf> {
1488 if let Some(value) = config.string("core.excludesFile") {
1491 let path = str::from_utf8(&value)
1492 .ok()
1493 .map(jj_lib::file_util::expand_home_path)?;
1494 Some(self.workspace_root().join(path))
1497 } else {
1498 xdg_config_home().map(|x| x.join("git").join("ignore"))
1499 }
1500 };
1501
1502 fn xdg_config_home() -> Option<PathBuf> {
1503 if let Ok(x) = std::env::var("XDG_CONFIG_HOME")
1504 && !x.is_empty()
1505 {
1506 return Some(PathBuf::from(x));
1507 }
1508 etcetera::home_dir().ok().map(|home| home.join(".config"))
1509 }
1510
1511 let mut git_ignores = GitIgnoreFile::empty();
1512 if let Ok(git_backend) = jj_lib::git::get_git_backend(self.repo().store()) {
1513 let git_repo = git_backend.git_repo();
1514 if let Some(excludes_file_path) = get_excludes_file_path(&git_repo.config_snapshot()) {
1515 git_ignores = git_ignores.chain_with_file("", excludes_file_path)?;
1516 }
1517 git_ignores = git_ignores
1518 .chain_with_file("", git_backend.git_repo_path().join("info").join("exclude"))?;
1519 } else if let Ok(git_config) = gix::config::File::from_globals()
1520 && let Some(excludes_file_path) = get_excludes_file_path(&git_config)
1521 {
1522 git_ignores = git_ignores.chain_with_file("", excludes_file_path)?;
1523 }
1524 Ok(git_ignores)
1525 }
1526
1527 pub fn diff_renderer(&self, formats: Vec<DiffFormat>) -> DiffRenderer<'_> {
1529 DiffRenderer::new(
1530 self.repo().as_ref(),
1531 self.path_converter(),
1532 self.env.conflict_marker_style(),
1533 formats,
1534 )
1535 }
1536
1537 pub fn diff_renderer_for(
1539 &self,
1540 args: &DiffFormatArgs,
1541 ) -> Result<DiffRenderer<'_>, CommandError> {
1542 let formats = diff_util::diff_formats_for(self.settings(), args)?;
1543 Ok(self.diff_renderer(formats))
1544 }
1545
1546 pub fn diff_renderer_for_log(
1550 &self,
1551 args: &DiffFormatArgs,
1552 patch: bool,
1553 ) -> Result<Option<DiffRenderer<'_>>, CommandError> {
1554 let formats = diff_util::diff_formats_for_log(self.settings(), args, patch)?;
1555 Ok((!formats.is_empty()).then(|| self.diff_renderer(formats)))
1556 }
1557
1558 pub fn diff_editor(
1562 &self,
1563 ui: &Ui,
1564 tool_name: Option<&str>,
1565 ) -> Result<DiffEditor, CommandError> {
1566 let base_ignores = self.base_ignores()?;
1567 let conflict_marker_style = self.env.conflict_marker_style();
1568 if let Some(name) = tool_name {
1569 Ok(DiffEditor::with_name(
1570 name,
1571 self.settings(),
1572 base_ignores,
1573 conflict_marker_style,
1574 )?)
1575 } else {
1576 Ok(DiffEditor::from_settings(
1577 ui,
1578 self.settings(),
1579 base_ignores,
1580 conflict_marker_style,
1581 )?)
1582 }
1583 }
1584
1585 pub fn diff_selector(
1589 &self,
1590 ui: &Ui,
1591 tool_name: Option<&str>,
1592 force_interactive: bool,
1593 ) -> Result<DiffSelector, CommandError> {
1594 if tool_name.is_some() || force_interactive {
1595 Ok(DiffSelector::Interactive(self.diff_editor(ui, tool_name)?))
1596 } else {
1597 Ok(DiffSelector::NonInteractive)
1598 }
1599 }
1600
1601 pub fn merge_editor(
1605 &self,
1606 ui: &Ui,
1607 tool_name: Option<&str>,
1608 ) -> Result<MergeEditor, MergeToolConfigError> {
1609 let conflict_marker_style = self.env.conflict_marker_style();
1610 if let Some(name) = tool_name {
1611 MergeEditor::with_name(
1612 name,
1613 self.settings(),
1614 self.path_converter().clone(),
1615 conflict_marker_style,
1616 )
1617 } else {
1618 MergeEditor::from_settings(
1619 ui,
1620 self.settings(),
1621 self.path_converter().clone(),
1622 conflict_marker_style,
1623 )
1624 }
1625 }
1626
1627 pub fn text_editor(&self) -> Result<TextEditor, ConfigGetError> {
1629 TextEditor::from_settings(self.settings())
1630 }
1631
1632 pub fn resolve_single_op(&self, op_str: &str) -> Result<Operation, OpsetEvaluationError> {
1633 op_walk::resolve_op_with_repo(self.repo(), op_str)
1634 }
1635
1636 pub fn resolve_single_rev(
1639 &self,
1640 ui: &Ui,
1641 revision_arg: &RevisionArg,
1642 ) -> Result<Commit, CommandError> {
1643 let expression = self.parse_revset(ui, revision_arg)?;
1644 revset_util::evaluate_revset_to_single_commit(revision_arg.as_ref(), &expression, || {
1645 self.commit_summary_template()
1646 })
1647 }
1648
1649 pub fn resolve_revsets_ordered(
1652 &self,
1653 ui: &Ui,
1654 revision_args: &[RevisionArg],
1655 ) -> Result<IndexSet<CommitId>, CommandError> {
1656 let mut all_commits = IndexSet::new();
1657 for revision_arg in revision_args {
1658 let expression = self.parse_revset(ui, revision_arg)?;
1659 for commit_id in expression.evaluate_to_commit_ids()? {
1660 all_commits.insert(commit_id?);
1661 }
1662 }
1663 Ok(all_commits)
1664 }
1665
1666 pub fn resolve_some_revsets(
1669 &self,
1670 ui: &Ui,
1671 revision_args: &[RevisionArg],
1672 ) -> Result<IndexSet<CommitId>, CommandError> {
1673 let all_commits = self.resolve_revsets_ordered(ui, revision_args)?;
1674 if all_commits.is_empty() {
1675 Err(user_error("Empty revision set"))
1676 } else {
1677 Ok(all_commits)
1678 }
1679 }
1680
1681 pub fn parse_revset(
1682 &self,
1683 ui: &Ui,
1684 revision_arg: &RevisionArg,
1685 ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1686 let mut diagnostics = RevsetDiagnostics::new();
1687 let context = self.env.revset_parse_context();
1688 let expression = revset::parse(&mut diagnostics, revision_arg.as_ref(), &context)?;
1689 print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1690 Ok(self.attach_revset_evaluator(expression))
1691 }
1692
1693 pub fn parse_union_revsets(
1695 &self,
1696 ui: &Ui,
1697 revision_args: &[RevisionArg],
1698 ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1699 let mut diagnostics = RevsetDiagnostics::new();
1700 let context = self.env.revset_parse_context();
1701 let expressions: Vec<_> = revision_args
1702 .iter()
1703 .map(|arg| revset::parse(&mut diagnostics, arg.as_ref(), &context))
1704 .try_collect()?;
1705 print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1706 let expression = RevsetExpression::union_all(&expressions);
1707 Ok(self.attach_revset_evaluator(expression))
1708 }
1709
1710 pub fn attach_revset_evaluator(
1711 &self,
1712 expression: Arc<UserRevsetExpression>,
1713 ) -> RevsetExpressionEvaluator<'_> {
1714 RevsetExpressionEvaluator::new(
1715 self.repo().as_ref(),
1716 self.env.command.revset_extensions().clone(),
1717 self.id_prefix_context(),
1718 expression,
1719 )
1720 }
1721
1722 pub fn id_prefix_context(&self) -> &IdPrefixContext {
1723 self.user_repo
1724 .id_prefix_context
1725 .get_or_init(|| self.env.new_id_prefix_context())
1726 }
1727
1728 pub fn parse_template<'a, C, L>(
1730 &self,
1731 ui: &Ui,
1732 language: &L,
1733 template_text: &str,
1734 ) -> Result<TemplateRenderer<'a, C>, CommandError>
1735 where
1736 C: Clone + 'a,
1737 L: TemplateLanguage<'a> + ?Sized,
1738 L::Property: WrapTemplateProperty<'a, C>,
1739 {
1740 self.env.parse_template(ui, language, template_text)
1741 }
1742
1743 fn reparse_valid_template<'a, C, L>(
1745 &self,
1746 language: &L,
1747 template_text: &str,
1748 ) -> TemplateRenderer<'a, C>
1749 where
1750 C: Clone + 'a,
1751 L: TemplateLanguage<'a> + ?Sized,
1752 L::Property: WrapTemplateProperty<'a, C>,
1753 {
1754 template_builder::parse(
1755 language,
1756 &mut TemplateDiagnostics::new(),
1757 template_text,
1758 &self.env.template_aliases_map,
1759 )
1760 .expect("parse error should be confined by WorkspaceCommandHelper::new()")
1761 }
1762
1763 pub fn parse_commit_template(
1765 &self,
1766 ui: &Ui,
1767 template_text: &str,
1768 ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
1769 let language = self.commit_template_language();
1770 self.parse_template(ui, &language, template_text)
1771 }
1772
1773 pub fn parse_operation_template(
1775 &self,
1776 ui: &Ui,
1777 template_text: &str,
1778 ) -> Result<TemplateRenderer<'_, Operation>, CommandError> {
1779 let language = self.operation_template_language();
1780 self.parse_template(ui, &language, template_text)
1781 }
1782
1783 pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
1785 self.env
1786 .commit_template_language(self.repo().as_ref(), self.id_prefix_context())
1787 }
1788
1789 pub fn operation_template_language(&self) -> OperationTemplateLanguage {
1791 OperationTemplateLanguage::new(
1792 self.workspace.repo_loader(),
1793 Some(self.repo().op_id()),
1794 self.env.operation_template_extensions(),
1795 )
1796 }
1797
1798 pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
1800 let language = self.commit_template_language();
1801 self.reparse_valid_template(&language, &self.commit_summary_template_text)
1802 .labeled(["commit"])
1803 }
1804
1805 pub fn operation_summary_template(&self) -> TemplateRenderer<'_, Operation> {
1807 let language = self.operation_template_language();
1808 self.reparse_valid_template(&language, &self.op_summary_template_text)
1809 .labeled(["operation"])
1810 }
1811
1812 pub fn short_change_id_template(&self) -> TemplateRenderer<'_, Commit> {
1813 let language = self.commit_template_language();
1814 self.reparse_valid_template(&language, SHORT_CHANGE_ID_TEMPLATE_TEXT)
1815 .labeled(["commit"])
1816 }
1817
1818 pub fn format_commit_summary(&self, commit: &Commit) -> String {
1823 let output = self.commit_summary_template().format_plain_text(commit);
1824 output.into_string_lossy()
1825 }
1826
1827 #[instrument(skip_all)]
1831 pub fn write_commit_summary(
1832 &self,
1833 formatter: &mut dyn Formatter,
1834 commit: &Commit,
1835 ) -> std::io::Result<()> {
1836 self.commit_summary_template().format(commit, formatter)
1837 }
1838
1839 pub fn check_rewritable<'a>(
1840 &self,
1841 commits: impl IntoIterator<Item = &'a CommitId>,
1842 ) -> Result<(), CommandError> {
1843 let commit_ids = commits.into_iter().cloned().collect_vec();
1844 let to_rewrite_expr = RevsetExpression::commits(commit_ids);
1845 self.check_rewritable_expr(&to_rewrite_expr)
1846 }
1847
1848 pub fn check_rewritable_expr(
1849 &self,
1850 to_rewrite_expr: &Arc<ResolvedRevsetExpression>,
1851 ) -> Result<(), CommandError> {
1852 let repo = self.repo().as_ref();
1853 let Some(commit_id) = self.env.find_immutable_commit(repo, to_rewrite_expr)? else {
1854 return Ok(());
1855 };
1856 let error = if &commit_id == repo.store().root_commit_id() {
1857 user_error(format!("The root commit {commit_id:.12} is immutable"))
1858 } else {
1859 let mut error = user_error(format!("Commit {commit_id:.12} is immutable"));
1860 let commit = repo.store().get_commit(&commit_id)?;
1861 error.add_formatted_hint_with(|formatter| {
1862 write!(formatter, "Could not modify commit: ")?;
1863 self.write_commit_summary(formatter, &commit)?;
1864 Ok(())
1865 });
1866 error.add_hint("Immutable commits are used to protect shared history.");
1867 error.add_hint(indoc::indoc! {"
1868 For more information, see:
1869 - https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits
1870 - `jj help -k config`, \"Set of immutable commits\""});
1871
1872 let id_prefix_context =
1875 IdPrefixContext::new(self.env.command.revset_extensions().clone());
1876 let (lower_bound, upper_bound) = RevsetExpressionEvaluator::new(
1877 repo,
1878 self.env.command.revset_extensions().clone(),
1879 &id_prefix_context,
1880 self.env.immutable_expression(),
1881 )
1882 .resolve()?
1883 .intersection(&to_rewrite_expr.descendants())
1884 .evaluate(repo)?
1885 .count_estimate()?;
1886 let exact = upper_bound == Some(lower_bound);
1887 let or_more = if exact { "" } else { " or more" };
1888 error.add_hint(format!(
1889 "This operation would rewrite {lower_bound}{or_more} immutable commits."
1890 ));
1891
1892 error
1893 };
1894 Err(error)
1895 }
1896
1897 #[instrument(skip_all)]
1898 fn snapshot_working_copy(
1899 &mut self,
1900 ui: &Ui,
1901 ) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
1902 let workspace_name = self.workspace_name().to_owned();
1903 let repo = self.repo().clone();
1904 let auto_tracking_matcher = self
1905 .auto_tracking_matcher(ui)
1906 .map_err(snapshot_command_error)?;
1907 let options = self
1908 .snapshot_options_with_start_tracking_matcher(&auto_tracking_matcher)
1909 .map_err(snapshot_command_error)?;
1910
1911 let mut locked_ws = self
1913 .workspace
1914 .start_working_copy_mutation()
1915 .map_err(snapshot_command_error)?;
1916
1917 let Some((repo, wc_commit)) =
1918 handle_stale_working_copy(locked_ws.locked_wc(), repo, &workspace_name)?
1919 else {
1920 return Ok(SnapshotStats::default());
1923 };
1924
1925 self.user_repo = ReadonlyUserRepo::new(repo);
1926 let (new_tree, 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 .block_on()
1934 .map_err(snapshot_command_error)?
1935 };
1936 if new_tree.tree_ids_and_labels() != wc_commit.tree().tree_ids_and_labels() {
1937 let mut tx =
1938 start_repo_transaction(&self.user_repo.repo, self.env.command.string_args());
1939 tx.set_is_snapshot(true);
1940 let mut_repo = tx.repo_mut();
1941 let commit = mut_repo
1942 .rewrite_commit(&wc_commit)
1943 .set_tree(new_tree)
1944 .write()
1945 .map_err(snapshot_command_error)?;
1946 mut_repo
1947 .set_wc_commit(workspace_name, commit.id().clone())
1948 .map_err(snapshot_command_error)?;
1949
1950 let num_rebased = mut_repo
1952 .rebase_descendants()
1953 .map_err(snapshot_command_error)?;
1954 if num_rebased > 0 {
1955 writeln!(
1956 ui.status(),
1957 "Rebased {num_rebased} descendant commits onto updated working copy"
1958 )
1959 .map_err(snapshot_command_error)?;
1960 }
1961
1962 #[cfg(feature = "git")]
1963 if self.working_copy_shared_with_git {
1964 let old_tree = wc_commit.tree();
1965 let new_tree = commit.tree();
1966 export_working_copy_changes_to_git(ui, mut_repo, &old_tree, &new_tree)
1967 .map_err(snapshot_command_error)?;
1968 }
1969
1970 let repo = tx
1971 .commit("snapshot working copy")
1972 .map_err(snapshot_command_error)?;
1973 self.user_repo = ReadonlyUserRepo::new(repo);
1974 }
1975 locked_ws
1976 .finish(self.user_repo.repo.op_id().clone())
1977 .map_err(snapshot_command_error)?;
1978 Ok(stats)
1979 }
1980
1981 fn update_working_copy(
1982 &mut self,
1983 ui: &Ui,
1984 maybe_old_commit: Option<&Commit>,
1985 new_commit: &Commit,
1986 ) -> Result<(), CommandError> {
1987 assert!(self.may_update_working_copy);
1988 let stats = update_working_copy(
1989 &self.user_repo.repo,
1990 &mut self.workspace,
1991 maybe_old_commit,
1992 new_commit,
1993 )?;
1994 self.print_updated_working_copy_stats(ui, maybe_old_commit, new_commit, &stats)
1995 }
1996
1997 fn print_updated_working_copy_stats(
1998 &self,
1999 ui: &Ui,
2000 maybe_old_commit: Option<&Commit>,
2001 new_commit: &Commit,
2002 stats: &CheckoutStats,
2003 ) -> Result<(), CommandError> {
2004 if Some(new_commit) != maybe_old_commit
2005 && let Some(mut formatter) = ui.status_formatter()
2006 {
2007 let template = self.commit_summary_template();
2008 write!(formatter, "Working copy (@) now at: ")?;
2009 template.format(new_commit, formatter.as_mut())?;
2010 writeln!(formatter)?;
2011 for parent in new_commit.parents() {
2012 let parent = parent?;
2013 write!(formatter, "Parent commit (@-) : ")?;
2015 template.format(&parent, formatter.as_mut())?;
2016 writeln!(formatter)?;
2017 }
2018 }
2019 print_checkout_stats(ui, stats, new_commit)?;
2020 if Some(new_commit) != maybe_old_commit
2021 && let Some(mut formatter) = ui.status_formatter()
2022 && new_commit.has_conflict()
2023 {
2024 let conflicts = new_commit.tree().conflicts().collect_vec();
2025 writeln!(
2026 formatter.labeled("warning").with_heading("Warning: "),
2027 "There are unresolved conflicts at these paths:"
2028 )?;
2029 print_conflicted_paths(conflicts, formatter.as_mut(), self)?;
2030 }
2031 Ok(())
2032 }
2033
2034 pub fn start_transaction(&mut self) -> WorkspaceCommandTransaction<'_> {
2035 let tx = start_repo_transaction(self.repo(), self.env.command.string_args());
2036 let id_prefix_context = mem::take(&mut self.user_repo.id_prefix_context);
2037 WorkspaceCommandTransaction {
2038 helper: self,
2039 tx,
2040 id_prefix_context,
2041 }
2042 }
2043
2044 fn finish_transaction(
2045 &mut self,
2046 ui: &Ui,
2047 mut tx: Transaction,
2048 description: impl Into<String>,
2049 _git_import_export_lock: &GitImportExportLock,
2050 ) -> Result<(), CommandError> {
2051 let num_rebased = tx.repo_mut().rebase_descendants()?;
2052 if num_rebased > 0 {
2053 writeln!(ui.status(), "Rebased {num_rebased} descendant commits")?;
2054 }
2055
2056 for (name, wc_commit_id) in &tx.repo().view().wc_commit_ids().clone() {
2057 let wc_expr = RevsetExpression::commit(wc_commit_id.clone());
2061 let is_immutable = match self.env.find_immutable_commit(tx.repo(), &wc_expr) {
2062 Ok(commit_id) => commit_id.is_some(),
2063 Err(CommandError { error, .. }) => {
2064 writeln!(
2065 ui.warning_default(),
2066 "Failed to check mutability of the new working-copy revision."
2067 )?;
2068 print_error_sources(ui, Some(&error))?;
2069 break;
2071 }
2072 };
2073 if is_immutable {
2074 let wc_commit = tx.repo().store().get_commit(wc_commit_id)?;
2075 tx.repo_mut().check_out(name.clone(), &wc_commit)?;
2076 writeln!(
2077 ui.warning_default(),
2078 "The working-copy commit in workspace '{name}' became immutable, so a new \
2079 commit has been created on top of it.",
2080 name = name.as_symbol()
2081 )?;
2082 }
2083 }
2084 if let Err(err) =
2085 revset_util::try_resolve_trunk_alias(tx.repo(), &self.env.revset_parse_context())
2086 {
2087 if tx.repo().view().wc_commit_ids().is_empty() {
2089 writeln!(
2090 ui.warning_default(),
2091 "Failed to resolve `revset-aliases.trunk()`: {err}"
2092 )?;
2093 }
2094 writeln!(
2095 ui.hint_default(),
2096 "Use `jj config edit --repo` to adjust the `trunk()` alias."
2097 )?;
2098 }
2099
2100 let old_repo = tx.base_repo().clone();
2101
2102 let maybe_old_wc_commit = old_repo
2103 .view()
2104 .get_wc_commit_id(self.workspace_name())
2105 .map(|commit_id| tx.base_repo().store().get_commit(commit_id))
2106 .transpose()?;
2107 let maybe_new_wc_commit = tx
2108 .repo()
2109 .view()
2110 .get_wc_commit_id(self.workspace_name())
2111 .map(|commit_id| tx.repo().store().get_commit(commit_id))
2112 .transpose()?;
2113
2114 #[cfg(feature = "git")]
2115 if self.working_copy_shared_with_git {
2116 use std::error::Error as _;
2117 if let Some(wc_commit) = &maybe_new_wc_commit {
2118 match jj_lib::git::reset_head(tx.repo_mut(), wc_commit) {
2125 Ok(()) => {}
2126 Err(err @ jj_lib::git::GitResetHeadError::UpdateHeadRef(_)) => {
2127 writeln!(ui.warning_default(), "{err}")?;
2128 print_error_sources(ui, err.source())?;
2129 }
2130 Err(err) => return Err(err.into()),
2131 }
2132 }
2133 let stats = jj_lib::git::export_refs(tx.repo_mut())?;
2134 crate::git_util::print_git_export_stats(ui, &stats)?;
2135 }
2136
2137 self.user_repo = ReadonlyUserRepo::new(tx.commit(description)?);
2138
2139 if self.may_update_working_copy {
2143 if let Some(new_commit) = &maybe_new_wc_commit {
2144 self.update_working_copy(ui, maybe_old_wc_commit.as_ref(), new_commit)?;
2145 } else {
2146 }
2149 }
2150
2151 self.report_repo_changes(ui, &old_repo)?;
2152
2153 let settings = self.settings();
2154 let missing_user_name = settings.user_name().is_empty();
2155 let missing_user_mail = settings.user_email().is_empty();
2156 if missing_user_name || missing_user_mail {
2157 let not_configured_msg = match (missing_user_name, missing_user_mail) {
2158 (true, true) => "Name and email not configured.",
2159 (true, false) => "Name not configured.",
2160 (false, true) => "Email not configured.",
2161 _ => unreachable!(),
2162 };
2163 writeln!(
2164 ui.warning_default(),
2165 "{not_configured_msg} Until configured, your commits will be created with the \
2166 empty identity, and can't be pushed to remotes."
2167 )?;
2168 writeln!(ui.hint_default(), "To configure, run:")?;
2169 if missing_user_name {
2170 writeln!(
2171 ui.hint_no_heading(),
2172 r#" jj config set --user user.name "Some One""#
2173 )?;
2174 }
2175 if missing_user_mail {
2176 writeln!(
2177 ui.hint_no_heading(),
2178 r#" jj config set --user user.email "someone@example.com""#
2179 )?;
2180 }
2181 }
2182 Ok(())
2183 }
2184
2185 fn report_repo_changes(
2188 &self,
2189 ui: &Ui,
2190 old_repo: &Arc<ReadonlyRepo>,
2191 ) -> Result<(), CommandError> {
2192 let Some(mut fmt) = ui.status_formatter() else {
2193 return Ok(());
2194 };
2195 let old_view = old_repo.view();
2196 let new_repo = self.repo().as_ref();
2197 let new_view = new_repo.view();
2198 let old_heads = RevsetExpression::commits(old_view.heads().iter().cloned().collect());
2199 let new_heads = RevsetExpression::commits(new_view.heads().iter().cloned().collect());
2200 let conflicts = RevsetExpression::filter(RevsetFilterPredicate::HasConflict)
2206 .filtered(RevsetFilterPredicate::File(FilesetExpression::all()));
2207 let removed_conflicts_expr = new_heads.range(&old_heads).intersection(&conflicts);
2208 let added_conflicts_expr = old_heads.range(&new_heads).intersection(&conflicts);
2209
2210 let get_commits =
2211 |expr: Arc<ResolvedRevsetExpression>| -> Result<Vec<Commit>, CommandError> {
2212 let commits = expr
2213 .evaluate(new_repo)?
2214 .iter()
2215 .commits(new_repo.store())
2216 .try_collect()?;
2217 Ok(commits)
2218 };
2219 let removed_conflict_commits = get_commits(removed_conflicts_expr)?;
2220 let added_conflict_commits = get_commits(added_conflicts_expr)?;
2221
2222 fn commits_by_change_id(commits: &[Commit]) -> IndexMap<&ChangeId, Vec<&Commit>> {
2223 let mut result: IndexMap<&ChangeId, Vec<&Commit>> = IndexMap::new();
2224 for commit in commits {
2225 result.entry(commit.change_id()).or_default().push(commit);
2226 }
2227 result
2228 }
2229 let removed_conflicts_by_change_id = commits_by_change_id(&removed_conflict_commits);
2230 let added_conflicts_by_change_id = commits_by_change_id(&added_conflict_commits);
2231 let mut resolved_conflicts_by_change_id = removed_conflicts_by_change_id.clone();
2232 resolved_conflicts_by_change_id
2233 .retain(|change_id, _commits| !added_conflicts_by_change_id.contains_key(change_id));
2234 let mut new_conflicts_by_change_id = added_conflicts_by_change_id.clone();
2235 new_conflicts_by_change_id
2236 .retain(|change_id, _commits| !removed_conflicts_by_change_id.contains_key(change_id));
2237
2238 if !resolved_conflicts_by_change_id.is_empty() {
2240 let num_resolved: usize = resolved_conflicts_by_change_id
2244 .values()
2245 .map(|commits| commits.len())
2246 .sum();
2247 writeln!(
2248 fmt,
2249 "Existing conflicts were resolved or abandoned from {num_resolved} commits."
2250 )?;
2251 }
2252 if !new_conflicts_by_change_id.is_empty() {
2253 let num_conflicted: usize = new_conflicts_by_change_id
2254 .values()
2255 .map(|commits| commits.len())
2256 .sum();
2257 writeln!(fmt, "New conflicts appeared in {num_conflicted} commits:")?;
2258 print_updated_commits(
2259 fmt.as_mut(),
2260 &self.commit_summary_template(),
2261 new_conflicts_by_change_id.values().flatten().copied(),
2262 )?;
2263 }
2264
2265 if !(added_conflict_commits.is_empty()
2269 || resolved_conflicts_by_change_id.is_empty() && new_conflicts_by_change_id.is_empty())
2270 {
2271 if new_conflicts_by_change_id.is_empty() {
2276 writeln!(
2277 fmt,
2278 "There are still unresolved conflicts in rebased descendants.",
2279 )?;
2280 }
2281
2282 self.report_repo_conflicts(
2283 fmt.as_mut(),
2284 new_repo,
2285 added_conflict_commits
2286 .iter()
2287 .map(|commit| commit.id().clone())
2288 .collect(),
2289 )?;
2290 }
2291
2292 Ok(())
2293 }
2294
2295 pub fn report_repo_conflicts(
2296 &self,
2297 fmt: &mut dyn Formatter,
2298 repo: &ReadonlyRepo,
2299 conflicted_commits: Vec<CommitId>,
2300 ) -> Result<(), CommandError> {
2301 if !self.settings().get_bool("hints.resolving-conflicts")? || conflicted_commits.is_empty()
2302 {
2303 return Ok(());
2304 }
2305
2306 let only_one_conflicted_commit = conflicted_commits.len() == 1;
2307 let root_conflicts_revset = RevsetExpression::commits(conflicted_commits)
2308 .roots()
2309 .evaluate(repo)?;
2310
2311 let root_conflict_commits: Vec<_> = root_conflicts_revset
2312 .iter()
2313 .commits(repo.store())
2314 .try_collect()?;
2315
2316 let instruction = if only_one_conflicted_commit {
2318 indoc! {"
2319 To resolve the conflicts, start by creating a commit on top of
2320 the conflicted commit:
2321 "}
2322 } else if root_conflict_commits.len() == 1 {
2323 indoc! {"
2324 To resolve the conflicts, start by creating a commit on top of
2325 the first conflicted commit:
2326 "}
2327 } else {
2328 indoc! {"
2329 To resolve the conflicts, start by creating a commit on top of
2330 one of the first conflicted commits:
2331 "}
2332 };
2333 write!(fmt.labeled("hint").with_heading("Hint: "), "{instruction}")?;
2334 let format_short_change_id = self.short_change_id_template();
2335 {
2336 let mut fmt = fmt.labeled("hint");
2337 for commit in &root_conflict_commits {
2338 write!(fmt, " jj new ")?;
2339 format_short_change_id.format(commit, *fmt)?;
2340 writeln!(fmt)?;
2341 }
2342 }
2343 writedoc!(
2344 fmt.labeled("hint"),
2345 "
2346 Then use `jj resolve`, or edit the conflict markers in the file directly.
2347 Once the conflicts are resolved, you can inspect the result with `jj diff`.
2348 Then run `jj squash` to move the resolution into the conflicted commit.
2349 ",
2350 )?;
2351 Ok(())
2352 }
2353
2354 pub fn get_advanceable_bookmarks<'a>(
2369 &self,
2370 ui: &Ui,
2371 from: impl IntoIterator<Item = &'a CommitId>,
2372 ) -> Result<Vec<AdvanceableBookmark>, CommandError> {
2373 let Some(ab_matcher) = load_advance_bookmarks_matcher(ui, self.settings())? else {
2374 return Ok(Vec::new());
2376 };
2377
2378 let mut advanceable_bookmarks = Vec::new();
2379 for from_commit in from {
2380 for (name, _) in self.repo().view().local_bookmarks_for_commit(from_commit) {
2381 if ab_matcher.is_match(name.as_str()) {
2382 advanceable_bookmarks.push(AdvanceableBookmark {
2383 name: name.to_owned(),
2384 old_commit_id: from_commit.clone(),
2385 });
2386 }
2387 }
2388 }
2389
2390 Ok(advanceable_bookmarks)
2391 }
2392}
2393
2394#[cfg(feature = "git")]
2395pub fn export_working_copy_changes_to_git(
2396 ui: &Ui,
2397 mut_repo: &mut MutableRepo,
2398 old_tree: &MergedTree,
2399 new_tree: &MergedTree,
2400) -> Result<(), CommandError> {
2401 let repo = mut_repo.base_repo().as_ref();
2402 jj_lib::git::update_intent_to_add(repo, old_tree, new_tree)?;
2403 let stats = jj_lib::git::export_refs(mut_repo)?;
2404 crate::git_util::print_git_export_stats(ui, &stats)?;
2405 Ok(())
2406}
2407#[cfg(not(feature = "git"))]
2408pub fn export_working_copy_changes_to_git(
2409 _ui: &Ui,
2410 _mut_repo: &mut MutableRepo,
2411 _old_tree: &MergedTree,
2412 _new_tree: &MergedTree,
2413) -> Result<(), CommandError> {
2414 Ok(())
2415}
2416
2417#[must_use]
2425pub struct WorkspaceCommandTransaction<'a> {
2426 helper: &'a mut WorkspaceCommandHelper,
2427 tx: Transaction,
2428 id_prefix_context: OnceCell<IdPrefixContext>,
2430}
2431
2432impl WorkspaceCommandTransaction<'_> {
2433 pub fn base_workspace_helper(&self) -> &WorkspaceCommandHelper {
2435 self.helper
2436 }
2437
2438 pub fn settings(&self) -> &UserSettings {
2440 self.helper.settings()
2441 }
2442
2443 pub fn base_repo(&self) -> &Arc<ReadonlyRepo> {
2444 self.tx.base_repo()
2445 }
2446
2447 pub fn repo(&self) -> &MutableRepo {
2448 self.tx.repo()
2449 }
2450
2451 pub fn repo_mut(&mut self) -> &mut MutableRepo {
2452 self.id_prefix_context.take(); self.tx.repo_mut()
2454 }
2455
2456 pub fn check_out(&mut self, commit: &Commit) -> Result<Commit, CheckOutCommitError> {
2457 let name = self.helper.workspace_name().to_owned();
2458 self.id_prefix_context.take(); self.tx.repo_mut().check_out(name, commit)
2460 }
2461
2462 pub fn edit(&mut self, commit: &Commit) -> Result<(), EditCommitError> {
2463 let name = self.helper.workspace_name().to_owned();
2464 self.id_prefix_context.take(); self.tx.repo_mut().edit(name, commit)
2466 }
2467
2468 pub fn format_commit_summary(&self, commit: &Commit) -> String {
2469 let output = self.commit_summary_template().format_plain_text(commit);
2470 output.into_string_lossy()
2471 }
2472
2473 pub fn write_commit_summary(
2474 &self,
2475 formatter: &mut dyn Formatter,
2476 commit: &Commit,
2477 ) -> std::io::Result<()> {
2478 self.commit_summary_template().format(commit, formatter)
2479 }
2480
2481 pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
2483 let language = self.commit_template_language();
2484 self.helper
2485 .reparse_valid_template(&language, &self.helper.commit_summary_template_text)
2486 .labeled(["commit"])
2487 }
2488
2489 pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
2492 let id_prefix_context = self
2493 .id_prefix_context
2494 .get_or_init(|| self.helper.env.new_id_prefix_context());
2495 self.helper
2496 .env
2497 .commit_template_language(self.tx.repo(), id_prefix_context)
2498 }
2499
2500 pub fn parse_commit_template(
2502 &self,
2503 ui: &Ui,
2504 template_text: &str,
2505 ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
2506 let language = self.commit_template_language();
2507 self.helper.env.parse_template(ui, &language, template_text)
2508 }
2509
2510 pub fn finish(self, ui: &Ui, description: impl Into<String>) -> Result<(), CommandError> {
2511 if !self.tx.repo().has_changes() {
2512 writeln!(ui.status(), "Nothing changed.")?;
2513 return Ok(());
2514 }
2515 let git_import_export_lock = self.helper.lock_git_import_export()?;
2518 self.helper
2519 .finish_transaction(ui, self.tx, description, &git_import_export_lock)
2520 }
2521
2522 pub fn into_inner(self) -> Transaction {
2527 self.tx
2528 }
2529
2530 pub fn advance_bookmarks(
2536 &mut self,
2537 bookmarks: Vec<AdvanceableBookmark>,
2538 move_to: &CommitId,
2539 ) -> Result<(), CommandError> {
2540 for bookmark in bookmarks {
2541 self.repo_mut().merge_local_bookmark(
2544 &bookmark.name,
2545 &RefTarget::normal(bookmark.old_commit_id),
2546 &RefTarget::normal(move_to.clone()),
2547 )?;
2548 }
2549 Ok(())
2550 }
2551}
2552
2553pub fn find_workspace_dir(cwd: &Path) -> &Path {
2554 cwd.ancestors()
2555 .find(|path| path.join(".jj").is_dir())
2556 .unwrap_or(cwd)
2557}
2558
2559fn map_workspace_load_error(err: WorkspaceLoadError, user_wc_path: Option<&str>) -> CommandError {
2560 match err {
2561 WorkspaceLoadError::NoWorkspaceHere(wc_path) => {
2562 let short_wc_path = user_wc_path.map_or(wc_path.as_ref(), Path::new);
2564 let message = format!(r#"There is no jj repo in "{}""#, short_wc_path.display());
2565 let git_dir = wc_path.join(".git");
2566 if git_dir.is_dir() {
2567 user_error(message).hinted(
2568 "It looks like this is a git repo. You can create a jj repo backed by it by \
2569 running this:
2570jj git init",
2571 )
2572 } else {
2573 user_error(message)
2574 }
2575 }
2576 WorkspaceLoadError::RepoDoesNotExist(repo_dir) => user_error(format!(
2577 "The repository directory at {} is missing. Was it moved?",
2578 repo_dir.display(),
2579 )),
2580 WorkspaceLoadError::StoreLoadError(err @ StoreLoadError::UnsupportedType { .. }) => {
2581 internal_error_with_message(
2582 "This version of the jj binary doesn't support this type of repo",
2583 err,
2584 )
2585 }
2586 WorkspaceLoadError::StoreLoadError(
2587 err @ (StoreLoadError::ReadError { .. } | StoreLoadError::Backend(_)),
2588 ) => internal_error_with_message("The repository appears broken or inaccessible", err),
2589 WorkspaceLoadError::StoreLoadError(StoreLoadError::Signing(err)) => user_error(err),
2590 WorkspaceLoadError::WorkingCopyState(err) => internal_error(err),
2591 WorkspaceLoadError::DecodeRepoPath(_) | WorkspaceLoadError::Path(_) => user_error(err),
2592 }
2593}
2594
2595pub fn start_repo_transaction(repo: &Arc<ReadonlyRepo>, string_args: &[String]) -> Transaction {
2596 let mut tx = repo.start_transaction();
2597 let shell_escape = |arg: &String| {
2600 if arg.as_bytes().iter().all(|b| {
2601 matches!(b,
2602 b'A'..=b'Z'
2603 | b'a'..=b'z'
2604 | b'0'..=b'9'
2605 | b','
2606 | b'-'
2607 | b'.'
2608 | b'/'
2609 | b':'
2610 | b'@'
2611 | b'_'
2612 )
2613 }) {
2614 arg.clone()
2615 } else {
2616 format!("'{}'", arg.replace('\'', "\\'"))
2617 }
2618 };
2619 let mut quoted_strings = vec!["jj".to_string()];
2620 quoted_strings.extend(string_args.iter().skip(1).map(shell_escape));
2621 tx.set_tag("args".to_string(), quoted_strings.join(" "));
2622 tx
2623}
2624
2625fn handle_stale_working_copy(
2631 locked_wc: &mut dyn LockedWorkingCopy,
2632 repo: Arc<ReadonlyRepo>,
2633 workspace_name: &WorkspaceName,
2634) -> Result<Option<(Arc<ReadonlyRepo>, Commit)>, SnapshotWorkingCopyError> {
2635 let get_wc_commit = |repo: &ReadonlyRepo| -> Result<Option<_>, _> {
2636 repo.view()
2637 .get_wc_commit_id(workspace_name)
2638 .map(|id| repo.store().get_commit(id))
2639 .transpose()
2640 .map_err(snapshot_command_error)
2641 };
2642 let Some(wc_commit) = get_wc_commit(&repo)? else {
2643 return Ok(None);
2644 };
2645 let old_op_id = locked_wc.old_operation_id().clone();
2646 match WorkingCopyFreshness::check_stale(locked_wc, &wc_commit, &repo) {
2647 Ok(WorkingCopyFreshness::Fresh) => Ok(Some((repo, wc_commit))),
2648 Ok(WorkingCopyFreshness::Updated(wc_operation)) => {
2649 let repo = repo
2650 .reload_at(&wc_operation)
2651 .map_err(snapshot_command_error)?;
2652 if let Some(wc_commit) = get_wc_commit(&repo)? {
2653 Ok(Some((repo, wc_commit)))
2654 } else {
2655 Ok(None)
2656 }
2657 }
2658 Ok(WorkingCopyFreshness::WorkingCopyStale) => {
2659 Err(SnapshotWorkingCopyError::StaleWorkingCopy(
2660 user_error(format!(
2661 "The working copy is stale (not updated since operation {}).",
2662 short_operation_hash(&old_op_id)
2663 ))
2664 .hinted(
2665 "Run `jj workspace update-stale` to update it.
2666See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy \
2667 for more information.",
2668 ),
2669 ))
2670 }
2671 Ok(WorkingCopyFreshness::SiblingOperation) => {
2672 Err(SnapshotWorkingCopyError::StaleWorkingCopy(
2673 internal_error(format!(
2674 "The repo was loaded at operation {}, which seems to be a sibling of the \
2675 working copy's operation {}",
2676 short_operation_hash(repo.op_id()),
2677 short_operation_hash(&old_op_id)
2678 ))
2679 .hinted(format!(
2680 "Run `jj op integrate {}` to add the working copy's operation to the \
2681 operation log.",
2682 short_operation_hash(&old_op_id)
2683 )),
2684 ))
2685 }
2686 Err(OpStoreError::ObjectNotFound { .. }) => {
2687 Err(SnapshotWorkingCopyError::StaleWorkingCopy(
2688 user_error("Could not read working copy's operation.").hinted(
2689 "Run `jj workspace update-stale` to recover.
2690See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy \
2691 for more information.",
2692 ),
2693 ))
2694 }
2695 Err(e) => Err(snapshot_command_error(e)),
2696 }
2697}
2698
2699fn update_stale_working_copy(
2700 mut locked_ws: LockedWorkspace,
2701 op_id: OperationId,
2702 stale_commit: &Commit,
2703 new_commit: &Commit,
2704) -> Result<CheckoutStats, CommandError> {
2705 if stale_commit.tree().tree_ids_and_labels()
2708 != locked_ws.locked_wc().old_tree().tree_ids_and_labels()
2709 {
2710 return Err(user_error("Concurrent working copy operation. Try again."));
2711 }
2712 let stats = locked_ws
2713 .locked_wc()
2714 .check_out(new_commit)
2715 .block_on()
2716 .map_err(|err| {
2717 internal_error_with_message(
2718 format!("Failed to check out commit {}", new_commit.id().hex()),
2719 err,
2720 )
2721 })?;
2722 locked_ws.finish(op_id)?;
2723
2724 Ok(stats)
2725}
2726
2727pub fn print_updated_commits<'a>(
2730 formatter: &mut dyn Formatter,
2731 template: &TemplateRenderer<Commit>,
2732 commits: impl IntoIterator<Item = &'a Commit>,
2733) -> io::Result<()> {
2734 let mut commits = commits.into_iter().fuse();
2735 for commit in commits.by_ref().take(10) {
2736 write!(formatter, " ")?;
2737 template.format(commit, formatter)?;
2738 writeln!(formatter)?;
2739 }
2740 if commits.next().is_some() {
2741 writeln!(formatter, " ...")?;
2742 }
2743 Ok(())
2744}
2745
2746#[instrument(skip_all)]
2747pub fn print_conflicted_paths(
2748 conflicts: Vec<(RepoPathBuf, BackendResult<MergedTreeValue>)>,
2749 formatter: &mut dyn Formatter,
2750 workspace_command: &WorkspaceCommandHelper,
2751) -> Result<(), CommandError> {
2752 let formatted_paths = conflicts
2753 .iter()
2754 .map(|(path, _conflict)| workspace_command.format_file_path(path))
2755 .collect_vec();
2756 let max_path_len = formatted_paths.iter().map(|p| p.len()).max().unwrap_or(0);
2757 let formatted_paths = formatted_paths
2758 .into_iter()
2759 .map(|p| format!("{:width$}", p, width = max_path_len.min(32) + 3));
2760
2761 for ((_, conflict), formatted_path) in std::iter::zip(conflicts, formatted_paths) {
2762 let conflict = conflict?.simplify();
2765 let sides = conflict.num_sides();
2766 let n_adds = conflict.adds().flatten().count();
2767 let deletions = sides - n_adds;
2768
2769 let mut seen_objects = BTreeMap::new(); if deletions > 0 {
2771 seen_objects.insert(
2772 format!(
2773 "{deletions} deletion{}",
2775 if deletions > 1 { "s" } else { "" }
2776 ),
2777 "normal", );
2779 }
2780 for term in itertools::chain(conflict.removes(), conflict.adds()).flatten() {
2784 seen_objects.insert(
2785 match term {
2786 TreeValue::File {
2787 executable: false, ..
2788 } => continue,
2789 TreeValue::File {
2790 executable: true, ..
2791 } => "an executable",
2792 TreeValue::Symlink(_) => "a symlink",
2793 TreeValue::Tree(_) => "a directory",
2794 TreeValue::GitSubmodule(_) => "a git submodule",
2795 }
2796 .to_string(),
2797 "difficult",
2798 );
2799 }
2800
2801 write!(formatter, "{formatted_path} ")?;
2802 {
2803 let mut formatter = formatter.labeled("conflict_description");
2804 let print_pair = |formatter: &mut dyn Formatter, (text, label): &(String, &str)| {
2805 write!(formatter.labeled(label), "{text}")
2806 };
2807 print_pair(
2808 *formatter,
2809 &(
2810 format!("{sides}-sided"),
2811 if sides > 2 { "difficult" } else { "normal" },
2812 ),
2813 )?;
2814 write!(formatter, " conflict")?;
2815
2816 if !seen_objects.is_empty() {
2817 write!(formatter, " including ")?;
2818 let seen_objects = seen_objects.into_iter().collect_vec();
2819 match &seen_objects[..] {
2820 [] => unreachable!(),
2821 [only] => print_pair(*formatter, only)?,
2822 [first, middle @ .., last] => {
2823 print_pair(*formatter, first)?;
2824 for pair in middle {
2825 write!(formatter, ", ")?;
2826 print_pair(*formatter, pair)?;
2827 }
2828 write!(formatter, " and ")?;
2829 print_pair(*formatter, last)?;
2830 }
2831 }
2832 }
2833 }
2834 writeln!(formatter)?;
2835 }
2836 Ok(())
2837}
2838
2839fn build_untracked_reason_message(reason: &UntrackedReason) -> Option<String> {
2841 match reason {
2842 UntrackedReason::FileTooLarge { size, max_size } => {
2843 let size_approx = HumanByteSize(*size);
2846 let max_size_approx = HumanByteSize(*max_size);
2847 Some(format!(
2848 "{size_approx} ({size} bytes); the maximum size allowed is {max_size_approx} \
2849 ({max_size} bytes)",
2850 ))
2851 }
2852 UntrackedReason::FileNotAutoTracked => None,
2856 }
2857}
2858
2859pub fn print_untracked_files(
2861 ui: &Ui,
2862 untracked_paths: &BTreeMap<RepoPathBuf, UntrackedReason>,
2863 path_converter: &RepoPathUiConverter,
2864) -> io::Result<()> {
2865 let mut untracked_paths = untracked_paths
2866 .iter()
2867 .filter_map(|(path, reason)| build_untracked_reason_message(reason).map(|m| (path, m)))
2868 .peekable();
2869
2870 if untracked_paths.peek().is_some() {
2871 writeln!(ui.warning_default(), "Refused to snapshot some files:")?;
2872 let mut formatter = ui.stderr_formatter();
2873 for (path, message) in untracked_paths {
2874 let ui_path = path_converter.format_file_path(path);
2875 writeln!(formatter, " {ui_path}: {message}")?;
2876 }
2877 }
2878
2879 Ok(())
2880}
2881
2882pub fn print_snapshot_stats(
2883 ui: &Ui,
2884 stats: &SnapshotStats,
2885 path_converter: &RepoPathUiConverter,
2886) -> io::Result<()> {
2887 print_untracked_files(ui, &stats.untracked_paths, path_converter)?;
2888
2889 let large_files_sizes = stats
2890 .untracked_paths
2891 .values()
2892 .filter_map(|reason| match reason {
2893 UntrackedReason::FileTooLarge { size, .. } => Some(size),
2894 UntrackedReason::FileNotAutoTracked => None,
2895 });
2896 if let Some(size) = large_files_sizes.max() {
2897 writedoc!(
2898 ui.hint_default(),
2899 r"
2900 This is to prevent large files from being added by accident. You can fix this by:
2901 - Adding the file to `.gitignore`
2902 - Run `jj config set --repo snapshot.max-new-file-size {size}`
2903 This will increase the maximum file size allowed for new files, in this repository only.
2904 - Run `jj --config snapshot.max-new-file-size={size} st`
2905 This will increase the maximum file size allowed for new files, for this command only.
2906 "
2907 )?;
2908 }
2909 Ok(())
2910}
2911
2912pub fn print_checkout_stats(
2913 ui: &Ui,
2914 stats: &CheckoutStats,
2915 new_commit: &Commit,
2916) -> Result<(), std::io::Error> {
2917 if stats.added_files > 0 || stats.updated_files > 0 || stats.removed_files > 0 {
2918 writeln!(
2919 ui.status(),
2920 "Added {} files, modified {} files, removed {} files",
2921 stats.added_files,
2922 stats.updated_files,
2923 stats.removed_files
2924 )?;
2925 }
2926 if stats.skipped_files != 0 {
2927 writeln!(
2928 ui.warning_default(),
2929 "{} of those updates were skipped because there were conflicting changes in the \
2930 working copy.",
2931 stats.skipped_files
2932 )?;
2933 writeln!(
2934 ui.hint_default(),
2935 "Inspect the changes compared to the intended target with `jj diff --from {}`.
2936Discard the conflicting changes with `jj restore --from {}`.",
2937 short_commit_hash(new_commit.id()),
2938 short_commit_hash(new_commit.id())
2939 )?;
2940 }
2941 Ok(())
2942}
2943
2944pub fn print_unmatched_explicit_paths<'a>(
2947 ui: &Ui,
2948 workspace_command: &WorkspaceCommandHelper,
2949 expression: &FilesetExpression,
2950 trees: impl IntoIterator<Item = &'a MergedTree>,
2951) -> io::Result<()> {
2952 let mut explicit_paths = expression.explicit_paths().collect_vec();
2953 for tree in trees {
2954 explicit_paths.retain(|&path| tree.path_value(path).unwrap().is_absent());
2956 }
2957
2958 if !explicit_paths.is_empty() {
2959 let ui_paths = explicit_paths
2960 .iter()
2961 .map(|&path| workspace_command.format_file_path(path))
2962 .join(", ");
2963 writeln!(
2964 ui.warning_default(),
2965 "No matching entries for paths: {ui_paths}"
2966 )?;
2967 }
2968
2969 Ok(())
2970}
2971
2972pub fn update_working_copy(
2973 repo: &Arc<ReadonlyRepo>,
2974 workspace: &mut Workspace,
2975 old_commit: Option<&Commit>,
2976 new_commit: &Commit,
2977) -> Result<CheckoutStats, CommandError> {
2978 let old_tree = old_commit.map(|commit| commit.tree());
2979 let stats = workspace
2982 .check_out(repo.op_id().clone(), old_tree.as_ref(), new_commit)
2983 .map_err(|err| {
2984 internal_error_with_message(
2985 format!("Failed to check out commit {}", new_commit.id().hex()),
2986 err,
2987 )
2988 })?;
2989 Ok(stats)
2990}
2991
2992#[cfg_attr(not(feature = "git"), expect(unused_variables))]
2994pub fn default_ignored_remote_name(store: &Store) -> Option<&'static RemoteName> {
2995 #[cfg(feature = "git")]
2996 {
2997 use jj_lib::git;
2998 if git::get_git_backend(store).is_ok() {
2999 return Some(git::REMOTE_NAME_FOR_LOCAL_GIT_REPO);
3000 }
3001 }
3002 None
3003}
3004
3005pub fn has_tracked_remote_bookmarks(repo: &dyn Repo, bookmark: &RefName) -> bool {
3008 let remote_matcher = match default_ignored_remote_name(repo.store()) {
3009 Some(remote) => StringExpression::exact(remote).negated().to_matcher(),
3010 None => StringMatcher::all(),
3011 };
3012 repo.view()
3013 .remote_bookmarks_matching(&StringMatcher::exact(bookmark), &remote_matcher)
3014 .any(|(_, remote_ref)| remote_ref.is_tracked())
3015}
3016
3017pub fn load_template_aliases(
3018 ui: &Ui,
3019 stacked_config: &StackedConfig,
3020) -> Result<TemplateAliasesMap, CommandError> {
3021 let table_name = ConfigNamePathBuf::from_iter(["template-aliases"]);
3022 let mut aliases_map = TemplateAliasesMap::new();
3023 for layer in stacked_config.layers() {
3026 let table = match layer.look_up_table(&table_name) {
3027 Ok(Some(table)) => table,
3028 Ok(None) => continue,
3029 Err(item) => {
3030 return Err(ConfigGetError::Type {
3031 name: table_name.to_string(),
3032 error: format!("Expected a table, but is {}", item.type_name()).into(),
3033 source_path: layer.path.clone(),
3034 }
3035 .into());
3036 }
3037 };
3038 for (decl, item) in table.iter() {
3039 let r = item
3040 .as_str()
3041 .ok_or_else(|| format!("Expected a string, but is {}", item.type_name()))
3042 .and_then(|v| aliases_map.insert(decl, v).map_err(|e| e.to_string()));
3043 if let Err(s) = r {
3044 writeln!(
3045 ui.warning_default(),
3046 "Failed to load `{table_name}.{decl}`: {s}"
3047 )?;
3048 }
3049 }
3050 }
3051 Ok(aliases_map)
3052}
3053
3054#[derive(Clone, Debug)]
3056pub struct LogContentFormat {
3057 width: usize,
3058 word_wrap: bool,
3059}
3060
3061impl LogContentFormat {
3062 pub fn new(ui: &Ui, settings: &UserSettings) -> Result<Self, ConfigGetError> {
3064 Ok(Self {
3065 width: ui.term_width(),
3066 word_wrap: settings.get_bool("ui.log-word-wrap")?,
3067 })
3068 }
3069
3070 #[must_use]
3072 pub fn sub_width(&self, width: usize) -> Self {
3073 Self {
3074 width: self.width.saturating_sub(width),
3075 word_wrap: self.word_wrap,
3076 }
3077 }
3078
3079 pub fn width(&self) -> usize {
3081 self.width
3082 }
3083
3084 pub fn write<E: From<io::Error>>(
3086 &self,
3087 formatter: &mut dyn Formatter,
3088 content_fn: impl FnOnce(&mut dyn Formatter) -> Result<(), E>,
3089 ) -> Result<(), E> {
3090 if self.word_wrap {
3091 let mut recorder = FormatRecorder::new(formatter.maybe_color());
3092 content_fn(&mut recorder)?;
3093 text_util::write_wrapped(formatter, &recorder, self.width)?;
3094 } else {
3095 content_fn(formatter)?;
3096 }
3097 Ok(())
3098 }
3099}
3100
3101pub fn short_commit_hash(commit_id: &CommitId) -> String {
3102 format!("{commit_id:.12}")
3103}
3104
3105pub fn short_change_hash(change_id: &ChangeId) -> String {
3106 format!("{change_id:.12}")
3107}
3108
3109pub fn short_operation_hash(operation_id: &OperationId) -> String {
3110 format!("{operation_id:.12}")
3111}
3112
3113#[derive(Clone, Debug)]
3115pub enum DiffSelector {
3116 NonInteractive,
3117 Interactive(DiffEditor),
3118}
3119
3120impl DiffSelector {
3121 pub fn is_interactive(&self) -> bool {
3122 matches!(self, Self::Interactive(_))
3123 }
3124
3125 pub fn select(
3130 &self,
3131 trees: Diff<&MergedTree>,
3132 tree_labels: Diff<String>,
3133 matcher: &dyn Matcher,
3134 format_instructions: impl FnOnce() -> String,
3135 ) -> Result<MergedTree, CommandError> {
3136 let selected_tree = restore_tree(
3137 trees.after,
3138 trees.before,
3139 tree_labels.after,
3140 tree_labels.before,
3141 matcher,
3142 )
3143 .block_on()?;
3144 match self {
3145 Self::NonInteractive => Ok(selected_tree),
3146 Self::Interactive(editor) => {
3147 Ok(editor.edit(
3151 Diff::new(trees.before, &selected_tree),
3152 matcher,
3153 format_instructions,
3154 )?)
3155 }
3156 }
3157 }
3158}
3159
3160#[derive(Clone, Debug)]
3162pub(crate) struct RemoteBookmarkNamePattern {
3163 pub bookmark: StringPattern,
3164 pub remote: StringPattern,
3165}
3166
3167impl FromStr for RemoteBookmarkNamePattern {
3168 type Err = String;
3169
3170 fn from_str(src: &str) -> Result<Self, Self::Err> {
3171 let (maybe_kind, pat) = src
3176 .split_once(':')
3177 .map_or((None, src), |(kind, pat)| (Some(kind), pat));
3178 let to_pattern = |pat: &str| {
3179 if let Some(kind) = maybe_kind {
3180 StringPattern::from_str_kind(pat, kind).map_err(|err| err.to_string())
3181 } else {
3182 StringPattern::glob(pat).map_err(|err| err.to_string())
3183 }
3184 };
3185 let (bookmark, remote) = pat.rsplit_once('@').ok_or_else(|| {
3187 "remote bookmark must be specified in bookmark@remote form".to_owned()
3188 })?;
3189 Ok(Self {
3190 bookmark: to_pattern(bookmark)?,
3191 remote: to_pattern(remote)?,
3192 })
3193 }
3194}
3195
3196impl RemoteBookmarkNamePattern {
3197 pub fn as_exact(&self) -> Option<RemoteRefSymbol<'_>> {
3198 let bookmark = RefName::new(self.bookmark.as_exact()?);
3199 let remote = RemoteName::new(self.remote.as_exact()?);
3200 Some(bookmark.to_remote_symbol(remote))
3201 }
3202}
3203
3204impl fmt::Display for RemoteBookmarkNamePattern {
3205 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3206 let Self { bookmark, remote } = self;
3209 write!(f, "{bookmark}@{remote}")
3210 }
3211}
3212
3213pub fn compute_commit_location(
3218 ui: &Ui,
3219 workspace_command: &WorkspaceCommandHelper,
3220 destination: Option<&[RevisionArg]>,
3221 insert_after: Option<&[RevisionArg]>,
3222 insert_before: Option<&[RevisionArg]>,
3223 commit_type: &str,
3224) -> Result<(Vec<CommitId>, Vec<CommitId>), CommandError> {
3225 let resolve_revisions =
3226 |revisions: Option<&[RevisionArg]>| -> Result<Option<Vec<CommitId>>, CommandError> {
3227 if let Some(revisions) = revisions {
3228 Ok(Some(
3229 workspace_command
3230 .resolve_revsets_ordered(ui, revisions)?
3231 .into_iter()
3232 .collect_vec(),
3233 ))
3234 } else {
3235 Ok(None)
3236 }
3237 };
3238 let destination_commit_ids = resolve_revisions(destination)?;
3239 let after_commit_ids = resolve_revisions(insert_after)?;
3240 let before_commit_ids = resolve_revisions(insert_before)?;
3241
3242 let (new_parent_ids, new_child_ids) =
3243 match (destination_commit_ids, after_commit_ids, before_commit_ids) {
3244 (Some(destination_commit_ids), None, None) => (destination_commit_ids, vec![]),
3245 (None, Some(after_commit_ids), Some(before_commit_ids)) => {
3246 (after_commit_ids, before_commit_ids)
3247 }
3248 (None, Some(after_commit_ids), None) => {
3249 let new_child_ids: Vec<_> = RevsetExpression::commits(after_commit_ids.clone())
3250 .children()
3251 .evaluate(workspace_command.repo().as_ref())?
3252 .iter()
3253 .try_collect()?;
3254
3255 (after_commit_ids, new_child_ids)
3256 }
3257 (None, None, Some(before_commit_ids)) => {
3258 let before_commits: Vec<_> = before_commit_ids
3259 .iter()
3260 .map(|id| workspace_command.repo().store().get_commit(id))
3261 .try_collect()?;
3262 let new_parent_ids = before_commits
3265 .iter()
3266 .flat_map(|commit| commit.parent_ids())
3267 .unique()
3268 .cloned()
3269 .collect_vec();
3270
3271 (new_parent_ids, before_commit_ids)
3272 }
3273 (Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
3274 panic!("destination cannot be used with insert_after/insert_before")
3275 }
3276 (None, None, None) => {
3277 panic!("expected at least one of destination or insert_after/insert_before")
3278 }
3279 };
3280
3281 if !new_child_ids.is_empty() {
3282 workspace_command.check_rewritable(new_child_ids.iter())?;
3283 ensure_no_commit_loop(
3284 workspace_command.repo().as_ref(),
3285 &RevsetExpression::commits(new_child_ids.clone()),
3286 &RevsetExpression::commits(new_parent_ids.clone()),
3287 commit_type,
3288 )?;
3289 }
3290
3291 if new_parent_ids.is_empty() {
3292 return Err(user_error("No revisions found to use as parent"));
3293 }
3294
3295 Ok((new_parent_ids, new_child_ids))
3296}
3297
3298fn ensure_no_commit_loop(
3301 repo: &ReadonlyRepo,
3302 children_expression: &Arc<ResolvedRevsetExpression>,
3303 parents_expression: &Arc<ResolvedRevsetExpression>,
3304 commit_type: &str,
3305) -> Result<(), CommandError> {
3306 if let Some(commit_id) = children_expression
3307 .dag_range_to(parents_expression)
3308 .evaluate(repo)?
3309 .iter()
3310 .next()
3311 {
3312 let commit_id = commit_id?;
3313 return Err(user_error(format!(
3314 "Refusing to create a loop: commit {} would be both an ancestor and a descendant of \
3315 the {commit_type}",
3316 short_commit_hash(&commit_id),
3317 )));
3318 }
3319 Ok(())
3320}
3321
3322#[derive(clap::Parser, Clone, Debug)]
3329#[command(name = "jj")]
3330pub struct Args {
3331 #[command(flatten)]
3332 pub global_args: GlobalArgs,
3333}
3334
3335#[derive(clap::Args, Clone, Debug)]
3336#[command(next_help_heading = "Global Options")]
3337pub struct GlobalArgs {
3338 #[arg(long, short = 'R', global = true, value_hint = clap::ValueHint::DirPath)]
3343 pub repository: Option<String>,
3344
3345 #[arg(long, global = true)]
3358 pub ignore_working_copy: bool,
3359
3360 #[arg(long, global = true)]
3369 pub ignore_immutable: bool,
3370
3371 #[arg(long, visible_alias = "at-op", global = true)]
3394 #[arg(add = ArgValueCandidates::new(complete::operations))]
3395 pub at_operation: Option<String>,
3396
3397 #[arg(long, global = true)]
3399 pub debug: bool,
3400
3401 #[command(flatten)]
3402 pub early_args: EarlyArgs,
3403}
3404
3405#[derive(clap::Args, Clone, Debug)]
3406pub struct EarlyArgs {
3407 #[arg(long, value_name = "WHEN", global = true)]
3409 pub color: Option<ColorChoice>,
3410
3411 #[arg(long, global = true, action = ArgAction::SetTrue)]
3418 pub quiet: Option<bool>,
3421
3422 #[arg(long, global = true, action = ArgAction::SetTrue)]
3424 pub no_pager: Option<bool>,
3427
3428 #[arg(long, value_name = "NAME=VALUE", global = true)]
3434 #[arg(add = ArgValueCompleter::new(complete::leaf_config_key_value))]
3435 pub config: Vec<String>,
3436
3437 #[arg(long, value_name = "PATH", global = true, value_hint = clap::ValueHint::FilePath)]
3439 pub config_file: Vec<String>,
3440}
3441
3442impl EarlyArgs {
3443 pub(crate) fn merged_config_args(&self, matches: &ArgMatches) -> Vec<(ConfigArgKind, &str)> {
3444 merge_args_with(
3445 matches,
3446 &[("config", &self.config), ("config_file", &self.config_file)],
3447 |id, value| match id {
3448 "config" => (ConfigArgKind::Item, value.as_ref()),
3449 "config_file" => (ConfigArgKind::File, value.as_ref()),
3450 _ => unreachable!("unexpected id {id:?}"),
3451 },
3452 )
3453 }
3454
3455 fn has_config_args(&self) -> bool {
3456 !self.config.is_empty() || !self.config_file.is_empty()
3457 }
3458}
3459
3460#[derive(Clone, Debug)]
3466pub struct RevisionArg(Cow<'static, str>);
3467
3468impl RevisionArg {
3469 pub const AT: Self = Self(Cow::Borrowed("@"));
3471}
3472
3473impl From<String> for RevisionArg {
3474 fn from(s: String) -> Self {
3475 Self(s.into())
3476 }
3477}
3478
3479impl AsRef<str> for RevisionArg {
3480 fn as_ref(&self) -> &str {
3481 &self.0
3482 }
3483}
3484
3485impl fmt::Display for RevisionArg {
3486 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3487 write!(f, "{}", self.0)
3488 }
3489}
3490
3491impl ValueParserFactory for RevisionArg {
3492 type Parser = MapValueParser<NonEmptyStringValueParser, fn(String) -> Self>;
3493
3494 fn value_parser() -> Self::Parser {
3495 NonEmptyStringValueParser::new().map(Self::from)
3496 }
3497}
3498
3499pub fn merge_args_with<'k, 'v, T, U>(
3507 matches: &ArgMatches,
3508 id_values: &[(&'k str, &'v [T])],
3509 mut convert: impl FnMut(&'k str, &'v T) -> U,
3510) -> Vec<U> {
3511 let mut pos_values: Vec<(usize, U)> = Vec::new();
3512 for (id, values) in id_values {
3513 pos_values.extend(itertools::zip_eq(
3514 matches.indices_of(id).into_iter().flatten(),
3515 values.iter().map(|v| convert(id, v)),
3516 ));
3517 }
3518 pos_values.sort_unstable_by_key(|&(pos, _)| pos);
3519 pos_values.into_iter().map(|(_, value)| value).collect()
3520}
3521
3522fn get_string_or_array(
3523 config: &StackedConfig,
3524 key: &'static str,
3525) -> Result<Vec<String>, ConfigGetError> {
3526 config
3527 .get(key)
3528 .map(|string| vec![string])
3529 .or_else(|_| config.get::<Vec<String>>(key))
3530}
3531
3532fn resolve_default_command(
3533 ui: &Ui,
3534 config: &StackedConfig,
3535 app: &Command,
3536 mut string_args: Vec<String>,
3537) -> Result<Vec<String>, CommandError> {
3538 const PRIORITY_FLAGS: &[&str] = &["--help", "-h", "--version", "-V"];
3539
3540 let has_priority_flag = string_args
3541 .iter()
3542 .any(|arg| PRIORITY_FLAGS.contains(&arg.as_str()));
3543 if has_priority_flag {
3544 return Ok(string_args);
3545 }
3546
3547 let app_clone = app
3548 .clone()
3549 .allow_external_subcommands(true)
3550 .ignore_errors(true);
3551 let matches = app_clone.try_get_matches_from(&string_args).ok();
3552
3553 if let Some(matches) = matches
3554 && matches.subcommand_name().is_none()
3555 {
3556 let args = get_string_or_array(config, "ui.default-command").optional()?;
3557 if args.is_none() {
3558 writeln!(
3559 ui.hint_default(),
3560 "Use `jj -h` for a list of available commands."
3561 )?;
3562 writeln!(
3563 ui.hint_no_heading(),
3564 "Run `jj config set --user ui.default-command log` to disable this message."
3565 )?;
3566 }
3567 let default_command = args.unwrap_or_else(|| vec!["log".to_string()]);
3568
3569 string_args.splice(1..1, default_command);
3571 }
3572 Ok(string_args)
3573}
3574
3575fn resolve_aliases(
3576 ui: &Ui,
3577 config: &StackedConfig,
3578 app: &Command,
3579 mut string_args: Vec<String>,
3580) -> Result<Vec<String>, CommandError> {
3581 let defined_aliases: HashSet<_> = config.table_keys("aliases").collect();
3582 let mut resolved_aliases = HashSet::new();
3583 let mut real_commands = HashSet::new();
3584 for command in app.get_subcommands() {
3585 real_commands.insert(command.get_name());
3586 for alias in command.get_all_aliases() {
3587 real_commands.insert(alias);
3588 }
3589 }
3590 for alias in defined_aliases.intersection(&real_commands).sorted() {
3591 writeln!(
3592 ui.warning_default(),
3593 "Cannot define an alias that overrides the built-in command '{alias}'"
3594 )?;
3595 }
3596
3597 loop {
3598 let app_clone = app.clone().allow_external_subcommands(true);
3599 let matches = app_clone.try_get_matches_from(&string_args).ok();
3600 if let Some((command_name, submatches)) = matches.as_ref().and_then(|m| m.subcommand())
3601 && !real_commands.contains(command_name)
3602 {
3603 let alias_name = command_name.to_string();
3604 let alias_args = submatches
3605 .get_many::<OsString>("")
3606 .unwrap_or_default()
3607 .map(|arg| arg.to_str().unwrap().to_string())
3608 .collect_vec();
3609 if resolved_aliases.contains(&*alias_name) {
3610 return Err(user_error(format!(
3611 "Recursive alias definition involving `{alias_name}`"
3612 )));
3613 }
3614 if let Some(&alias_name) = defined_aliases.get(&*alias_name) {
3615 let alias_definition: Vec<String> = config.get(["aliases", alias_name])?;
3616 assert!(string_args.ends_with(&alias_args));
3617 string_args.truncate(string_args.len() - 1 - alias_args.len());
3618 string_args.extend(alias_definition);
3619 string_args.extend_from_slice(&alias_args);
3620 resolved_aliases.insert(alias_name);
3621 continue;
3622 } else {
3623 return Ok(string_args);
3625 }
3626 }
3627 return Ok(string_args);
3629 }
3630}
3631
3632fn parse_early_args(
3634 app: &Command,
3635 args: &[String],
3636) -> Result<(EarlyArgs, Vec<ConfigLayer>), CommandError> {
3637 let early_matches = app
3639 .clone()
3640 .disable_version_flag(true)
3641 .disable_help_flag(true)
3643 .arg(
3645 clap::Arg::new("help")
3646 .short('h')
3647 .long("help")
3648 .global(true)
3649 .action(ArgAction::Count),
3650 )
3651 .ignore_errors(true)
3652 .try_get_matches_from(args)?;
3653 let args = EarlyArgs::from_arg_matches(&early_matches).unwrap();
3654
3655 let mut config_layers = parse_config_args(&args.merged_config_args(&early_matches))?;
3656 let mut layer = ConfigLayer::empty(ConfigSource::CommandArg);
3659 if let Some(choice) = args.color {
3660 layer.set_value("ui.color", choice.to_string()).unwrap();
3661 }
3662 if args.quiet.unwrap_or_default() {
3663 layer.set_value("ui.quiet", true).unwrap();
3664 }
3665 if args.no_pager.unwrap_or_default() {
3666 layer.set_value("ui.paginate", "never").unwrap();
3667 }
3668 if !layer.is_empty() {
3669 config_layers.push(layer);
3670 }
3671 Ok((args, config_layers))
3672}
3673
3674fn handle_shell_completion(
3675 ui: &Ui,
3676 app: &Command,
3677 config: &StackedConfig,
3678 cwd: &Path,
3679) -> Result<(), CommandError> {
3680 let mut orig_args = env::args_os();
3681
3682 let mut args = vec![];
3683 args.extend(orig_args.by_ref().take(2));
3686
3687 if orig_args.len() > 0 {
3691 let complete_index: Option<usize> = env::var("_CLAP_COMPLETE_INDEX")
3692 .ok()
3693 .and_then(|s| s.parse().ok());
3694 let resolved_aliases = if let Some(index) = complete_index {
3695 let pad_len = usize::saturating_sub(index + 1, orig_args.len());
3700 let padded_args = orig_args
3701 .by_ref()
3702 .chain(std::iter::repeat_n(OsString::new(), pad_len));
3703
3704 let mut expanded_args =
3706 expand_args_for_completion(ui, app, padded_args.take(index + 1), config)?;
3707
3708 unsafe {
3712 env::set_var(
3713 "_CLAP_COMPLETE_INDEX",
3714 (expanded_args.len() - 1).to_string(),
3715 );
3716 }
3717
3718 let split_off_padding = expanded_args.split_off(expanded_args.len() - pad_len);
3721 assert!(
3722 split_off_padding.iter().all(|s| s.is_empty()),
3723 "split-off padding should only consist of empty strings but was \
3724 {split_off_padding:?}",
3725 );
3726
3727 expanded_args.extend(to_string_args(orig_args)?);
3729 expanded_args
3730 } else {
3731 expand_args_for_completion(ui, app, orig_args, config)?
3732 };
3733 args.extend(resolved_aliases.into_iter().map(OsString::from));
3734 }
3735 let ran_completion = clap_complete::CompleteEnv::with_factory(|| {
3736 let mut app = app.clone();
3737 hide_short_subcommand_aliases(&mut app);
3740 app.allow_external_subcommands(true)
3742 })
3743 .try_complete(args.iter(), Some(cwd))?;
3744 assert!(
3745 ran_completion,
3746 "This function should not be called without the COMPLETE variable set."
3747 );
3748 Ok(())
3749}
3750
3751fn hide_short_subcommand_aliases(cmd: &mut Command) {
3754 for cmd in cmd.get_subcommands_mut() {
3755 hide_short_subcommand_aliases(cmd);
3756 }
3757 let (short_aliases, new_visible_aliases) = cmd
3758 .get_visible_aliases()
3759 .map(|name| name.to_owned())
3760 .partition::<Vec<_>, _>(|name| cmd.get_name().starts_with(name));
3761 if short_aliases.is_empty() {
3762 return;
3763 }
3764 *cmd = mem::take(cmd)
3765 .visible_alias(None)
3767 .visible_aliases(new_visible_aliases)
3768 .aliases(short_aliases);
3770}
3771
3772pub fn expand_args(
3773 ui: &Ui,
3774 app: &Command,
3775 args_os: impl IntoIterator<Item = OsString>,
3776 config: &StackedConfig,
3777) -> Result<Vec<String>, CommandError> {
3778 let string_args = to_string_args(args_os)?;
3779 let string_args = resolve_default_command(ui, config, app, string_args)?;
3780 resolve_aliases(ui, config, app, string_args)
3781}
3782
3783fn expand_args_for_completion(
3784 ui: &Ui,
3785 app: &Command,
3786 args_os: impl IntoIterator<Item = OsString>,
3787 config: &StackedConfig,
3788) -> Result<Vec<String>, CommandError> {
3789 let string_args = to_string_args(args_os)?;
3790
3791 let mut string_args = resolve_default_command(ui, config, app, string_args)?;
3795
3796 let cursor_arg = string_args.pop();
3799 let mut resolved_args = resolve_aliases(ui, config, app, string_args)?;
3800 resolved_args.extend(cursor_arg);
3801 Ok(resolved_args)
3802}
3803
3804fn to_string_args(
3805 args_os: impl IntoIterator<Item = OsString>,
3806) -> Result<Vec<String>, CommandError> {
3807 args_os
3808 .into_iter()
3809 .map(|arg_os| {
3810 arg_os
3811 .into_string()
3812 .map_err(|_| cli_error("Non-UTF-8 argument"))
3813 })
3814 .collect()
3815}
3816
3817fn parse_args(app: &Command, string_args: &[String]) -> Result<(ArgMatches, Args), clap::Error> {
3818 let matches = app
3819 .clone()
3820 .arg_required_else_help(true)
3821 .subcommand_required(true)
3822 .try_get_matches_from(string_args)?;
3823 let args = Args::from_arg_matches(&matches).unwrap();
3824 Ok((matches, args))
3825}
3826
3827fn command_name(mut matches: &ArgMatches) -> String {
3828 let mut command = String::new();
3829 while let Some((subcommand, new_matches)) = matches.subcommand() {
3830 if !command.is_empty() {
3831 command.push(' ');
3832 }
3833 command.push_str(subcommand);
3834 matches = new_matches;
3835 }
3836 command
3837}
3838
3839pub fn format_template<C: Clone>(ui: &Ui, arg: &C, template: &TemplateRenderer<C>) -> String {
3840 let mut output = vec![];
3841 template
3842 .format(arg, ui.new_formatter(&mut output).as_mut())
3843 .expect("write() to vec backed formatter should never fail");
3844 output.into_string_lossy()
3846}
3847
3848#[must_use]
3850pub struct CliRunner<'a> {
3851 tracing_subscription: TracingSubscription,
3852 app: Command,
3853 config_layers: Vec<ConfigLayer>,
3854 config_migrations: Vec<ConfigMigrationRule>,
3855 store_factories: StoreFactories,
3856 working_copy_factories: WorkingCopyFactories,
3857 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
3858 revset_extensions: RevsetExtensions,
3859 commit_template_extensions: Vec<Arc<dyn CommitTemplateLanguageExtension>>,
3860 operation_template_extensions: Vec<Arc<dyn OperationTemplateLanguageExtension>>,
3861 dispatch_fn: CliDispatchFn<'a>,
3862 dispatch_hook_fns: Vec<CliDispatchHookFn<'a>>,
3863 process_global_args_fns: Vec<ProcessGlobalArgsFn<'a>>,
3864}
3865
3866pub type CliDispatchFn<'a> =
3867 Box<dyn FnOnce(&mut Ui, &CommandHelper) -> Result<(), CommandError> + 'a>;
3868
3869type CliDispatchHookFn<'a> =
3870 Box<dyn FnOnce(&mut Ui, &CommandHelper, CliDispatchFn<'a>) -> Result<(), CommandError> + 'a>;
3871
3872type ProcessGlobalArgsFn<'a> =
3873 Box<dyn FnOnce(&mut Ui, &ArgMatches) -> Result<(), CommandError> + 'a>;
3874
3875impl<'a> CliRunner<'a> {
3876 pub fn init() -> Self {
3879 let tracing_subscription = TracingSubscription::init();
3880 crate::cleanup_guard::init();
3881 Self {
3882 tracing_subscription,
3883 app: crate::commands::default_app(),
3884 config_layers: crate::config::default_config_layers(),
3885 config_migrations: crate::config::default_config_migrations(),
3886 store_factories: StoreFactories::default(),
3887 working_copy_factories: default_working_copy_factories(),
3888 workspace_loader_factory: Box::new(DefaultWorkspaceLoaderFactory),
3889 revset_extensions: Default::default(),
3890 commit_template_extensions: vec![],
3891 operation_template_extensions: vec![],
3892 dispatch_fn: Box::new(crate::commands::run_command),
3893 dispatch_hook_fns: vec![],
3894 process_global_args_fns: vec![],
3895 }
3896 }
3897
3898 pub fn name(mut self, name: &str) -> Self {
3900 self.app = self.app.name(name.to_string());
3901 self
3902 }
3903
3904 pub fn about(mut self, about: &str) -> Self {
3906 self.app = self.app.about(about.to_string());
3907 self
3908 }
3909
3910 pub fn version(mut self, version: &str) -> Self {
3912 self.app = self.app.version(version.to_string());
3913 self
3914 }
3915
3916 pub fn add_extra_config(mut self, layer: ConfigLayer) -> Self {
3921 assert_eq!(layer.source, ConfigSource::Default);
3922 self.config_layers.push(layer);
3923 self
3924 }
3925
3926 pub fn add_extra_config_migration(mut self, rule: ConfigMigrationRule) -> Self {
3928 self.config_migrations.push(rule);
3929 self
3930 }
3931
3932 pub fn add_store_factories(mut self, store_factories: StoreFactories) -> Self {
3934 self.store_factories.merge(store_factories);
3935 self
3936 }
3937
3938 pub fn add_working_copy_factories(
3940 mut self,
3941 working_copy_factories: WorkingCopyFactories,
3942 ) -> Self {
3943 merge_factories_map(&mut self.working_copy_factories, working_copy_factories);
3944 self
3945 }
3946
3947 pub fn set_workspace_loader_factory(
3948 mut self,
3949 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
3950 ) -> Self {
3951 self.workspace_loader_factory = workspace_loader_factory;
3952 self
3953 }
3954
3955 pub fn add_symbol_resolver_extension(
3956 mut self,
3957 symbol_resolver: Box<dyn SymbolResolverExtension>,
3958 ) -> Self {
3959 self.revset_extensions.add_symbol_resolver(symbol_resolver);
3960 self
3961 }
3962
3963 pub fn add_revset_function_extension(
3964 mut self,
3965 name: &'static str,
3966 func: RevsetFunction,
3967 ) -> Self {
3968 self.revset_extensions.add_custom_function(name, func);
3969 self
3970 }
3971
3972 pub fn add_commit_template_extension(
3973 mut self,
3974 commit_template_extension: Box<dyn CommitTemplateLanguageExtension>,
3975 ) -> Self {
3976 self.commit_template_extensions
3977 .push(commit_template_extension.into());
3978 self
3979 }
3980
3981 pub fn add_operation_template_extension(
3982 mut self,
3983 operation_template_extension: Box<dyn OperationTemplateLanguageExtension>,
3984 ) -> Self {
3985 self.operation_template_extensions
3986 .push(operation_template_extension.into());
3987 self
3988 }
3989
3990 pub fn add_dispatch_hook<F>(mut self, dispatch_hook_fn: F) -> Self
3994 where
3995 F: FnOnce(&mut Ui, &CommandHelper, CliDispatchFn) -> Result<(), CommandError> + 'a,
3996 {
3997 self.dispatch_hook_fns.push(Box::new(dispatch_hook_fn));
3998 self
3999 }
4000
4001 pub fn add_subcommand<C, F>(mut self, custom_dispatch_fn: F) -> Self
4003 where
4004 C: clap::Subcommand,
4005 F: FnOnce(&mut Ui, &CommandHelper, C) -> Result<(), CommandError> + 'a,
4006 {
4007 let old_dispatch_fn = self.dispatch_fn;
4008 let new_dispatch_fn =
4009 move |ui: &mut Ui, command_helper: &CommandHelper| match C::from_arg_matches(
4010 command_helper.matches(),
4011 ) {
4012 Ok(command) => custom_dispatch_fn(ui, command_helper, command),
4013 Err(_) => old_dispatch_fn(ui, command_helper),
4014 };
4015 self.app = C::augment_subcommands(self.app);
4016 self.dispatch_fn = Box::new(new_dispatch_fn);
4017 self
4018 }
4019
4020 pub fn add_global_args<A, F>(mut self, process_before: F) -> Self
4022 where
4023 A: clap::Args,
4024 F: FnOnce(&mut Ui, A) -> Result<(), CommandError> + 'a,
4025 {
4026 let process_global_args_fn = move |ui: &mut Ui, matches: &ArgMatches| {
4027 let custom_args = A::from_arg_matches(matches).unwrap();
4028 process_before(ui, custom_args)
4029 };
4030 self.app = A::augment_args(self.app);
4031 self.process_global_args_fns
4032 .push(Box::new(process_global_args_fn));
4033 self
4034 }
4035
4036 #[instrument(skip_all)]
4037 fn run_internal(self, ui: &mut Ui, mut raw_config: RawConfig) -> Result<(), CommandError> {
4038 let cwd = env::current_dir()
4041 .and_then(dunce::canonicalize)
4042 .map_err(|_| {
4043 user_error("Could not determine current directory").hinted(
4044 "Did you update to a commit where the directory doesn't exist or can't be \
4045 accessed?",
4046 )
4047 })?;
4048 let mut config_env = ConfigEnv::from_environment();
4049 let mut last_config_migration_descriptions = Vec::new();
4050 let mut migrate_config = |config: &mut StackedConfig| -> Result<(), CommandError> {
4051 last_config_migration_descriptions =
4052 jj_lib::config::migrate(config, &self.config_migrations)?;
4053 Ok(())
4054 };
4055
4056 let maybe_cwd_workspace_loader = self
4062 .workspace_loader_factory
4063 .create(find_workspace_dir(&cwd))
4064 .map_err(|err| map_workspace_load_error(err, Some(".")));
4065 config_env.reload_user_config(&mut raw_config)?;
4066 if let Ok(loader) = &maybe_cwd_workspace_loader {
4067 config_env.reset_repo_path(loader.repo_path());
4068 config_env.reload_repo_config(ui, &mut raw_config)?;
4069 config_env.reset_workspace_path(loader.workspace_root());
4070 config_env.reload_workspace_config(ui, &mut raw_config)?;
4071 }
4072 let mut config = config_env.resolve_config(&raw_config)?;
4073 migrate_config(&mut config)?;
4074 ui.reset(&config)?;
4075
4076 if env::var_os("COMPLETE").is_some_and(|v| !v.is_empty() && v != "0") {
4077 return handle_shell_completion(&Ui::null(), &self.app, &config, &cwd);
4078 }
4079
4080 let string_args = expand_args(ui, &self.app, env::args_os(), &config)?;
4081 let (args, config_layers) = parse_early_args(&self.app, &string_args)?;
4082 if !config_layers.is_empty() {
4083 raw_config.as_mut().extend_layers(config_layers);
4084 config = config_env.resolve_config(&raw_config)?;
4085 migrate_config(&mut config)?;
4086 ui.reset(&config)?;
4087 }
4088
4089 if args.has_config_args() {
4090 warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
4091 }
4092
4093 let (matches, args) = parse_args(&self.app, &string_args)
4094 .map_err(|err| map_clap_cli_error(err, ui, &config))?;
4095 if args.global_args.debug {
4096 self.tracing_subscription.enable_debug_logging()?;
4098 }
4099 for process_global_args_fn in self.process_global_args_fns {
4100 process_global_args_fn(ui, &matches)?;
4101 }
4102 config_env.set_command_name(command_name(&matches));
4103
4104 let maybe_workspace_loader = if let Some(path) = &args.global_args.repository {
4105 let abs_path = cwd.join(path);
4107 let abs_path = dunce::canonicalize(&abs_path).unwrap_or(abs_path);
4108 let loader = self
4110 .workspace_loader_factory
4111 .create(&abs_path)
4112 .map_err(|err| map_workspace_load_error(err, Some(path)))?;
4113 config_env.reset_repo_path(loader.repo_path());
4114 config_env.reload_repo_config(ui, &mut raw_config)?;
4115 config_env.reset_workspace_path(loader.workspace_root());
4116 config_env.reload_workspace_config(ui, &mut raw_config)?;
4117 Ok(loader)
4118 } else {
4119 maybe_cwd_workspace_loader
4120 };
4121
4122 config = config_env.resolve_config(&raw_config)?;
4124 migrate_config(&mut config)?;
4125 ui.reset(&config)?;
4126
4127 for (source, desc) in &last_config_migration_descriptions {
4129 let source_str = match source {
4130 ConfigSource::Default => "default-provided",
4131 ConfigSource::EnvBase | ConfigSource::EnvOverrides => "environment-provided",
4132 ConfigSource::User => "user-level",
4133 ConfigSource::Repo => "repo-level",
4134 ConfigSource::Workspace => "workspace-level",
4135 ConfigSource::CommandArg => "CLI-provided",
4136 };
4137 writeln!(
4138 ui.warning_default(),
4139 "Deprecated {source_str} config: {desc}"
4140 )?;
4141 }
4142
4143 if args.global_args.repository.is_some() {
4144 warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
4145 }
4146
4147 let settings = UserSettings::from_config(config)?;
4148 let command_helper_data = CommandHelperData {
4149 app: self.app,
4150 cwd,
4151 string_args,
4152 matches,
4153 global_args: args.global_args,
4154 config_env,
4155 config_migrations: self.config_migrations,
4156 raw_config,
4157 settings,
4158 revset_extensions: self.revset_extensions.into(),
4159 commit_template_extensions: self.commit_template_extensions,
4160 operation_template_extensions: self.operation_template_extensions,
4161 maybe_workspace_loader,
4162 store_factories: self.store_factories,
4163 working_copy_factories: self.working_copy_factories,
4164 workspace_loader_factory: self.workspace_loader_factory,
4165 };
4166 let command_helper = CommandHelper {
4167 data: Rc::new(command_helper_data),
4168 };
4169 let dispatch_fn = self.dispatch_hook_fns.into_iter().fold(
4170 self.dispatch_fn,
4171 |old_dispatch_fn, dispatch_hook_fn| {
4172 Box::new(move |ui: &mut Ui, command_helper: &CommandHelper| {
4173 dispatch_hook_fn(ui, command_helper, old_dispatch_fn)
4174 })
4175 },
4176 );
4177 (dispatch_fn)(ui, &command_helper)
4178 }
4179
4180 #[must_use]
4181 #[instrument(skip(self))]
4182 pub fn run(mut self) -> u8 {
4183 crossterm::style::force_color_output(true);
4185 let config = config_from_environment(self.config_layers.drain(..));
4186 let mut ui = Ui::with_config(config.as_ref())
4189 .expect("default config should be valid, env vars are stringly typed");
4190 let result = self.run_internal(&mut ui, config);
4191 let exit_code = handle_command_result(&mut ui, result);
4192 ui.finalize_pager();
4193 exit_code
4194 }
4195}
4196
4197fn map_clap_cli_error(err: clap::Error, ui: &Ui, config: &StackedConfig) -> CommandError {
4198 if let Some(ContextValue::String(cmd)) = err.get(ContextKind::InvalidSubcommand) {
4199 let remove_useless_error_context = |mut err: clap::Error| {
4200 err.remove(ContextKind::SuggestedSubcommand);
4203 err.remove(ContextKind::Suggested); err.remove(ContextKind::Usage); err
4206 };
4207 match cmd.as_str() {
4208 "clone" | "init" => {
4211 let cmd = cmd.clone();
4212 return CommandError::from(remove_useless_error_context(err))
4213 .hinted(format!(
4214 "You probably want `jj git {cmd}`. See also `jj help git`."
4215 ))
4216 .hinted(format!(
4217 r#"You can configure `aliases.{cmd} = ["git", "{cmd}"]` if you want `jj {cmd}` to work and always use the Git backend."#
4218 ));
4219 }
4220 "amend" => {
4221 return CommandError::from(remove_useless_error_context(err))
4222 .hinted(
4223 r#"You probably want `jj squash`. You can configure `aliases.amend = ["squash"]` if you want `jj amend` to work."#);
4224 }
4225 _ => {}
4226 }
4227 }
4228 if let (Some(ContextValue::String(arg)), Some(ContextValue::String(value))) = (
4229 err.get(ContextKind::InvalidArg),
4230 err.get(ContextKind::InvalidValue),
4231 ) && arg.as_str() == "--template <TEMPLATE>"
4232 && value.is_empty()
4233 {
4234 if let Ok(template_aliases) = load_template_aliases(ui, config) {
4236 return CommandError::from(err).hinted(format_template_aliases_hint(&template_aliases));
4237 }
4238 }
4239 CommandError::from(err)
4240}
4241
4242fn format_template_aliases_hint(template_aliases: &TemplateAliasesMap) -> String {
4243 let mut hint = String::from("The following template aliases are defined:\n");
4244 hint.push_str(
4245 &template_aliases
4246 .symbol_names()
4247 .sorted_unstable()
4248 .map(|name| format!("- {name}"))
4249 .join("\n"),
4250 );
4251 hint
4252}
4253
4254fn warn_if_args_mismatch(
4256 ui: &Ui,
4257 app: &Command,
4258 config: &StackedConfig,
4259 expected_args: &[String],
4260) -> Result<(), CommandError> {
4261 let new_string_args = expand_args(ui, app, env::args_os(), config).ok();
4262 if new_string_args.as_deref() != Some(expected_args) {
4263 writeln!(
4264 ui.warning_default(),
4265 "Command aliases cannot be loaded from -R/--repository path or --config/--config-file \
4266 arguments."
4267 )?;
4268 }
4269 Ok(())
4270}
4271
4272#[cfg(test)]
4273mod tests {
4274 use clap::CommandFactory as _;
4275
4276 use super::*;
4277
4278 #[derive(clap::Parser, Clone, Debug)]
4279 pub struct TestArgs {
4280 #[arg(long)]
4281 pub foo: Vec<u32>,
4282 #[arg(long)]
4283 pub bar: Vec<u32>,
4284 #[arg(long)]
4285 pub baz: bool,
4286 }
4287
4288 #[test]
4289 fn test_merge_args_with() {
4290 let command = TestArgs::command();
4291 let parse = |args: &[&str]| -> Vec<(&'static str, u32)> {
4292 let matches = command.clone().try_get_matches_from(args).unwrap();
4293 let args = TestArgs::from_arg_matches(&matches).unwrap();
4294 merge_args_with(
4295 &matches,
4296 &[("foo", &args.foo), ("bar", &args.bar)],
4297 |id, value| (id, *value),
4298 )
4299 };
4300
4301 assert_eq!(parse(&["jj"]), vec![]);
4302 assert_eq!(parse(&["jj", "--foo=1"]), vec![("foo", 1)]);
4303 assert_eq!(
4304 parse(&["jj", "--foo=1", "--bar=2"]),
4305 vec![("foo", 1), ("bar", 2)]
4306 );
4307 assert_eq!(
4308 parse(&["jj", "--foo=1", "--baz", "--bar=2", "--foo", "3"]),
4309 vec![("foo", 1), ("bar", 2), ("foo", 3)]
4310 );
4311 }
4312}