// Copyright 2022 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use core::fmt; use std::collections::{HashMap, HashSet}; use std::env::{self, ArgsOs, VarError}; use std::ffi::{OsStr, OsString}; use std::fmt::Debug; use std::io::{self, Write as _}; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::process::ExitCode; use std::rc::Rc; use std::str::FromStr; use std::sync::Arc; use std::time::SystemTime; use std::{fs, iter, str}; use clap::builder::{NonEmptyStringValueParser, TypedValueParser, ValueParserFactory}; use clap::{Arg, ArgAction, ArgMatches, Command, FromArgMatches}; use indexmap::{IndexMap, IndexSet}; use itertools::Itertools; use jj_lib::backend::{BackendError, ChangeId, CommitId, MergedTreeId}; use jj_lib::commit::Commit; use jj_lib::git::{GitConfigParseError, GitExportError, GitImportError, GitRemoteManagementError}; use jj_lib::git_backend::GitBackend; use jj_lib::gitignore::{GitIgnoreError, GitIgnoreFile}; use jj_lib::hex_util::to_reverse_hex; use jj_lib::id_prefix::IdPrefixContext; use jj_lib::matchers::{EverythingMatcher, Matcher, PrefixMatcher}; use jj_lib::merged_tree::MergedTree; use jj_lib::object_id::ObjectId; use jj_lib::op_heads_store::{self, OpHeadResolutionError}; use jj_lib::op_store::{OpStoreError, OperationId, WorkspaceId}; use jj_lib::op_walk::OpsetEvaluationError; use jj_lib::operation::Operation; use jj_lib::repo::{ CheckOutCommitError, EditCommitError, MutableRepo, ReadonlyRepo, Repo, RepoLoader, RepoLoaderError, RewriteRootCommit, StoreFactories, StoreLoadError, }; use jj_lib::repo_path::{FsPathParseError, RepoPath, RepoPathBuf}; use jj_lib::revset::{ DefaultSymbolResolver, Revset, RevsetAliasesMap, RevsetCommitRef, RevsetEvaluationError, RevsetExpression, RevsetFilterPredicate, RevsetIteratorExt, RevsetParseContext, RevsetParseError, RevsetParseErrorKind, RevsetResolutionError, RevsetWorkspaceContext, }; use jj_lib::rewrite::restore_tree; use jj_lib::settings::{ConfigResultExt as _, UserSettings}; use jj_lib::signing::SignInitError; use jj_lib::str_util::{StringPattern, StringPatternParseError}; use jj_lib::transaction::Transaction; use jj_lib::tree::TreeMergeError; use jj_lib::view::View; use jj_lib::working_copy::{ CheckoutStats, LockedWorkingCopy, ResetError, SnapshotError, SnapshotOptions, WorkingCopy, WorkingCopyFactory, WorkingCopyStateError, }; use jj_lib::workspace::{ default_working_copy_factories, LockedWorkspace, Workspace, WorkspaceInitError, WorkspaceLoadError, WorkspaceLoader, }; use jj_lib::{dag_walk, file_util, git, op_walk, revset}; use once_cell::unsync::OnceCell; use thiserror::Error; use toml_edit; use tracing::instrument; use tracing_chrome::ChromeLayerBuilder; use tracing_subscriber::prelude::*; use crate::config::{ new_config_path, AnnotatedValue, CommandNameAndArgs, ConfigSource, LayeredConfigs, }; use crate::formatter::{FormatRecorder, Formatter, PlainTextFormatter}; use crate::git_util::{ is_colocated_git_workspace, print_failed_git_export, print_git_import_stats, }; use crate::merge_tools::{ConflictResolveError, DiffEditError, DiffGenerateError}; use crate::template_parser::{TemplateAliasesMap, TemplateParseError}; use crate::templater::Template; use crate::ui::{ColorChoice, Ui}; use crate::{commit_templater, text_util}; #[derive(Clone, Debug)] pub enum CommandError { UserError { err: Arc, hint: Option, }, ConfigError(String), /// Invalid command line CliError(String), /// Invalid command line detected by clap ClapCliError(Arc), BrokenPipe, InternalError(Arc), } /// Wraps error with user-visible message. #[derive(Debug, Error)] #[error("{message}")] struct ErrorWithMessage { message: String, source: Box, } impl ErrorWithMessage { fn new( message: impl Into, source: impl Into>, ) -> Self { ErrorWithMessage { message: message.into(), source: source.into(), } } } pub fn user_error(err: impl Into>) -> CommandError { user_error_with_hint_opt(err, None) } pub fn user_error_with_hint( err: impl Into>, hint: impl Into, ) -> CommandError { user_error_with_hint_opt(err, Some(hint.into())) } pub fn user_error_with_message( message: impl Into, source: impl Into>, ) -> CommandError { user_error_with_hint_opt(ErrorWithMessage::new(message, source), None) } pub fn user_error_with_message_and_hint( message: impl Into, hint: impl Into, source: impl Into>, ) -> CommandError { user_error_with_hint_opt(ErrorWithMessage::new(message, source), Some(hint.into())) } pub fn user_error_with_hint_opt( err: impl Into>, hint: Option, ) -> CommandError { CommandError::UserError { err: Arc::from(err.into()), hint, } } pub fn internal_error(err: impl Into>) -> CommandError { CommandError::InternalError(Arc::from(err.into())) } pub fn internal_error_with_message( message: impl Into, source: impl Into>, ) -> CommandError { CommandError::InternalError(Arc::new(ErrorWithMessage::new(message, source))) } fn format_similarity_hint>(candidates: &[S]) -> Option { match candidates { [] => None, names => { let quoted_names = names .iter() .map(|s| format!(r#""{}""#, s.as_ref())) .join(", "); Some(format!("Did you mean {quoted_names}?")) } } } fn print_error_sources(ui: &Ui, source: Option<&dyn std::error::Error>) -> io::Result<()> { let Some(err) = source else { return Ok(()); }; if err.source().is_none() { writeln!(ui.stderr(), "Caused by: {err}")?; } else { writeln!(ui.stderr(), "Caused by:")?; for (i, err) in iter::successors(Some(err), |err| err.source()).enumerate() { writeln!(ui.stderr(), "{n}: {err}", n = i + 1)?; } } Ok(()) } impl From for CommandError { fn from(err: std::io::Error) -> Self { if err.kind() == std::io::ErrorKind::BrokenPipe { CommandError::BrokenPipe } else { user_error(err) } } } impl From for CommandError { fn from(err: config::ConfigError) -> Self { CommandError::ConfigError(err.to_string()) } } impl From for CommandError { fn from(err: crate::config::ConfigError) -> Self { CommandError::ConfigError(err.to_string()) } } impl From for CommandError { fn from(err: RewriteRootCommit) -> Self { internal_error_with_message("Attempted to rewrite the root commit", err) } } impl From for CommandError { fn from(err: EditCommitError) -> Self { internal_error_with_message("Failed to edit a commit", err) } } impl From for CommandError { fn from(err: CheckOutCommitError) -> Self { internal_error_with_message("Failed to check out a commit", err) } } impl From for CommandError { fn from(err: BackendError) -> Self { internal_error_with_message("Unexpected error from backend", err) } } impl From for CommandError { fn from(err: WorkspaceInitError) -> Self { match err { WorkspaceInitError::DestinationExists(_) => { user_error("The target repo already exists") } WorkspaceInitError::NonUnicodePath => { user_error("The target repo path contains non-unicode characters") } WorkspaceInitError::CheckOutCommit(err) => { internal_error_with_message("Failed to check out the initial commit", err) } WorkspaceInitError::Path(err) => { internal_error_with_message("Failed to access the repository", err) } WorkspaceInitError::PathNotFound(path) => { user_error(format!("{} doesn't exist", path.display())) } WorkspaceInitError::Backend(err) => { user_error_with_message("Failed to access the repository", err) } WorkspaceInitError::WorkingCopyState(err) => { internal_error_with_message("Failed to access the repository", err) } WorkspaceInitError::SignInit(err @ SignInitError::UnknownBackend(_)) => user_error(err), WorkspaceInitError::SignInit(err) => internal_error(err), } } } impl From for CommandError { fn from(err: OpHeadResolutionError) -> Self { match err { OpHeadResolutionError::NoHeads => { internal_error_with_message("Corrupt repository", err) } } } } impl From for CommandError { fn from(err: OpsetEvaluationError) -> Self { match err { OpsetEvaluationError::OpsetResolution(err) => user_error(err), OpsetEvaluationError::OpHeadResolution(err) => err.into(), OpsetEvaluationError::OpStore(err) => err.into(), } } } impl From for CommandError { fn from(err: SnapshotError) -> Self { match err { SnapshotError::NewFileTooLarge { .. } => user_error_with_message_and_hint( "Failed to snapshot the working copy", r#"Increase the value of the `snapshot.max-new-file-size` config option if you want this file to be snapshotted. Otherwise add it to your `.gitignore` file."#, err, ), err => internal_error_with_message("Failed to snapshot the working copy", err), } } } impl From for CommandError { fn from(err: TreeMergeError) -> Self { internal_error_with_message("Merge failed", err) } } impl From for CommandError { fn from(err: OpStoreError) -> Self { internal_error_with_message("Failed to load an operation", err) } } impl From for CommandError { fn from(err: RepoLoaderError) -> Self { internal_error_with_message("Failed to load the repo", err) } } impl From for CommandError { fn from(err: ResetError) -> Self { internal_error_with_message("Failed to reset the working copy", err) } } impl From for CommandError { fn from(err: DiffEditError) -> Self { user_error_with_message("Failed to edit diff", err) } } impl From for CommandError { fn from(err: DiffGenerateError) -> Self { user_error_with_message("Failed to generate diff", err) } } impl From for CommandError { fn from(err: ConflictResolveError) -> Self { user_error_with_message("Failed to resolve conflicts", err) } } impl From for CommandError { fn from(err: git2::Error) -> Self { user_error_with_message("Git operation failed", err) } } impl From for CommandError { fn from(err: GitImportError) -> Self { let message = format!("Failed to import refs from underlying Git repo: {err}"); let hint = match &err { GitImportError::MissingHeadTarget { .. } | GitImportError::MissingRefAncestor { .. } => Some( "\ Is this Git repository a shallow or partial clone (cloned with the --depth or --filter \ argument)? jj currently does not support shallow/partial clones. To use jj with this \ repository, try unshallowing the repository (https://stackoverflow.com/q/6802145) or re-cloning with the full repository contents." .to_string(), ), GitImportError::RemoteReservedForLocalGitRepo => { Some("Run `jj git remote rename` to give different name.".to_string()) } GitImportError::InternalBackend(_) => None, GitImportError::InternalGitError(_) => None, GitImportError::UnexpectedBackend => None, }; user_error_with_hint_opt(message, hint) } } impl From for CommandError { fn from(err: GitExportError) -> Self { internal_error_with_message("Failed to export refs to underlying Git repo", err) } } impl From for CommandError { fn from(err: GitRemoteManagementError) -> Self { user_error(err) } } impl From for CommandError { fn from(err: RevsetEvaluationError) -> Self { user_error(err) } } impl From for CommandError { fn from(err: RevsetParseError) -> Self { let message = iter::successors(Some(&err), |e| e.origin()).join("\n"); // Only for the top-level error as we can't attach hint to inner errors let hint = match err.kind() { RevsetParseErrorKind::NotPrefixOperator { op: _, similar_op, description, } | RevsetParseErrorKind::NotPostfixOperator { op: _, similar_op, description, } | RevsetParseErrorKind::NotInfixOperator { op: _, similar_op, description, } => Some(format!("Did you mean '{similar_op}' for {description}?")), RevsetParseErrorKind::NoSuchFunction { name: _, candidates, } => format_similarity_hint(candidates), _ => None, }; user_error_with_hint_opt(format!("Failed to parse revset: {message}"), hint) } } impl From for CommandError { fn from(err: RevsetResolutionError) -> Self { let hint = match &err { RevsetResolutionError::NoSuchRevision { name: _, candidates, } => format_similarity_hint(candidates), RevsetResolutionError::EmptyString | RevsetResolutionError::WorkspaceMissingWorkingCopy { .. } | RevsetResolutionError::AmbiguousCommitIdPrefix(_) | RevsetResolutionError::AmbiguousChangeIdPrefix(_) | RevsetResolutionError::StoreError(_) => None, }; user_error_with_hint_opt(err, hint) } } impl From for CommandError { fn from(err: TemplateParseError) -> Self { let message = iter::successors(Some(&err), |e| e.origin()).join("\n"); user_error(format!("Failed to parse template: {message}")) } } impl From for CommandError { fn from(err: FsPathParseError) -> Self { user_error(err) } } impl From for CommandError { fn from(err: clap::Error) -> Self { CommandError::ClapCliError(Arc::new(err)) } } impl From for CommandError { fn from(err: GitConfigParseError) -> Self { internal_error_with_message("Failed to parse Git config", err) } } impl From for CommandError { fn from(err: WorkingCopyStateError) -> Self { internal_error_with_message("Failed to access working copy state", err) } } impl From for CommandError { fn from(err: GitIgnoreError) -> Self { user_error_with_message("Failed to process .gitignore.", err) } } #[derive(Clone)] struct ChromeTracingFlushGuard { _inner: Option>, } impl Debug for ChromeTracingFlushGuard { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Self { _inner } = self; f.debug_struct("ChromeTracingFlushGuard") .finish_non_exhaustive() } } /// Handle to initialize or change tracing subscription. #[derive(Clone, Debug)] pub struct TracingSubscription { reload_log_filter: tracing_subscriber::reload::Handle< tracing_subscriber::EnvFilter, tracing_subscriber::Registry, >, _chrome_tracing_flush_guard: ChromeTracingFlushGuard, } impl TracingSubscription { /// Initializes tracing with the default configuration. This should be /// called as early as possible. pub fn init() -> Self { let filter = tracing_subscriber::EnvFilter::builder() .with_default_directive(tracing::metadata::LevelFilter::ERROR.into()) .from_env_lossy(); let (filter, reload_log_filter) = tracing_subscriber::reload::Layer::new(filter); let (chrome_tracing_layer, chrome_tracing_flush_guard) = match std::env::var("JJ_TRACE") { Ok(filename) => { let filename = if filename.is_empty() { format!( "jj-trace-{}.json", SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs(), ) } else { filename }; let include_args = std::env::var("JJ_TRACE_INCLUDE_ARGS").is_ok(); let (layer, guard) = ChromeLayerBuilder::new() .file(filename) .include_args(include_args) .build(); ( Some(layer), ChromeTracingFlushGuard { _inner: Some(Rc::new(guard)), }, ) } Err(_) => (None, ChromeTracingFlushGuard { _inner: None }), }; tracing_subscriber::registry() .with( tracing_subscriber::fmt::Layer::default() .with_writer(std::io::stderr) .with_filter(filter), ) .with(chrome_tracing_layer) .init(); TracingSubscription { reload_log_filter, _chrome_tracing_flush_guard: chrome_tracing_flush_guard, } } pub fn enable_debug_logging(&self) -> Result<(), CommandError> { self.reload_log_filter .modify(|filter| { *filter = tracing_subscriber::EnvFilter::builder() .with_default_directive(tracing::metadata::LevelFilter::DEBUG.into()) .from_env_lossy() }) .map_err(|err| internal_error_with_message("failed to enable debug logging", err))?; tracing::info!("debug logging enabled"); Ok(()) } } pub struct CommandHelper { app: Command, cwd: PathBuf, string_args: Vec, matches: ArgMatches, global_args: GlobalArgs, settings: UserSettings, layered_configs: LayeredConfigs, maybe_workspace_loader: Result, store_factories: StoreFactories, working_copy_factories: HashMap>, } impl CommandHelper { #[allow(clippy::too_many_arguments)] pub fn new( app: Command, cwd: PathBuf, string_args: Vec, matches: ArgMatches, global_args: GlobalArgs, settings: UserSettings, layered_configs: LayeredConfigs, maybe_workspace_loader: Result, store_factories: StoreFactories, working_copy_factories: HashMap>, ) -> Self { // `cwd` is canonicalized for consistency with `Workspace::workspace_root()` and // to easily compute relative paths between them. let cwd = cwd.canonicalize().unwrap_or(cwd); Self { app, cwd, string_args, matches, global_args, settings, layered_configs, maybe_workspace_loader, store_factories, working_copy_factories, } } pub fn app(&self) -> &Command { &self.app } pub fn cwd(&self) -> &Path { &self.cwd } pub fn string_args(&self) -> &Vec { &self.string_args } pub fn matches(&self) -> &ArgMatches { &self.matches } pub fn global_args(&self) -> &GlobalArgs { &self.global_args } pub fn settings(&self) -> &UserSettings { &self.settings } pub fn resolved_config_values( &self, prefix: &[&str], ) -> Result, crate::config::ConfigError> { self.layered_configs.resolved_config_values(prefix) } /// Loads template aliases from the configs. /// /// For most commands that depend on a loaded repo, you should use /// `WorkspaceCommandHelper::template_aliases_map()` instead. pub fn load_template_aliases(&self, ui: &Ui) -> Result { load_template_aliases(ui, &self.layered_configs) } pub fn workspace_loader(&self) -> Result<&WorkspaceLoader, CommandError> { self.maybe_workspace_loader.as_ref().map_err(Clone::clone) } /// Loads workspace and repo, then snapshots the working copy if allowed. #[instrument(skip(self, ui))] pub fn workspace_helper(&self, ui: &mut Ui) -> Result { let mut workspace_command = self.workspace_helper_no_snapshot(ui)?; workspace_command.maybe_snapshot(ui)?; Ok(workspace_command) } /// Loads workspace and repo, but never snapshots the working copy. Most /// commands should use `workspace_helper()` instead. #[instrument(skip(self, ui))] pub fn workspace_helper_no_snapshot( &self, ui: &mut Ui, ) -> Result { let workspace = self.load_workspace()?; let op_head = self.resolve_operation(ui, workspace.repo_loader())?; let repo = workspace.repo_loader().load_at(&op_head)?; self.for_loaded_repo(ui, workspace, repo) } pub fn get_working_copy_factory(&self) -> Result<&dyn WorkingCopyFactory, CommandError> { let loader = self.workspace_loader()?; // We convert StoreLoadError -> WorkspaceLoadError -> CommandError let factory: Result<_, WorkspaceLoadError> = loader .get_working_copy_factory(&self.working_copy_factories) .map_err(|e| e.into()); let factory = factory .map_err(|err| map_workspace_load_error(err, self.global_args.repository.as_deref()))?; Ok(factory) } #[instrument(skip_all)] pub fn load_workspace(&self) -> Result { let loader = self.workspace_loader()?; loader .load( &self.settings, &self.store_factories, &self.working_copy_factories, ) .map_err(|err| map_workspace_load_error(err, self.global_args.repository.as_deref())) } #[instrument(skip_all)] pub fn resolve_operation( &self, ui: &mut Ui, repo_loader: &RepoLoader, ) -> Result { if self.global_args.at_operation == "@" { op_heads_store::resolve_op_heads( repo_loader.op_heads_store().as_ref(), repo_loader.op_store(), |op_heads| { writeln!( ui.stderr(), "Concurrent modification detected, resolving automatically.", )?; let base_repo = repo_loader.load_at(&op_heads[0])?; // TODO: It may be helpful to print each operation we're merging here let mut tx = start_repo_transaction(&base_repo, &self.settings, &self.string_args); for other_op_head in op_heads.into_iter().skip(1) { tx.merge_operation(other_op_head)?; let num_rebased = tx.mut_repo().rebase_descendants(&self.settings)?; if num_rebased > 0 { writeln!( ui.stderr(), "Rebased {num_rebased} descendant commits onto commits rewritten \ by other operation" )?; } } Ok(tx .write("resolve concurrent operations") .leave_unpublished() .operation() .clone()) }, ) } else { let operation = op_walk::resolve_op_for_load(repo_loader, &self.global_args.at_operation)?; Ok(operation) } } #[instrument(skip_all)] pub fn for_loaded_repo( &self, ui: &mut Ui, workspace: Workspace, repo: Arc, ) -> Result { WorkspaceCommandHelper::new(ui, self, workspace, repo) } } /// A ReadonlyRepo along with user-config-dependent derived data. The derived /// data is lazily loaded. struct ReadonlyUserRepo { repo: Arc, id_prefix_context: OnceCell, } impl ReadonlyUserRepo { fn new(repo: Arc) -> Self { Self { repo, id_prefix_context: OnceCell::new(), } } pub fn git_backend(&self) -> Option<&GitBackend> { self.repo.store().backend_impl().downcast_ref() } } // Provides utilities for writing a command that works on a workspace (like most // commands do). pub struct WorkspaceCommandHelper { cwd: PathBuf, string_args: Vec, global_args: GlobalArgs, settings: UserSettings, workspace: Workspace, user_repo: ReadonlyUserRepo, revset_aliases_map: RevsetAliasesMap, template_aliases_map: TemplateAliasesMap, may_update_working_copy: bool, working_copy_shared_with_git: bool, } impl WorkspaceCommandHelper { #[instrument(skip_all)] pub fn new( ui: &mut Ui, command: &CommandHelper, workspace: Workspace, repo: Arc, ) -> Result { let revset_aliases_map = load_revset_aliases(ui, &command.layered_configs)?; let template_aliases_map = command.load_template_aliases(ui)?; // Parse commit_summary template early to report error before starting mutable // operation. // TODO: Parsed template can be cached if it doesn't capture repo let id_prefix_context = IdPrefixContext::default(); parse_commit_summary_template( repo.as_ref(), workspace.workspace_id(), &id_prefix_context, &template_aliases_map, &command.settings, )?; let loaded_at_head = command.global_args.at_operation == "@"; let may_update_working_copy = loaded_at_head && !command.global_args.ignore_working_copy; let working_copy_shared_with_git = is_colocated_git_workspace(&workspace, &repo); let helper = Self { cwd: command.cwd.clone(), string_args: command.string_args.clone(), global_args: command.global_args.clone(), settings: command.settings.clone(), workspace, user_repo: ReadonlyUserRepo::new(repo), revset_aliases_map, template_aliases_map, may_update_working_copy, working_copy_shared_with_git, }; // Parse short-prefixes revset early to report error before starting mutable // operation. helper.id_prefix_context()?; Ok(helper) } pub fn git_backend(&self) -> Option<&GitBackend> { self.user_repo.git_backend() } pub fn check_working_copy_writable(&self) -> Result<(), CommandError> { if self.may_update_working_copy { Ok(()) } else { let hint = if self.global_args.ignore_working_copy { "Don't use --ignore-working-copy." } else { "Don't use --at-op." }; Err(user_error_with_hint( "This command must be able to update the working copy.", hint, )) } } /// Snapshot the working copy if allowed, and import Git refs if the working /// copy is collocated with Git. #[instrument(skip_all)] pub fn maybe_snapshot(&mut self, ui: &mut Ui) -> Result<(), CommandError> { if self.may_update_working_copy { if self.working_copy_shared_with_git { self.import_git_head(ui)?; } // Because the Git refs (except HEAD) aren't imported yet, the ref // pointing to the new working-copy commit might not be exported. // In that situation, the ref would be conflicted anyway, so export // failure is okay. self.snapshot_working_copy(ui)?; // import_git_refs() can rebase the working-copy commit. if self.working_copy_shared_with_git { self.import_git_refs(ui)?; } } Ok(()) } /// Imports new HEAD from the colocated Git repo. /// /// If the Git HEAD has changed, this function abandons our old checkout and /// checks out the new Git HEAD. The working-copy state will be reset to /// point to the new Git HEAD. The working-copy contents won't be updated. #[instrument(skip_all)] fn import_git_head(&mut self, ui: &mut Ui) -> Result<(), CommandError> { assert!(self.may_update_working_copy); let mut tx = self.start_transaction(); git::import_head(tx.mut_repo())?; if !tx.mut_repo().has_changes() { return Ok(()); } // TODO: There are various ways to get duplicated working-copy // commits. Some of them could be mitigated by checking the working-copy // operation id after acquiring the lock, but that isn't enough. // // - moved HEAD was observed by multiple jj processes, and new working-copy // commits are created concurrently. // - new HEAD was exported by jj, but the operation isn't committed yet. // - new HEAD was exported by jj, but the new working-copy commit isn't checked // out yet. let mut tx = tx.into_inner(); let old_git_head = self.repo().view().git_head().clone(); let new_git_head = tx.mut_repo().view().git_head().clone(); if let Some(new_git_head_id) = new_git_head.as_normal() { let workspace_id = self.workspace_id().to_owned(); if let Some(old_wc_commit_id) = self.repo().view().get_wc_commit_id(&workspace_id) { tx.mut_repo() .record_abandoned_commit(old_wc_commit_id.clone()); } let new_git_head_commit = tx.mut_repo().store().get_commit(new_git_head_id)?; tx.mut_repo() .check_out(workspace_id, &self.settings, &new_git_head_commit)?; let mut locked_ws = self.workspace.start_working_copy_mutation()?; // The working copy was presumably updated by the git command that updated // HEAD, so we just need to reset our working copy // state to it without updating working copy files. locked_ws.locked_wc().reset(&new_git_head_commit)?; tx.mut_repo().rebase_descendants(&self.settings)?; self.user_repo = ReadonlyUserRepo::new(tx.commit("import git head")); locked_ws.finish(self.user_repo.repo.op_id().clone())?; if old_git_head.is_present() { writeln!( ui.stderr(), "Reset the working copy parent to the new Git HEAD." )?; } else { // Don't print verbose message on initial checkout. } } else { // Unlikely, but the HEAD ref got deleted by git? self.finish_transaction(ui, tx, "import git head")?; } Ok(()) } /// Imports branches and tags from the underlying Git repo, abandons old /// branches. /// /// If the working-copy branch is rebased, and if update is allowed, the new /// working-copy commit will be checked out. /// /// This function does not import the Git HEAD, but the HEAD may be reset to /// the working copy parent if the repository is colocated. #[instrument(skip_all)] fn import_git_refs(&mut self, ui: &mut Ui) -> Result<(), CommandError> { let git_settings = self.settings.git_settings(); let mut tx = self.start_transaction(); // Automated import shouldn't fail because of reserved remote name. let stats = git::import_some_refs(tx.mut_repo(), &git_settings, |ref_name| { !git::is_reserved_git_remote_ref(ref_name) })?; if !tx.mut_repo().has_changes() { return Ok(()); } print_git_import_stats(ui, tx.repo(), &stats, false)?; let mut tx = tx.into_inner(); // Rebase here to show slightly different status message. let num_rebased = tx.mut_repo().rebase_descendants(&self.settings)?; if num_rebased > 0 { writeln!( ui.stderr(), "Rebased {num_rebased} descendant commits off of commits rewritten from git" )?; } self.finish_transaction(ui, tx, "import git refs")?; writeln!( ui.stderr(), "Done importing changes from the underlying Git repo." )?; Ok(()) } pub fn repo(&self) -> &Arc { &self.user_repo.repo } pub fn working_copy(&self) -> &dyn WorkingCopy { self.workspace.working_copy() } pub fn unchecked_start_working_copy_mutation( &mut self, ) -> Result<(LockedWorkspace, Commit), CommandError> { self.check_working_copy_writable()?; let wc_commit = if let Some(wc_commit_id) = self.get_wc_commit_id() { self.repo().store().get_commit(wc_commit_id)? } else { return Err(user_error("Nothing checked out in this workspace")); }; let locked_ws = self.workspace.start_working_copy_mutation()?; Ok((locked_ws, wc_commit)) } pub fn start_working_copy_mutation( &mut self, ) -> Result<(LockedWorkspace, Commit), CommandError> { let (mut locked_ws, wc_commit) = self.unchecked_start_working_copy_mutation()?; if wc_commit.tree_id() != locked_ws.locked_wc().old_tree_id() { return Err(user_error("Concurrent working copy operation. Try again.")); } Ok((locked_ws, wc_commit)) } pub fn workspace_root(&self) -> &PathBuf { self.workspace.workspace_root() } pub fn workspace_id(&self) -> &WorkspaceId { self.workspace.workspace_id() } pub fn get_wc_commit_id(&self) -> Option<&CommitId> { self.repo().view().get_wc_commit_id(self.workspace_id()) } pub fn working_copy_shared_with_git(&self) -> bool { self.working_copy_shared_with_git } pub fn format_file_path(&self, file: &RepoPath) -> String { file_util::relative_path(&self.cwd, &file.to_fs_path(self.workspace_root())) .to_str() .unwrap() .to_owned() } /// Parses a path relative to cwd into a RepoPath, which is relative to the /// workspace root. pub fn parse_file_path(&self, input: &str) -> Result { RepoPathBuf::parse_fs_path(&self.cwd, self.workspace_root(), input) } pub fn matcher_from_values(&self, values: &[String]) -> Result, CommandError> { if values.is_empty() { Ok(Box::new(EverythingMatcher)) } else { // TODO: Add support for globs and other formats let paths: Vec<_> = values .iter() .map(|v| self.parse_file_path(v)) .try_collect()?; Ok(Box::new(PrefixMatcher::new(paths))) } } #[instrument(skip_all)] pub fn base_ignores(&self) -> Result, GitIgnoreError> { fn get_excludes_file_path(config: &gix::config::File) -> Option { // TODO: maybe use path_by_key() and interpolate(), which can process non-utf-8 // path on Unix. if let Some(value) = config.string_by_key("core.excludesFile") { str::from_utf8(&value) .ok() .map(crate::git_util::expand_git_path) } else { xdg_config_home().ok().map(|x| x.join("git").join("ignore")) } } fn xdg_config_home() -> Result { if let Ok(x) = std::env::var("XDG_CONFIG_HOME") { if !x.is_empty() { return Ok(PathBuf::from(x)); } } std::env::var("HOME").map(|x| Path::new(&x).join(".config")) } let mut git_ignores = GitIgnoreFile::empty(); if let Some(git_backend) = self.git_backend() { let git_repo = git_backend.git_repo(); if let Some(excludes_file_path) = get_excludes_file_path(&git_repo.config_snapshot()) { git_ignores = git_ignores.chain_with_file("", excludes_file_path)?; } git_ignores = git_ignores .chain_with_file("", git_backend.git_repo_path().join("info").join("exclude"))?; } else if let Ok(git_config) = gix::config::File::from_globals() { if let Some(excludes_file_path) = get_excludes_file_path(&git_config) { git_ignores = git_ignores.chain_with_file("", excludes_file_path)?; } } Ok(git_ignores) } pub fn resolve_single_op(&self, op_str: &str) -> Result { op_walk::resolve_op_with_repo(self.repo(), op_str) } /// Resolve a revset to a single revision. Return an error if the revset is /// empty or has multiple revisions. pub fn resolve_single_rev(&self, revision_str: &str) -> Result { let revset_expression = self.parse_revset(revision_str)?; let revset = self.evaluate_revset(revset_expression.clone())?; let mut iter = revset.iter().commits(self.repo().store()).fuse(); match (iter.next(), iter.next()) { (Some(commit), None) => Ok(commit?), (None, _) => Err(user_error(format!( r#"Revset "{revision_str}" didn't resolve to any revisions"# ))), (Some(commit0), Some(commit1)) => { let mut iter = [commit0, commit1].into_iter().chain(iter); let commits: Vec<_> = iter.by_ref().take(5).try_collect()?; let elided = iter.next().is_some(); let commits_summary = commits .iter() .map(|c| self.format_commit_summary(c)) .join("\n") + elided.then_some("\n...").unwrap_or_default(); let hint = if commits[0].change_id() == commits[1].change_id() { // Separate hint if there's commits with same change id format!( r#"The revset "{revision_str}" resolved to these revisions: {commits_summary} Some of these commits have the same change id. Abandon one of them with `jj abandon -r `."#, ) } else if let RevsetExpression::CommitRef(RevsetCommitRef::Symbol(branch_name)) = revset_expression.as_ref() { // Separate hint if there's a conflicted branch format!( r#"Branch {branch_name} resolved to multiple revisions because it's conflicted. It resolved to these revisions: {commits_summary} Set which revision the branch points to with `jj branch set {branch_name} -r `."#, ) } else { format!( r#"The revset "{revision_str}" resolved to these revisions: {commits_summary}"#, ) }; Err(user_error_with_hint( format!(r#"Revset "{revision_str}" resolved to more than one revision"#), hint, )) } } } /// Resolve a revset any number of revisions (including 0). pub fn resolve_revset(&self, revision_str: &str) -> Result, CommandError> { let revset_expression = self.parse_revset(revision_str)?; let revset = self.evaluate_revset(revset_expression)?; Ok(revset.iter().commits(self.repo().store()).try_collect()?) } /// Resolve a revset any number of revisions (including 0), but require the /// user to indicate if they allow multiple revisions by prefixing the /// expression with `all:`. pub fn resolve_revset_default_single( &self, revision_str: &str, ) -> Result, CommandError> { // TODO: Let pest parse the prefix too once we've dropped support for `:` if let Some(revision_str) = revision_str.strip_prefix("all:") { self.resolve_revset(revision_str) } else { self.resolve_single_rev(revision_str) .map_err(|err| match err { CommandError::UserError { err, hint } => CommandError::UserError { err, hint: Some(format!( "{old_hint}Prefix the expression with 'all' to allow any number of \ revisions (i.e. 'all:{}').", revision_str, old_hint = hint.map(|hint| format!("{hint}\n")).unwrap_or_default() )), }, err => err, }) .map(|commit| vec![commit]) } } pub fn parse_revset( &self, revision_str: &str, ) -> Result, RevsetParseError> { let expression = revset::parse(revision_str, &self.revset_parse_context())?; Ok(revset::optimize(expression)) } pub fn evaluate_revset<'repo>( &'repo self, revset_expression: Rc, ) -> Result, CommandError> { let symbol_resolver = self.revset_symbol_resolver()?; let revset_expression = revset_expression.resolve_user_expression(self.repo().as_ref(), &symbol_resolver)?; Ok(revset_expression.evaluate(self.repo().as_ref())?) } pub(crate) fn revset_parse_context(&self) -> RevsetParseContext { let workspace_context = RevsetWorkspaceContext { cwd: &self.cwd, workspace_id: self.workspace_id(), workspace_root: self.workspace.workspace_root(), }; RevsetParseContext { aliases_map: &self.revset_aliases_map, user_email: self.settings.user_email(), workspace: Some(workspace_context), } } pub(crate) fn revset_symbol_resolver(&self) -> Result, CommandError> { let id_prefix_context = self.id_prefix_context()?; let commit_id_resolver: revset::PrefixResolver = Box::new(|repo, prefix| id_prefix_context.resolve_commit_prefix(repo, prefix)); let change_id_resolver: revset::PrefixResolver> = Box::new(|repo, prefix| id_prefix_context.resolve_change_prefix(repo, prefix)); let symbol_resolver = DefaultSymbolResolver::new(self.repo().as_ref()) .with_commit_id_resolver(commit_id_resolver) .with_change_id_resolver(change_id_resolver); Ok(symbol_resolver) } pub fn id_prefix_context(&self) -> Result<&IdPrefixContext, CommandError> { self.user_repo.id_prefix_context.get_or_try_init(|| { let mut context: IdPrefixContext = IdPrefixContext::default(); let revset_string: String = self .settings .config() .get_string("revsets.short-prefixes") .unwrap_or_else(|_| self.settings.default_revset()); if !revset_string.is_empty() { let disambiguation_revset = self.parse_revset(&revset_string).map_err(|err| { CommandError::ConfigError(format!("Invalid `revsets.short-prefixes`: {err}")) })?; context = context.disambiguate_within(disambiguation_revset); } Ok(context) }) } pub fn template_aliases_map(&self) -> &TemplateAliasesMap { &self.template_aliases_map } pub fn parse_commit_template( &self, template_text: &str, ) -> Result + '_>, CommandError> { let id_prefix_context = self.id_prefix_context()?; let template = commit_templater::parse( self.repo().as_ref(), self.workspace_id(), id_prefix_context, template_text, &self.template_aliases_map, )?; Ok(template) } /// Returns one-line summary of the given `commit`. pub fn format_commit_summary(&self, commit: &Commit) -> String { let mut output = Vec::new(); self.write_commit_summary(&mut PlainTextFormatter::new(&mut output), commit) .expect("write() to PlainTextFormatter should never fail"); String::from_utf8(output).expect("template output should be utf-8 bytes") } /// Writes one-line summary of the given `commit`. #[instrument(skip_all)] pub fn write_commit_summary( &self, formatter: &mut dyn Formatter, commit: &Commit, ) -> std::io::Result<()> { let id_prefix_context = self .id_prefix_context() .expect("parse error should be confined by WorkspaceCommandHelper::new()"); let template = parse_commit_summary_template( self.repo().as_ref(), self.workspace_id(), id_prefix_context, &self.template_aliases_map, &self.settings, ) .expect("parse error should be confined by WorkspaceCommandHelper::new()"); template.format(commit, formatter)?; Ok(()) } pub fn check_rewritable<'a>( &self, commits: impl IntoIterator, ) -> Result<(), CommandError> { let to_rewrite_revset = RevsetExpression::commits( commits .into_iter() .map(|commit| commit.id().clone()) .collect(), ); let (params, immutable_heads_str) = self .revset_aliases_map .get_function("immutable_heads") .unwrap(); if !params.is_empty() { return Err(user_error( r#"The `revset-aliases.immutable_heads()` function must be declared without arguments."#, )); } let immutable_heads_revset = self.parse_revset(immutable_heads_str)?; let immutable_revset = immutable_heads_revset .ancestors() .union(&RevsetExpression::commit( self.repo().store().root_commit_id().clone(), )); let revset = self.evaluate_revset(to_rewrite_revset.intersection(&immutable_revset))?; if let Some(commit) = revset.iter().commits(self.repo().store()).next() { let commit = commit?; let error = if commit.id() == self.repo().store().root_commit_id() { user_error(format!( "The root commit {} is immutable", short_commit_hash(commit.id()), )) } else { user_error_with_hint( format!("Commit {} is immutable", short_commit_hash(commit.id()),), "Configure the set of immutable commits via \ `revset-aliases.immutable_heads()`.", ) }; return Err(error); } Ok(()) } pub fn check_non_empty(&self, commits: &[Commit]) -> Result<(), CommandError> { if commits.is_empty() { return Err(user_error("Empty revision set")); } Ok(()) } #[instrument(skip_all)] fn snapshot_working_copy(&mut self, ui: &mut Ui) -> Result<(), CommandError> { let workspace_id = self.workspace_id().to_owned(); let get_wc_commit = |repo: &ReadonlyRepo| -> Result, _> { repo.view() .get_wc_commit_id(&workspace_id) .map(|id| repo.store().get_commit(id)) .transpose() }; let repo = self.repo().clone(); let Some(wc_commit) = get_wc_commit(&repo)? else { // If the workspace has been deleted, it's unclear what to do, so we just skip // committing the working copy. return Ok(()); }; let base_ignores = self.base_ignores()?; // Compare working-copy tree and operation with repo's, and reload as needed. let mut locked_ws = self.workspace.start_working_copy_mutation()?; let old_op_id = locked_ws.locked_wc().old_operation_id().clone(); let (repo, wc_commit) = match check_stale_working_copy(locked_ws.locked_wc(), &wc_commit, &repo) { Ok(WorkingCopyFreshness::Fresh) => (repo, wc_commit), Ok(WorkingCopyFreshness::Updated(wc_operation)) => { let repo = repo.reload_at(&wc_operation)?; let wc_commit = if let Some(wc_commit) = get_wc_commit(&repo)? { wc_commit } else { return Ok(()); // The workspace has been deleted (see // above) }; (repo, wc_commit) } Ok(WorkingCopyFreshness::WorkingCopyStale) => { return Err(user_error_with_hint( format!( "The working copy is stale (not updated since operation {}).", short_operation_hash(&old_op_id) ), "Run `jj workspace update-stale` to update it. See https://github.com/martinvonz/jj/blob/main/docs/working-copy.md#stale-working-copy \ for more information.", )); } Ok(WorkingCopyFreshness::SiblingOperation) => { return Err(internal_error(format!( "The repo was loaded at operation {}, which seems to be a sibling of the \ working copy's operation {}", short_operation_hash(repo.op_id()), short_operation_hash(&old_op_id) ))); } Err(OpStoreError::ObjectNotFound { .. }) => { return Err(user_error_with_hint( "Could not read working copy's operation.", "Run `jj workspace update-stale` to recover. See https://github.com/martinvonz/jj/blob/main/docs/working-copy.md#stale-working-copy \ for more information.", )) } Err(e) => return Err(e.into()), }; self.user_repo = ReadonlyUserRepo::new(repo); let progress = crate::progress::snapshot_progress(ui); let new_tree_id = locked_ws.locked_wc().snapshot(SnapshotOptions { base_ignores, fsmonitor_kind: self.settings.fsmonitor_kind()?, progress: progress.as_ref().map(|x| x as _), max_new_file_size: self.settings.max_new_file_size()?, })?; drop(progress); if new_tree_id != *wc_commit.tree_id() { let mut tx = start_repo_transaction(&self.user_repo.repo, &self.settings, &self.string_args); tx.set_is_snapshot(true); let mut_repo = tx.mut_repo(); let commit = mut_repo .rewrite_commit(&self.settings, &wc_commit) .set_tree_id(new_tree_id) .write()?; mut_repo.set_wc_commit(workspace_id, commit.id().clone())?; // Rebase descendants let num_rebased = mut_repo.rebase_descendants(&self.settings)?; if num_rebased > 0 { writeln!( ui.stderr(), "Rebased {num_rebased} descendant commits onto updated working copy" )?; } if self.working_copy_shared_with_git { let failed_branches = git::export_refs(mut_repo)?; print_failed_git_export(ui, &failed_branches)?; } self.user_repo = ReadonlyUserRepo::new(tx.commit("snapshot working copy")); } locked_ws.finish(self.user_repo.repo.op_id().clone())?; Ok(()) } fn update_working_copy( &mut self, ui: &mut Ui, maybe_old_commit: Option<&Commit>, new_commit: &Commit, ) -> Result<(), CommandError> { assert!(self.may_update_working_copy); let stats = update_working_copy( &self.user_repo.repo, &mut self.workspace, maybe_old_commit, new_commit, )?; if Some(new_commit) != maybe_old_commit { write!(ui.stderr(), "Working copy now at: ")?; ui.stderr_formatter().with_label("working_copy", |fmt| { self.write_commit_summary(fmt, new_commit) })?; writeln!(ui.stderr())?; for parent in new_commit.parents() { // "Working copy now at: " write!(ui.stderr(), "Parent commit : ")?; self.write_commit_summary(ui.stderr_formatter().as_mut(), &parent)?; writeln!(ui.stderr())?; } } if let Some(stats) = stats { print_checkout_stats(ui, stats, new_commit)?; } Ok(()) } pub fn start_transaction(&mut self) -> WorkspaceCommandTransaction { let tx = start_repo_transaction(self.repo(), &self.settings, &self.string_args); WorkspaceCommandTransaction { helper: self, tx } } fn finish_transaction( &mut self, ui: &mut Ui, mut tx: Transaction, description: impl Into, ) -> Result<(), CommandError> { if !tx.mut_repo().has_changes() { writeln!(ui.stderr(), "Nothing changed.")?; return Ok(()); } let num_rebased = tx.mut_repo().rebase_descendants(&self.settings)?; if num_rebased > 0 { writeln!(ui.stderr(), "Rebased {num_rebased} descendant commits")?; } let old_repo = tx.base_repo().clone(); let maybe_old_wc_commit = old_repo .view() .get_wc_commit_id(self.workspace_id()) .map(|commit_id| tx.base_repo().store().get_commit(commit_id)) .transpose()?; let maybe_new_wc_commit = tx .repo() .view() .get_wc_commit_id(self.workspace_id()) .map(|commit_id| tx.repo().store().get_commit(commit_id)) .transpose()?; if self.working_copy_shared_with_git { let git_repo = self.git_backend().unwrap().open_git_repo()?; if let Some(wc_commit) = &maybe_new_wc_commit { git::reset_head(tx.mut_repo(), &git_repo, wc_commit)?; } let failed_branches = git::export_refs(tx.mut_repo())?; print_failed_git_export(ui, &failed_branches)?; } self.user_repo = ReadonlyUserRepo::new(tx.commit(description)); self.report_repo_changes(ui, &old_repo)?; if self.may_update_working_copy { if let Some(new_commit) = &maybe_new_wc_commit { self.update_working_copy(ui, maybe_old_wc_commit.as_ref(), new_commit)?; } else { // It seems the workspace was deleted, so we shouldn't try to // update it. } } let settings = &self.settings; if settings.user_name().is_empty() || settings.user_email().is_empty() { writeln!( ui.warning(), r#"Name and email not configured. Until configured, your commits will be created with the empty identity, and can't be pushed to remotes. To configure, run: jj config set --user user.name "Some One" jj config set --user user.email "someone@example.com""# )?; } Ok(()) } /// Inform the user about important changes to the repo since the previous /// operation (when `old_repo` was loaded). fn report_repo_changes( &self, ui: &mut Ui, old_repo: &Arc, ) -> Result<(), CommandError> { let old_view = old_repo.view(); let new_repo = self.repo().as_ref(); let new_view = new_repo.view(); let old_heads = RevsetExpression::commits(old_view.heads().iter().cloned().collect()); let new_heads = RevsetExpression::commits(new_view.heads().iter().cloned().collect()); // Filter the revsets by conflicts instead of reading all commits and doing the // filtering here. That way, we can afford to evaluate the revset even if there // are millions of commits added to the repo, assuming the revset engine can // efficiently skip non-conflicting commits. Filter out empty commits mostly so // `jj new ` doesn't result in a message about new conflicts. let conflicts = RevsetExpression::filter(RevsetFilterPredicate::HasConflict) .intersection(&RevsetExpression::filter(RevsetFilterPredicate::File(None))); let removed_conflicts_expr = new_heads.range(&old_heads).intersection(&conflicts); let added_conflicts_expr = old_heads.range(&new_heads).intersection(&conflicts); let get_commits = |expr: Rc| -> Result, CommandError> { let commits = expr .evaluate_programmatic(new_repo)? .iter() .commits(new_repo.store()) .try_collect()?; Ok(commits) }; let removed_conflict_commits = get_commits(removed_conflicts_expr)?; let added_conflict_commits = get_commits(added_conflicts_expr)?; fn commits_by_change_id(commits: &[Commit]) -> IndexMap<&ChangeId, Vec<&Commit>> { let mut result: IndexMap<&ChangeId, Vec<&Commit>> = IndexMap::new(); for commit in commits { result.entry(commit.change_id()).or_default().push(commit); } result } let removed_conflicts_by_change_id = commits_by_change_id(&removed_conflict_commits); let added_conflicts_by_change_id = commits_by_change_id(&added_conflict_commits); let mut resolved_conflicts_by_change_id = removed_conflicts_by_change_id.clone(); resolved_conflicts_by_change_id .retain(|change_id, _commits| !added_conflicts_by_change_id.contains_key(change_id)); let mut new_conflicts_by_change_id = added_conflicts_by_change_id.clone(); new_conflicts_by_change_id .retain(|change_id, _commits| !removed_conflicts_by_change_id.contains_key(change_id)); // TODO: Also report new divergence and maybe resolved divergence let mut fmt = ui.stderr_formatter(); if !resolved_conflicts_by_change_id.is_empty() { writeln!( fmt, "Existing conflicts were resolved or abandoned from these commits:" )?; for (_, old_commits) in &resolved_conflicts_by_change_id { // TODO: Report which ones were resolved and which ones were abandoned. However, // that involves resolving the change_id among the visible commits in the new // repo, which isn't currently supported by Google's revset engine. for commit in old_commits { write!(fmt, " ")?; self.write_commit_summary(fmt.as_mut(), commit)?; writeln!(fmt)?; } } } if !new_conflicts_by_change_id.is_empty() { writeln!(fmt, "New conflicts appeared in these commits:")?; for (_, new_commits) in &new_conflicts_by_change_id { for commit in new_commits { write!(fmt, " ")?; self.write_commit_summary(fmt.as_mut(), commit)?; writeln!(fmt)?; } } } // Hint that the user might want to `jj new` to the first conflict commit to // resolve conflicts. Only show the hints if there were any new or resolved // conflicts, and only if there are still some conflicts. if !(added_conflict_commits.is_empty() || resolved_conflicts_by_change_id.is_empty() && new_conflicts_by_change_id.is_empty()) { // If the user just resolved some conflict and squashed them in, there won't be // any new conflicts. Clarify to them that there are still some other conflicts // to resolve. (We don't mention conflicts in commits that weren't affected by // the operation, however.) if new_conflicts_by_change_id.is_empty() { writeln!( fmt, "There are still unresolved conflicts in rebased descendants.", )?; } let root_conflicts_revset = RevsetExpression::commits( added_conflict_commits .iter() .map(|commit| commit.id().clone()) .collect(), ) .roots() .evaluate_programmatic(new_repo)?; let root_conflict_commits: Vec<_> = root_conflicts_revset .iter() .commits(new_repo.store()) .try_collect()?; if !root_conflict_commits.is_empty() { fmt.push_label("hint")?; if added_conflict_commits.len() == 1 { writeln!(fmt, "To resolve the conflicts, start by updating to it:",)?; } else if root_conflict_commits.len() == 1 { writeln!( fmt, "To resolve the conflicts, start by updating to the first one:", )?; } else { writeln!( fmt, "To resolve the conflicts, start by updating to one of the first ones:", )?; } for commit in root_conflict_commits { writeln!(fmt, " jj new {}", short_change_hash(commit.change_id()))?; } writeln!( fmt, r#"Then use `jj resolve`, or edit the conflict markers in the file directly. Once the conflicts are resolved, you may want inspect the result with `jj diff`. Then run `jj squash` to move the resolution into the conflicted commit."#, )?; fmt.pop_label()?; } } Ok(()) } } #[must_use] pub struct WorkspaceCommandTransaction<'a> { helper: &'a mut WorkspaceCommandHelper, tx: Transaction, } impl WorkspaceCommandTransaction<'_> { /// Workspace helper that may use the base repo. pub fn base_workspace_helper(&self) -> &WorkspaceCommandHelper { self.helper } pub fn base_repo(&self) -> &Arc { self.tx.base_repo() } pub fn repo(&self) -> &MutableRepo { self.tx.repo() } pub fn mut_repo(&mut self) -> &mut MutableRepo { self.tx.mut_repo() } pub fn check_out(&mut self, commit: &Commit) -> Result { let workspace_id = self.helper.workspace_id().to_owned(); let settings = &self.helper.settings; self.tx.mut_repo().check_out(workspace_id, settings, commit) } pub fn edit(&mut self, commit: &Commit) -> Result<(), EditCommitError> { let workspace_id = self.helper.workspace_id().to_owned(); self.tx.mut_repo().edit(workspace_id, commit) } pub fn run_mergetool( &self, ui: &Ui, tree: &MergedTree, repo_path: &RepoPath, ) -> Result { let settings = &self.helper.settings; Ok(crate::merge_tools::run_mergetool( ui, tree, repo_path, settings, )?) } pub fn edit_diff( &self, ui: &Ui, left_tree: &MergedTree, right_tree: &MergedTree, matcher: &dyn Matcher, instructions: &str, ) -> Result { let base_ignores = self.helper.base_ignores()?; let settings = &self.helper.settings; Ok(crate::merge_tools::edit_diff( ui, left_tree, right_tree, matcher, instructions, base_ignores, settings, )?) } pub fn select_diff( &self, ui: &Ui, left_tree: &MergedTree, right_tree: &MergedTree, matcher: &dyn Matcher, instructions: &str, interactive: bool, ) -> Result { if interactive { self.edit_diff(ui, left_tree, right_tree, matcher, instructions) } else { let new_tree_id = restore_tree(right_tree, left_tree, matcher)?; Ok(new_tree_id) } } pub fn format_commit_summary(&self, commit: &Commit) -> String { let mut output = Vec::new(); self.write_commit_summary(&mut PlainTextFormatter::new(&mut output), commit) .expect("write() to PlainTextFormatter should never fail"); String::from_utf8(output).expect("template output should be utf-8 bytes") } pub fn write_commit_summary( &self, formatter: &mut dyn Formatter, commit: &Commit, ) -> std::io::Result<()> { // TODO: Use the disambiguation revset let id_prefix_context = IdPrefixContext::default(); let template = parse_commit_summary_template( self.tx.repo(), self.helper.workspace_id(), &id_prefix_context, &self.helper.template_aliases_map, &self.helper.settings, ) .expect("parse error should be confined by WorkspaceCommandHelper::new()"); template.format(commit, formatter) } pub fn finish(self, ui: &mut Ui, description: impl Into) -> Result<(), CommandError> { self.helper.finish_transaction(ui, self.tx, description) } pub fn into_inner(self) -> Transaction { self.tx } } fn find_workspace_dir(cwd: &Path) -> &Path { cwd.ancestors() .find(|path| path.join(".jj").is_dir()) .unwrap_or(cwd) } fn map_workspace_load_error(err: WorkspaceLoadError, workspace_path: Option<&str>) -> CommandError { match err { WorkspaceLoadError::NoWorkspaceHere(wc_path) => { // Prefer user-specified workspace_path_str instead of absolute wc_path. let workspace_path_str = workspace_path.unwrap_or("."); let message = format!(r#"There is no jj repo in "{workspace_path_str}""#); let git_dir = wc_path.join(".git"); if git_dir.is_dir() { user_error_with_hint( message, "It looks like this is a git repo. You can create a jj repo backed by it by \ running this: jj git init --git-repo=.", ) } else { user_error(message) } } WorkspaceLoadError::RepoDoesNotExist(repo_dir) => user_error(format!( "The repository directory at {} is missing. Was it moved?", repo_dir.display(), )), WorkspaceLoadError::StoreLoadError(err @ StoreLoadError::UnsupportedType { .. }) => { internal_error_with_message( "This version of the jj binary doesn't support this type of repo", err, ) } WorkspaceLoadError::StoreLoadError( err @ (StoreLoadError::ReadError { .. } | StoreLoadError::Backend(_)), ) => internal_error_with_message("The repository appears broken or inaccessible", err), WorkspaceLoadError::StoreLoadError(StoreLoadError::Signing( err @ SignInitError::UnknownBackend(_), )) => user_error(err), WorkspaceLoadError::StoreLoadError(err) => internal_error(err), WorkspaceLoadError::NonUnicodePath | WorkspaceLoadError::Path(_) => user_error(err), } } pub fn start_repo_transaction( repo: &Arc, settings: &UserSettings, string_args: &[String], ) -> Transaction { let mut tx = repo.start_transaction(settings); // TODO: Either do better shell-escaping here or store the values in some list // type (which we currently don't have). let shell_escape = |arg: &String| { if arg.as_bytes().iter().all(|b| { matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b',' | b'-' | b'.' | b'/' | b':' | b'@' | b'_' ) }) { arg.clone() } else { format!("'{}'", arg.replace('\'', "\\'")) } }; let mut quoted_strings = vec!["jj".to_string()]; quoted_strings.extend(string_args.iter().skip(1).map(shell_escape)); tx.set_tag("args".to_string(), quoted_strings.join(" ")); tx } /// Whether the working copy is stale or not. #[derive(Clone, Debug, Eq, PartialEq)] pub enum WorkingCopyFreshness { /// The working copy isn't stale, and no need to reload the repo. Fresh, /// The working copy was updated since we loaded the repo. The repo must be /// reloaded at the working copy's operation. Updated(Box), /// The working copy is behind the latest operation. WorkingCopyStale, /// The working copy is a sibling of the latest operation. SiblingOperation, } #[instrument(skip_all)] pub fn check_stale_working_copy( locked_wc: &dyn LockedWorkingCopy, wc_commit: &Commit, repo: &ReadonlyRepo, ) -> Result { // Check if the working copy's tree matches the repo's view let wc_tree_id = locked_wc.old_tree_id(); if wc_commit.tree_id() == wc_tree_id { // The working copy isn't stale, and no need to reload the repo. Ok(WorkingCopyFreshness::Fresh) } else { let wc_operation_data = repo .op_store() .read_operation(locked_wc.old_operation_id())?; let wc_operation = Operation::new( repo.op_store().clone(), locked_wc.old_operation_id().clone(), wc_operation_data, ); let repo_operation = repo.operation(); let ancestor_op = dag_walk::closest_common_node_ok( [Ok(wc_operation.clone())], [Ok(repo_operation.clone())], |op: &Operation| op.id().clone(), |op: &Operation| op.parents().collect_vec(), )? .expect("unrelated operations"); if ancestor_op.id() == repo_operation.id() { // The working copy was updated since we loaded the repo. The repo must be // reloaded at the working copy's operation. Ok(WorkingCopyFreshness::Updated(Box::new(wc_operation))) } else if ancestor_op.id() == wc_operation.id() { // The working copy was not updated when some repo operation committed, // meaning that it's stale compared to the repo view. Ok(WorkingCopyFreshness::WorkingCopyStale) } else { Ok(WorkingCopyFreshness::SiblingOperation) } } } pub fn print_checkout_stats( ui: &mut Ui, stats: CheckoutStats, new_commit: &Commit, ) -> Result<(), std::io::Error> { if stats.added_files > 0 || stats.updated_files > 0 || stats.removed_files > 0 { writeln!( ui.stderr(), "Added {} files, modified {} files, removed {} files", stats.added_files, stats.updated_files, stats.removed_files )?; } if stats.skipped_files != 0 { writeln!( ui.warning(), "{} of those updates were skipped because there were conflicting changes in the \ working copy.", stats.skipped_files )?; writeln!( ui.hint(), "Hint: Inspect the changes compared to the intended target with `jj diff --from {}`. Discard the conflicting changes with `jj restore --from {}`.", short_commit_hash(new_commit.id()), short_commit_hash(new_commit.id()) )?; } Ok(()) } pub fn print_trackable_remote_branches(ui: &Ui, view: &View) -> io::Result<()> { let remote_branch_names = view .branches() .filter(|(_, branch_target)| branch_target.local_target.is_present()) .flat_map(|(name, branch_target)| { branch_target .remote_refs .into_iter() .filter(|&(_, remote_ref)| !remote_ref.is_tracking()) .map(move |(remote, _)| format!("{name}@{remote}")) }) .collect_vec(); if remote_branch_names.is_empty() { return Ok(()); } writeln!( ui.hint(), "The following remote branches aren't associated with the existing local branches:" )?; let mut formatter = ui.stderr_formatter(); for full_name in &remote_branch_names { write!(formatter, " ")?; writeln!(formatter.labeled("branch"), "{full_name}")?; } drop(formatter); writeln!( ui.hint(), "Hint: Run `jj branch track {names}` to keep local branches updated on future pulls.", names = remote_branch_names.join(" "), )?; Ok(()) } pub fn parse_string_pattern(src: &str) -> Result { if let Some((kind, pat)) = src.split_once(':') { StringPattern::from_str_kind(pat, kind) } else { Ok(StringPattern::exact(src)) } } /// Resolves revsets into revisions for use; useful for rebases or operations /// that take multiple parents. pub fn resolve_all_revs( workspace_command: &WorkspaceCommandHelper, revisions: &[RevisionArg], ) -> Result, CommandError> { let commits = resolve_multiple_nonempty_revsets_default_single(workspace_command, revisions)?; let root_commit_id = workspace_command.repo().store().root_commit_id(); if commits.len() >= 2 && commits.iter().any(|c| c.id() == root_commit_id) { Err(user_error("Cannot merge with root revision")) } else { Ok(commits) } } fn load_revset_aliases( ui: &Ui, layered_configs: &LayeredConfigs, ) -> Result { const TABLE_KEY: &str = "revset-aliases"; let mut aliases_map = RevsetAliasesMap::new(); // Load from all config layers in order. 'f(x)' in default layer should be // overridden by 'f(a)' in user. for (_, config) in layered_configs.sources() { let table = if let Some(table) = config.get_table(TABLE_KEY).optional()? { table } else { continue; }; for (decl, value) in table.into_iter().sorted_by(|a, b| a.0.cmp(&b.0)) { let r = value .into_string() .map_err(|e| e.to_string()) .and_then(|v| aliases_map.insert(&decl, v).map_err(|e| e.to_string())); if let Err(s) = r { writeln!(ui.warning(), r#"Failed to load "{TABLE_KEY}.{decl}": {s}"#)?; } } } Ok(aliases_map) } pub fn resolve_multiple_nonempty_revsets( revision_args: &[RevisionArg], workspace_command: &WorkspaceCommandHelper, ) -> Result, CommandError> { let mut acc = IndexSet::new(); for revset in revision_args { let revisions = workspace_command.resolve_revset(revset)?; workspace_command.check_non_empty(&revisions)?; acc.extend(revisions); } Ok(acc) } pub fn resolve_multiple_nonempty_revsets_default_single( workspace_command: &WorkspaceCommandHelper, revisions: &[RevisionArg], ) -> Result, CommandError> { let mut all_commits = IndexSet::new(); for revision_str in revisions { let commits = workspace_command.resolve_revset_default_single(revision_str)?; workspace_command.check_non_empty(&commits)?; for commit in commits { let commit_hash = short_commit_hash(commit.id()); if !all_commits.insert(commit) { return Err(user_error(format!( r#"More than one revset resolved to revision {commit_hash}"#, ))); } } } Ok(all_commits) } pub fn update_working_copy( repo: &Arc, workspace: &mut Workspace, old_commit: Option<&Commit>, new_commit: &Commit, ) -> Result, CommandError> { let old_tree_id = old_commit.map(|commit| commit.tree_id().clone()); let stats = if Some(new_commit.tree_id()) != old_tree_id.as_ref() { // TODO: CheckoutError::ConcurrentCheckout should probably just result in a // warning for most commands (but be an error for the checkout command) let stats = workspace .check_out(repo.op_id().clone(), old_tree_id.as_ref(), new_commit) .map_err(|err| { internal_error_with_message( format!("Failed to check out commit {}", new_commit.id().hex()), err, ) })?; Some(stats) } else { // Record new operation id which represents the latest working-copy state let locked_ws = workspace.start_working_copy_mutation()?; locked_ws.finish(repo.op_id().clone())?; None }; Ok(stats) } fn load_template_aliases( ui: &Ui, layered_configs: &LayeredConfigs, ) -> Result { const TABLE_KEY: &str = "template-aliases"; let mut aliases_map = TemplateAliasesMap::new(); // Load from all config layers in order. 'f(x)' in default layer should be // overridden by 'f(a)' in user. for (_, config) in layered_configs.sources() { let table = if let Some(table) = config.get_table(TABLE_KEY).optional()? { table } else { continue; }; for (decl, value) in table.into_iter().sorted_by(|a, b| a.0.cmp(&b.0)) { let r = value .into_string() .map_err(|e| e.to_string()) .and_then(|v| aliases_map.insert(&decl, v).map_err(|e| e.to_string())); if let Err(s) = r { writeln!(ui.warning(), r#"Failed to load "{TABLE_KEY}.{decl}": {s}"#)?; } } } Ok(aliases_map) } #[instrument(skip_all)] fn parse_commit_summary_template<'a>( repo: &'a dyn Repo, workspace_id: &WorkspaceId, id_prefix_context: &'a IdPrefixContext, aliases_map: &TemplateAliasesMap, settings: &UserSettings, ) -> Result + 'a>, CommandError> { let template_text = settings.config().get_string("templates.commit_summary")?; Ok(commit_templater::parse( repo, workspace_id, id_prefix_context, &template_text, aliases_map, )?) } /// Helper to reformat content of log-like commands. #[derive(Clone, Debug)] pub enum LogContentFormat { NoWrap, Wrap { term_width: usize }, } impl LogContentFormat { pub fn new(ui: &Ui, settings: &UserSettings) -> Result { if settings.config().get_bool("ui.log-word-wrap")? { let term_width = usize::from(ui.term_width().unwrap_or(80)); Ok(LogContentFormat::Wrap { term_width }) } else { Ok(LogContentFormat::NoWrap) } } pub fn write( &self, formatter: &mut dyn Formatter, content_fn: impl FnOnce(&mut dyn Formatter) -> std::io::Result<()>, ) -> std::io::Result<()> { self.write_graph_text(formatter, content_fn, || 0) } pub fn write_graph_text( &self, formatter: &mut dyn Formatter, content_fn: impl FnOnce(&mut dyn Formatter) -> std::io::Result<()>, graph_width_fn: impl FnOnce() -> usize, ) -> std::io::Result<()> { match self { LogContentFormat::NoWrap => content_fn(formatter), LogContentFormat::Wrap { term_width } => { let mut recorder = FormatRecorder::new(); content_fn(&mut recorder)?; text_util::write_wrapped( formatter, &recorder, term_width.saturating_sub(graph_width_fn()), )?; Ok(()) } } } } // TODO: Use a proper TOML library to serialize instead. pub fn serialize_config_value(value: &config::Value) -> String { match &value.kind { config::ValueKind::Table(table) => format!( "{{{}}}", // TODO: Remove sorting when config crate maintains deterministic ordering. table .iter() .sorted_by_key(|(k, _)| *k) .map(|(k, v)| format!("{k}={}", serialize_config_value(v))) .join(", ") ), config::ValueKind::Array(vals) => { format!("[{}]", vals.iter().map(serialize_config_value).join(", ")) } config::ValueKind::String(val) => format!("{val:?}"), _ => value.to_string(), } } pub fn write_config_value_to_file( key: &str, value_str: &str, path: &Path, ) -> Result<(), CommandError> { // Read config let config_toml = std::fs::read_to_string(path).or_else(|err| { match err.kind() { // If config doesn't exist yet, read as empty and we'll write one. std::io::ErrorKind::NotFound => Ok("".to_string()), _ => Err(user_error_with_message( format!("Failed to read file {path}", path = path.display()), err, )), } })?; let mut doc = toml_edit::Document::from_str(&config_toml).map_err(|err| { user_error_with_message( format!("Failed to parse file {path}", path = path.display()), err, ) })?; // Apply config value // Interpret value as string if it can't be parsed as a TOML value. // TODO(#531): Infer types based on schema (w/ --type arg to override). let item = match toml_edit::Value::from_str(value_str) { Ok(value) => toml_edit::value(value), _ => toml_edit::value(value_str), }; let mut target_table = doc.as_table_mut(); let mut key_parts_iter = key.split('.'); // Note: split guarantees at least one item. let last_key_part = key_parts_iter.next_back().unwrap(); for key_part in key_parts_iter { target_table = target_table .entry(key_part) .or_insert_with(|| toml_edit::Item::Table(toml_edit::Table::new())) .as_table_mut() .ok_or_else(|| { user_error(format!( "Failed to set {key}: would overwrite non-table value with parent table" )) })?; } // Error out if overwriting non-scalar value for key (table or array) with // scalar. match target_table.get(last_key_part) { None | Some(toml_edit::Item::None | toml_edit::Item::Value(_)) => {} Some(toml_edit::Item::Table(_) | toml_edit::Item::ArrayOfTables(_)) => { return Err(user_error(format!( "Failed to set {key}: would overwrite entire table" ))); } } target_table[last_key_part] = item; // Write config back std::fs::write(path, doc.to_string()).map_err(|err| { user_error_with_message( format!("Failed to write file {path}", path = path.display()), err, ) }) } pub fn get_new_config_file_path( config_source: &ConfigSource, command: &CommandHelper, ) -> Result { let edit_path = match config_source { // TODO(#531): Special-case for editors that can't handle viewing directories? ConfigSource::User => { new_config_path()?.ok_or_else(|| user_error("No repo config path found to edit"))? } ConfigSource::Repo => command.workspace_loader()?.repo_path().join("config.toml"), _ => { return Err(user_error(format!( "Can't get path for config source {config_source:?}" ))); } }; Ok(edit_path) } pub fn run_ui_editor(settings: &UserSettings, edit_path: &PathBuf) -> Result<(), CommandError> { let editor: CommandNameAndArgs = settings .config() .get("ui.editor") .map_err(|err| CommandError::ConfigError(format!("ui.editor: {err}")))?; let exit_status = editor.to_command().arg(edit_path).status().map_err(|err| { user_error_with_message( format!( // The executable couldn't be found or run; command-line arguments are not relevant "Failed to run editor '{name}'", name = editor.split_name(), ), err, ) })?; if !exit_status.success() { return Err(user_error(format!( "Editor '{editor}' exited with an error" ))); } Ok(()) } pub fn edit_temp_file( error_name: &str, tempfile_suffix: &str, dir: &Path, content: &str, settings: &UserSettings, ) -> Result { let path = (|| -> Result<_, io::Error> { let mut file = tempfile::Builder::new() .prefix("editor-") .suffix(tempfile_suffix) .tempfile_in(dir)?; file.write_all(content.as_bytes())?; let (_, path) = file.keep().map_err(|e| e.error)?; Ok(path) })() .map_err(|e| { user_error_with_message( format!( r#"Failed to create {} file in "{}""#, error_name, dir.display(), ), e, ) })?; run_ui_editor(settings, &path)?; let edited = fs::read_to_string(&path).map_err(|e| { user_error_with_message( format!(r#"Failed to read {} file "{}""#, error_name, path.display()), e, ) })?; // Delete the file only if everything went well. // TODO: Tell the user the name of the file we left behind. std::fs::remove_file(path).ok(); Ok(edited) } pub fn short_commit_hash(commit_id: &CommitId) -> String { commit_id.hex()[0..12].to_string() } pub fn short_change_hash(change_id: &ChangeId) -> String { // TODO: We could avoid the unwrap() and make this more efficient by converting // straight from binary. to_reverse_hex(&change_id.hex()[0..12]).unwrap() } pub fn short_operation_hash(operation_id: &OperationId) -> String { operation_id.hex()[0..12].to_string() } #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct RemoteBranchName { pub branch: String, pub remote: String, } impl fmt::Display for RemoteBranchName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let RemoteBranchName { branch, remote } = self; write!(f, "{branch}@{remote}") } } #[derive(Clone, Debug)] pub struct RemoteBranchNamePattern { pub branch: StringPattern, pub remote: StringPattern, } impl FromStr for RemoteBranchNamePattern { type Err = String; fn from_str(src: &str) -> Result { // The kind prefix applies to both branch and remote fragments. It's // weird that unanchored patterns like substring:branch@remote is split // into two, but I can't think of a better syntax. // TODO: should we disable substring pattern? what if we added regex? let (maybe_kind, pat) = src .split_once(':') .map_or((None, src), |(kind, pat)| (Some(kind), pat)); let to_pattern = |pat: &str| { if let Some(kind) = maybe_kind { StringPattern::from_str_kind(pat, kind).map_err(|err| err.to_string()) } else { Ok(StringPattern::exact(pat)) } }; // TODO: maybe reuse revset parser to handle branch/remote name containing @ let (branch, remote) = pat .rsplit_once('@') .ok_or_else(|| "remote branch must be specified in branch@remote form".to_owned())?; Ok(RemoteBranchNamePattern { branch: to_pattern(branch)?, remote: to_pattern(remote)?, }) } } impl RemoteBranchNamePattern { pub fn is_exact(&self) -> bool { self.branch.is_exact() && self.remote.is_exact() } } impl fmt::Display for RemoteBranchNamePattern { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let RemoteBranchNamePattern { branch, remote } = self; write!(f, "{branch}@{remote}") } } /// Jujutsu (An experimental VCS) /// /// To get started, see the tutorial at https://github.com/martinvonz/jj/blob/main/docs/tutorial.md. #[derive(clap::Parser, Clone, Debug)] #[command(name = "jj")] pub struct Args { #[command(flatten)] pub global_args: GlobalArgs, } #[derive(clap::Args, Clone, Debug)] #[command(next_help_heading = "Global Options")] pub struct GlobalArgs { /// Path to repository to operate on /// /// By default, Jujutsu searches for the closest .jj/ directory in an /// ancestor of the current working directory. #[arg(long, short = 'R', global = true, value_hint = clap::ValueHint::DirPath)] pub repository: Option, /// Don't snapshot the working copy, and don't update it /// /// By default, Jujutsu snapshots the working copy at the beginning of every /// command. The working copy is also updated at the end of the command, /// if the command modified the working-copy commit (`@`). If you want /// to avoid snapshotting the working copy and instead see a possibly /// stale working copy commit, you can use `--ignore-working-copy`. /// This may be useful e.g. in a command prompt, especially if you have /// another process that commits the working copy. /// /// Loading the repository is at a specific operation with `--at-operation` /// implies `--ignore-working-copy`. #[arg(long, global = true)] pub ignore_working_copy: bool, /// Operation to load the repo at /// /// Operation to load the repo at. By default, Jujutsu loads the repo at the /// most recent operation. You can use `--at-op=` to see what /// the repo looked like at an earlier operation. For example `jj /// --at-op= st` will show you what `jj st` would have /// shown you when the given operation had just finished. /// /// Use `jj op log` to find the operation ID you want. Any unambiguous /// prefix of the operation ID is enough. /// /// When loading the repo at an earlier operation, the working copy will be /// ignored, as if `--ignore-working-copy` had been specified. /// /// It is possible to run mutating commands when loading the repo at an /// earlier operation. Doing that is equivalent to having run concurrent /// commands starting at the earlier operation. There's rarely a reason to /// do that, but it is possible. #[arg(long, visible_alias = "at-op", global = true, default_value = "@")] pub at_operation: String, /// Enable debug logging #[arg(long, global = true)] pub debug: bool, #[command(flatten)] pub early_args: EarlyArgs, } #[derive(clap::Args, Clone, Debug)] pub struct EarlyArgs { /// When to colorize output (always, never, auto) #[arg(long, value_name = "WHEN", global = true)] pub color: Option, /// Disable the pager #[arg(long, value_name = "WHEN", global = true, action = ArgAction::SetTrue)] // Parsing with ignore_errors will crash if this is bool, so use // Option. pub no_pager: Option, /// Additional configuration options (can be repeated) // TODO: Introduce a `--config` option with simpler syntax for simple // cases, designed so that `--config ui.color=auto` works #[arg(long, value_name = "TOML", global = true)] pub config_toml: Vec, } /// Create a description from a list of paragraphs. /// /// Based on the Git CLI behavior. See `opt_parse_m()` and `cleanup_mode` in /// `git/builtin/commit.c`. pub fn join_message_paragraphs(paragraphs: &[String]) -> String { // Ensure each paragraph ends with a newline, then add another newline between // paragraphs. paragraphs .iter() .map(|p| text_util::complete_newline(p.as_str())) .join("\n") } #[derive(Clone, Debug)] pub struct RevisionArg(String); impl Deref for RevisionArg { type Target = str; fn deref(&self) -> &Self::Target { self.0.as_str() } } #[derive(Clone)] pub struct RevisionArgValueParser; impl TypedValueParser for RevisionArgValueParser { type Value = RevisionArg; fn parse_ref( &self, cmd: &Command, arg: Option<&Arg>, value: &OsStr, ) -> Result { let string = NonEmptyStringValueParser::new().parse(cmd, arg, value.to_os_string())?; Ok(RevisionArg(string)) } } impl ValueParserFactory for RevisionArg { type Parser = RevisionArgValueParser; fn value_parser() -> RevisionArgValueParser { RevisionArgValueParser } } fn resolve_default_command( ui: &Ui, config: &config::Config, app: &Command, mut string_args: Vec, ) -> Result, CommandError> { const PRIORITY_FLAGS: &[&str] = &["help", "--help", "-h", "--version", "-V"]; let has_priority_flag = string_args .iter() .any(|arg| PRIORITY_FLAGS.contains(&arg.as_str())); if has_priority_flag { return Ok(string_args); } let app_clone = app .clone() .allow_external_subcommands(true) .ignore_errors(true); let matches = app_clone.try_get_matches_from(&string_args).ok(); if let Some(matches) = matches { if matches.subcommand_name().is_none() { if config.get_string("ui.default-command").is_err() { writeln!( ui.hint(), "Hint: Use `jj -h` for a list of available commands." )?; writeln!( ui.hint(), "Run `jj config set --user ui.default-command log` to disable this message." )?; } let default_command = config .get_string("ui.default-command") .unwrap_or_else(|_| "log".to_string()); // Insert the default command directly after the path to the binary. string_args.insert(1, default_command); } } Ok(string_args) } fn resolve_aliases( config: &config::Config, app: &Command, mut string_args: Vec, ) -> Result, CommandError> { let mut aliases_map = config.get_table("aliases")?; if let Ok(alias_map) = config.get_table("alias") { for (alias, definition) in alias_map { if aliases_map.insert(alias.clone(), definition).is_some() { return Err(user_error_with_hint( format!(r#"Alias "{alias}" is defined in both [aliases] and [alias]"#), "[aliases] is the preferred section for aliases. Please remove the alias from \ [alias].", )); } } } let mut resolved_aliases = HashSet::new(); let mut real_commands = HashSet::new(); for command in app.get_subcommands() { real_commands.insert(command.get_name().to_string()); for alias in command.get_all_aliases() { real_commands.insert(alias.to_string()); } } loop { let app_clone = app.clone().allow_external_subcommands(true); let matches = app_clone.try_get_matches_from(&string_args).ok(); if let Some((command_name, submatches)) = matches.as_ref().and_then(|m| m.subcommand()) { if !real_commands.contains(command_name) { let alias_name = command_name.to_string(); let alias_args = submatches .get_many::("") .unwrap_or_default() .map(|arg| arg.to_str().unwrap().to_string()) .collect_vec(); if resolved_aliases.contains(&alias_name) { return Err(user_error(format!( r#"Recursive alias definition involving "{alias_name}""# ))); } if let Some(value) = aliases_map.remove(&alias_name) { if let Ok(alias_definition) = value.try_deserialize::>() { assert!(string_args.ends_with(&alias_args)); string_args.truncate(string_args.len() - 1 - alias_args.len()); string_args.extend(alias_definition); string_args.extend_from_slice(&alias_args); resolved_aliases.insert(alias_name.clone()); continue; } else { return Err(user_error(format!( r#"Alias definition for "{alias_name}" must be a string list"# ))); } } else { // Not a real command and not an alias, so return what we've resolved so far return Ok(string_args); } } } // No more alias commands, or hit unknown option return Ok(string_args); } } /// Parse args that must be interpreted early, e.g. before printing help. fn handle_early_args( ui: &mut Ui, app: &Command, args: &[String], layered_configs: &mut LayeredConfigs, ) -> Result<(), CommandError> { // ignore_errors() bypasses errors like missing subcommand let early_matches = app .clone() .disable_version_flag(true) .disable_help_flag(true) .disable_help_subcommand(true) .ignore_errors(true) .try_get_matches_from(args)?; let mut args: EarlyArgs = EarlyArgs::from_arg_matches(&early_matches).unwrap(); if let Some(choice) = args.color { args.config_toml.push(format!(r#"ui.color="{choice}""#)); } if args.no_pager.unwrap_or_default() { args.config_toml.push(r#"ui.paginate="never""#.to_owned()); } if !args.config_toml.is_empty() { layered_configs.parse_config_args(&args.config_toml)?; ui.reset(&layered_configs.merge())?; } Ok(()) } pub fn expand_args( ui: &Ui, app: &Command, args_os: ArgsOs, config: &config::Config, ) -> Result, CommandError> { let mut string_args: Vec = vec![]; for arg_os in args_os { if let Some(string_arg) = arg_os.to_str() { string_args.push(string_arg.to_owned()); } else { return Err(CommandError::CliError("Non-utf8 argument".to_string())); } } let string_args = resolve_default_command(ui, config, app, string_args)?; resolve_aliases(config, app, string_args) } pub fn parse_args( ui: &mut Ui, app: &Command, tracing_subscription: &TracingSubscription, string_args: &[String], layered_configs: &mut LayeredConfigs, ) -> Result<(ArgMatches, Args), CommandError> { handle_early_args(ui, app, string_args, layered_configs)?; let matches = app .clone() .arg_required_else_help(true) .subcommand_required(true) .try_get_matches_from(string_args)?; let args: Args = Args::from_arg_matches(&matches).unwrap(); if args.global_args.debug { // TODO: set up debug logging as early as possible tracing_subscription.enable_debug_logging()?; } Ok((matches, args)) } const BROKEN_PIPE_EXIT_CODE: u8 = 3; pub fn handle_command_result( ui: &mut Ui, result: Result<(), CommandError>, ) -> std::io::Result { match &result { Ok(()) => Ok(ExitCode::SUCCESS), Err(CommandError::UserError { err, hint }) => { writeln!(ui.error(), "Error: {err}")?; print_error_sources(ui, err.source())?; if let Some(hint) = hint { writeln!(ui.hint(), "Hint: {hint}")?; } Ok(ExitCode::from(1)) } Err(CommandError::ConfigError(message)) => { writeln!(ui.error(), "Config error: {message}")?; writeln!( ui.hint(), "For help, see https://github.com/martinvonz/jj/blob/main/docs/config.md." )?; Ok(ExitCode::from(1)) } Err(CommandError::CliError(message)) => { writeln!(ui.error(), "Error: {message}")?; Ok(ExitCode::from(2)) } Err(CommandError::ClapCliError(inner)) => { let clap_str = if ui.color() { inner.render().ansi().to_string() } else { inner.render().to_string() }; match inner.kind() { clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => { ui.request_pager() } _ => {} }; // Definitions for exit codes and streams come from // https://github.com/clap-rs/clap/blob/master/src/error/mod.rs match inner.kind() { clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => { write!(ui.stdout(), "{clap_str}")?; Ok(ExitCode::SUCCESS) } _ => { write!(ui.stderr(), "{clap_str}")?; Ok(ExitCode::from(2)) } } } Err(CommandError::BrokenPipe) => { // A broken pipe is not an error, but a signal to exit gracefully. Ok(ExitCode::from(BROKEN_PIPE_EXIT_CODE)) } Err(CommandError::InternalError(err)) => { writeln!(ui.error(), "Internal error: {err}")?; print_error_sources(ui, err.source())?; Ok(ExitCode::from(255)) } } } /// CLI command builder and runner. #[must_use] pub struct CliRunner { tracing_subscription: TracingSubscription, app: Command, extra_configs: Option, store_factories: Option, working_copy_factories: Option>>, dispatch_fn: CliDispatchFn, start_hook_fns: Vec, process_global_args_fns: Vec, } type CliDispatchFn = Box Result<(), CommandError>>; type ProcessGlobalArgsFn = Box Result<(), CommandError>>; impl CliRunner { /// Initializes CLI environment and returns a builder. This should be called /// as early as possible. pub fn init() -> Self { let tracing_subscription = TracingSubscription::init(); crate::cleanup_guard::init(); CliRunner { tracing_subscription, app: crate::commands::default_app(), extra_configs: None, store_factories: None, working_copy_factories: None, dispatch_fn: Box::new(crate::commands::run_command), start_hook_fns: vec![], process_global_args_fns: vec![], } } /// Set the version to be displayed by `jj version`. pub fn version(mut self, version: &str) -> Self { self.app = self.app.version(version.to_string()); self } /// Adds default configs in addition to the normal defaults. pub fn set_extra_config(mut self, extra_configs: config::Config) -> Self { self.extra_configs = Some(extra_configs); self } /// Replaces `StoreFactories` to be used. pub fn set_store_factories(mut self, store_factories: StoreFactories) -> Self { self.store_factories = Some(store_factories); self } /// Replaces working copy factories to be used. pub fn set_working_copy_factories( mut self, working_copy_factories: HashMap>, ) -> Self { self.working_copy_factories = Some(working_copy_factories); self } pub fn add_start_hook(mut self, start_hook_fn: CliDispatchFn) -> Self { self.start_hook_fns.push(start_hook_fn); self } /// Registers new subcommands in addition to the default ones. pub fn add_subcommand(mut self, custom_dispatch_fn: F) -> Self where C: clap::Subcommand, F: FnOnce(&mut Ui, &CommandHelper, C) -> Result<(), CommandError> + 'static, { let old_dispatch_fn = self.dispatch_fn; let new_dispatch_fn = move |ui: &mut Ui, command_helper: &CommandHelper| match C::from_arg_matches( command_helper.matches(), ) { Ok(command) => custom_dispatch_fn(ui, command_helper, command), Err(_) => old_dispatch_fn(ui, command_helper), }; self.app = C::augment_subcommands(self.app); self.dispatch_fn = Box::new(new_dispatch_fn); self } /// Registers new global arguments in addition to the default ones. pub fn add_global_args(mut self, process_before: F) -> Self where A: clap::Args, F: FnOnce(&mut Ui, A) -> Result<(), CommandError> + 'static, { let process_global_args_fn = move |ui: &mut Ui, matches: &ArgMatches| { let custom_args = A::from_arg_matches(matches).unwrap(); process_before(ui, custom_args) }; self.app = A::augment_args(self.app); self.process_global_args_fns .push(Box::new(process_global_args_fn)); self } #[instrument(skip_all)] fn run_internal( self, ui: &mut Ui, mut layered_configs: LayeredConfigs, ) -> Result<(), CommandError> { let cwd = env::current_dir().map_err(|_| { user_error_with_hint( "Could not determine current directory", "Did you check-out a commit where the directory doesn't exist?", ) })?; // Use cwd-relative workspace configs to resolve default command and // aliases. WorkspaceLoader::init() won't do any heavy lifting other // than the path resolution. let maybe_cwd_workspace_loader = WorkspaceLoader::init(find_workspace_dir(&cwd)) .map_err(|err| map_workspace_load_error(err, None)); layered_configs.read_user_config()?; if let Ok(loader) = &maybe_cwd_workspace_loader { layered_configs.read_repo_config(loader.repo_path())?; } let config = layered_configs.merge(); ui.reset(&config)?; let string_args = expand_args(ui, &self.app, env::args_os(), &config)?; let (matches, args) = parse_args( ui, &self.app, &self.tracing_subscription, &string_args, &mut layered_configs, )?; for process_global_args_fn in self.process_global_args_fns { process_global_args_fn(ui, &matches)?; } let maybe_workspace_loader = if let Some(path) = &args.global_args.repository { // Invalid -R path is an error. No need to proceed. let loader = WorkspaceLoader::init(&cwd.join(path)) .map_err(|err| map_workspace_load_error(err, Some(path)))?; layered_configs.read_repo_config(loader.repo_path())?; Ok(loader) } else { maybe_cwd_workspace_loader }; // Apply workspace configs and --config-toml arguments. let config = layered_configs.merge(); ui.reset(&config)?; // If -R is specified, check if the expanded arguments differ. Aliases // can also be injected by --config-toml, but that's obviously wrong. if args.global_args.repository.is_some() { let new_string_args = expand_args(ui, &self.app, env::args_os(), &config).ok(); if new_string_args.as_ref() != Some(&string_args) { writeln!( ui.warning(), "Command aliases cannot be loaded from -R/--repository path" )?; } } let settings = UserSettings::from_config(config); let working_copy_factories = self .working_copy_factories .unwrap_or_else(default_working_copy_factories); let command_helper = CommandHelper::new( self.app, cwd, string_args, matches, args.global_args, settings, layered_configs, maybe_workspace_loader, self.store_factories.unwrap_or_default(), working_copy_factories, ); for start_hook_fn in self.start_hook_fns { start_hook_fn(ui, &command_helper)?; } (self.dispatch_fn)(ui, &command_helper) } #[must_use] #[instrument(skip(self))] pub fn run(mut self) -> ExitCode { let mut default_config = crate::config::default_config(); if let Some(extra_configs) = self.extra_configs.take() { default_config = config::Config::builder() .add_source(default_config) .add_source(extra_configs) .build() .unwrap(); } let layered_configs = LayeredConfigs::from_environment(default_config); let mut ui = Ui::with_config(&layered_configs.merge()) .expect("default config should be valid, env vars are stringly typed"); let result = self.run_internal(&mut ui, layered_configs); let exit_code = handle_command_result(&mut ui, result) .unwrap_or_else(|_| ExitCode::from(BROKEN_PIPE_EXIT_CODE)); ui.finalize_pager(); exit_code } }