2622 lines · 98.7 KB · Rust
RawDownload
// 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.