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