use std::path::Path;
use anyhow::Result;
macro_rules! define_commit_action_kinds {
( $( $variant:ident { $label:literal, $prompt1:expr, $prompt2:expr } ),* $(,)? ) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CommitActionKind {
$( $variant, )*
}
impl CommitActionKind {
pub fn label(self) -> &'static str {
match self {
$( Self::$variant => $label, )*
}
}
pub fn input_prompt(self) -> Option<&'static str> {
match self {
$( Self::$variant => $prompt1, )*
}
}
pub fn second_input_prompt(self) -> Option<&'static str> {
match self {
$( Self::$variant => $prompt2, )*
}
}
pub fn needs_input(self) -> bool {
self.input_prompt().is_some()
}
pub fn needs_second_input(self) -> bool {
self.second_input_prompt().is_some()
}
pub fn into_action(self, input1: String, input2: String) -> CommitAction {
match self {
Self::CheckoutDetached => CommitAction::CheckoutDetached,
Self::CreateBranchHere => CommitAction::CreateBranchHere(input1),
Self::CherryPick => CommitAction::CherryPick,
Self::Revert => CommitAction::Revert,
Self::RebaseOnto => CommitAction::RebaseOnto,
Self::ResetSoft => CommitAction::ResetSoft,
Self::ResetMixed => CommitAction::ResetMixed,
Self::ResetHard => CommitAction::ResetHard,
Self::CreateTag => CommitAction::CreateTag(input1),
Self::CreateAnnotatedTag => CommitAction::CreateAnnotatedTag(input1, input2),
}
}
pub fn as_simple_action(self) -> Option<CommitAction> {
if self.needs_input() {
None
} else {
Some(self.into_action(String::new(), String::new()))
}
}
}
};
}
define_commit_action_kinds! {
CheckoutDetached { "Checkout (detached HEAD)", None, None },
CreateBranchHere { "Create branch here\u{2026}", Some("Branch name:"), None },
CherryPick { "Cherry-pick onto current branch", None, None },
Revert { "Revert commit", None, None },
RebaseOnto { "Rebase current branch onto this", None, None },
ResetSoft { "Reset here \u{2014} soft (keep staged)", None, None },
ResetMixed { "Reset here \u{2014} mixed (keep files)", None, None },
ResetHard { "Reset here \u{2014} hard (discard all)", None, None },
CreateTag { "Create tag here", Some("Tag name:"), None },
CreateAnnotatedTag { "Create annotated tag here\u{2026}", Some("Tag name:"), Some("Tag message:") }
}
pub const COMMIT_MENU_GROUPS: &[&[CommitActionKind]] = &[
&[
CommitActionKind::CheckoutDetached,
CommitActionKind::CreateBranchHere,
],
&[
CommitActionKind::CherryPick,
CommitActionKind::Revert,
CommitActionKind::RebaseOnto,
],
&[
CommitActionKind::ResetSoft,
CommitActionKind::ResetMixed,
CommitActionKind::ResetHard,
],
&[
CommitActionKind::CreateTag,
CommitActionKind::CreateAnnotatedTag,
],
];
#[derive(Debug, Clone)]
pub enum CommitAction {
CheckoutDetached,
CreateBranchHere(String),
CherryPick,
Revert,
RebaseOnto,
ResetSoft,
ResetMixed,
ResetHard,
CreateTag(String),
CreateAnnotatedTag(String, String), }
impl CommitAction {
pub fn kind(&self) -> CommitActionKind {
match self {
Self::CheckoutDetached => CommitActionKind::CheckoutDetached,
Self::CreateBranchHere(_) => CommitActionKind::CreateBranchHere,
Self::CherryPick => CommitActionKind::CherryPick,
Self::Revert => CommitActionKind::Revert,
Self::RebaseOnto => CommitActionKind::RebaseOnto,
Self::ResetSoft => CommitActionKind::ResetSoft,
Self::ResetMixed => CommitActionKind::ResetMixed,
Self::ResetHard => CommitActionKind::ResetHard,
Self::CreateTag(_) => CommitActionKind::CreateTag,
Self::CreateAnnotatedTag(_, _) => CommitActionKind::CreateAnnotatedTag,
}
}
pub fn label(&self) -> &'static str {
self.kind().label()
}
pub fn execute(&self, workdir: &Path, oid: &str) -> Result<()> {
match self {
Self::CheckoutDetached => {
let repo = crate::features::repo::open_repo(workdir)?;
crate::features::repo::checkout_commit_detached(&repo, oid)
}
Self::CreateBranchHere(name) => {
let repo = crate::features::repo::open_repo(workdir)?;
crate::features::branches::create_branch_at_commit(&repo, name, oid)?;
Ok(())
}
Self::CherryPick => crate::features::repo::cherry_pick_commit(workdir, oid),
Self::Revert => crate::features::repo::revert_commit(workdir, oid),
Self::RebaseOnto => crate::features::branches::rebase_onto(workdir, oid),
Self::ResetSoft => crate::features::repo::reset_to_commit(workdir, oid, "soft"),
Self::ResetMixed => crate::features::repo::reset_to_commit(workdir, oid, "mixed"),
Self::ResetHard => crate::features::repo::reset_to_commit(workdir, oid, "hard"),
Self::CreateTag(name) => {
let repo = crate::features::repo::open_repo(workdir)?;
crate::features::branches::create_tag(&repo, name, oid)
}
Self::CreateAnnotatedTag(name, message) => {
let repo = crate::features::repo::open_repo(workdir)?;
crate::features::branches::create_annotated_tag(&repo, name, message, oid)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_kinds_have_labels() {
use CommitActionKind::*;
let kinds = [
CheckoutDetached,
CreateBranchHere,
CherryPick,
Revert,
RebaseOnto,
ResetSoft,
ResetMixed,
ResetHard,
CreateTag,
CreateAnnotatedTag,
];
for k in kinds {
assert!(!k.label().is_empty(), "{k:?} has empty label");
}
}
#[test]
fn needs_input_variants() {
assert!(!CommitActionKind::CheckoutDetached.needs_input());
assert!(!CommitActionKind::CherryPick.needs_input());
assert!(CommitActionKind::CreateBranchHere.needs_input());
assert!(CommitActionKind::CreateTag.needs_input());
assert!(CommitActionKind::CreateAnnotatedTag.needs_input());
assert!(CommitActionKind::CreateAnnotatedTag.needs_second_input());
assert!(!CommitActionKind::CreateTag.needs_second_input());
}
#[test]
fn into_action_round_trips() {
let a = CommitActionKind::CreateBranchHere.into_action("my-branch".into(), String::new());
assert!(matches!(a, CommitAction::CreateBranchHere(ref n) if n == "my-branch"));
let b = CommitActionKind::CreateAnnotatedTag.into_action("v1.0".into(), "release".into());
assert!(
matches!(b, CommitAction::CreateAnnotatedTag(ref n, ref m) if n == "v1.0" && m == "release")
);
}
#[test]
fn as_simple_action_returns_none_for_input_kinds() {
assert!(CommitActionKind::CreateBranchHere
.as_simple_action()
.is_none());
assert!(CommitActionKind::CreateTag.as_simple_action().is_none());
}
#[test]
fn as_simple_action_returns_some_for_no_input_kinds() {
assert!(CommitActionKind::CherryPick.as_simple_action().is_some());
assert!(CommitActionKind::ResetHard.as_simple_action().is_some());
}
#[test]
fn menu_groups_cover_all_kinds() {
use std::collections::HashSet;
let mut seen: HashSet<CommitActionKind> = HashSet::new();
for group in COMMIT_MENU_GROUPS {
for &kind in *group {
seen.insert(kind);
}
}
assert_eq!(seen.len(), 10);
}
}