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(self.change_id())";
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 let git_import_export_lock = self
1127 .lock_git_import_export()
1128 .map_err(snapshot_command_error)?;
1129
1130 if self.working_copy_shared_with_git {
1133 let repo = self.repo().clone();
1134 let op_heads_store = repo.loader().op_heads_store();
1135 let op_heads = op_heads_store
1136 .get_op_heads()
1137 .block_on()
1138 .map_err(snapshot_command_error)?;
1139 if std::slice::from_ref(repo.op_id()) != op_heads {
1140 let op = self
1141 .env
1142 .command
1143 .resolve_operation(ui, repo.loader())
1144 .map_err(snapshot_command_error)?;
1145 let current_repo = repo.loader().load_at(&op).map_err(snapshot_command_error)?;
1146 self.user_repo = ReadonlyUserRepo::new(current_repo);
1147 }
1148 }
1149
1150 #[cfg(feature = "git")]
1151 if self.working_copy_shared_with_git {
1152 self.import_git_head(ui, &git_import_export_lock)
1153 .map_err(snapshot_command_error)?;
1154 }
1155 let stats = self.snapshot_working_copy(ui)?;
1160
1161 #[cfg(feature = "git")]
1163 if self.working_copy_shared_with_git {
1164 self.import_git_refs(ui, &git_import_export_lock)
1165 .map_err(snapshot_command_error)?;
1166 }
1167 Ok(stats)
1168 }
1169
1170 #[instrument(skip_all)]
1173 pub fn maybe_snapshot(&mut self, ui: &Ui) -> Result<(), CommandError> {
1174 let stats = self
1175 .maybe_snapshot_impl(ui)
1176 .map_err(|err| err.into_command_error())?;
1177 print_snapshot_stats(ui, &stats, self.env().path_converter())?;
1178 Ok(())
1179 }
1180
1181 #[cfg(feature = "git")]
1188 #[instrument(skip_all)]
1189 fn import_git_head(
1190 &mut self,
1191 ui: &Ui,
1192 git_import_export_lock: &GitImportExportLock,
1193 ) -> Result<(), CommandError> {
1194 assert!(self.may_update_working_copy);
1195 let mut tx = self.start_transaction();
1196 jj_lib::git::import_head(tx.repo_mut())?;
1197 if !tx.repo().has_changes() {
1198 return Ok(());
1199 }
1200
1201 let mut tx = tx.into_inner();
1202 let old_git_head = self.repo().view().git_head().clone();
1203 let new_git_head = tx.repo().view().git_head().clone();
1204 if let Some(new_git_head_id) = new_git_head.as_normal() {
1205 let workspace_name = self.workspace_name().to_owned();
1206 let new_git_head_commit = tx.repo().store().get_commit(new_git_head_id)?;
1207 let wc_commit = tx
1208 .repo_mut()
1209 .check_out(workspace_name, &new_git_head_commit)?;
1210 let mut locked_ws = self.workspace.start_working_copy_mutation()?;
1211 locked_ws.locked_wc().reset(&wc_commit).block_on()?;
1215 tx.repo_mut().rebase_descendants()?;
1216 self.user_repo = ReadonlyUserRepo::new(tx.commit("import git head")?);
1217 locked_ws.finish(self.user_repo.repo.op_id().clone())?;
1218 if old_git_head.is_present() {
1219 writeln!(
1220 ui.status(),
1221 "Reset the working copy parent to the new Git HEAD."
1222 )?;
1223 } else {
1224 }
1226 } else {
1227 self.finish_transaction(ui, tx, "import git head", git_import_export_lock)?;
1229 }
1230 Ok(())
1231 }
1232
1233 #[cfg(feature = "git")]
1242 #[instrument(skip_all)]
1243 fn import_git_refs(
1244 &mut self,
1245 ui: &Ui,
1246 git_import_export_lock: &GitImportExportLock,
1247 ) -> Result<(), CommandError> {
1248 use jj_lib::git;
1249 let git_settings = git::GitSettings::from_settings(self.settings())?;
1250 let mut tx = self.start_transaction();
1251 let stats = git::import_refs(tx.repo_mut(), &git_settings)?;
1252 crate::git_util::print_git_import_stats(ui, tx.repo(), &stats, false)?;
1253 if !tx.repo().has_changes() {
1254 return Ok(());
1255 }
1256
1257 let mut tx = tx.into_inner();
1258 let num_rebased = tx.repo_mut().rebase_descendants()?;
1260 if num_rebased > 0 {
1261 writeln!(
1262 ui.status(),
1263 "Rebased {num_rebased} descendant commits off of commits rewritten from git"
1264 )?;
1265 }
1266 self.finish_transaction(ui, tx, "import git refs", git_import_export_lock)?;
1267 writeln!(
1268 ui.status(),
1269 "Done importing changes from the underlying Git repo."
1270 )?;
1271 Ok(())
1272 }
1273
1274 pub fn repo(&self) -> &Arc<ReadonlyRepo> {
1275 &self.user_repo.repo
1276 }
1277
1278 pub fn repo_path(&self) -> &Path {
1279 self.workspace.repo_path()
1280 }
1281
1282 pub fn workspace(&self) -> &Workspace {
1283 &self.workspace
1284 }
1285
1286 pub fn working_copy(&self) -> &dyn WorkingCopy {
1287 self.workspace.working_copy()
1288 }
1289
1290 pub fn env(&self) -> &WorkspaceCommandEnvironment {
1291 &self.env
1292 }
1293
1294 pub fn unchecked_start_working_copy_mutation(
1295 &mut self,
1296 ) -> Result<(LockedWorkspace<'_>, Commit), CommandError> {
1297 self.check_working_copy_writable()?;
1298 let wc_commit = if let Some(wc_commit_id) = self.get_wc_commit_id() {
1299 self.repo().store().get_commit(wc_commit_id)?
1300 } else {
1301 return Err(user_error("Nothing checked out in this workspace"));
1302 };
1303
1304 let locked_ws = self.workspace.start_working_copy_mutation()?;
1305
1306 Ok((locked_ws, wc_commit))
1307 }
1308
1309 pub fn start_working_copy_mutation(
1310 &mut self,
1311 ) -> Result<(LockedWorkspace<'_>, Commit), CommandError> {
1312 let (mut locked_ws, wc_commit) = self.unchecked_start_working_copy_mutation()?;
1313 if wc_commit.tree_ids() != locked_ws.locked_wc().old_tree().tree_ids() {
1314 return Err(user_error("Concurrent working copy operation. Try again."));
1315 }
1316 Ok((locked_ws, wc_commit))
1317 }
1318
1319 fn create_and_check_out_recovery_commit(
1320 &mut self,
1321 ui: &Ui,
1322 ) -> Result<SnapshotStats, CommandError> {
1323 self.check_working_copy_writable()?;
1324
1325 let workspace_name = self.workspace_name().to_owned();
1326 let mut locked_ws = self.workspace.start_working_copy_mutation()?;
1327 let (repo, new_commit) = working_copy::create_and_check_out_recovery_commit(
1328 locked_ws.locked_wc(),
1329 &self.user_repo.repo,
1330 workspace_name,
1331 "RECOVERY COMMIT FROM `jj workspace update-stale`
1332
1333This commit contains changes that were written to the working copy by an
1334operation that was subsequently lost (or was at least unavailable when you ran
1335`jj workspace update-stale`). Because the operation was lost, we don't know
1336what the parent commits are supposed to be. That means that the diff compared
1337to the current parents may contain changes from multiple commits.
1338",
1339 )?;
1340
1341 writeln!(
1342 ui.status(),
1343 "Created and checked out recovery commit {}",
1344 short_commit_hash(new_commit.id())
1345 )?;
1346 locked_ws.finish(repo.op_id().clone())?;
1347 self.user_repo = ReadonlyUserRepo::new(repo);
1348
1349 self.maybe_snapshot_impl(ui)
1350 .map_err(|err| err.into_command_error())
1351 }
1352
1353 pub fn workspace_root(&self) -> &Path {
1354 self.workspace.workspace_root()
1355 }
1356
1357 pub fn workspace_name(&self) -> &WorkspaceName {
1358 self.workspace.workspace_name()
1359 }
1360
1361 pub fn get_wc_commit_id(&self) -> Option<&CommitId> {
1362 self.repo().view().get_wc_commit_id(self.workspace_name())
1363 }
1364
1365 pub fn working_copy_shared_with_git(&self) -> bool {
1366 self.working_copy_shared_with_git
1367 }
1368
1369 pub fn format_file_path(&self, file: &RepoPath) -> String {
1370 self.path_converter().format_file_path(file)
1371 }
1372
1373 pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
1376 self.path_converter().parse_file_path(input)
1377 }
1378
1379 pub fn parse_file_patterns(
1381 &self,
1382 ui: &Ui,
1383 values: &[String],
1384 ) -> Result<FilesetExpression, CommandError> {
1385 if values.is_empty() {
1389 Ok(FilesetExpression::all())
1390 } else {
1391 self.parse_union_filesets(ui, values)
1392 }
1393 }
1394
1395 pub fn parse_union_filesets(
1397 &self,
1398 ui: &Ui,
1399 file_args: &[String], ) -> Result<FilesetExpression, CommandError> {
1401 let mut diagnostics = FilesetDiagnostics::new();
1402 let expressions: Vec<_> = file_args
1403 .iter()
1404 .map(|arg| fileset::parse_maybe_bare(&mut diagnostics, arg, self.path_converter()))
1405 .try_collect()?;
1406 print_parse_diagnostics(ui, "In fileset expression", &diagnostics)?;
1407 Ok(FilesetExpression::union_all(expressions))
1408 }
1409
1410 pub fn auto_tracking_matcher(&self, ui: &Ui) -> Result<Box<dyn Matcher>, CommandError> {
1411 let mut diagnostics = FilesetDiagnostics::new();
1412 let pattern = self.settings().get_string("snapshot.auto-track")?;
1413 let expression = fileset::parse(
1414 &mut diagnostics,
1415 &pattern,
1416 &RepoPathUiConverter::Fs {
1417 cwd: "".into(),
1418 base: "".into(),
1419 },
1420 )?;
1421 print_parse_diagnostics(ui, "In `snapshot.auto-track`", &diagnostics)?;
1422 Ok(expression.to_matcher())
1423 }
1424
1425 pub fn snapshot_options_with_start_tracking_matcher<'a>(
1426 &self,
1427 start_tracking_matcher: &'a dyn Matcher,
1428 ) -> Result<SnapshotOptions<'a>, CommandError> {
1429 let base_ignores = self.base_ignores()?;
1430 let HumanByteSize(mut max_new_file_size) = self
1431 .settings()
1432 .get_value_with("snapshot.max-new-file-size", TryInto::try_into)?;
1433 if max_new_file_size == 0 {
1434 max_new_file_size = u64::MAX;
1435 }
1436 Ok(SnapshotOptions {
1437 base_ignores,
1438 progress: None,
1439 start_tracking_matcher,
1440 force_tracking_matcher: &NothingMatcher,
1441 max_new_file_size,
1442 })
1443 }
1444
1445 pub(crate) fn path_converter(&self) -> &RepoPathUiConverter {
1446 self.env.path_converter()
1447 }
1448
1449 #[cfg(not(feature = "git"))]
1450 pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1451 Ok(GitIgnoreFile::empty())
1452 }
1453
1454 #[cfg(feature = "git")]
1455 #[instrument(skip_all)]
1456 pub fn base_ignores(&self) -> Result<Arc<GitIgnoreFile>, GitIgnoreError> {
1457 let get_excludes_file_path = |config: &gix::config::File| -> Option<PathBuf> {
1458 if let Some(value) = config.string("core.excludesFile") {
1461 let path = str::from_utf8(&value)
1462 .ok()
1463 .map(jj_lib::file_util::expand_home_path)?;
1464 Some(self.workspace_root().join(path))
1467 } else {
1468 xdg_config_home().ok().map(|x| x.join("git").join("ignore"))
1469 }
1470 };
1471
1472 fn xdg_config_home() -> Result<PathBuf, std::env::VarError> {
1473 if let Ok(x) = std::env::var("XDG_CONFIG_HOME")
1474 && !x.is_empty()
1475 {
1476 return Ok(PathBuf::from(x));
1477 }
1478 std::env::var("HOME").map(|x| Path::new(&x).join(".config"))
1479 }
1480
1481 let mut git_ignores = GitIgnoreFile::empty();
1482 if let Ok(git_backend) = jj_lib::git::get_git_backend(self.repo().store()) {
1483 let git_repo = git_backend.git_repo();
1484 if let Some(excludes_file_path) = get_excludes_file_path(&git_repo.config_snapshot()) {
1485 git_ignores = git_ignores.chain_with_file("", excludes_file_path)?;
1486 }
1487 git_ignores = git_ignores
1488 .chain_with_file("", git_backend.git_repo_path().join("info").join("exclude"))?;
1489 } else if let Ok(git_config) = gix::config::File::from_globals()
1490 && let Some(excludes_file_path) = get_excludes_file_path(&git_config)
1491 {
1492 git_ignores = git_ignores.chain_with_file("", excludes_file_path)?;
1493 }
1494 Ok(git_ignores)
1495 }
1496
1497 pub fn diff_renderer(&self, formats: Vec<DiffFormat>) -> DiffRenderer<'_> {
1499 DiffRenderer::new(
1500 self.repo().as_ref(),
1501 self.path_converter(),
1502 self.env.conflict_marker_style(),
1503 formats,
1504 )
1505 }
1506
1507 pub fn diff_renderer_for(
1509 &self,
1510 args: &DiffFormatArgs,
1511 ) -> Result<DiffRenderer<'_>, CommandError> {
1512 let formats = diff_util::diff_formats_for(self.settings(), args)?;
1513 Ok(self.diff_renderer(formats))
1514 }
1515
1516 pub fn diff_renderer_for_log(
1520 &self,
1521 args: &DiffFormatArgs,
1522 patch: bool,
1523 ) -> Result<Option<DiffRenderer<'_>>, CommandError> {
1524 let formats = diff_util::diff_formats_for_log(self.settings(), args, patch)?;
1525 Ok((!formats.is_empty()).then(|| self.diff_renderer(formats)))
1526 }
1527
1528 pub fn diff_editor(
1532 &self,
1533 ui: &Ui,
1534 tool_name: Option<&str>,
1535 ) -> Result<DiffEditor, CommandError> {
1536 let base_ignores = self.base_ignores()?;
1537 let conflict_marker_style = self.env.conflict_marker_style();
1538 if let Some(name) = tool_name {
1539 Ok(DiffEditor::with_name(
1540 name,
1541 self.settings(),
1542 base_ignores,
1543 conflict_marker_style,
1544 )?)
1545 } else {
1546 Ok(DiffEditor::from_settings(
1547 ui,
1548 self.settings(),
1549 base_ignores,
1550 conflict_marker_style,
1551 )?)
1552 }
1553 }
1554
1555 pub fn diff_selector(
1559 &self,
1560 ui: &Ui,
1561 tool_name: Option<&str>,
1562 force_interactive: bool,
1563 ) -> Result<DiffSelector, CommandError> {
1564 if tool_name.is_some() || force_interactive {
1565 Ok(DiffSelector::Interactive(self.diff_editor(ui, tool_name)?))
1566 } else {
1567 Ok(DiffSelector::NonInteractive)
1568 }
1569 }
1570
1571 pub fn merge_editor(
1575 &self,
1576 ui: &Ui,
1577 tool_name: Option<&str>,
1578 ) -> Result<MergeEditor, MergeToolConfigError> {
1579 let conflict_marker_style = self.env.conflict_marker_style();
1580 if let Some(name) = tool_name {
1581 MergeEditor::with_name(
1582 name,
1583 self.settings(),
1584 self.path_converter().clone(),
1585 conflict_marker_style,
1586 )
1587 } else {
1588 MergeEditor::from_settings(
1589 ui,
1590 self.settings(),
1591 self.path_converter().clone(),
1592 conflict_marker_style,
1593 )
1594 }
1595 }
1596
1597 pub fn text_editor(&self) -> Result<TextEditor, ConfigGetError> {
1599 TextEditor::from_settings(self.settings())
1600 }
1601
1602 pub fn resolve_single_op(&self, op_str: &str) -> Result<Operation, OpsetEvaluationError> {
1603 op_walk::resolve_op_with_repo(self.repo(), op_str)
1604 }
1605
1606 pub fn resolve_single_rev(
1609 &self,
1610 ui: &Ui,
1611 revision_arg: &RevisionArg,
1612 ) -> Result<Commit, CommandError> {
1613 let expression = self.parse_revset(ui, revision_arg)?;
1614 revset_util::evaluate_revset_to_single_commit(revision_arg.as_ref(), &expression, || {
1615 self.commit_summary_template()
1616 })
1617 }
1618
1619 pub fn resolve_some_revsets_default_single(
1622 &self,
1623 ui: &Ui,
1624 revision_args: &[RevisionArg],
1625 ) -> Result<IndexSet<CommitId>, CommandError> {
1626 let mut all_commits = IndexSet::new();
1627 for revision_arg in revision_args {
1628 let (expression, modifier) = self.parse_revset_with_modifier(ui, revision_arg)?;
1629 let all = match modifier {
1630 Some(RevsetModifier::All) => true,
1631 None => self.settings().get_bool("ui.always-allow-large-revsets")?,
1632 };
1633 if all {
1634 for commit_id in expression.evaluate_to_commit_ids()? {
1635 all_commits.insert(commit_id?);
1636 }
1637 } else {
1638 let commit = revset_util::evaluate_revset_to_single_commit(
1639 revision_arg.as_ref(),
1640 &expression,
1641 || self.commit_summary_template(),
1642 )?;
1643 if !all_commits.insert(commit.id().clone()) {
1644 let commit_hash = short_commit_hash(commit.id());
1645 return Err(user_error(format!(
1646 r#"More than one revset resolved to revision {commit_hash}"#,
1647 )));
1648 }
1649 }
1650 }
1651 if all_commits.is_empty() {
1652 Err(user_error("Empty revision set"))
1653 } else {
1654 Ok(all_commits)
1655 }
1656 }
1657
1658 pub fn parse_revset(
1659 &self,
1660 ui: &Ui,
1661 revision_arg: &RevisionArg,
1662 ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1663 let (expression, modifier) = self.parse_revset_with_modifier(ui, revision_arg)?;
1664 let (None | Some(RevsetModifier::All)) = modifier;
1667 Ok(expression)
1668 }
1669
1670 fn parse_revset_with_modifier(
1671 &self,
1672 ui: &Ui,
1673 revision_arg: &RevisionArg,
1674 ) -> Result<(RevsetExpressionEvaluator<'_>, Option<RevsetModifier>), CommandError> {
1675 let mut diagnostics = RevsetDiagnostics::new();
1676 let context = self.env.revset_parse_context();
1677 let (expression, modifier) =
1678 revset::parse_with_modifier(&mut diagnostics, revision_arg.as_ref(), &context)?;
1679 print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1680 Ok((self.attach_revset_evaluator(expression), modifier))
1681 }
1682
1683 pub fn parse_union_revsets(
1685 &self,
1686 ui: &Ui,
1687 revision_args: &[RevisionArg],
1688 ) -> Result<RevsetExpressionEvaluator<'_>, CommandError> {
1689 let mut diagnostics = RevsetDiagnostics::new();
1690 let context = self.env.revset_parse_context();
1691 let expressions: Vec<_> = revision_args
1692 .iter()
1693 .map(|arg| revset::parse_with_modifier(&mut diagnostics, arg.as_ref(), &context))
1694 .map_ok(|(expression, None | Some(RevsetModifier::All))| expression)
1695 .try_collect()?;
1696 print_parse_diagnostics(ui, "In revset expression", &diagnostics)?;
1697 let expression = RevsetExpression::union_all(&expressions);
1698 Ok(self.attach_revset_evaluator(expression))
1699 }
1700
1701 pub fn attach_revset_evaluator(
1702 &self,
1703 expression: Arc<UserRevsetExpression>,
1704 ) -> RevsetExpressionEvaluator<'_> {
1705 RevsetExpressionEvaluator::new(
1706 self.repo().as_ref(),
1707 self.env.command.revset_extensions().clone(),
1708 self.id_prefix_context(),
1709 expression,
1710 )
1711 }
1712
1713 pub fn id_prefix_context(&self) -> &IdPrefixContext {
1714 self.user_repo
1715 .id_prefix_context
1716 .get_or_init(|| self.env.new_id_prefix_context())
1717 }
1718
1719 pub fn parse_template<'a, C, L>(
1721 &self,
1722 ui: &Ui,
1723 language: &L,
1724 template_text: &str,
1725 ) -> Result<TemplateRenderer<'a, C>, CommandError>
1726 where
1727 C: Clone + 'a,
1728 L: TemplateLanguage<'a> + ?Sized,
1729 L::Property: WrapTemplateProperty<'a, C>,
1730 {
1731 self.env.parse_template(ui, language, template_text)
1732 }
1733
1734 fn reparse_valid_template<'a, C, L>(
1736 &self,
1737 language: &L,
1738 template_text: &str,
1739 ) -> TemplateRenderer<'a, C>
1740 where
1741 C: Clone + 'a,
1742 L: TemplateLanguage<'a> + ?Sized,
1743 L::Property: WrapTemplateProperty<'a, C>,
1744 {
1745 template_builder::parse(
1746 language,
1747 &mut TemplateDiagnostics::new(),
1748 template_text,
1749 &self.env.template_aliases_map,
1750 )
1751 .expect("parse error should be confined by WorkspaceCommandHelper::new()")
1752 }
1753
1754 pub fn parse_commit_template(
1756 &self,
1757 ui: &Ui,
1758 template_text: &str,
1759 ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
1760 let language = self.commit_template_language();
1761 self.parse_template(ui, &language, template_text)
1762 }
1763
1764 pub fn parse_operation_template(
1766 &self,
1767 ui: &Ui,
1768 template_text: &str,
1769 ) -> Result<TemplateRenderer<'_, Operation>, CommandError> {
1770 let language = self.operation_template_language();
1771 self.parse_template(ui, &language, template_text)
1772 }
1773
1774 pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
1776 self.env
1777 .commit_template_language(self.repo().as_ref(), self.id_prefix_context())
1778 }
1779
1780 pub fn operation_template_language(&self) -> OperationTemplateLanguage {
1782 OperationTemplateLanguage::new(
1783 self.workspace.repo_loader(),
1784 Some(self.repo().op_id()),
1785 self.env.operation_template_extensions(),
1786 )
1787 }
1788
1789 pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
1791 let language = self.commit_template_language();
1792 self.reparse_valid_template(&language, &self.commit_summary_template_text)
1793 .labeled(["commit"])
1794 }
1795
1796 pub fn operation_summary_template(&self) -> TemplateRenderer<'_, Operation> {
1798 let language = self.operation_template_language();
1799 self.reparse_valid_template(&language, &self.op_summary_template_text)
1800 .labeled(["operation"])
1801 }
1802
1803 pub fn short_change_id_template(&self) -> TemplateRenderer<'_, Commit> {
1804 let language = self.commit_template_language();
1805 self.reparse_valid_template(&language, SHORT_CHANGE_ID_TEMPLATE_TEXT)
1806 .labeled(["commit"])
1807 }
1808
1809 pub fn format_commit_summary(&self, commit: &Commit) -> String {
1814 let output = self.commit_summary_template().format_plain_text(commit);
1815 output.into_string_lossy()
1816 }
1817
1818 #[instrument(skip_all)]
1822 pub fn write_commit_summary(
1823 &self,
1824 formatter: &mut dyn Formatter,
1825 commit: &Commit,
1826 ) -> std::io::Result<()> {
1827 self.commit_summary_template().format(commit, formatter)
1828 }
1829
1830 pub fn check_rewritable<'a>(
1831 &self,
1832 commits: impl IntoIterator<Item = &'a CommitId>,
1833 ) -> Result<(), CommandError> {
1834 let commit_ids = commits.into_iter().cloned().collect_vec();
1835 let to_rewrite_expr = RevsetExpression::commits(commit_ids);
1836 self.check_rewritable_expr(&to_rewrite_expr)
1837 }
1838
1839 pub fn check_rewritable_expr(
1840 &self,
1841 to_rewrite_expr: &Arc<ResolvedRevsetExpression>,
1842 ) -> Result<(), CommandError> {
1843 let repo = self.repo().as_ref();
1844 let Some(commit_id) = self.env.find_immutable_commit(repo, to_rewrite_expr)? else {
1845 return Ok(());
1846 };
1847 let error = if &commit_id == repo.store().root_commit_id() {
1848 user_error(format!("The root commit {commit_id:.12} is immutable"))
1849 } else {
1850 let mut error = user_error(format!("Commit {commit_id:.12} is immutable"));
1851 let commit = repo.store().get_commit(&commit_id)?;
1852 error.add_formatted_hint_with(|formatter| {
1853 write!(formatter, "Could not modify commit: ")?;
1854 self.write_commit_summary(formatter, &commit)?;
1855 Ok(())
1856 });
1857 error.add_hint("Immutable commits are used to protect shared history.");
1858 error.add_hint(indoc::indoc! {"
1859 For more information, see:
1860 - https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits
1861 - `jj help -k config`, \"Set of immutable commits\""});
1862
1863 let id_prefix_context =
1866 IdPrefixContext::new(self.env.command.revset_extensions().clone());
1867 let (lower_bound, upper_bound) = RevsetExpressionEvaluator::new(
1868 repo,
1869 self.env.command.revset_extensions().clone(),
1870 &id_prefix_context,
1871 self.env.immutable_expression(),
1872 )
1873 .resolve()?
1874 .intersection(&to_rewrite_expr.descendants())
1875 .evaluate(repo)?
1876 .count_estimate()?;
1877 let exact = upper_bound == Some(lower_bound);
1878 let or_more = if exact { "" } else { " or more" };
1879 error.add_hint(format!(
1880 "This operation would rewrite {lower_bound}{or_more} immutable commits."
1881 ));
1882
1883 error
1884 };
1885 Err(error)
1886 }
1887
1888 #[instrument(skip_all)]
1889 fn snapshot_working_copy(
1890 &mut self,
1891 ui: &Ui,
1892 ) -> Result<SnapshotStats, SnapshotWorkingCopyError> {
1893 let workspace_name = self.workspace_name().to_owned();
1894 let repo = self.repo().clone();
1895 let auto_tracking_matcher = self
1896 .auto_tracking_matcher(ui)
1897 .map_err(snapshot_command_error)?;
1898 let options = self
1899 .snapshot_options_with_start_tracking_matcher(&auto_tracking_matcher)
1900 .map_err(snapshot_command_error)?;
1901
1902 let mut locked_ws = self
1904 .workspace
1905 .start_working_copy_mutation()
1906 .map_err(snapshot_command_error)?;
1907
1908 let Some((repo, wc_commit)) =
1909 handle_stale_working_copy(locked_ws.locked_wc(), repo, &workspace_name)?
1910 else {
1911 return Ok(SnapshotStats::default());
1914 };
1915
1916 self.user_repo = ReadonlyUserRepo::new(repo);
1917 let (new_tree, stats) = {
1918 let mut options = options;
1919 let progress = crate::progress::snapshot_progress(ui);
1920 options.progress = progress.as_ref().map(|x| x as _);
1921 locked_ws
1922 .locked_wc()
1923 .snapshot(&options)
1924 .block_on()
1925 .map_err(snapshot_command_error)?
1926 };
1927 if new_tree.tree_ids() != wc_commit.tree_ids() {
1928 let mut tx =
1929 start_repo_transaction(&self.user_repo.repo, self.env.command.string_args());
1930 tx.set_is_snapshot(true);
1931 let mut_repo = tx.repo_mut();
1932 let commit = mut_repo
1933 .rewrite_commit(&wc_commit)
1934 .set_tree(new_tree)
1935 .write()
1936 .map_err(snapshot_command_error)?;
1937 mut_repo
1938 .set_wc_commit(workspace_name, commit.id().clone())
1939 .map_err(snapshot_command_error)?;
1940
1941 let num_rebased = mut_repo
1943 .rebase_descendants()
1944 .map_err(snapshot_command_error)?;
1945 if num_rebased > 0 {
1946 writeln!(
1947 ui.status(),
1948 "Rebased {num_rebased} descendant commits onto updated working copy"
1949 )
1950 .map_err(snapshot_command_error)?;
1951 }
1952
1953 #[cfg(feature = "git")]
1954 if self.working_copy_shared_with_git {
1955 let old_tree = wc_commit.tree();
1956 let new_tree = commit.tree();
1957 export_working_copy_changes_to_git(ui, mut_repo, &old_tree, &new_tree)
1958 .map_err(snapshot_command_error)?;
1959 }
1960
1961 let repo = tx
1962 .commit("snapshot working copy")
1963 .map_err(snapshot_command_error)?;
1964 self.user_repo = ReadonlyUserRepo::new(repo);
1965 }
1966 locked_ws
1967 .finish(self.user_repo.repo.op_id().clone())
1968 .map_err(snapshot_command_error)?;
1969 Ok(stats)
1970 }
1971
1972 fn update_working_copy(
1973 &mut self,
1974 ui: &Ui,
1975 maybe_old_commit: Option<&Commit>,
1976 new_commit: &Commit,
1977 ) -> Result<(), CommandError> {
1978 assert!(self.may_update_working_copy);
1979 let stats = update_working_copy(
1980 &self.user_repo.repo,
1981 &mut self.workspace,
1982 maybe_old_commit,
1983 new_commit,
1984 )?;
1985 self.print_updated_working_copy_stats(ui, maybe_old_commit, new_commit, &stats)
1986 }
1987
1988 fn print_updated_working_copy_stats(
1989 &self,
1990 ui: &Ui,
1991 maybe_old_commit: Option<&Commit>,
1992 new_commit: &Commit,
1993 stats: &CheckoutStats,
1994 ) -> Result<(), CommandError> {
1995 if Some(new_commit) != maybe_old_commit
1996 && let Some(mut formatter) = ui.status_formatter()
1997 {
1998 let template = self.commit_summary_template();
1999 write!(formatter, "Working copy (@) now at: ")?;
2000 template.format(new_commit, formatter.as_mut())?;
2001 writeln!(formatter)?;
2002 for parent in new_commit.parents() {
2003 let parent = parent?;
2004 write!(formatter, "Parent commit (@-) : ")?;
2006 template.format(&parent, formatter.as_mut())?;
2007 writeln!(formatter)?;
2008 }
2009 }
2010 print_checkout_stats(ui, stats, new_commit)?;
2011 if Some(new_commit) != maybe_old_commit
2012 && let Some(mut formatter) = ui.status_formatter()
2013 && new_commit.has_conflict()
2014 {
2015 let conflicts = new_commit.tree().conflicts().collect_vec();
2016 writeln!(
2017 formatter.labeled("warning").with_heading("Warning: "),
2018 "There are unresolved conflicts at these paths:"
2019 )?;
2020 print_conflicted_paths(conflicts, formatter.as_mut(), self)?;
2021 }
2022 Ok(())
2023 }
2024
2025 pub fn start_transaction(&mut self) -> WorkspaceCommandTransaction<'_> {
2026 let tx = start_repo_transaction(self.repo(), self.env.command.string_args());
2027 let id_prefix_context = mem::take(&mut self.user_repo.id_prefix_context);
2028 WorkspaceCommandTransaction {
2029 helper: self,
2030 tx,
2031 id_prefix_context,
2032 }
2033 }
2034
2035 fn finish_transaction(
2036 &mut self,
2037 ui: &Ui,
2038 mut tx: Transaction,
2039 description: impl Into<String>,
2040 _git_import_export_lock: &GitImportExportLock,
2041 ) -> Result<(), CommandError> {
2042 let num_rebased = tx.repo_mut().rebase_descendants()?;
2043 if num_rebased > 0 {
2044 writeln!(ui.status(), "Rebased {num_rebased} descendant commits")?;
2045 }
2046
2047 for (name, wc_commit_id) in &tx.repo().view().wc_commit_ids().clone() {
2048 if self
2049 .env
2050 .find_immutable_commit(tx.repo(), &RevsetExpression::commit(wc_commit_id.clone()))?
2051 .is_some()
2052 {
2053 let wc_commit = tx.repo().store().get_commit(wc_commit_id)?;
2054 tx.repo_mut().check_out(name.clone(), &wc_commit)?;
2055 writeln!(
2056 ui.warning_default(),
2057 "The working-copy commit in workspace '{name}' became immutable, so a new \
2058 commit has been created on top of it.",
2059 name = name.as_symbol()
2060 )?;
2061 }
2062 }
2063
2064 let old_repo = tx.base_repo().clone();
2065
2066 let maybe_old_wc_commit = old_repo
2067 .view()
2068 .get_wc_commit_id(self.workspace_name())
2069 .map(|commit_id| tx.base_repo().store().get_commit(commit_id))
2070 .transpose()?;
2071 let maybe_new_wc_commit = tx
2072 .repo()
2073 .view()
2074 .get_wc_commit_id(self.workspace_name())
2075 .map(|commit_id| tx.repo().store().get_commit(commit_id))
2076 .transpose()?;
2077
2078 #[cfg(feature = "git")]
2079 if self.working_copy_shared_with_git {
2080 use std::error::Error as _;
2081 if let Some(wc_commit) = &maybe_new_wc_commit {
2082 match jj_lib::git::reset_head(tx.repo_mut(), wc_commit) {
2089 Ok(()) => {}
2090 Err(err @ jj_lib::git::GitResetHeadError::UpdateHeadRef(_)) => {
2091 writeln!(ui.warning_default(), "{err}")?;
2092 crate::command_error::print_error_sources(ui, err.source())?;
2093 }
2094 Err(err) => return Err(err.into()),
2095 }
2096 }
2097 let stats = jj_lib::git::export_refs(tx.repo_mut())?;
2098 crate::git_util::print_git_export_stats(ui, &stats)?;
2099 }
2100
2101 self.user_repo = ReadonlyUserRepo::new(tx.commit(description)?);
2102
2103 if self.may_update_working_copy {
2107 if let Some(new_commit) = &maybe_new_wc_commit {
2108 self.update_working_copy(ui, maybe_old_wc_commit.as_ref(), new_commit)?;
2109 } else {
2110 }
2113 }
2114
2115 self.report_repo_changes(ui, &old_repo)?;
2116
2117 let settings = self.settings();
2118 let missing_user_name = settings.user_name().is_empty();
2119 let missing_user_mail = settings.user_email().is_empty();
2120 if missing_user_name || missing_user_mail {
2121 let not_configured_msg = match (missing_user_name, missing_user_mail) {
2122 (true, true) => "Name and email not configured.",
2123 (true, false) => "Name not configured.",
2124 (false, true) => "Email not configured.",
2125 _ => unreachable!(),
2126 };
2127 writeln!(
2128 ui.warning_default(),
2129 "{not_configured_msg} Until configured, your commits will be created with the \
2130 empty identity, and can't be pushed to remotes."
2131 )?;
2132 writeln!(ui.hint_default(), "To configure, run:")?;
2133 if missing_user_name {
2134 writeln!(
2135 ui.hint_no_heading(),
2136 r#" jj config set --user user.name "Some One""#
2137 )?;
2138 }
2139 if missing_user_mail {
2140 writeln!(
2141 ui.hint_no_heading(),
2142 r#" jj config set --user user.email "someone@example.com""#
2143 )?;
2144 }
2145 }
2146 Ok(())
2147 }
2148
2149 fn report_repo_changes(
2152 &self,
2153 ui: &Ui,
2154 old_repo: &Arc<ReadonlyRepo>,
2155 ) -> Result<(), CommandError> {
2156 let Some(mut fmt) = ui.status_formatter() else {
2157 return Ok(());
2158 };
2159 let old_view = old_repo.view();
2160 let new_repo = self.repo().as_ref();
2161 let new_view = new_repo.view();
2162 let old_heads = RevsetExpression::commits(old_view.heads().iter().cloned().collect());
2163 let new_heads = RevsetExpression::commits(new_view.heads().iter().cloned().collect());
2164 let conflicts = RevsetExpression::filter(RevsetFilterPredicate::HasConflict)
2170 .filtered(RevsetFilterPredicate::File(FilesetExpression::all()));
2171 let removed_conflicts_expr = new_heads.range(&old_heads).intersection(&conflicts);
2172 let added_conflicts_expr = old_heads.range(&new_heads).intersection(&conflicts);
2173
2174 let get_commits =
2175 |expr: Arc<ResolvedRevsetExpression>| -> Result<Vec<Commit>, CommandError> {
2176 let commits = expr
2177 .evaluate(new_repo)?
2178 .iter()
2179 .commits(new_repo.store())
2180 .try_collect()?;
2181 Ok(commits)
2182 };
2183 let removed_conflict_commits = get_commits(removed_conflicts_expr)?;
2184 let added_conflict_commits = get_commits(added_conflicts_expr)?;
2185
2186 fn commits_by_change_id(commits: &[Commit]) -> IndexMap<&ChangeId, Vec<&Commit>> {
2187 let mut result: IndexMap<&ChangeId, Vec<&Commit>> = IndexMap::new();
2188 for commit in commits {
2189 result.entry(commit.change_id()).or_default().push(commit);
2190 }
2191 result
2192 }
2193 let removed_conflicts_by_change_id = commits_by_change_id(&removed_conflict_commits);
2194 let added_conflicts_by_change_id = commits_by_change_id(&added_conflict_commits);
2195 let mut resolved_conflicts_by_change_id = removed_conflicts_by_change_id.clone();
2196 resolved_conflicts_by_change_id
2197 .retain(|change_id, _commits| !added_conflicts_by_change_id.contains_key(change_id));
2198 let mut new_conflicts_by_change_id = added_conflicts_by_change_id.clone();
2199 new_conflicts_by_change_id
2200 .retain(|change_id, _commits| !removed_conflicts_by_change_id.contains_key(change_id));
2201
2202 if !resolved_conflicts_by_change_id.is_empty() {
2204 let num_resolved: usize = resolved_conflicts_by_change_id
2208 .values()
2209 .map(|commits| commits.len())
2210 .sum();
2211 writeln!(
2212 fmt,
2213 "Existing conflicts were resolved or abandoned from {num_resolved} commits."
2214 )?;
2215 }
2216 if !new_conflicts_by_change_id.is_empty() {
2217 let num_conflicted: usize = new_conflicts_by_change_id
2218 .values()
2219 .map(|commits| commits.len())
2220 .sum();
2221 writeln!(fmt, "New conflicts appeared in {num_conflicted} commits:")?;
2222 print_updated_commits(
2223 fmt.as_mut(),
2224 &self.commit_summary_template(),
2225 new_conflicts_by_change_id.values().flatten().copied(),
2226 )?;
2227 }
2228
2229 if !(added_conflict_commits.is_empty()
2233 || resolved_conflicts_by_change_id.is_empty() && new_conflicts_by_change_id.is_empty())
2234 {
2235 if new_conflicts_by_change_id.is_empty() {
2240 writeln!(
2241 fmt,
2242 "There are still unresolved conflicts in rebased descendants.",
2243 )?;
2244 }
2245
2246 self.report_repo_conflicts(
2247 fmt.as_mut(),
2248 new_repo,
2249 added_conflict_commits
2250 .iter()
2251 .map(|commit| commit.id().clone())
2252 .collect(),
2253 )?;
2254 }
2255 revset_util::warn_unresolvable_trunk(ui, new_repo, &self.env.revset_parse_context())?;
2256
2257 Ok(())
2258 }
2259
2260 pub fn report_repo_conflicts(
2261 &self,
2262 fmt: &mut dyn Formatter,
2263 repo: &ReadonlyRepo,
2264 conflicted_commits: Vec<CommitId>,
2265 ) -> Result<(), CommandError> {
2266 if !self.settings().get_bool("hints.resolving-conflicts")? || conflicted_commits.is_empty()
2267 {
2268 return Ok(());
2269 }
2270
2271 let only_one_conflicted_commit = conflicted_commits.len() == 1;
2272 let root_conflicts_revset = RevsetExpression::commits(conflicted_commits)
2273 .roots()
2274 .evaluate(repo)?;
2275
2276 let root_conflict_commits: Vec<_> = root_conflicts_revset
2277 .iter()
2278 .commits(repo.store())
2279 .try_collect()?;
2280
2281 let instruction = if only_one_conflicted_commit {
2283 indoc! {"
2284 To resolve the conflicts, start by creating a commit on top of
2285 the conflicted commit:
2286 "}
2287 } else if root_conflict_commits.len() == 1 {
2288 indoc! {"
2289 To resolve the conflicts, start by creating a commit on top of
2290 the first conflicted commit:
2291 "}
2292 } else {
2293 indoc! {"
2294 To resolve the conflicts, start by creating a commit on top of
2295 one of the first conflicted commits:
2296 "}
2297 };
2298 write!(fmt.labeled("hint").with_heading("Hint: "), "{instruction}")?;
2299 let format_short_change_id = self.short_change_id_template();
2300 {
2301 let mut fmt = fmt.labeled("hint");
2302 for commit in &root_conflict_commits {
2303 write!(fmt, " jj new ")?;
2304 format_short_change_id.format(commit, *fmt)?;
2305 writeln!(fmt)?;
2306 }
2307 }
2308 writedoc!(
2309 fmt.labeled("hint"),
2310 "
2311 Then use `jj resolve`, or edit the conflict markers in the file directly.
2312 Once the conflicts are resolved, you can inspect the result with `jj diff`.
2313 Then run `jj squash` to move the resolution into the conflicted commit.
2314 ",
2315 )?;
2316 Ok(())
2317 }
2318
2319 pub fn get_advanceable_bookmarks<'a>(
2334 &self,
2335 ui: &Ui,
2336 from: impl IntoIterator<Item = &'a CommitId>,
2337 ) -> Result<Vec<AdvanceableBookmark>, CommandError> {
2338 let Some(ab_matcher) = load_advance_bookmarks_matcher(ui, self.settings())? else {
2339 return Ok(Vec::new());
2341 };
2342
2343 let mut advanceable_bookmarks = Vec::new();
2344 for from_commit in from {
2345 for (name, _) in self.repo().view().local_bookmarks_for_commit(from_commit) {
2346 if ab_matcher.is_match(name.as_str()) {
2347 advanceable_bookmarks.push(AdvanceableBookmark {
2348 name: name.to_owned(),
2349 old_commit_id: from_commit.clone(),
2350 });
2351 }
2352 }
2353 }
2354
2355 Ok(advanceable_bookmarks)
2356 }
2357}
2358
2359#[cfg(feature = "git")]
2360pub fn export_working_copy_changes_to_git(
2361 ui: &Ui,
2362 mut_repo: &mut MutableRepo,
2363 old_tree: &MergedTree,
2364 new_tree: &MergedTree,
2365) -> Result<(), CommandError> {
2366 let repo = mut_repo.base_repo().as_ref();
2367 jj_lib::git::update_intent_to_add(repo, old_tree, new_tree)?;
2368 let stats = jj_lib::git::export_refs(mut_repo)?;
2369 crate::git_util::print_git_export_stats(ui, &stats)?;
2370 Ok(())
2371}
2372#[cfg(not(feature = "git"))]
2373pub fn export_working_copy_changes_to_git(
2374 _ui: &Ui,
2375 _mut_repo: &mut MutableRepo,
2376 _old_tree: &MergedTree,
2377 _new_tree: &MergedTree,
2378) -> Result<(), CommandError> {
2379 Ok(())
2380}
2381
2382#[must_use]
2390pub struct WorkspaceCommandTransaction<'a> {
2391 helper: &'a mut WorkspaceCommandHelper,
2392 tx: Transaction,
2393 id_prefix_context: OnceCell<IdPrefixContext>,
2395}
2396
2397impl WorkspaceCommandTransaction<'_> {
2398 pub fn base_workspace_helper(&self) -> &WorkspaceCommandHelper {
2400 self.helper
2401 }
2402
2403 pub fn settings(&self) -> &UserSettings {
2405 self.helper.settings()
2406 }
2407
2408 pub fn base_repo(&self) -> &Arc<ReadonlyRepo> {
2409 self.tx.base_repo()
2410 }
2411
2412 pub fn repo(&self) -> &MutableRepo {
2413 self.tx.repo()
2414 }
2415
2416 pub fn repo_mut(&mut self) -> &mut MutableRepo {
2417 self.id_prefix_context.take(); self.tx.repo_mut()
2419 }
2420
2421 pub fn check_out(&mut self, commit: &Commit) -> Result<Commit, CheckOutCommitError> {
2422 let name = self.helper.workspace_name().to_owned();
2423 self.id_prefix_context.take(); self.tx.repo_mut().check_out(name, commit)
2425 }
2426
2427 pub fn edit(&mut self, commit: &Commit) -> Result<(), EditCommitError> {
2428 let name = self.helper.workspace_name().to_owned();
2429 self.id_prefix_context.take(); self.tx.repo_mut().edit(name, commit)
2431 }
2432
2433 pub fn format_commit_summary(&self, commit: &Commit) -> String {
2434 let output = self.commit_summary_template().format_plain_text(commit);
2435 output.into_string_lossy()
2436 }
2437
2438 pub fn write_commit_summary(
2439 &self,
2440 formatter: &mut dyn Formatter,
2441 commit: &Commit,
2442 ) -> std::io::Result<()> {
2443 self.commit_summary_template().format(commit, formatter)
2444 }
2445
2446 pub fn commit_summary_template(&self) -> TemplateRenderer<'_, Commit> {
2448 let language = self.commit_template_language();
2449 self.helper
2450 .reparse_valid_template(&language, &self.helper.commit_summary_template_text)
2451 .labeled(["commit"])
2452 }
2453
2454 pub fn commit_template_language(&self) -> CommitTemplateLanguage<'_> {
2457 let id_prefix_context = self
2458 .id_prefix_context
2459 .get_or_init(|| self.helper.env.new_id_prefix_context());
2460 self.helper
2461 .env
2462 .commit_template_language(self.tx.repo(), id_prefix_context)
2463 }
2464
2465 pub fn parse_commit_template(
2467 &self,
2468 ui: &Ui,
2469 template_text: &str,
2470 ) -> Result<TemplateRenderer<'_, Commit>, CommandError> {
2471 let language = self.commit_template_language();
2472 self.helper.env.parse_template(ui, &language, template_text)
2473 }
2474
2475 pub fn finish(self, ui: &Ui, description: impl Into<String>) -> Result<(), CommandError> {
2476 if !self.tx.repo().has_changes() {
2477 writeln!(ui.status(), "Nothing changed.")?;
2478 return Ok(());
2479 }
2480 let git_import_export_lock = self.helper.lock_git_import_export()?;
2483 self.helper
2484 .finish_transaction(ui, self.tx, description, &git_import_export_lock)
2485 }
2486
2487 pub fn into_inner(self) -> Transaction {
2492 self.tx
2493 }
2494
2495 pub fn advance_bookmarks(
2501 &mut self,
2502 bookmarks: Vec<AdvanceableBookmark>,
2503 move_to: &CommitId,
2504 ) -> Result<(), CommandError> {
2505 for bookmark in bookmarks {
2506 self.repo_mut().merge_local_bookmark(
2509 &bookmark.name,
2510 &RefTarget::normal(bookmark.old_commit_id),
2511 &RefTarget::normal(move_to.clone()),
2512 )?;
2513 }
2514 Ok(())
2515 }
2516}
2517
2518pub fn find_workspace_dir(cwd: &Path) -> &Path {
2519 cwd.ancestors()
2520 .find(|path| path.join(".jj").is_dir())
2521 .unwrap_or(cwd)
2522}
2523
2524fn map_workspace_load_error(err: WorkspaceLoadError, user_wc_path: Option<&str>) -> CommandError {
2525 match err {
2526 WorkspaceLoadError::NoWorkspaceHere(wc_path) => {
2527 let short_wc_path = user_wc_path.map_or(wc_path.as_ref(), Path::new);
2529 let message = format!(r#"There is no jj repo in "{}""#, short_wc_path.display());
2530 let git_dir = wc_path.join(".git");
2531 if git_dir.is_dir() {
2532 user_error_with_hint(
2533 message,
2534 "It looks like this is a git repo. You can create a jj repo backed by it by \
2535 running this:
2536jj git init",
2537 )
2538 } else {
2539 user_error(message)
2540 }
2541 }
2542 WorkspaceLoadError::RepoDoesNotExist(repo_dir) => user_error(format!(
2543 "The repository directory at {} is missing. Was it moved?",
2544 repo_dir.display(),
2545 )),
2546 WorkspaceLoadError::StoreLoadError(err @ StoreLoadError::UnsupportedType { .. }) => {
2547 internal_error_with_message(
2548 "This version of the jj binary doesn't support this type of repo",
2549 err,
2550 )
2551 }
2552 WorkspaceLoadError::StoreLoadError(
2553 err @ (StoreLoadError::ReadError { .. } | StoreLoadError::Backend(_)),
2554 ) => internal_error_with_message("The repository appears broken or inaccessible", err),
2555 WorkspaceLoadError::StoreLoadError(StoreLoadError::Signing(err)) => user_error(err),
2556 WorkspaceLoadError::WorkingCopyState(err) => internal_error(err),
2557 WorkspaceLoadError::DecodeRepoPath(_) | WorkspaceLoadError::Path(_) => user_error(err),
2558 }
2559}
2560
2561pub fn start_repo_transaction(repo: &Arc<ReadonlyRepo>, string_args: &[String]) -> Transaction {
2562 let mut tx = repo.start_transaction();
2563 let shell_escape = |arg: &String| {
2566 if arg.as_bytes().iter().all(|b| {
2567 matches!(b,
2568 b'A'..=b'Z'
2569 | b'a'..=b'z'
2570 | b'0'..=b'9'
2571 | b','
2572 | b'-'
2573 | b'.'
2574 | b'/'
2575 | b':'
2576 | b'@'
2577 | b'_'
2578 )
2579 }) {
2580 arg.clone()
2581 } else {
2582 format!("'{}'", arg.replace('\'', "\\'"))
2583 }
2584 };
2585 let mut quoted_strings = vec!["jj".to_string()];
2586 quoted_strings.extend(string_args.iter().skip(1).map(shell_escape));
2587 tx.set_tag("args".to_string(), quoted_strings.join(" "));
2588 tx
2589}
2590
2591fn handle_stale_working_copy(
2597 locked_wc: &mut dyn LockedWorkingCopy,
2598 repo: Arc<ReadonlyRepo>,
2599 workspace_name: &WorkspaceName,
2600) -> Result<Option<(Arc<ReadonlyRepo>, Commit)>, SnapshotWorkingCopyError> {
2601 let get_wc_commit = |repo: &ReadonlyRepo| -> Result<Option<_>, _> {
2602 repo.view()
2603 .get_wc_commit_id(workspace_name)
2604 .map(|id| repo.store().get_commit(id))
2605 .transpose()
2606 .map_err(snapshot_command_error)
2607 };
2608 let Some(wc_commit) = get_wc_commit(&repo)? else {
2609 return Ok(None);
2610 };
2611 let old_op_id = locked_wc.old_operation_id().clone();
2612 match WorkingCopyFreshness::check_stale(locked_wc, &wc_commit, &repo) {
2613 Ok(WorkingCopyFreshness::Fresh) => Ok(Some((repo, wc_commit))),
2614 Ok(WorkingCopyFreshness::Updated(wc_operation)) => {
2615 let repo = repo
2616 .reload_at(&wc_operation)
2617 .map_err(snapshot_command_error)?;
2618 if let Some(wc_commit) = get_wc_commit(&repo)? {
2619 Ok(Some((repo, wc_commit)))
2620 } else {
2621 Ok(None)
2622 }
2623 }
2624 Ok(WorkingCopyFreshness::WorkingCopyStale) => Err(
2625 SnapshotWorkingCopyError::StaleWorkingCopy(user_error_with_hint(
2626 format!(
2627 "The working copy is stale (not updated since operation {}).",
2628 short_operation_hash(&old_op_id)
2629 ),
2630 "Run `jj workspace update-stale` to update it.
2631See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy \
2632 for more information.",
2633 )),
2634 ),
2635 Ok(WorkingCopyFreshness::SiblingOperation) => Err(
2636 SnapshotWorkingCopyError::StaleWorkingCopy(internal_error(format!(
2637 "The repo was loaded at operation {}, which seems to be a sibling of the working \
2638 copy's operation {}",
2639 short_operation_hash(repo.op_id()),
2640 short_operation_hash(&old_op_id)
2641 ))),
2642 ),
2643 Err(OpStoreError::ObjectNotFound { .. }) => Err(
2644 SnapshotWorkingCopyError::StaleWorkingCopy(user_error_with_hint(
2645 "Could not read working copy's operation.",
2646 "Run `jj workspace update-stale` to recover.
2647See https://docs.jj-vcs.dev/latest/working-copy/#stale-working-copy \
2648 for more information.",
2649 )),
2650 ),
2651 Err(e) => Err(snapshot_command_error(e)),
2652 }
2653}
2654
2655fn update_stale_working_copy(
2656 mut locked_ws: LockedWorkspace,
2657 op_id: OperationId,
2658 stale_commit: &Commit,
2659 new_commit: &Commit,
2660) -> Result<CheckoutStats, CommandError> {
2661 if stale_commit.tree_ids() != locked_ws.locked_wc().old_tree().tree_ids() {
2664 return Err(user_error("Concurrent working copy operation. Try again."));
2665 }
2666 let stats = locked_ws
2667 .locked_wc()
2668 .check_out(new_commit)
2669 .block_on()
2670 .map_err(|err| {
2671 internal_error_with_message(
2672 format!("Failed to check out commit {}", new_commit.id().hex()),
2673 err,
2674 )
2675 })?;
2676 locked_ws.finish(op_id)?;
2677
2678 Ok(stats)
2679}
2680
2681pub fn print_updated_commits<'a>(
2684 formatter: &mut dyn Formatter,
2685 template: &TemplateRenderer<Commit>,
2686 commits: impl IntoIterator<Item = &'a Commit>,
2687) -> io::Result<()> {
2688 let mut commits = commits.into_iter().fuse();
2689 for commit in commits.by_ref().take(10) {
2690 write!(formatter, " ")?;
2691 template.format(commit, formatter)?;
2692 writeln!(formatter)?;
2693 }
2694 if commits.next().is_some() {
2695 writeln!(formatter, " ...")?;
2696 }
2697 Ok(())
2698}
2699
2700#[instrument(skip_all)]
2701pub fn print_conflicted_paths(
2702 conflicts: Vec<(RepoPathBuf, BackendResult<MergedTreeValue>)>,
2703 formatter: &mut dyn Formatter,
2704 workspace_command: &WorkspaceCommandHelper,
2705) -> Result<(), CommandError> {
2706 let formatted_paths = conflicts
2707 .iter()
2708 .map(|(path, _conflict)| workspace_command.format_file_path(path))
2709 .collect_vec();
2710 let max_path_len = formatted_paths.iter().map(|p| p.len()).max().unwrap_or(0);
2711 let formatted_paths = formatted_paths
2712 .into_iter()
2713 .map(|p| format!("{:width$}", p, width = max_path_len.min(32) + 3));
2714
2715 for ((_, conflict), formatted_path) in std::iter::zip(conflicts, formatted_paths) {
2716 let conflict = conflict?.simplify();
2719 let sides = conflict.num_sides();
2720 let n_adds = conflict.adds().flatten().count();
2721 let deletions = sides - n_adds;
2722
2723 let mut seen_objects = BTreeMap::new(); if deletions > 0 {
2725 seen_objects.insert(
2726 format!(
2727 "{deletions} deletion{}",
2729 if deletions > 1 { "s" } else { "" }
2730 ),
2731 "normal", );
2733 }
2734 for term in itertools::chain(conflict.removes(), conflict.adds()).flatten() {
2738 seen_objects.insert(
2739 match term {
2740 TreeValue::File {
2741 executable: false, ..
2742 } => continue,
2743 TreeValue::File {
2744 executable: true, ..
2745 } => "an executable",
2746 TreeValue::Symlink(_) => "a symlink",
2747 TreeValue::Tree(_) => "a directory",
2748 TreeValue::GitSubmodule(_) => "a git submodule",
2749 }
2750 .to_string(),
2751 "difficult",
2752 );
2753 }
2754
2755 write!(formatter, "{formatted_path} ")?;
2756 {
2757 let mut formatter = formatter.labeled("conflict_description");
2758 let print_pair = |formatter: &mut dyn Formatter, (text, label): &(String, &str)| {
2759 write!(formatter.labeled(label), "{text}")
2760 };
2761 print_pair(
2762 *formatter,
2763 &(
2764 format!("{sides}-sided"),
2765 if sides > 2 { "difficult" } else { "normal" },
2766 ),
2767 )?;
2768 write!(formatter, " conflict")?;
2769
2770 if !seen_objects.is_empty() {
2771 write!(formatter, " including ")?;
2772 let seen_objects = seen_objects.into_iter().collect_vec();
2773 match &seen_objects[..] {
2774 [] => unreachable!(),
2775 [only] => print_pair(*formatter, only)?,
2776 [first, middle @ .., last] => {
2777 print_pair(*formatter, first)?;
2778 for pair in middle {
2779 write!(formatter, ", ")?;
2780 print_pair(*formatter, pair)?;
2781 }
2782 write!(formatter, " and ")?;
2783 print_pair(*formatter, last)?;
2784 }
2785 }
2786 }
2787 }
2788 writeln!(formatter)?;
2789 }
2790 Ok(())
2791}
2792
2793fn build_untracked_reason_message(reason: &UntrackedReason) -> Option<String> {
2795 match reason {
2796 UntrackedReason::FileTooLarge { size, max_size } => {
2797 let size_approx = HumanByteSize(*size);
2800 let max_size_approx = HumanByteSize(*max_size);
2801 Some(format!(
2802 "{size_approx} ({size} bytes); the maximum size allowed is {max_size_approx} \
2803 ({max_size} bytes)",
2804 ))
2805 }
2806 UntrackedReason::FileNotAutoTracked => None,
2810 }
2811}
2812
2813pub fn print_untracked_files(
2815 ui: &Ui,
2816 untracked_paths: &BTreeMap<RepoPathBuf, UntrackedReason>,
2817 path_converter: &RepoPathUiConverter,
2818) -> io::Result<()> {
2819 let mut untracked_paths = untracked_paths
2820 .iter()
2821 .filter_map(|(path, reason)| build_untracked_reason_message(reason).map(|m| (path, m)))
2822 .peekable();
2823
2824 if untracked_paths.peek().is_some() {
2825 writeln!(ui.warning_default(), "Refused to snapshot some files:")?;
2826 let mut formatter = ui.stderr_formatter();
2827 for (path, message) in untracked_paths {
2828 let ui_path = path_converter.format_file_path(path);
2829 writeln!(formatter, " {ui_path}: {message}")?;
2830 }
2831 }
2832
2833 Ok(())
2834}
2835
2836pub fn print_snapshot_stats(
2837 ui: &Ui,
2838 stats: &SnapshotStats,
2839 path_converter: &RepoPathUiConverter,
2840) -> io::Result<()> {
2841 print_untracked_files(ui, &stats.untracked_paths, path_converter)?;
2842
2843 let large_files_sizes = stats
2844 .untracked_paths
2845 .values()
2846 .filter_map(|reason| match reason {
2847 UntrackedReason::FileTooLarge { size, .. } => Some(size),
2848 UntrackedReason::FileNotAutoTracked => None,
2849 });
2850 if let Some(size) = large_files_sizes.max() {
2851 writedoc!(
2852 ui.hint_default(),
2853 r"
2854 This is to prevent large files from being added by accident. You can fix this by:
2855 - Adding the file to `.gitignore`
2856 - Run `jj config set --repo snapshot.max-new-file-size {size}`
2857 This will increase the maximum file size allowed for new files, in this repository only.
2858 - Run `jj --config snapshot.max-new-file-size={size} st`
2859 This will increase the maximum file size allowed for new files, for this command only.
2860 "
2861 )?;
2862 }
2863 Ok(())
2864}
2865
2866pub fn print_checkout_stats(
2867 ui: &Ui,
2868 stats: &CheckoutStats,
2869 new_commit: &Commit,
2870) -> Result<(), std::io::Error> {
2871 if stats.added_files > 0 || stats.updated_files > 0 || stats.removed_files > 0 {
2872 writeln!(
2873 ui.status(),
2874 "Added {} files, modified {} files, removed {} files",
2875 stats.added_files,
2876 stats.updated_files,
2877 stats.removed_files
2878 )?;
2879 }
2880 if stats.skipped_files != 0 {
2881 writeln!(
2882 ui.warning_default(),
2883 "{} of those updates were skipped because there were conflicting changes in the \
2884 working copy.",
2885 stats.skipped_files
2886 )?;
2887 writeln!(
2888 ui.hint_default(),
2889 "Inspect the changes compared to the intended target with `jj diff --from {}`.
2890Discard the conflicting changes with `jj restore --from {}`.",
2891 short_commit_hash(new_commit.id()),
2892 short_commit_hash(new_commit.id())
2893 )?;
2894 }
2895 Ok(())
2896}
2897
2898pub fn print_unmatched_explicit_paths<'a>(
2901 ui: &Ui,
2902 workspace_command: &WorkspaceCommandHelper,
2903 expression: &FilesetExpression,
2904 trees: impl IntoIterator<Item = &'a MergedTree>,
2905) -> io::Result<()> {
2906 let mut explicit_paths = expression.explicit_paths().collect_vec();
2907 for tree in trees {
2908 explicit_paths.retain(|&path| tree.path_value(path).unwrap().is_absent());
2910 }
2911
2912 if !explicit_paths.is_empty() {
2913 let ui_paths = explicit_paths
2914 .iter()
2915 .map(|&path| workspace_command.format_file_path(path))
2916 .join(", ");
2917 writeln!(
2918 ui.warning_default(),
2919 "No matching entries for paths: {ui_paths}"
2920 )?;
2921 }
2922
2923 Ok(())
2924}
2925
2926pub fn update_working_copy(
2927 repo: &Arc<ReadonlyRepo>,
2928 workspace: &mut Workspace,
2929 old_commit: Option<&Commit>,
2930 new_commit: &Commit,
2931) -> Result<CheckoutStats, CommandError> {
2932 let old_tree = old_commit.map(|commit| commit.tree());
2933 let stats = workspace
2936 .check_out(repo.op_id().clone(), old_tree.as_ref(), new_commit)
2937 .map_err(|err| {
2938 internal_error_with_message(
2939 format!("Failed to check out commit {}", new_commit.id().hex()),
2940 err,
2941 )
2942 })?;
2943 Ok(stats)
2944}
2945
2946pub fn default_ignored_remote_name(store: &Store) -> Option<&'static RemoteName> {
2948 #[cfg(feature = "git")]
2949 {
2950 use jj_lib::git;
2951 if git::get_git_backend(store).is_ok() {
2952 return Some(git::REMOTE_NAME_FOR_LOCAL_GIT_REPO);
2953 }
2954 }
2955 let _ = store;
2956 None
2957}
2958
2959pub fn has_tracked_remote_bookmarks(repo: &dyn Repo, bookmark: &RefName) -> bool {
2962 let remote_matcher = match default_ignored_remote_name(repo.store()) {
2963 Some(remote) => StringExpression::exact(remote).negated().to_matcher(),
2964 None => StringMatcher::all(),
2965 };
2966 repo.view()
2967 .remote_bookmarks_matching(&StringMatcher::exact(bookmark), &remote_matcher)
2968 .any(|(_, remote_ref)| remote_ref.is_tracked())
2969}
2970
2971pub fn load_template_aliases(
2972 ui: &Ui,
2973 stacked_config: &StackedConfig,
2974) -> Result<TemplateAliasesMap, CommandError> {
2975 let table_name = ConfigNamePathBuf::from_iter(["template-aliases"]);
2976 let mut aliases_map = TemplateAliasesMap::new();
2977 for layer in stacked_config.layers() {
2980 let table = match layer.look_up_table(&table_name) {
2981 Ok(Some(table)) => table,
2982 Ok(None) => continue,
2983 Err(item) => {
2984 return Err(ConfigGetError::Type {
2985 name: table_name.to_string(),
2986 error: format!("Expected a table, but is {}", item.type_name()).into(),
2987 source_path: layer.path.clone(),
2988 }
2989 .into());
2990 }
2991 };
2992 for (decl, item) in table.iter() {
2993 let r = item
2994 .as_str()
2995 .ok_or_else(|| format!("Expected a string, but is {}", item.type_name()))
2996 .and_then(|v| aliases_map.insert(decl, v).map_err(|e| e.to_string()));
2997 if let Err(s) = r {
2998 writeln!(
2999 ui.warning_default(),
3000 "Failed to load `{table_name}.{decl}`: {s}"
3001 )?;
3002 }
3003 }
3004 }
3005 Ok(aliases_map)
3006}
3007
3008#[derive(Clone, Debug)]
3010pub struct LogContentFormat {
3011 width: usize,
3012 word_wrap: bool,
3013}
3014
3015impl LogContentFormat {
3016 pub fn new(ui: &Ui, settings: &UserSettings) -> Result<Self, ConfigGetError> {
3018 Ok(Self {
3019 width: ui.term_width(),
3020 word_wrap: settings.get_bool("ui.log-word-wrap")?,
3021 })
3022 }
3023
3024 #[must_use]
3026 pub fn sub_width(&self, width: usize) -> Self {
3027 Self {
3028 width: self.width.saturating_sub(width),
3029 word_wrap: self.word_wrap,
3030 }
3031 }
3032
3033 pub fn width(&self) -> usize {
3035 self.width
3036 }
3037
3038 pub fn write<E: From<io::Error>>(
3040 &self,
3041 formatter: &mut dyn Formatter,
3042 content_fn: impl FnOnce(&mut dyn Formatter) -> Result<(), E>,
3043 ) -> Result<(), E> {
3044 if self.word_wrap {
3045 let mut recorder = FormatRecorder::new();
3046 content_fn(&mut recorder)?;
3047 text_util::write_wrapped(formatter, &recorder, self.width)?;
3048 } else {
3049 content_fn(formatter)?;
3050 }
3051 Ok(())
3052 }
3053}
3054
3055pub fn short_commit_hash(commit_id: &CommitId) -> String {
3056 format!("{commit_id:.12}")
3057}
3058
3059pub fn short_change_hash(change_id: &ChangeId) -> String {
3060 format!("{change_id:.12}")
3061}
3062
3063pub fn short_operation_hash(operation_id: &OperationId) -> String {
3064 format!("{operation_id:.12}")
3065}
3066
3067#[derive(Clone, Debug)]
3069pub enum DiffSelector {
3070 NonInteractive,
3071 Interactive(DiffEditor),
3072}
3073
3074impl DiffSelector {
3075 pub fn is_interactive(&self) -> bool {
3076 matches!(self, Self::Interactive(_))
3077 }
3078
3079 pub fn select(
3084 &self,
3085 trees: Diff<&MergedTree>,
3086 matcher: &dyn Matcher,
3087 format_instructions: impl FnOnce() -> String,
3088 ) -> Result<MergedTree, CommandError> {
3089 let selected_tree = restore_tree(trees.after, trees.before, matcher).block_on()?;
3090 match self {
3091 Self::NonInteractive => Ok(selected_tree),
3092 Self::Interactive(editor) => {
3093 Ok(editor.edit(
3097 Diff::new(trees.before, &selected_tree),
3098 matcher,
3099 format_instructions,
3100 )?)
3101 }
3102 }
3103 }
3104}
3105
3106#[derive(Clone, Debug)]
3107pub struct RemoteBookmarkNamePattern {
3108 pub bookmark: StringPattern,
3109 pub remote: StringPattern,
3110}
3111
3112impl FromStr for RemoteBookmarkNamePattern {
3113 type Err = String;
3114
3115 fn from_str(src: &str) -> Result<Self, Self::Err> {
3116 let (maybe_kind, pat) = src
3121 .split_once(':')
3122 .map_or((None, src), |(kind, pat)| (Some(kind), pat));
3123 let to_pattern = |pat: &str| {
3124 if let Some(kind) = maybe_kind {
3125 StringPattern::from_str_kind(pat, kind).map_err(|err| err.to_string())
3126 } else {
3127 Ok(StringPattern::exact(pat))
3128 }
3129 };
3130 let (bookmark, remote) = pat.rsplit_once('@').ok_or_else(|| {
3132 "remote bookmark must be specified in bookmark@remote form".to_owned()
3133 })?;
3134 Ok(Self {
3135 bookmark: to_pattern(bookmark)?,
3136 remote: to_pattern(remote)?,
3137 })
3138 }
3139}
3140
3141impl RemoteBookmarkNamePattern {
3142 pub fn as_exact(&self) -> Option<RemoteRefSymbol<'_>> {
3143 let bookmark = RefName::new(self.bookmark.as_exact()?);
3144 let remote = RemoteName::new(self.remote.as_exact()?);
3145 Some(bookmark.to_remote_symbol(remote))
3146 }
3147}
3148
3149impl fmt::Display for RemoteBookmarkNamePattern {
3150 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3151 let Self { bookmark, remote } = self;
3154 write!(f, "{bookmark}@{remote}")
3155 }
3156}
3157
3158pub fn compute_commit_location(
3163 ui: &Ui,
3164 workspace_command: &WorkspaceCommandHelper,
3165 destination: Option<&[RevisionArg]>,
3166 insert_after: Option<&[RevisionArg]>,
3167 insert_before: Option<&[RevisionArg]>,
3168 commit_type: &str,
3169) -> Result<(Vec<CommitId>, Vec<CommitId>), CommandError> {
3170 let resolve_revisions =
3171 |revisions: Option<&[RevisionArg]>| -> Result<Option<Vec<CommitId>>, CommandError> {
3172 if let Some(revisions) = revisions {
3173 Ok(Some(
3174 workspace_command
3175 .resolve_some_revsets_default_single(ui, revisions)?
3176 .into_iter()
3177 .collect_vec(),
3178 ))
3179 } else {
3180 Ok(None)
3181 }
3182 };
3183 let destination_commit_ids = resolve_revisions(destination)?;
3184 let after_commit_ids = resolve_revisions(insert_after)?;
3185 let before_commit_ids = resolve_revisions(insert_before)?;
3186
3187 let (new_parent_ids, new_child_ids) =
3188 match (destination_commit_ids, after_commit_ids, before_commit_ids) {
3189 (Some(destination_commit_ids), None, None) => (destination_commit_ids, vec![]),
3190 (None, Some(after_commit_ids), Some(before_commit_ids)) => {
3191 (after_commit_ids, before_commit_ids)
3192 }
3193 (None, Some(after_commit_ids), None) => {
3194 let new_child_ids: Vec<_> = RevsetExpression::commits(after_commit_ids.clone())
3195 .children()
3196 .evaluate(workspace_command.repo().as_ref())?
3197 .iter()
3198 .try_collect()?;
3199
3200 (after_commit_ids, new_child_ids)
3201 }
3202 (None, None, Some(before_commit_ids)) => {
3203 let before_commits: Vec<_> = before_commit_ids
3204 .iter()
3205 .map(|id| workspace_command.repo().store().get_commit(id))
3206 .try_collect()?;
3207 let new_parent_ids = before_commits
3210 .iter()
3211 .flat_map(|commit| commit.parent_ids())
3212 .unique()
3213 .cloned()
3214 .collect_vec();
3215
3216 (new_parent_ids, before_commit_ids)
3217 }
3218 (Some(_), Some(_), _) | (Some(_), _, Some(_)) => {
3219 panic!("destination cannot be used with insert_after/insert_before")
3220 }
3221 (None, None, None) => {
3222 panic!("expected at least one of destination or insert_after/insert_before")
3223 }
3224 };
3225
3226 if !new_child_ids.is_empty() {
3227 workspace_command.check_rewritable(new_child_ids.iter())?;
3228 ensure_no_commit_loop(
3229 workspace_command.repo().as_ref(),
3230 &RevsetExpression::commits(new_child_ids.clone()),
3231 &RevsetExpression::commits(new_parent_ids.clone()),
3232 commit_type,
3233 )?;
3234 }
3235
3236 Ok((new_parent_ids, new_child_ids))
3237}
3238
3239fn ensure_no_commit_loop(
3242 repo: &ReadonlyRepo,
3243 children_expression: &Arc<ResolvedRevsetExpression>,
3244 parents_expression: &Arc<ResolvedRevsetExpression>,
3245 commit_type: &str,
3246) -> Result<(), CommandError> {
3247 if let Some(commit_id) = children_expression
3248 .dag_range_to(parents_expression)
3249 .evaluate(repo)?
3250 .iter()
3251 .next()
3252 {
3253 let commit_id = commit_id?;
3254 return Err(user_error(format!(
3255 "Refusing to create a loop: commit {} would be both an ancestor and a descendant of \
3256 the {commit_type}",
3257 short_commit_hash(&commit_id),
3258 )));
3259 }
3260 Ok(())
3261}
3262
3263#[derive(clap::Parser, Clone, Debug)]
3270#[command(name = "jj")]
3271pub struct Args {
3272 #[command(flatten)]
3273 pub global_args: GlobalArgs,
3274}
3275
3276#[derive(clap::Args, Clone, Debug)]
3277#[command(next_help_heading = "Global Options")]
3278pub struct GlobalArgs {
3279 #[arg(long, short = 'R', global = true, value_hint = clap::ValueHint::DirPath)]
3284 pub repository: Option<String>,
3285 #[arg(long, global = true)]
3298 pub ignore_working_copy: bool,
3299 #[arg(long, global = true)]
3308 pub ignore_immutable: bool,
3309 #[arg(
3332 long,
3333 visible_alias = "at-op",
3334 global = true,
3335 add = ArgValueCandidates::new(complete::operations),
3336 )]
3337 pub at_operation: Option<String>,
3338 #[arg(long, global = true)]
3340 pub debug: bool,
3341
3342 #[command(flatten)]
3343 pub early_args: EarlyArgs,
3344}
3345
3346#[derive(clap::Args, Clone, Debug)]
3347pub struct EarlyArgs {
3348 #[arg(long, value_name = "WHEN", global = true)]
3350 pub color: Option<ColorChoice>,
3351 #[arg(long, global = true, action = ArgAction::SetTrue)]
3358 pub quiet: Option<bool>,
3361 #[arg(long, global = true, action = ArgAction::SetTrue)]
3363 pub no_pager: Option<bool>,
3366 #[arg(long, value_name = "NAME=VALUE", global = true, add = ArgValueCompleter::new(complete::leaf_config_key_value))]
3372 pub config: Vec<String>,
3373 #[arg(long, value_name = "PATH", global = true, value_hint = clap::ValueHint::FilePath)]
3375 pub config_file: Vec<String>,
3376}
3377
3378impl EarlyArgs {
3379 pub(crate) fn merged_config_args(&self, matches: &ArgMatches) -> Vec<(ConfigArgKind, &str)> {
3380 merge_args_with(
3381 matches,
3382 &[("config", &self.config), ("config_file", &self.config_file)],
3383 |id, value| match id {
3384 "config" => (ConfigArgKind::Item, value.as_ref()),
3385 "config_file" => (ConfigArgKind::File, value.as_ref()),
3386 _ => unreachable!("unexpected id {id:?}"),
3387 },
3388 )
3389 }
3390
3391 fn has_config_args(&self) -> bool {
3392 !self.config.is_empty() || !self.config_file.is_empty()
3393 }
3394}
3395
3396#[derive(Clone, Debug)]
3402pub struct RevisionArg(Cow<'static, str>);
3403
3404impl RevisionArg {
3405 pub const AT: Self = Self(Cow::Borrowed("@"));
3407}
3408
3409impl From<String> for RevisionArg {
3410 fn from(s: String) -> Self {
3411 Self(s.into())
3412 }
3413}
3414
3415impl AsRef<str> for RevisionArg {
3416 fn as_ref(&self) -> &str {
3417 &self.0
3418 }
3419}
3420
3421impl fmt::Display for RevisionArg {
3422 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3423 write!(f, "{}", self.0)
3424 }
3425}
3426
3427impl ValueParserFactory for RevisionArg {
3428 type Parser = MapValueParser<NonEmptyStringValueParser, fn(String) -> Self>;
3429
3430 fn value_parser() -> Self::Parser {
3431 NonEmptyStringValueParser::new().map(Self::from)
3432 }
3433}
3434
3435pub fn merge_args_with<'k, 'v, T, U>(
3443 matches: &ArgMatches,
3444 id_values: &[(&'k str, &'v [T])],
3445 mut convert: impl FnMut(&'k str, &'v T) -> U,
3446) -> Vec<U> {
3447 let mut pos_values: Vec<(usize, U)> = Vec::new();
3448 for (id, values) in id_values {
3449 pos_values.extend(itertools::zip_eq(
3450 matches.indices_of(id).into_iter().flatten(),
3451 values.iter().map(|v| convert(id, v)),
3452 ));
3453 }
3454 pos_values.sort_unstable_by_key(|&(pos, _)| pos);
3455 pos_values.into_iter().map(|(_, value)| value).collect()
3456}
3457
3458fn get_string_or_array(
3459 config: &StackedConfig,
3460 key: &'static str,
3461) -> Result<Vec<String>, ConfigGetError> {
3462 config
3463 .get(key)
3464 .map(|string| vec![string])
3465 .or_else(|_| config.get::<Vec<String>>(key))
3466}
3467
3468fn resolve_default_command(
3469 ui: &Ui,
3470 config: &StackedConfig,
3471 app: &Command,
3472 mut string_args: Vec<String>,
3473) -> Result<Vec<String>, CommandError> {
3474 const PRIORITY_FLAGS: &[&str] = &["--help", "-h", "--version", "-V"];
3475
3476 let has_priority_flag = string_args
3477 .iter()
3478 .any(|arg| PRIORITY_FLAGS.contains(&arg.as_str()));
3479 if has_priority_flag {
3480 return Ok(string_args);
3481 }
3482
3483 let app_clone = app
3484 .clone()
3485 .allow_external_subcommands(true)
3486 .ignore_errors(true);
3487 let matches = app_clone.try_get_matches_from(&string_args).ok();
3488
3489 if let Some(matches) = matches
3490 && matches.subcommand_name().is_none()
3491 {
3492 let args = get_string_or_array(config, "ui.default-command").optional()?;
3493 if args.is_none() {
3494 writeln!(
3495 ui.hint_default(),
3496 "Use `jj -h` for a list of available commands."
3497 )?;
3498 writeln!(
3499 ui.hint_no_heading(),
3500 "Run `jj config set --user ui.default-command log` to disable this message."
3501 )?;
3502 }
3503 let default_command = args.unwrap_or_else(|| vec!["log".to_string()]);
3504
3505 string_args.splice(1..1, default_command);
3507 }
3508 Ok(string_args)
3509}
3510
3511fn resolve_aliases(
3512 ui: &Ui,
3513 config: &StackedConfig,
3514 app: &Command,
3515 mut string_args: Vec<String>,
3516) -> Result<Vec<String>, CommandError> {
3517 let defined_aliases: HashSet<_> = config.table_keys("aliases").collect();
3518 let mut resolved_aliases = HashSet::new();
3519 let mut real_commands = HashSet::new();
3520 for command in app.get_subcommands() {
3521 real_commands.insert(command.get_name());
3522 for alias in command.get_all_aliases() {
3523 real_commands.insert(alias);
3524 }
3525 }
3526 for alias in defined_aliases.intersection(&real_commands).sorted() {
3527 writeln!(
3528 ui.warning_default(),
3529 "Cannot define an alias that overrides the built-in command '{alias}'"
3530 )?;
3531 }
3532
3533 loop {
3534 let app_clone = app.clone().allow_external_subcommands(true);
3535 let matches = app_clone.try_get_matches_from(&string_args).ok();
3536 if let Some((command_name, submatches)) = matches.as_ref().and_then(|m| m.subcommand())
3537 && !real_commands.contains(command_name)
3538 {
3539 let alias_name = command_name.to_string();
3540 let alias_args = submatches
3541 .get_many::<OsString>("")
3542 .unwrap_or_default()
3543 .map(|arg| arg.to_str().unwrap().to_string())
3544 .collect_vec();
3545 if resolved_aliases.contains(&*alias_name) {
3546 return Err(user_error(format!(
3547 "Recursive alias definition involving `{alias_name}`"
3548 )));
3549 }
3550 if let Some(&alias_name) = defined_aliases.get(&*alias_name) {
3551 let alias_definition: Vec<String> = config.get(["aliases", alias_name])?;
3552 assert!(string_args.ends_with(&alias_args));
3553 string_args.truncate(string_args.len() - 1 - alias_args.len());
3554 string_args.extend(alias_definition);
3555 string_args.extend_from_slice(&alias_args);
3556 resolved_aliases.insert(alias_name);
3557 continue;
3558 } else {
3559 return Ok(string_args);
3561 }
3562 }
3563 return Ok(string_args);
3565 }
3566}
3567
3568fn parse_early_args(
3570 app: &Command,
3571 args: &[String],
3572) -> Result<(EarlyArgs, Vec<ConfigLayer>), CommandError> {
3573 let early_matches = app
3575 .clone()
3576 .disable_version_flag(true)
3577 .disable_help_flag(true)
3579 .arg(
3581 clap::Arg::new("help")
3582 .short('h')
3583 .long("help")
3584 .global(true)
3585 .action(ArgAction::Count),
3586 )
3587 .ignore_errors(true)
3588 .try_get_matches_from(args)?;
3589 let args = EarlyArgs::from_arg_matches(&early_matches).unwrap();
3590
3591 let mut config_layers = parse_config_args(&args.merged_config_args(&early_matches))?;
3592 let mut layer = ConfigLayer::empty(ConfigSource::CommandArg);
3595 if let Some(choice) = args.color {
3596 layer.set_value("ui.color", choice.to_string()).unwrap();
3597 }
3598 if args.quiet.unwrap_or_default() {
3599 layer.set_value("ui.quiet", true).unwrap();
3600 }
3601 if args.no_pager.unwrap_or_default() {
3602 layer.set_value("ui.paginate", "never").unwrap();
3603 }
3604 if !layer.is_empty() {
3605 config_layers.push(layer);
3606 }
3607 Ok((args, config_layers))
3608}
3609
3610fn handle_shell_completion(
3611 ui: &Ui,
3612 app: &Command,
3613 config: &StackedConfig,
3614 cwd: &Path,
3615) -> Result<(), CommandError> {
3616 let mut orig_args = env::args_os();
3617
3618 let mut args = vec![];
3619 args.extend(orig_args.by_ref().take(2));
3622
3623 if orig_args.len() > 0 {
3627 let complete_index: Option<usize> = env::var("_CLAP_COMPLETE_INDEX")
3628 .ok()
3629 .and_then(|s| s.parse().ok());
3630 let resolved_aliases = if let Some(index) = complete_index {
3631 let pad_len = usize::saturating_sub(index + 1, orig_args.len());
3636 let padded_args = orig_args
3637 .by_ref()
3638 .chain(std::iter::repeat_n(OsString::new(), pad_len));
3639
3640 let mut expanded_args = expand_args(ui, app, padded_args.take(index + 1), config)?;
3642
3643 unsafe {
3647 env::set_var(
3648 "_CLAP_COMPLETE_INDEX",
3649 (expanded_args.len() - 1).to_string(),
3650 );
3651 }
3652
3653 let split_off_padding = expanded_args.split_off(expanded_args.len() - pad_len);
3656 assert!(
3657 split_off_padding.iter().all(|s| s.is_empty()),
3658 "split-off padding should only consist of empty strings but was \
3659 {split_off_padding:?}",
3660 );
3661
3662 expanded_args.extend(to_string_args(orig_args)?);
3664 expanded_args
3665 } else {
3666 expand_args(ui, app, orig_args, config)?
3667 };
3668 args.extend(resolved_aliases.into_iter().map(OsString::from));
3669 }
3670 let ran_completion = clap_complete::CompleteEnv::with_factory(|| {
3671 app.clone()
3672 .allow_external_subcommands(true)
3674 })
3675 .try_complete(args.iter(), Some(cwd))?;
3676 assert!(
3677 ran_completion,
3678 "This function should not be called without the COMPLETE variable set."
3679 );
3680 Ok(())
3681}
3682
3683pub fn expand_args(
3684 ui: &Ui,
3685 app: &Command,
3686 args_os: impl IntoIterator<Item = OsString>,
3687 config: &StackedConfig,
3688) -> Result<Vec<String>, CommandError> {
3689 let string_args = to_string_args(args_os)?;
3690 let string_args = resolve_default_command(ui, config, app, string_args)?;
3691 resolve_aliases(ui, config, app, string_args)
3692}
3693
3694fn to_string_args(
3695 args_os: impl IntoIterator<Item = OsString>,
3696) -> Result<Vec<String>, CommandError> {
3697 args_os
3698 .into_iter()
3699 .map(|arg_os| {
3700 arg_os
3701 .into_string()
3702 .map_err(|_| cli_error("Non-UTF-8 argument"))
3703 })
3704 .collect()
3705}
3706
3707fn parse_args(app: &Command, string_args: &[String]) -> Result<(ArgMatches, Args), clap::Error> {
3708 let matches = app
3709 .clone()
3710 .arg_required_else_help(true)
3711 .subcommand_required(true)
3712 .try_get_matches_from(string_args)?;
3713 let args = Args::from_arg_matches(&matches).unwrap();
3714 Ok((matches, args))
3715}
3716
3717fn command_name(mut matches: &ArgMatches) -> String {
3718 let mut command = String::new();
3719 while let Some((subcommand, new_matches)) = matches.subcommand() {
3720 if !command.is_empty() {
3721 command.push(' ');
3722 }
3723 command.push_str(subcommand);
3724 matches = new_matches;
3725 }
3726 command
3727}
3728
3729pub fn format_template<C: Clone>(ui: &Ui, arg: &C, template: &TemplateRenderer<C>) -> String {
3730 let mut output = vec![];
3731 template
3732 .format(arg, ui.new_formatter(&mut output).as_mut())
3733 .expect("write() to vec backed formatter should never fail");
3734 output.into_string_lossy()
3736}
3737
3738#[must_use]
3740pub struct CliRunner<'a> {
3741 tracing_subscription: TracingSubscription,
3742 app: Command,
3743 config_layers: Vec<ConfigLayer>,
3744 config_migrations: Vec<ConfigMigrationRule>,
3745 store_factories: StoreFactories,
3746 working_copy_factories: WorkingCopyFactories,
3747 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
3748 revset_extensions: RevsetExtensions,
3749 commit_template_extensions: Vec<Arc<dyn CommitTemplateLanguageExtension>>,
3750 operation_template_extensions: Vec<Arc<dyn OperationTemplateLanguageExtension>>,
3751 dispatch_fn: CliDispatchFn<'a>,
3752 dispatch_hook_fns: Vec<CliDispatchHookFn<'a>>,
3753 process_global_args_fns: Vec<ProcessGlobalArgsFn<'a>>,
3754}
3755
3756pub type CliDispatchFn<'a> =
3757 Box<dyn FnOnce(&mut Ui, &CommandHelper) -> Result<(), CommandError> + 'a>;
3758
3759type CliDispatchHookFn<'a> =
3760 Box<dyn FnOnce(&mut Ui, &CommandHelper, CliDispatchFn<'a>) -> Result<(), CommandError> + 'a>;
3761
3762type ProcessGlobalArgsFn<'a> =
3763 Box<dyn FnOnce(&mut Ui, &ArgMatches) -> Result<(), CommandError> + 'a>;
3764
3765impl<'a> CliRunner<'a> {
3766 pub fn init() -> Self {
3769 let tracing_subscription = TracingSubscription::init();
3770 crate::cleanup_guard::init();
3771 Self {
3772 tracing_subscription,
3773 app: crate::commands::default_app(),
3774 config_layers: crate::config::default_config_layers(),
3775 config_migrations: crate::config::default_config_migrations(),
3776 store_factories: StoreFactories::default(),
3777 working_copy_factories: default_working_copy_factories(),
3778 workspace_loader_factory: Box::new(DefaultWorkspaceLoaderFactory),
3779 revset_extensions: Default::default(),
3780 commit_template_extensions: vec![],
3781 operation_template_extensions: vec![],
3782 dispatch_fn: Box::new(crate::commands::run_command),
3783 dispatch_hook_fns: vec![],
3784 process_global_args_fns: vec![],
3785 }
3786 }
3787
3788 pub fn name(mut self, name: &str) -> Self {
3790 self.app = self.app.name(name.to_string());
3791 self
3792 }
3793
3794 pub fn about(mut self, about: &str) -> Self {
3796 self.app = self.app.about(about.to_string());
3797 self
3798 }
3799
3800 pub fn version(mut self, version: &str) -> Self {
3802 self.app = self.app.version(version.to_string());
3803 self
3804 }
3805
3806 pub fn add_extra_config(mut self, layer: ConfigLayer) -> Self {
3811 assert_eq!(layer.source, ConfigSource::Default);
3812 self.config_layers.push(layer);
3813 self
3814 }
3815
3816 pub fn add_extra_config_migration(mut self, rule: ConfigMigrationRule) -> Self {
3818 self.config_migrations.push(rule);
3819 self
3820 }
3821
3822 pub fn add_store_factories(mut self, store_factories: StoreFactories) -> Self {
3824 self.store_factories.merge(store_factories);
3825 self
3826 }
3827
3828 pub fn add_working_copy_factories(
3830 mut self,
3831 working_copy_factories: WorkingCopyFactories,
3832 ) -> Self {
3833 merge_factories_map(&mut self.working_copy_factories, working_copy_factories);
3834 self
3835 }
3836
3837 pub fn set_workspace_loader_factory(
3838 mut self,
3839 workspace_loader_factory: Box<dyn WorkspaceLoaderFactory>,
3840 ) -> Self {
3841 self.workspace_loader_factory = workspace_loader_factory;
3842 self
3843 }
3844
3845 pub fn add_symbol_resolver_extension(
3846 mut self,
3847 symbol_resolver: Box<dyn SymbolResolverExtension>,
3848 ) -> Self {
3849 self.revset_extensions.add_symbol_resolver(symbol_resolver);
3850 self
3851 }
3852
3853 pub fn add_revset_function_extension(
3854 mut self,
3855 name: &'static str,
3856 func: RevsetFunction,
3857 ) -> Self {
3858 self.revset_extensions.add_custom_function(name, func);
3859 self
3860 }
3861
3862 pub fn add_commit_template_extension(
3863 mut self,
3864 commit_template_extension: Box<dyn CommitTemplateLanguageExtension>,
3865 ) -> Self {
3866 self.commit_template_extensions
3867 .push(commit_template_extension.into());
3868 self
3869 }
3870
3871 pub fn add_operation_template_extension(
3872 mut self,
3873 operation_template_extension: Box<dyn OperationTemplateLanguageExtension>,
3874 ) -> Self {
3875 self.operation_template_extensions
3876 .push(operation_template_extension.into());
3877 self
3878 }
3879
3880 pub fn add_dispatch_hook<F>(mut self, dispatch_hook_fn: F) -> Self
3884 where
3885 F: FnOnce(&mut Ui, &CommandHelper, CliDispatchFn) -> Result<(), CommandError> + 'a,
3886 {
3887 self.dispatch_hook_fns.push(Box::new(dispatch_hook_fn));
3888 self
3889 }
3890
3891 pub fn add_subcommand<C, F>(mut self, custom_dispatch_fn: F) -> Self
3893 where
3894 C: clap::Subcommand,
3895 F: FnOnce(&mut Ui, &CommandHelper, C) -> Result<(), CommandError> + 'a,
3896 {
3897 let old_dispatch_fn = self.dispatch_fn;
3898 let new_dispatch_fn =
3899 move |ui: &mut Ui, command_helper: &CommandHelper| match C::from_arg_matches(
3900 command_helper.matches(),
3901 ) {
3902 Ok(command) => custom_dispatch_fn(ui, command_helper, command),
3903 Err(_) => old_dispatch_fn(ui, command_helper),
3904 };
3905 self.app = C::augment_subcommands(self.app);
3906 self.dispatch_fn = Box::new(new_dispatch_fn);
3907 self
3908 }
3909
3910 pub fn add_global_args<A, F>(mut self, process_before: F) -> Self
3912 where
3913 A: clap::Args,
3914 F: FnOnce(&mut Ui, A) -> Result<(), CommandError> + 'a,
3915 {
3916 let process_global_args_fn = move |ui: &mut Ui, matches: &ArgMatches| {
3917 let custom_args = A::from_arg_matches(matches).unwrap();
3918 process_before(ui, custom_args)
3919 };
3920 self.app = A::augment_args(self.app);
3921 self.process_global_args_fns
3922 .push(Box::new(process_global_args_fn));
3923 self
3924 }
3925
3926 #[instrument(skip_all)]
3927 fn run_internal(self, ui: &mut Ui, mut raw_config: RawConfig) -> Result<(), CommandError> {
3928 let cwd = env::current_dir()
3931 .and_then(dunce::canonicalize)
3932 .map_err(|_| {
3933 user_error_with_hint(
3934 "Could not determine current directory",
3935 "Did you update to a commit where the directory doesn't exist or can't be \
3936 accessed?",
3937 )
3938 })?;
3939 let mut config_env = ConfigEnv::from_environment();
3940 let mut last_config_migration_descriptions = Vec::new();
3941 let mut migrate_config = |config: &mut StackedConfig| -> Result<(), CommandError> {
3942 last_config_migration_descriptions =
3943 jj_lib::config::migrate(config, &self.config_migrations)?;
3944 Ok(())
3945 };
3946
3947 let maybe_cwd_workspace_loader = self
3953 .workspace_loader_factory
3954 .create(find_workspace_dir(&cwd))
3955 .map_err(|err| map_workspace_load_error(err, Some(".")));
3956 config_env.reload_user_config(&mut raw_config)?;
3957 if let Ok(loader) = &maybe_cwd_workspace_loader {
3958 config_env.reset_repo_path(loader.repo_path());
3959 config_env.reload_repo_config(&mut raw_config)?;
3960 config_env.reset_workspace_path(loader.workspace_root());
3961 config_env.reload_workspace_config(&mut raw_config)?;
3962 }
3963 let mut config = config_env.resolve_config(&raw_config)?;
3964 migrate_config(&mut config)?;
3965 ui.reset(&config)?;
3966
3967 if env::var_os("COMPLETE").is_some() {
3968 return handle_shell_completion(&Ui::null(), &self.app, &config, &cwd);
3969 }
3970
3971 let string_args = expand_args(ui, &self.app, env::args_os(), &config)?;
3972 let (args, config_layers) = parse_early_args(&self.app, &string_args)?;
3973 if !config_layers.is_empty() {
3974 raw_config.as_mut().extend_layers(config_layers);
3975 config = config_env.resolve_config(&raw_config)?;
3976 migrate_config(&mut config)?;
3977 ui.reset(&config)?;
3978 }
3979
3980 if args.has_config_args() {
3981 warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
3982 }
3983
3984 let (matches, args) = parse_args(&self.app, &string_args)
3985 .map_err(|err| map_clap_cli_error(err, ui, &config))?;
3986 if args.global_args.debug {
3987 self.tracing_subscription.enable_debug_logging()?;
3989 }
3990 for process_global_args_fn in self.process_global_args_fns {
3991 process_global_args_fn(ui, &matches)?;
3992 }
3993 config_env.set_command_name(command_name(&matches));
3994
3995 let maybe_workspace_loader = if let Some(path) = &args.global_args.repository {
3996 let abs_path = cwd.join(path);
3998 let abs_path = dunce::canonicalize(&abs_path).unwrap_or(abs_path);
3999 let loader = self
4001 .workspace_loader_factory
4002 .create(&abs_path)
4003 .map_err(|err| map_workspace_load_error(err, Some(path)))?;
4004 config_env.reset_repo_path(loader.repo_path());
4005 config_env.reload_repo_config(&mut raw_config)?;
4006 config_env.reset_workspace_path(loader.workspace_root());
4007 config_env.reload_workspace_config(&mut raw_config)?;
4008 Ok(loader)
4009 } else {
4010 maybe_cwd_workspace_loader
4011 };
4012
4013 config = config_env.resolve_config(&raw_config)?;
4015 migrate_config(&mut config)?;
4016 ui.reset(&config)?;
4017
4018 for (source, desc) in &last_config_migration_descriptions {
4020 let source_str = match source {
4021 ConfigSource::Default => "default-provided",
4022 ConfigSource::EnvBase | ConfigSource::EnvOverrides => "environment-provided",
4023 ConfigSource::User => "user-level",
4024 ConfigSource::Repo => "repo-level",
4025 ConfigSource::Workspace => "workspace-level",
4026 ConfigSource::CommandArg => "CLI-provided",
4027 };
4028 writeln!(
4029 ui.warning_default(),
4030 "Deprecated {source_str} config: {desc}"
4031 )?;
4032 }
4033
4034 if args.global_args.repository.is_some() {
4035 warn_if_args_mismatch(ui, &self.app, &config, &string_args)?;
4036 }
4037
4038 let settings = UserSettings::from_config(config)?;
4039 let command_helper_data = CommandHelperData {
4040 app: self.app,
4041 cwd,
4042 string_args,
4043 matches,
4044 global_args: args.global_args,
4045 config_env,
4046 config_migrations: self.config_migrations,
4047 raw_config,
4048 settings,
4049 revset_extensions: self.revset_extensions.into(),
4050 commit_template_extensions: self.commit_template_extensions,
4051 operation_template_extensions: self.operation_template_extensions,
4052 maybe_workspace_loader,
4053 store_factories: self.store_factories,
4054 working_copy_factories: self.working_copy_factories,
4055 workspace_loader_factory: self.workspace_loader_factory,
4056 };
4057 let command_helper = CommandHelper {
4058 data: Rc::new(command_helper_data),
4059 };
4060 let dispatch_fn = self.dispatch_hook_fns.into_iter().fold(
4061 self.dispatch_fn,
4062 |old_dispatch_fn, dispatch_hook_fn| {
4063 Box::new(move |ui: &mut Ui, command_helper: &CommandHelper| {
4064 dispatch_hook_fn(ui, command_helper, old_dispatch_fn)
4065 })
4066 },
4067 );
4068 (dispatch_fn)(ui, &command_helper)
4069 }
4070
4071 #[must_use]
4072 #[instrument(skip(self))]
4073 pub fn run(mut self) -> u8 {
4074 crossterm::style::force_color_output(true);
4076 let config = config_from_environment(self.config_layers.drain(..));
4077 let mut ui = Ui::with_config(config.as_ref())
4080 .expect("default config should be valid, env vars are stringly typed");
4081 let result = self.run_internal(&mut ui, config);
4082 let exit_code = handle_command_result(&mut ui, result);
4083 ui.finalize_pager();
4084 exit_code
4085 }
4086}
4087
4088fn map_clap_cli_error(err: clap::Error, ui: &Ui, config: &StackedConfig) -> CommandError {
4089 if let Some(ContextValue::String(cmd)) = err.get(ContextKind::InvalidSubcommand) {
4090 let remove_useless_error_context = |mut err: clap::Error| {
4091 err.remove(ContextKind::SuggestedSubcommand);
4094 err.remove(ContextKind::Suggested); err.remove(ContextKind::Usage); err
4097 };
4098 match cmd.as_str() {
4099 "clone" | "init" => {
4102 let cmd = cmd.clone();
4103 return CommandError::from(remove_useless_error_context(err))
4104 .hinted(format!(
4105 "You probably want `jj git {cmd}`. See also `jj help git`."
4106 ))
4107 .hinted(format!(
4108 r#"You can configure `aliases.{cmd} = ["git", "{cmd}"]` if you want `jj {cmd}` to work and always use the Git backend."#
4109 ));
4110 }
4111 "amend" => {
4112 return CommandError::from(remove_useless_error_context(err))
4113 .hinted(
4114 r#"You probably want `jj squash`. You can configure `aliases.amend = ["squash"]` if you want `jj amend` to work."#);
4115 }
4116 _ => {}
4117 }
4118 }
4119 if let (Some(ContextValue::String(arg)), Some(ContextValue::String(value))) = (
4120 err.get(ContextKind::InvalidArg),
4121 err.get(ContextKind::InvalidValue),
4122 ) && arg.as_str() == "--template <TEMPLATE>"
4123 && value.is_empty()
4124 {
4125 if let Ok(template_aliases) = load_template_aliases(ui, config) {
4127 return CommandError::from(err).hinted(format_template_aliases_hint(&template_aliases));
4128 }
4129 }
4130 CommandError::from(err)
4131}
4132
4133fn format_template_aliases_hint(template_aliases: &TemplateAliasesMap) -> String {
4134 let mut hint = String::from("The following template aliases are defined:\n");
4135 hint.push_str(
4136 &template_aliases
4137 .symbol_names()
4138 .sorted_unstable()
4139 .map(|name| format!("- {name}"))
4140 .join("\n"),
4141 );
4142 hint
4143}
4144
4145fn warn_if_args_mismatch(
4147 ui: &Ui,
4148 app: &Command,
4149 config: &StackedConfig,
4150 expected_args: &[String],
4151) -> Result<(), CommandError> {
4152 let new_string_args = expand_args(ui, app, env::args_os(), config).ok();
4153 if new_string_args.as_deref() != Some(expected_args) {
4154 writeln!(
4155 ui.warning_default(),
4156 "Command aliases cannot be loaded from -R/--repository path or --config/--config-file \
4157 arguments."
4158 )?;
4159 }
4160 Ok(())
4161}
4162
4163#[cfg(test)]
4164mod tests {
4165 use clap::CommandFactory as _;
4166
4167 use super::*;
4168
4169 #[derive(clap::Parser, Clone, Debug)]
4170 pub struct TestArgs {
4171 #[arg(long)]
4172 pub foo: Vec<u32>,
4173 #[arg(long)]
4174 pub bar: Vec<u32>,
4175 #[arg(long)]
4176 pub baz: bool,
4177 }
4178
4179 #[test]
4180 fn test_merge_args_with() {
4181 let command = TestArgs::command();
4182 let parse = |args: &[&str]| -> Vec<(&'static str, u32)> {
4183 let matches = command.clone().try_get_matches_from(args).unwrap();
4184 let args = TestArgs::from_arg_matches(&matches).unwrap();
4185 merge_args_with(
4186 &matches,
4187 &[("foo", &args.foo), ("bar", &args.bar)],
4188 |id, value| (id, *value),
4189 )
4190 };
4191
4192 assert_eq!(parse(&["jj"]), vec![]);
4193 assert_eq!(parse(&["jj", "--foo=1"]), vec![("foo", 1)]);
4194 assert_eq!(
4195 parse(&["jj", "--foo=1", "--bar=2"]),
4196 vec![("foo", 1), ("bar", 2)]
4197 );
4198 assert_eq!(
4199 parse(&["jj", "--foo=1", "--baz", "--bar=2", "--foo", "3"]),
4200 vec![("foo", 1), ("bar", 2), ("foo", 3)]
4201 );
4202 }
4203}