jj-cli 0.14.0

Jujutsu - an experimental version control system
Documentation
// 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::io::Write;

use itertools::Itertools;
use jj_lib::commit::Commit;
use jj_lib::repo::Repo;
use jj_lib::revset::{RevsetExpression, RevsetIteratorExt};

use crate::cli_util::{
    short_commit_hash, user_error, CommandError, CommandHelper, WorkspaceCommandHelper,
};
use crate::ui::Ui;

/// Move the current working copy commit to the next child revision in the
/// repository.
///
/// The command moves you to the next child in a linear fashion.
///
/// ```text
/// D      D @
/// |      |/
/// C @ => C
/// |/     |
/// B      B
/// ```
///
/// If `--edit` is passed, it will move you directly to the child
/// revision.
///
/// ```text
/// D    D
/// |    |
/// C    C
/// |    |
/// B => @
/// |    |
/// @    A
/// ```
#[derive(clap::Args, Clone, Debug)]
#[command(verbatim_doc_comment)]
pub(crate) struct NextArgs {
    /// How many revisions to move forward. By default advances to the next
    /// child.
    #[arg(default_value = "1")]
    amount: u64,
    /// Instead of creating a new working-copy commit on top of the target
    /// commit (like `jj new`), edit the target commit directly (like `jj
    /// edit`).
    #[arg(long)]
    edit: bool,
}

pub fn choose_commit<'a>(
    ui: &mut Ui,
    workspace_command: &WorkspaceCommandHelper,
    cmd: &str,
    commits: &'a [Commit],
) -> Result<&'a Commit, CommandError> {
    writeln!(ui.stdout(), "ambiguous {cmd} commit, choose one to target:")?;
    let mut formatter = ui.stdout_formatter();
    let mut choices: Vec<String> = Default::default();
    for (i, commit) in commits.iter().enumerate() {
        write!(formatter, "{}: ", i + 1)?;
        workspace_command.write_commit_summary(formatter.as_mut(), commit)?;
        writeln!(formatter)?;
        choices.push(format!("{}", i + 1));
    }
    writeln!(formatter, "q: quit the prompt")?;
    choices.push("q".to_string());
    drop(formatter);

    let choice = ui.prompt_choice(
        "enter the index of the commit you want to target",
        &choices,
        None,
    )?;
    if choice == "q" {
        return Err(user_error("ambiguous target commit"));
    }

    Ok(&commits[choice.parse::<usize>().unwrap() - 1])
}

pub(crate) fn cmd_next(
    ui: &mut Ui,
    command: &CommandHelper,
    args: &NextArgs,
) -> Result<(), CommandError> {
    let mut workspace_command = command.workspace_helper(ui)?;
    let edit = args.edit;
    let amount = args.amount;
    let current_wc_id = workspace_command
        .get_wc_commit_id()
        .ok_or_else(|| user_error("This command requires a working copy"))?;
    let current_wc = workspace_command.repo().store().get_commit(current_wc_id)?;
    let current_short = short_commit_hash(current_wc.id());
    // If we're editing, start at the working-copy commit.
    // Otherwise start from our direct parent.
    let start_id = if edit {
        current_wc_id
    } else {
        match current_wc.parent_ids() {
            [parent_id] => parent_id,
            _ => return Err(user_error("Cannot run `jj next` on a merge commit")),
        }
    };
    let descendant_expression = RevsetExpression::commit(start_id.clone()).descendants_at(amount);
    let target_expression = if edit {
        descendant_expression
    } else {
        descendant_expression.minus(&RevsetExpression::commit(current_wc_id.clone()).descendants())
    };
    let targets: Vec<Commit> = target_expression
        .evaluate_programmatic(workspace_command.repo().as_ref())?
        .iter()
        .commits(workspace_command.repo().store())
        .take(2)
        .try_collect()?;
    let target = match targets.as_slice() {
        [target] => target,
        [] => {
            // We found no descendant.
            return Err(user_error(format!(
                "No descendant found {amount} commit{} forward",
                if amount > 1 { "s" } else { "" }
            )));
        }
        commits => choose_commit(ui, &workspace_command, "next", commits)?,
    };
    let target_short = short_commit_hash(target.id());
    // We're editing, just move to the target commit.
    if edit {
        // We're editing, the target must be rewritable.
        workspace_command.check_rewritable([target])?;
        let mut tx = workspace_command.start_transaction();
        tx.edit(target)?;
        tx.finish(
            ui,
            format!("next: {current_short} -> editing {target_short}"),
        )?;
        return Ok(());
    }
    let mut tx = workspace_command.start_transaction();
    // Move the working-copy commit to the new parent.
    tx.check_out(target)?;
    tx.finish(ui, format!("next: {current_short} -> {target_short}"))?;
    Ok(())
}