Skip to main content

gkit_core/
stmb.rs

1//! `stmb` — "switch to main branch": finish a feature branch by returning to the
2//! base/integration branch, updating it, and deleting the (merged) feature branch.
3//!
4//! Port of the zsh `stmb`, with **proper, safe branch handling**: base is resolved
5//! (not hardcoded `dev`), and the feature branch is **safe-deleted** (`git branch
6//! -d`, which refuses an unmerged branch) instead of the original force `-D` — so
7//! you can't silently lose unpushed/unmerged work. Force is opt-in.
8
9/// The decided plan, computed purely from repo state.
10#[derive(Debug, PartialEq, Eq)]
11pub struct Plan {
12    pub base: String,
13    /// Feature branch to delete after switching; `None` when already on base.
14    pub delete_feature: Option<String>,
15}
16
17/// Decide what `stmb` should do. `current` is the current branch (`None` = detached).
18/// Refuses states that aren't safe to auto-handle.
19pub fn plan(current: Option<&str>, base: &str, dirty: bool) -> Result<Plan, String> {
20    if dirty {
21        return Err("working tree has uncommitted changes — commit or stash before stmb".into());
22    }
23    match current {
24        None => Err("detached HEAD — checkout a branch before stmb".into()),
25        Some(cur) if cur == base => Ok(Plan {
26            base: base.to_string(),
27            delete_feature: None,
28        }),
29        Some(cur) => Ok(Plan {
30            base: base.to_string(),
31            delete_feature: Some(cur.to_string()),
32        }),
33    }
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn refuses_dirty_tree() {
42        assert!(plan(Some("feat"), "dev", true)
43            .unwrap_err()
44            .contains("uncommitted"));
45    }
46
47    #[test]
48    fn refuses_detached() {
49        assert!(plan(None, "dev", false).unwrap_err().contains("detached"));
50    }
51
52    #[test]
53    fn on_base_deletes_nothing() {
54        assert_eq!(
55            plan(Some("dev"), "dev", false).unwrap(),
56            Plan {
57                base: "dev".into(),
58                delete_feature: None
59            }
60        );
61    }
62
63    #[test]
64    fn on_feature_deletes_it() {
65        assert_eq!(
66            plan(Some("feat-x"), "dev", false).unwrap(),
67            Plan {
68                base: "dev".into(),
69                delete_feature: Some("feat-x".into())
70            }
71        );
72    }
73}