// 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::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, str}; use clap::builder::{ MapValueParser, NonEmptyStringValueParser, TypedValueParser, ValueParserFactory,};use clap::error::{ContextKind, ContextValue};use clap::{ArgAction, ArgMatches, Command, FromArgMatches};use indexmap::{IndexMap, IndexSet};use itertools::Itertools;use jj_lib::backend::{ChangeId, CommitId, MergedTreeId};use jj_lib::commit::Commit;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_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, StoreFactories, StoreLoadError,};use jj_lib::repo_path::{FsPathParseError, RepoPath, RepoPathBuf};use jj_lib::revset::{ DefaultSymbolResolver, Revset, RevsetAliasesMap, RevsetCommitRef, RevsetExpression, RevsetFilterPredicate, RevsetIteratorExt, RevsetParseContext, RevsetParseError, 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::view::View;use jj_lib::working_copy::{ CheckoutStats, LockedWorkingCopy, SnapshotOptions, WorkingCopy, WorkingCopyFactory,};use jj_lib::workspace::{ default_working_copy_factories, LockedWorkspace, Workspace, WorkspaceLoadError, WorkspaceLoader,};use jj_lib::{dag_walk, file_util, git, op_heads_store, op_walk, revset};use once_cell::unsync::OnceCell;use tracing::instrument;use tracing_chrome::ChromeLayerBuilder;use tracing_subscriber::prelude::*; use crate::command_error::{ handle_command_result, internal_error, internal_error_with_message, user_error, user_error_with_hint, user_error_with_message, CommandError,};use crate::commit_templater::{CommitTemplateLanguage, CommitTemplateLanguageExtension};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::{DiffEditor, MergeEditor, MergeToolConfigError};use crate::template_builder::TemplateLanguage;use crate::template_parser::TemplateAliasesMap;use crate::templater::Template;use crate::ui::{ColorChoice, Ui};use crate::{template_builder, text_util}; #[derive(Clone)]struct ChromeTracingFlushGuard { _inner: Option<Rc<tracing_chrome::FlushGuard>>,} 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<String>, matches: ArgMatches, global_args: GlobalArgs, settings: UserSettings, layered_configs: LayeredConfigs, commit_template_extension: Option<Arc<dyn CommitTemplateLanguageExtension>>, maybe_workspace_loader: Result<WorkspaceLoader, CommandError>, store_factories: StoreFactories, working_copy_factories: HashMap<String, Box<dyn WorkingCopyFactory>>,} impl CommandHelper { pub fn app(&self) -> &Command { &self.app } /// Canonical form of the current working directory path. /// /// A loaded `Workspace::workspace_root()` also returns a canonical path, so /// relative paths can be easily computed from these paths. pub fn cwd(&self) -> &Path { &self.cwd } pub fn string_args(&self) -> &Vec<String> { &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<Vec<AnnotatedValue>, 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. fn load_template_aliases(&self, ui: &Ui) -> Result<TemplateAliasesMap, CommandError> { load_template_aliases(ui, &self.layered_configs) } /// Parses template of the given language into evaluation tree. /// /// This function also loads template aliases from the settings. Use /// `WorkspaceCommandHelper::parse_template()` if you've already /// instantiated the workspace helper. pub fn parse_template<'a, L: TemplateLanguage<'a> + ?Sized>( &self, ui: &Ui, language: &L, template_text: &str, ) -> Result<Box<dyn Template<L::Context> + 'a>, CommandError> { let aliases = self.load_template_aliases(ui)?; Ok(template_builder::parse(language, template_text, &aliases)?) } 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<WorkspaceCommandHelper, CommandError> { 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<WorkspaceCommandHelper, CommandError> { 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<Workspace, CommandError> { 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<Operation, CommandError> { 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<ReadonlyRepo>, ) -> Result<WorkspaceCommandHelper, CommandError> { 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<ReadonlyRepo>, id_prefix_context: OnceCell<IdPrefixContext>,} impl ReadonlyUserRepo { fn new(repo: Arc<ReadonlyRepo>) -> 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`]/// (which most commands do).pub struct WorkspaceCommandHelper { cwd: PathBuf, string_args: Vec<String>, global_args: GlobalArgs, settings: UserSettings, workspace: Workspace, user_repo: ReadonlyUserRepo, // TODO: Parsed template can be cached if it doesn't capture 'repo lifetime commit_summary_template_text: String, commit_template_extension: Option<Arc<dyn CommitTemplateLanguageExtension>>, 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<ReadonlyRepo>, ) -> Result<Self, CommandError> { let settings = command.settings.clone(); let commit_summary_template_text = settings.config().get_string("templates.commit_summary")?; let revset_aliases_map = load_revset_aliases(ui, &command.layered_configs)?; let template_aliases_map = command.load_template_aliases(ui)?; 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, workspace, user_repo: ReadonlyUserRepo::new(repo), commit_summary_template_text, commit_template_extension: command.commit_template_extension.clone(), revset_aliases_map, template_aliases_map, may_update_working_copy, working_copy_shared_with_git, }; // Parse commit_summary template (and short-prefixes revset) early to // report error before starting mutable operation. helper.parse_commit_template(&helper.commit_summary_template_text)?; 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();Lines 1–500 of 2622. Load more
// 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::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, str}; use clap::builder::{ MapValueParser, NonEmptyStringValueParser, TypedValueParser, ValueParserFactory, }; use clap::error::{ContextKind, ContextValue}; use clap::{ArgAction, ArgMatches, Command, FromArgMatches}; use indexmap::{IndexMap, IndexSet}; use itertools::Itertools; use jj_lib::backend::{ChangeId, CommitId, MergedTreeId}; use jj_lib::commit::Commit; 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_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, StoreFactories, StoreLoadError, }; use jj_lib::repo_path::{FsPathParseError, RepoPath, RepoPathBuf}; use jj_lib::revset::{ DefaultSymbolResolver, Revset, RevsetAliasesMap, RevsetCommitRef, RevsetExpression, RevsetFilterPredicate, RevsetIteratorExt, RevsetParseContext, RevsetParseError, 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::view::View; use jj_lib::working_copy::{ CheckoutStats, LockedWorkingCopy, SnapshotOptions, WorkingCopy, WorkingCopyFactory, }; use jj_lib::workspace::{ default_working_copy_factories, LockedWorkspace, Workspace, WorkspaceLoadError, WorkspaceLoader, }; use jj_lib::{dag_walk, file_util, git, op_heads_store, op_walk, revset}; use once_cell::unsync::OnceCell; use tracing::instrument; use tracing_chrome::ChromeLayerBuilder; use tracing_subscriber::prelude::*; use crate::command_error::{ handle_command_result, internal_error, internal_error_with_message, user_error, user_error_with_hint, user_error_with_message, CommandError, }; use crate::commit_templater::{CommitTemplateLanguage, CommitTemplateLanguageExtension}; 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::{DiffEditor, MergeEditor, MergeToolConfigError}; use crate::template_builder::TemplateLanguage; use crate::template_parser::TemplateAliasesMap; use crate::templater::Template; use crate::ui::{ColorChoice, Ui}; use crate::{template_builder, text_util}; #[derive(Clone)] struct ChromeTracingFlushGuard { _inner: Option<Rc<tracing_chrome::FlushGuard>>, } 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<String>, matches: ArgMatches, global_args: GlobalArgs, settings: UserSettings, layered_configs: LayeredConfigs, commit_template_extension: Option<Arc<dyn CommitTemplateLanguageExtension>>, maybe_workspace_loader: Result<WorkspaceLoader, CommandError>, store_factories: StoreFactories, working_copy_factories: HashMap<String, Box<dyn WorkingCopyFactory>>, } impl CommandHelper { pub fn app(&self) -> &Command { &self.app } /// Canonical form of the current working directory path. /// /// A loaded `Workspace::workspace_root()` also returns a canonical path, so /// relative paths can be easily computed from these paths. pub fn cwd(&self) -> &Path { &self.cwd } pub fn string_args(&self) -> &Vec<String> { &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<Vec<AnnotatedValue>, 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. fn load_template_aliases(&self, ui: &Ui) -> Result<TemplateAliasesMap, CommandError> { load_template_aliases(ui, &self.layered_configs) } /// Parses template of the given language into evaluation tree. /// /// This function also loads template aliases from the settings. Use /// `WorkspaceCommandHelper::parse_template()` if you've already /// instantiated the workspace helper. pub fn parse_template<'a, L: TemplateLanguage<'a> + ?Sized>( &self, ui: &Ui, language: &L, template_text: &str, ) -> Result<Box<dyn Template<L::Context> + 'a>, CommandError> { let aliases = self.load_template_aliases(ui)?; Ok(template_builder::parse(language, template_text, &aliases)?) } 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<WorkspaceCommandHelper, CommandError> { 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<WorkspaceCommandHelper, CommandError> { 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<Workspace, CommandError> { 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<Operation, CommandError> { 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<ReadonlyRepo>, ) -> Result<WorkspaceCommandHelper, CommandError> { 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<ReadonlyRepo>, id_prefix_context: OnceCell<IdPrefixContext>, } impl ReadonlyUserRepo { fn new(repo: Arc<ReadonlyRepo>) -> 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`] /// (which most commands do). pub struct WorkspaceCommandHelper { cwd: PathBuf, string_args: Vec<String>, global_args: GlobalArgs, settings: UserSettings, workspace: Workspace, user_repo: ReadonlyUserRepo, // TODO: Parsed template can be cached if it doesn't capture 'repo lifetime commit_summary_template_text: String, commit_template_extension: Option<Arc<dyn CommitTemplateLanguageExtension>>, 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<ReadonlyRepo>, ) -> Result<Self, CommandError> { let settings = command.settings.clone(); let commit_summary_template_text = settings.config().get_string("templates.commit_summary")?; let revset_aliases_map = load_revset_aliases(ui, &command.layered_configs)?; let template_aliases_map = command.load_template_aliases(ui)?; 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, workspace, user_repo: ReadonlyUserRepo::new(repo), commit_summary_template_text, commit_template_extension: command.commit_template_extension.clone(), revset_aliases_map, template_aliases_map, may_update_working_copy, working_copy_shared_with_git, }; // Parse commit_summary template (and short-prefixes revset) early to // report error before starting mutable operation. helper.parse_commit_template(&helper.commit_summary_template_text)?; 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();