Martin von Zweigbergkiter_util: make all fallible_*() async2daed5c3 months agoHistory
286 lines · 9.9 KB · Rust
RawDownload
// Copyright 2020 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 std::collections::HashSet;use std::io::Write as _; use bstr::ByteVec as _;use clap_complete::ArgValueCompleter;use futures::future::try_join_all;use indexmap::IndexSet;use itertools::Itertools as _;use jj_lib::backend::CommitId;use jj_lib::repo::Repo as _;use jj_lib::rewrite::merge_commit_trees;use jj_lib::rewrite::rebase_commit;use tracing::instrument; use crate::cli_util::CommandHelper;use crate::cli_util::RevisionArg;use crate::cli_util::compute_commit_location;use crate::cli_util::merge_args_with;use crate::command_error::CommandError;use crate::complete;use crate::description_util::add_trailers;use crate::description_util::join_message_paragraphs;use crate::text_util;use crate::ui::Ui; /// Create a new, empty change and (by default) edit it in the working copy////// By default, `jj` will edit the new change, making the [working copy]/// represent the new commit. This can be avoided with `--no-edit`.////// Note that you can create a merge commit by specifying multiple revisions as/// argument. For example, `jj new @ main` will create a new commit with the/// working copy and the `main` bookmark as parents.////// [working copy]:///     https://docs.jj-vcs.dev/latest/working-copy/#[derive(clap::Args, Clone, Debug)]#[command(group(clap::ArgGroup::new("revisions").multiple(true)))]pub(crate) struct NewArgs {    /// Parent(s) of the new change [default: @] [aliases: -o, -r]    #[arg(group = "revisions", value_name = "REVSETS")]    #[arg(add = ArgValueCompleter::new(complete::revset_expression_all))]    revisions_pos: Option<Vec<RevisionArg>>,     #[arg(        short = 'o',        group = "revisions",        hide = true,        short_aliases = ['d', 'r'],        value_name = "REVSETS",     )]    #[arg(add = ArgValueCompleter::new(complete::revset_expression_all))]    revisions_opt: Option<Vec<RevisionArg>>,     /// The change description to use    #[arg(long = "message", short, value_name = "MESSAGE")]    message_paragraphs: Vec<String>,     /// Do not edit the newly created change    #[arg(long, conflicts_with = "_edit")]    no_edit: bool,     /// No-op flag to pair with --no-edit    #[arg(long, hide = true)]    _edit: bool,     /// Insert the new change after the given commit(s)    ///    /// Example: `jj new --insert-after A` creates a new change between `A` and    /// its children:    ///    /// ```text    ///                 B   C    ///                  \ /    ///     B   C   =>    @    ///      \ /          |    ///       A           A    /// ```    ///    /// Specifying `--insert-after` multiple times will relocate all children of    /// the given commits.    ///    /// Example: `jj new --insert-after A --insert-after X` creates a change    /// with `A` and `X` as parents, and rebases all children on top of the new    /// change:    ///    /// ```text    ///                 B   Y    ///                  \ /    ///     B  Y    =>    @    ///     |  |         / \    ///     A  X        A   X    /// ```    #[arg(        long,        short = 'A',        visible_alias = "after",        conflicts_with = "revisions",        value_name = "REVSETS",        verbatim_doc_comment    )]    #[arg(add = ArgValueCompleter::new(complete::revset_expression_all))]    insert_after: Option<Vec<RevisionArg>>,     /// Insert the new change before the given commit(s)    ///    /// Example: `jj new --insert-before C` creates a new change between `C` and    /// its parents:    ///    /// ```text    ///                    C    ///                    |    ///       C     =>     @    ///      / \          / \    ///     A   B        A   B    /// ```    ///    /// `--insert-after` and `--insert-before` can be combined.    ///    /// Example: `jj new --insert-after A --insert-before D`:    ///    /// ```text    ///     D            D    ///     |           / \    ///     C          |   C    ///     |    =>    @   |    ///     B          |   B    ///     |           \ /    ///     A            A    /// ```    ///    /// Similar to `--insert-after`, you can specify `--insert-before` multiple    /// times.    #[arg(        long,        short = 'B',        visible_alias = "before",        conflicts_with = "revisions",        value_name = "REVSETS",        verbatim_doc_comment    )]    #[arg(add = ArgValueCompleter::new(complete::revset_expression_mutable))]    insert_before: Option<Vec<RevisionArg>>,} #[instrument(skip_all)]pub(crate) async fn cmd_new(    ui: &mut Ui,    command: &CommandHelper,    args: &NewArgs,) -> Result<(), CommandError> {    let mut workspace_command = command.workspace_helper(ui).await?;     let revision_args = match (&args.revisions_pos, &args.revisions_opt) {        (None, None) => (args.insert_before.is_none() && args.insert_after.is_none())            .then(|| vec![RevisionArg::AT]),        (None, Some(args)) | (Some(args), None) => Some(args.clone()),        (Some(pos), Some(opt)) => Some(merge_args_with(            command.matches().subcommand_matches("new").unwrap(),            &[("revisions_pos", pos), ("revisions_opt", opt)],            |_id, value| value.clone(),        )),    };    let (parent_commit_ids, child_commit_ids) = compute_commit_location(        ui,        &workspace_command,        revision_args.as_deref(),        args.insert_after.as_deref(),        args.insert_before.as_deref(),        "new commit",    )    .await?;    let parent_commits = try_join_all(        parent_commit_ids            .iter()            .map(|commit_id| workspace_command.repo().store().get_commit_async(commit_id)),    )    .await?;    let mut advance_bookmarks_target = None;    let mut advanceable_bookmarks = vec![];     if args.insert_before.is_none() && args.insert_after.is_none() {        let should_advance_bookmarks = parent_commits.len() == 1;        if should_advance_bookmarks {            advance_bookmarks_target = Some(parent_commit_ids[0].clone());            advanceable_bookmarks =                workspace_command.get_advanceable_bookmarks(ui, parent_commits[0].parent_ids())?;        }    }     let parent_commit_ids_set: HashSet<CommitId> = parent_commit_ids.iter().cloned().collect();     let mut tx = workspace_command.start_transaction();    let merged_tree = merge_commit_trees(tx.repo(), &parent_commits).await?;    let mut commit_builder = tx        .repo_mut()        .new_commit(parent_commit_ids, merged_tree)        .detach();    let description = if !args.message_paragraphs.is_empty() {        join_message_paragraphs(&args.message_paragraphs)    } else {        let template_text = tx.settings().get_string("templates.new_description")?;        if template_text.is_empty() {            String::new()        } else {            let template = tx.parse_commit_template(ui, &template_text)?;            let temp_commit = commit_builder.write_hidden().await?;            text_util::complete_newline(                template.format_plain_text(&temp_commit).into_string_lossy(),            )        }    };    if !description.is_empty() {        // The first trailer would become the first line of the description.        // Also, a commit with no description is treated in a special way in jujutsu: it        // can be discarded as soon as it's no longer the working copy. Adding a        // trailer to an empty description would break that logic.        commit_builder.set_description(&description);        let description = add_trailers(ui, &tx, &commit_builder).await?;        commit_builder.set_description(&description);    }    let new_commit = commit_builder.write(tx.repo_mut()).await?;     let child_commits = try_join_all(        child_commit_ids            .iter()            .map(|commit_id| tx.repo().store().get_commit_async(commit_id)),    )    .await?;    let mut num_rebased = 0;    for child_commit in child_commits {        let mut new_parent_ids = IndexSet::new();        // If the original parents of the children commits are the parents of the new        // commit, replace them with the new commit since we are "inserting" the new        // commit in between the parents and the children.        for old_parent_id in child_commit.parent_ids() {            if parent_commit_ids_set.contains(old_parent_id) {                new_parent_ids.insert(new_commit.id().clone());            } else {                new_parent_ids.insert(old_parent_id.clone());            }        }        new_parent_ids.insert(new_commit.id().clone());        let new_parent_ids = new_parent_ids.into_iter().collect_vec();        rebase_commit(tx.repo_mut(), child_commit, new_parent_ids).await?;        num_rebased += 1;    }    num_rebased += tx.repo_mut().rebase_descendants().await?;     if args.no_edit {        if let Some(mut formatter) = ui.status_formatter() {            write!(formatter, "Created new commit ")?;            tx.write_commit_summary(formatter.as_mut(), &new_commit)?;            writeln!(formatter)?;        }    } else {        tx.edit(&new_commit)?;        // The description of the new commit will be printed by tx.finish()    }    if num_rebased > 0 {        writeln!(ui.status(), "Rebased {num_rebased} descendant commits.")?;    }     // Does nothing if there's no bookmarks to advance.    if let Some(target) = advance_bookmarks_target {        tx.advance_bookmarks(advanceable_bookmarks, &target).await?;    }     tx.finish(ui, "new empty commit").await?;    Ok(())}