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