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