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