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