refactor: extract branch commands into commands/branch module

This commit is contained in:
Samuel Tardieu 2023-01-17 21:27:35 +01:00
parent 8f8fd7c89a
commit 3d09f675bd
2 changed files with 330 additions and 314 deletions

324
src/commands/branch.rs Normal file
View file

@ -0,0 +1,324 @@
use std::collections::BTreeSet;
use clap::builder::NonEmptyStringValueParser;
use itertools::Itertools;
use jujutsu_lib::backend::{CommitId, ObjectId};
use jujutsu_lib::op_store::RefTarget;
use jujutsu_lib::repo::RepoRef;
use jujutsu_lib::view::View;
use crate::cli_util::{
user_error, user_error_with_hint, CommandError, CommandHelper, RevisionArg,
WorkspaceCommandHelper,
};
use crate::commands::make_branch_term;
use crate::formatter::Formatter;
use crate::ui::Ui;
/// Manage branches.
///
/// For information about branches, see
/// https://github.com/martinvonz/jj/blob/main/docs/branches.md.
#[derive(clap::Subcommand, Clone, Debug)]
pub enum BranchSubcommand {
/// Create a new branch.
#[command(visible_alias("c"))]
Create {
/// The branch's target revision.
#[arg(long, short)]
revision: Option<RevisionArg>,
/// The branches to create.
#[arg(required = true, value_parser=NonEmptyStringValueParser::new())]
names: Vec<String>,
},
/// Delete an existing branch and propagate the deletion to remotes on the
/// next push.
#[command(visible_alias("d"))]
Delete {
/// The branches to delete.
#[arg(required = true)]
names: Vec<String>,
},
/// Forget everything about a branch, including its local and remote
/// targets.
///
/// A forgotten branch will not impact remotes on future pushes. It will be
/// recreated on future pulls if it still exists in the remote.
#[command(visible_alias("f"))]
Forget {
/// The branches to forget.
#[arg(required_unless_present_any(&["glob"]))]
names: Vec<String>,
/// A glob pattern indicating branches to forget.
#[arg(long)]
glob: Vec<String>,
},
/// List branches and their targets
///
/// A remote branch will be included only if its target is different from
/// the local target. For a conflicted branch (both local and remote), old
/// target revisions are preceded by a "-" and new target revisions are
/// preceded by a "+". For information about branches, see
/// https://github.com/martinvonz/jj/blob/main/docs/branches.md.
#[command(visible_alias("l"))]
List,
/// Update a given branch to point to a certain commit.
#[command(visible_alias("s"))]
Set {
/// The branch's target revision.
#[arg(long, short)]
revision: Option<RevisionArg>,
/// Allow moving the branch backwards or sideways.
#[arg(long, short = 'B')]
allow_backwards: bool,
/// The branches to update.
#[arg(required = true)]
names: Vec<String>,
},
}
pub fn cmd_branch(
ui: &mut Ui,
command: &CommandHelper,
subcommand: &BranchSubcommand,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let view = workspace_command.repo().view();
fn validate_branch_names_exist<'a>(
view: &'a View,
names: &'a [String],
) -> Result<(), CommandError> {
for branch_name in names {
if view.get_local_branch(branch_name).is_none() {
return Err(user_error(format!("No such branch: {branch_name}")));
}
}
Ok(())
}
fn find_globs(view: &View, globs: &[String]) -> Result<Vec<String>, CommandError> {
let globs: Vec<glob::Pattern> = globs
.iter()
.map(|glob| glob::Pattern::new(glob))
.try_collect()?;
let matching_branches = view
.branches()
.iter()
.map(|(branch_name, _branch_target)| branch_name)
.filter(|branch_name| globs.iter().any(|glob| glob.matches(branch_name)))
.cloned()
.collect();
Ok(matching_branches)
}
match subcommand {
BranchSubcommand::Create { revision, names } => {
let branch_names: Vec<&str> = names
.iter()
.map(|branch_name| match view.get_local_branch(branch_name) {
Some(_) => Err(user_error_with_hint(
format!("Branch already exists: {branch_name}"),
"Use `jj branch set` to update it.",
)),
None => Ok(branch_name.as_str()),
})
.try_collect()?;
if branch_names.len() > 1 {
writeln!(
ui.warning(),
"warning: Creating multiple branches ({}).",
branch_names.len()
)?;
}
let target_commit =
workspace_command.resolve_single_rev(revision.as_deref().unwrap_or("@"))?;
let mut tx = workspace_command.start_transaction(&format!(
"create {} pointing to commit {}",
make_branch_term(&branch_names),
target_commit.id().hex()
));
for branch_name in branch_names {
tx.mut_repo().set_local_branch(
branch_name.to_string(),
RefTarget::Normal(target_commit.id().clone()),
);
}
workspace_command.finish_transaction(ui, tx)?;
}
BranchSubcommand::Set {
revision,
allow_backwards,
names: branch_names,
} => {
if branch_names.len() > 1 {
writeln!(
ui.warning(),
"warning: Updating multiple branches ({}).",
branch_names.len()
)?;
}
let target_commit =
workspace_command.resolve_single_rev(revision.as_deref().unwrap_or("@"))?;
if !allow_backwards
&& !branch_names.iter().all(|branch_name| {
is_fast_forward(
workspace_command.repo().as_repo_ref(),
branch_name,
target_commit.id(),
)
})
{
return Err(user_error_with_hint(
"Refusing to move branch backwards or sideways.",
"Use --allow-backwards to allow it.",
));
}
let mut tx = workspace_command.start_transaction(&format!(
"point {} to commit {}",
make_branch_term(branch_names),
target_commit.id().hex()
));
for branch_name in branch_names {
tx.mut_repo().set_local_branch(
branch_name.to_string(),
RefTarget::Normal(target_commit.id().clone()),
);
}
workspace_command.finish_transaction(ui, tx)?;
}
BranchSubcommand::Delete { names } => {
validate_branch_names_exist(view, names)?;
let mut tx =
workspace_command.start_transaction(&format!("delete {}", make_branch_term(names)));
for branch_name in names {
tx.mut_repo().remove_local_branch(branch_name);
}
workspace_command.finish_transaction(ui, tx)?;
}
BranchSubcommand::Forget { names, glob } => {
validate_branch_names_exist(view, names)?;
let globbed_names = find_globs(view, glob)?;
let names: BTreeSet<String> = names.iter().cloned().chain(globbed_names).collect();
let branch_term = make_branch_term(names.iter().collect_vec().as_slice());
let mut tx = workspace_command.start_transaction(&format!("forget {branch_term}"));
for branch_name in names {
tx.mut_repo().remove_branch(&branch_name);
}
workspace_command.finish_transaction(ui, tx)?;
}
BranchSubcommand::List => {
list_branches(ui, command, &workspace_command)?;
}
}
Ok(())
}
fn list_branches(
ui: &mut Ui,
_command: &CommandHelper,
workspace_command: &WorkspaceCommandHelper,
) -> Result<(), CommandError> {
let repo = workspace_command.repo();
let print_branch_target =
|formatter: &mut dyn Formatter, target: Option<&RefTarget>| -> Result<(), CommandError> {
match target {
Some(RefTarget::Normal(id)) => {
write!(formatter, ": ")?;
let commit = repo.store().get_commit(id)?;
workspace_command.write_commit_summary(formatter, &commit)?;
writeln!(formatter)?;
}
Some(RefTarget::Conflict { adds, removes }) => {
write!(formatter, " ")?;
write!(formatter.labeled("conflict"), "(conflicted)")?;
writeln!(formatter, ":")?;
for id in removes {
let commit = repo.store().get_commit(id)?;
write!(formatter, " - ")?;
workspace_command.write_commit_summary(formatter, &commit)?;
writeln!(formatter)?;
}
for id in adds {
let commit = repo.store().get_commit(id)?;
write!(formatter, " + ")?;
workspace_command.write_commit_summary(formatter, &commit)?;
writeln!(formatter)?;
}
}
None => {
writeln!(formatter, " (deleted)")?;
}
}
Ok(())
};
let mut formatter = ui.stdout_formatter();
let formatter = formatter.as_mut();
let index = repo.index();
for (name, branch_target) in repo.view().branches() {
write!(formatter.labeled("branch"), "{name}")?;
print_branch_target(formatter, branch_target.local_target.as_ref())?;
for (remote, remote_target) in branch_target
.remote_targets
.iter()
.sorted_by_key(|(name, _target)| name.to_owned())
{
if Some(remote_target) == branch_target.local_target.as_ref() {
continue;
}
write!(formatter, " ")?;
write!(formatter.labeled("branch"), "@{remote}")?;
if let Some(local_target) = branch_target.local_target.as_ref() {
let remote_ahead_count = index
.walk_revs(&remote_target.adds(), &local_target.adds())
.count();
let local_ahead_count = index
.walk_revs(&local_target.adds(), &remote_target.adds())
.count();
if remote_ahead_count != 0 && local_ahead_count == 0 {
write!(formatter, " (ahead by {remote_ahead_count} commits)")?;
} else if remote_ahead_count == 0 && local_ahead_count != 0 {
write!(formatter, " (behind by {local_ahead_count} commits)")?;
} else if remote_ahead_count != 0 && local_ahead_count != 0 {
write!(
formatter,
" (ahead by {remote_ahead_count} commits, behind by {local_ahead_count} \
commits)"
)?;
}
}
print_branch_target(formatter, Some(remote_target))?;
}
}
Ok(())
}
fn is_fast_forward(repo: RepoRef, branch_name: &str, new_target_id: &CommitId) -> bool {
if let Some(current_target) = repo.view().get_local_branch(branch_name) {
current_target
.adds()
.iter()
.any(|add| repo.index().is_ancestor(add, new_target_id))
} else {
true
}
}

View file

@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
mod branch;
mod git;
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::collections::{BTreeMap, HashSet};
use std::fmt::Debug;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::PathBuf;
@ -30,16 +31,15 @@ use jujutsu_lib::commit::Commit;
use jujutsu_lib::dag_walk::topo_order_reverse;
use jujutsu_lib::index::IndexEntry;
use jujutsu_lib::matchers::EverythingMatcher;
use jujutsu_lib::op_store::{RefTarget, WorkspaceId};
use jujutsu_lib::op_store::WorkspaceId;
use jujutsu_lib::operation::Operation;
use jujutsu_lib::repo::{ReadonlyRepo, RepoRef};
use jujutsu_lib::repo::ReadonlyRepo;
use jujutsu_lib::repo_path::RepoPath;
use jujutsu_lib::revset::{RevsetAliasesMap, RevsetExpression};
use jujutsu_lib::revset_graph_iterator::{RevsetGraphEdge, RevsetGraphEdgeType};
use jujutsu_lib::rewrite::{back_out_commit, merge_commit_trees, rebase_commit, DescendantRebaser};
use jujutsu_lib::settings::UserSettings;
use jujutsu_lib::tree::{merge_trees, Tree};
use jujutsu_lib::view::View;
use jujutsu_lib::workspace::{Workspace, WorkspaceLoader};
use jujutsu_lib::{conflicts, file_util, revset};
use maplit::{hashmap, hashset};
@ -104,7 +104,7 @@ enum Commands {
Rebase(RebaseArgs),
Backout(BackoutArgs),
#[command(subcommand)]
Branch(BranchSubcommand),
Branch(branch::BranchSubcommand),
/// Undo an operation (shortcut for `jj op undo`)
Undo(OperationUndoArgs),
#[command(subcommand)]
@ -741,76 +741,6 @@ struct BackoutArgs {
destination: Vec<RevisionArg>,
}
/// Manage branches.
///
/// For information about branches, see
/// https://github.com/martinvonz/jj/blob/main/docs/branches.md.
#[derive(clap::Subcommand, Clone, Debug)]
enum BranchSubcommand {
/// Create a new branch.
#[command(visible_alias("c"))]
Create {
/// The branch's target revision.
#[arg(long, short)]
revision: Option<RevisionArg>,
/// The branches to create.
#[arg(required = true, value_parser=NonEmptyStringValueParser::new())]
names: Vec<String>,
},
/// Delete an existing branch and propagate the deletion to remotes on the
/// next push.
#[command(visible_alias("d"))]
Delete {
/// The branches to delete.
#[arg(required = true)]
names: Vec<String>,
},
/// Forget everything about a branch, including its local and remote
/// targets.
///
/// A forgotten branch will not impact remotes on future pushes. It will be
/// recreated on future pulls if it still exists in the remote.
#[command(visible_alias("f"))]
Forget {
/// The branches to forget.
#[arg(required_unless_present_any(&["glob"]))]
names: Vec<String>,
/// A glob pattern indicating branches to forget.
#[arg(long)]
glob: Vec<String>,
},
/// List branches and their targets
///
/// A remote branch will be included only if its target is different from
/// the local target. For a conflicted branch (both local and remote), old
/// target revisions are preceded by a "-" and new target revisions are
/// preceded by a "+". For information about branches, see
/// https://github.com/martinvonz/jj/blob/main/docs/branches.md.
#[command(visible_alias("l"))]
List,
/// Update a given branch to point to a certain commit.
#[command(visible_alias("s"))]
Set {
/// The branch's target revision.
#[arg(long, short)]
revision: Option<RevisionArg>,
/// Allow moving the branch backwards or sideways.
#[arg(long, short = 'B')]
allow_backwards: bool,
/// The branches to update.
#[arg(required = true)]
names: Vec<String>,
},
}
/// Commands for working with the operation log
///
/// Commands for working with the operation log. For information about the
@ -3003,17 +2933,6 @@ fn cmd_backout(
Ok(())
}
fn is_fast_forward(repo: RepoRef, branch_name: &str, new_target_id: &CommitId) -> bool {
if let Some(current_target) = repo.view().get_local_branch(branch_name) {
current_target
.adds()
.iter()
.any(|add| repo.index().is_ancestor(add, new_target_id))
} else {
true
}
}
fn make_branch_term(branch_names: &[impl AsRef<str>]) -> String {
match branch_names {
[branch_name] => format!("branch {}", branch_name.as_ref()),
@ -3026,233 +2945,6 @@ fn make_branch_term(branch_names: &[impl AsRef<str>]) -> String {
}
}
fn cmd_branch(
ui: &mut Ui,
command: &CommandHelper,
subcommand: &BranchSubcommand,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let view = workspace_command.repo().view();
fn validate_branch_names_exist<'a>(
view: &'a View,
names: &'a [String],
) -> Result<(), CommandError> {
for branch_name in names {
if view.get_local_branch(branch_name).is_none() {
return Err(user_error(format!("No such branch: {branch_name}")));
}
}
Ok(())
}
fn find_globs(view: &View, globs: &[String]) -> Result<Vec<String>, CommandError> {
let globs: Vec<glob::Pattern> = globs
.iter()
.map(|glob| glob::Pattern::new(glob))
.try_collect()?;
let matching_branches = view
.branches()
.iter()
.map(|(branch_name, _branch_target)| branch_name)
.filter(|branch_name| globs.iter().any(|glob| glob.matches(branch_name)))
.cloned()
.collect();
Ok(matching_branches)
}
match subcommand {
BranchSubcommand::Create { revision, names } => {
let branch_names: Vec<&str> = names
.iter()
.map(|branch_name| match view.get_local_branch(branch_name) {
Some(_) => Err(user_error_with_hint(
format!("Branch already exists: {branch_name}"),
"Use `jj branch set` to update it.",
)),
None => Ok(branch_name.as_str()),
})
.try_collect()?;
if branch_names.len() > 1 {
writeln!(
ui.warning(),
"warning: Creating multiple branches ({}).",
branch_names.len()
)?;
}
let target_commit =
workspace_command.resolve_single_rev(revision.as_deref().unwrap_or("@"))?;
let mut tx = workspace_command.start_transaction(&format!(
"create {} pointing to commit {}",
make_branch_term(&branch_names),
target_commit.id().hex()
));
for branch_name in branch_names {
tx.mut_repo().set_local_branch(
branch_name.to_string(),
RefTarget::Normal(target_commit.id().clone()),
);
}
workspace_command.finish_transaction(ui, tx)?;
}
BranchSubcommand::Set {
revision,
allow_backwards,
names: branch_names,
} => {
if branch_names.len() > 1 {
writeln!(
ui.warning(),
"warning: Updating multiple branches ({}).",
branch_names.len()
)?;
}
let target_commit =
workspace_command.resolve_single_rev(revision.as_deref().unwrap_or("@"))?;
if !allow_backwards
&& !branch_names.iter().all(|branch_name| {
is_fast_forward(
workspace_command.repo().as_repo_ref(),
branch_name,
target_commit.id(),
)
})
{
return Err(user_error_with_hint(
"Refusing to move branch backwards or sideways.",
"Use --allow-backwards to allow it.",
));
}
let mut tx = workspace_command.start_transaction(&format!(
"point {} to commit {}",
make_branch_term(branch_names),
target_commit.id().hex()
));
for branch_name in branch_names {
tx.mut_repo().set_local_branch(
branch_name.to_string(),
RefTarget::Normal(target_commit.id().clone()),
);
}
workspace_command.finish_transaction(ui, tx)?;
}
BranchSubcommand::Delete { names } => {
validate_branch_names_exist(view, names)?;
let mut tx =
workspace_command.start_transaction(&format!("delete {}", make_branch_term(names)));
for branch_name in names {
tx.mut_repo().remove_local_branch(branch_name);
}
workspace_command.finish_transaction(ui, tx)?;
}
BranchSubcommand::Forget { names, glob } => {
validate_branch_names_exist(view, names)?;
let globbed_names = find_globs(view, glob)?;
let names: BTreeSet<String> = names.iter().cloned().chain(globbed_names).collect();
let branch_term = make_branch_term(names.iter().collect_vec().as_slice());
let mut tx = workspace_command.start_transaction(&format!("forget {branch_term}"));
for branch_name in names {
tx.mut_repo().remove_branch(&branch_name);
}
workspace_command.finish_transaction(ui, tx)?;
}
BranchSubcommand::List => {
list_branches(ui, command, &workspace_command)?;
}
}
Ok(())
}
fn list_branches(
ui: &mut Ui,
_command: &CommandHelper,
workspace_command: &WorkspaceCommandHelper,
) -> Result<(), CommandError> {
let repo = workspace_command.repo();
let print_branch_target =
|formatter: &mut dyn Formatter, target: Option<&RefTarget>| -> Result<(), CommandError> {
match target {
Some(RefTarget::Normal(id)) => {
write!(formatter, ": ")?;
let commit = repo.store().get_commit(id)?;
workspace_command.write_commit_summary(formatter, &commit)?;
writeln!(formatter)?;
}
Some(RefTarget::Conflict { adds, removes }) => {
write!(formatter, " ")?;
write!(formatter.labeled("conflict"), "(conflicted)")?;
writeln!(formatter, ":")?;
for id in removes {
let commit = repo.store().get_commit(id)?;
write!(formatter, " - ")?;
workspace_command.write_commit_summary(formatter, &commit)?;
writeln!(formatter)?;
}
for id in adds {
let commit = repo.store().get_commit(id)?;
write!(formatter, " + ")?;
workspace_command.write_commit_summary(formatter, &commit)?;
writeln!(formatter)?;
}
}
None => {
writeln!(formatter, " (deleted)")?;
}
}
Ok(())
};
let mut formatter = ui.stdout_formatter();
let formatter = formatter.as_mut();
let index = repo.index();
for (name, branch_target) in repo.view().branches() {
write!(formatter.labeled("branch"), "{name}")?;
print_branch_target(formatter, branch_target.local_target.as_ref())?;
for (remote, remote_target) in branch_target
.remote_targets
.iter()
.sorted_by_key(|(name, _target)| name.to_owned())
{
if Some(remote_target) == branch_target.local_target.as_ref() {
continue;
}
write!(formatter, " ")?;
write!(formatter.labeled("branch"), "@{remote}")?;
if let Some(local_target) = branch_target.local_target.as_ref() {
let remote_ahead_count = index
.walk_revs(&remote_target.adds(), &local_target.adds())
.count();
let local_ahead_count = index
.walk_revs(&local_target.adds(), &remote_target.adds())
.count();
if remote_ahead_count != 0 && local_ahead_count == 0 {
write!(formatter, " (ahead by {remote_ahead_count} commits)")?;
} else if remote_ahead_count == 0 && local_ahead_count != 0 {
write!(formatter, " (behind by {local_ahead_count} commits)")?;
} else if remote_ahead_count != 0 && local_ahead_count != 0 {
write!(
formatter,
" (ahead by {remote_ahead_count} commits, behind by {local_ahead_count} \
commits)"
)?;
}
}
print_branch_target(formatter, Some(remote_target))?;
}
}
Ok(())
}
fn cmd_debug(
ui: &mut Ui,
command: &CommandHelper,
@ -3763,7 +3455,7 @@ pub fn run_command(
Commands::Rebase(sub_args) => cmd_rebase(ui, command_helper, sub_args),
Commands::Backout(sub_args) => cmd_backout(ui, command_helper, sub_args),
Commands::Resolve(sub_args) => cmd_resolve(ui, command_helper, sub_args),
Commands::Branch(sub_args) => cmd_branch(ui, command_helper, sub_args),
Commands::Branch(sub_args) => branch::cmd_branch(ui, command_helper, sub_args),
Commands::Undo(sub_args) => cmd_op_undo(ui, command_helper, sub_args),
Commands::Operation(sub_args) => cmd_operation(ui, command_helper, sub_args),
Commands::Workspace(sub_args) => cmd_workspace(ui, command_helper, sub_args),