gitkraft_core/features/commits/
actions.rs1use std::path::Path;
15
16use anyhow::Result;
17
18macro_rules! define_commit_action_kinds {
25 ( $( $variant:ident { $label:literal, $prompt1:expr, $prompt2:expr } ),* $(,)? ) => {
26
27 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31 pub enum CommitActionKind {
32 $( $variant, )*
33 }
34
35 impl CommitActionKind {
36 pub fn label(self) -> &'static str {
38 match self {
39 $( Self::$variant => $label, )*
40 }
41 }
42
43 pub fn input_prompt(self) -> Option<&'static str> {
47 match self {
48 $( Self::$variant => $prompt1, )*
49 }
50 }
51
52 pub fn second_input_prompt(self) -> Option<&'static str> {
55 match self {
56 $( Self::$variant => $prompt2, )*
57 }
58 }
59
60 pub fn needs_input(self) -> bool {
63 self.input_prompt().is_some()
64 }
65
66 pub fn needs_second_input(self) -> bool {
68 self.second_input_prompt().is_some()
69 }
70
71 pub fn into_action(self, input1: String, input2: String) -> CommitAction {
75 match self {
76 Self::CheckoutDetached => CommitAction::CheckoutDetached,
77 Self::CreateBranchHere => CommitAction::CreateBranchHere(input1),
78 Self::CherryPick => CommitAction::CherryPick,
79 Self::Revert => CommitAction::Revert,
80 Self::RebaseOnto => CommitAction::RebaseOnto,
81 Self::ResetSoft => CommitAction::ResetSoft,
82 Self::ResetMixed => CommitAction::ResetMixed,
83 Self::ResetHard => CommitAction::ResetHard,
84 Self::CreateTag => CommitAction::CreateTag(input1),
85 Self::CreateAnnotatedTag => CommitAction::CreateAnnotatedTag(input1, input2),
86 }
87 }
88
89 pub fn as_simple_action(self) -> Option<CommitAction> {
92 if self.needs_input() {
93 None
94 } else {
95 Some(self.into_action(String::new(), String::new()))
96 }
97 }
98 }
99 };
100}
101
102define_commit_action_kinds! {
105 CheckoutDetached { "Checkout (detached HEAD)", None, None },
106 CreateBranchHere { "Create branch here\u{2026}", Some("Branch name:"), None },
107 CherryPick { "Cherry-pick onto current branch", None, None },
108 Revert { "Revert commit", None, None },
109 RebaseOnto { "Rebase current branch onto this", None, None },
110 ResetSoft { "Reset here \u{2014} soft (keep staged)", None, None },
111 ResetMixed { "Reset here \u{2014} mixed (keep files)", None, None },
112 ResetHard { "Reset here \u{2014} hard (discard all)", None, None },
113 CreateTag { "Create tag here", Some("Tag name:"), None },
114 CreateAnnotatedTag { "Create annotated tag here\u{2026}", Some("Tag name:"), Some("Tag message:") }
115}
116
117pub const COMMIT_MENU_GROUPS: &[&[CommitActionKind]] = &[
125 &[
127 CommitActionKind::CheckoutDetached,
128 CommitActionKind::CreateBranchHere,
129 ],
130 &[
132 CommitActionKind::CherryPick,
133 CommitActionKind::Revert,
134 CommitActionKind::RebaseOnto,
135 ],
136 &[
138 CommitActionKind::ResetSoft,
139 CommitActionKind::ResetMixed,
140 CommitActionKind::ResetHard,
141 ],
142 &[
144 CommitActionKind::CreateTag,
145 CommitActionKind::CreateAnnotatedTag,
146 ],
147];
148
149#[derive(Debug, Clone)]
153pub enum CommitAction {
154 CheckoutDetached,
155 CreateBranchHere(String),
156 CherryPick,
157 Revert,
158 RebaseOnto,
159 ResetSoft,
160 ResetMixed,
161 ResetHard,
162 CreateTag(String),
163 CreateAnnotatedTag(String, String), }
165
166impl CommitAction {
167 pub fn kind(&self) -> CommitActionKind {
169 match self {
170 Self::CheckoutDetached => CommitActionKind::CheckoutDetached,
171 Self::CreateBranchHere(_) => CommitActionKind::CreateBranchHere,
172 Self::CherryPick => CommitActionKind::CherryPick,
173 Self::Revert => CommitActionKind::Revert,
174 Self::RebaseOnto => CommitActionKind::RebaseOnto,
175 Self::ResetSoft => CommitActionKind::ResetSoft,
176 Self::ResetMixed => CommitActionKind::ResetMixed,
177 Self::ResetHard => CommitActionKind::ResetHard,
178 Self::CreateTag(_) => CommitActionKind::CreateTag,
179 Self::CreateAnnotatedTag(_, _) => CommitActionKind::CreateAnnotatedTag,
180 }
181 }
182
183 pub fn label(&self) -> &'static str {
185 self.kind().label()
186 }
187
188 pub fn execute(&self, workdir: &Path, oid: &str) -> Result<()> {
192 match self {
193 Self::CheckoutDetached => {
194 let repo = crate::features::repo::open_repo(workdir)?;
195 crate::features::repo::checkout_commit_detached(&repo, oid)
196 }
197 Self::CreateBranchHere(name) => {
198 let repo = crate::features::repo::open_repo(workdir)?;
199 crate::features::branches::create_branch_at_commit(&repo, name, oid)?;
200 Ok(())
201 }
202 Self::CherryPick => crate::features::repo::cherry_pick_commit(workdir, oid),
203 Self::Revert => crate::features::repo::revert_commit(workdir, oid),
204 Self::RebaseOnto => crate::features::branches::rebase_onto(workdir, oid),
205 Self::ResetSoft => crate::features::repo::reset_to_commit(workdir, oid, "soft"),
206 Self::ResetMixed => crate::features::repo::reset_to_commit(workdir, oid, "mixed"),
207 Self::ResetHard => crate::features::repo::reset_to_commit(workdir, oid, "hard"),
208 Self::CreateTag(name) => {
209 let repo = crate::features::repo::open_repo(workdir)?;
210 crate::features::branches::create_tag(&repo, name, oid)
211 }
212 Self::CreateAnnotatedTag(name, message) => {
213 let repo = crate::features::repo::open_repo(workdir)?;
214 crate::features::branches::create_annotated_tag(&repo, name, message, oid)
215 }
216 }
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn all_kinds_have_labels() {
226 use CommitActionKind::*;
227 let kinds = [
228 CheckoutDetached,
229 CreateBranchHere,
230 CherryPick,
231 Revert,
232 RebaseOnto,
233 ResetSoft,
234 ResetMixed,
235 ResetHard,
236 CreateTag,
237 CreateAnnotatedTag,
238 ];
239 for k in kinds {
240 assert!(!k.label().is_empty(), "{k:?} has empty label");
241 }
242 }
243
244 #[test]
245 fn needs_input_variants() {
246 assert!(!CommitActionKind::CheckoutDetached.needs_input());
247 assert!(!CommitActionKind::CherryPick.needs_input());
248 assert!(CommitActionKind::CreateBranchHere.needs_input());
249 assert!(CommitActionKind::CreateTag.needs_input());
250 assert!(CommitActionKind::CreateAnnotatedTag.needs_input());
251 assert!(CommitActionKind::CreateAnnotatedTag.needs_second_input());
252 assert!(!CommitActionKind::CreateTag.needs_second_input());
253 }
254
255 #[test]
256 fn into_action_round_trips() {
257 let a = CommitActionKind::CreateBranchHere.into_action("my-branch".into(), String::new());
258 assert!(matches!(a, CommitAction::CreateBranchHere(ref n) if n == "my-branch"));
259
260 let b = CommitActionKind::CreateAnnotatedTag.into_action("v1.0".into(), "release".into());
261 assert!(
262 matches!(b, CommitAction::CreateAnnotatedTag(ref n, ref m) if n == "v1.0" && m == "release")
263 );
264 }
265
266 #[test]
267 fn as_simple_action_returns_none_for_input_kinds() {
268 assert!(CommitActionKind::CreateBranchHere
269 .as_simple_action()
270 .is_none());
271 assert!(CommitActionKind::CreateTag.as_simple_action().is_none());
272 }
273
274 #[test]
275 fn as_simple_action_returns_some_for_no_input_kinds() {
276 assert!(CommitActionKind::CherryPick.as_simple_action().is_some());
277 assert!(CommitActionKind::ResetHard.as_simple_action().is_some());
278 }
279
280 #[test]
281 fn menu_groups_cover_all_kinds() {
282 use std::collections::HashSet;
283 let mut seen: HashSet<CommitActionKind> = HashSet::new();
284 for group in COMMIT_MENU_GROUPS {
285 for &kind in *group {
286 seen.insert(kind);
287 }
288 }
289 assert_eq!(seen.len(), 10);
291 }
292}