Skip to main content

ghl/
cmds.rs

1use std::ffi::OsString;
2
3use ytil_sys::cli::Args;
4use ytil_sys::pico_args::Arguments;
5
6pub mod branch;
7pub mod issue;
8pub mod list;
9pub mod pr;
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub enum Help {
13    Root,
14    List,
15    Issue,
16    Pr,
17    Branch,
18}
19
20impl Help {
21    pub(crate) fn from_args(args: &[OsString]) -> Self {
22        if has_command(args, "issue") {
23            return Self::Issue;
24        }
25        if has_command(args, "pr") {
26            return Self::Pr;
27        }
28        if has_command(args, "branch") {
29            return Self::Branch;
30        }
31        if args.is_empty() || args.iter().all(|arg| arg == "--help") {
32            Self::Root
33        } else {
34            Self::List
35        }
36    }
37
38    pub const fn text(self) -> &'static str {
39        match self {
40            Self::Root => include_str!("../help.txt"),
41            Self::List => include_str!("../help/list/help.txt"),
42            Self::Issue => include_str!("../help/issue/help.txt"),
43            Self::Pr => include_str!("../help/pr/help.txt"),
44            Self::Branch => include_str!("../help/branch/help.txt"),
45        }
46    }
47}
48
49pub enum Cmd {
50    Help(Help),
51    List(Arguments),
52    Issue,
53    Pr,
54    Branch,
55}
56
57impl Cmd {
58    pub fn from_env() -> rootcause::Result<Self> {
59        let args = Arguments::from_env();
60        let help = Help::from_args(&args.clone().finish());
61        Self::try_from(args).inspect_err(|_| eprintln!("{}", help.text()))
62    }
63}
64
65impl TryFrom<Arguments> for Cmd {
66    type Error = rootcause::Report;
67
68    fn try_from(args: Arguments) -> Result<Self, Self::Error> {
69        let raw_args = args.clone().finish();
70        if args.has_help() {
71            return Ok(Self::Help(Help::from_args(&raw_args)));
72        }
73
74        if has_command(&raw_args, "issue") {
75            return Ok(Self::Issue);
76        }
77
78        if has_command(&raw_args, "pr") {
79            return Ok(Self::Pr);
80        }
81
82        if has_command(&raw_args, "branch") {
83            return Ok(Self::Branch);
84        }
85
86        Ok(Self::List(args))
87    }
88}
89
90fn has_command(args: &[OsString], command: &str) -> bool {
91    let mut skip_next = false;
92    for argument in args {
93        if skip_next {
94            skip_next = false;
95            continue;
96        }
97
98        let argument = argument.to_string_lossy();
99        if matches!(argument.as_ref(), "--search" | "--merge-state") {
100            skip_next = true;
101            continue;
102        }
103        if argument.starts_with("--search=") || argument.starts_with("--merge-state=") {
104            continue;
105        }
106        if argument == command {
107            return true;
108        }
109    }
110
111    false
112}
113
114#[cfg(test)]
115mod tests {
116    use std::ffi::OsString;
117
118    use test_that::prelude::*;
119    use ytil_sys::pico_args::Arguments;
120
121    use super::*;
122
123    #[test]
124    fn test_parse_when_help_is_requested_returns_help() {
125        assert!(matches!(parse(&["--help"]), Ok(Cmd::Help(Help::Root))));
126    }
127
128    #[rstest::rstest]
129    #[case::issue("issue", "issue")]
130    #[case::pull_request("pr", "pr")]
131    #[case::branch("branch", "branch")]
132    fn test_parse_when_named_command_is_supplied_returns_command(#[case] command: &str, #[case] expected: &str) {
133        let parsed = parse(&[command]);
134        assert!(matches!(
135            (expected, parsed),
136            ("issue", Ok(Cmd::Issue)) | ("pr", Ok(Cmd::Pr)) | ("branch", Ok(Cmd::Branch))
137        ));
138    }
139
140    #[test]
141    fn test_parse_when_no_named_command_is_supplied_returns_list_command() {
142        assert!(matches!(parse(&["--search", "lint"]), Ok(Cmd::List(_))));
143    }
144
145    #[test]
146    fn test_parse_when_command_name_is_search_value_returns_list_command() {
147        assert!(matches!(parse(&["--search", "issue"]), Ok(Cmd::List(_))));
148    }
149
150    #[rstest::rstest]
151    #[case::root(&["--help"], Help::Root)]
152    #[case::list(&["--search", "lint", "--help"], Help::List)]
153    #[case::issue(&["issue", "--help"], Help::Issue)]
154    #[case::pull_request(&["pr", "--help"], Help::Pr)]
155    #[case::branch(&["branch", "--help"], Help::Branch)]
156    fn test_help_when_command_varies_selects_the_matching_command(#[case] raw: &[&str], #[case] expected: Help) {
157        let args = raw.iter().map(OsString::from).collect::<Vec<_>>();
158        assert_that!(Help::from_args(&args), eq(expected));
159    }
160
161    fn parse(args: &[&str]) -> rootcause::Result<Cmd> {
162        Cmd::try_from(Arguments::from_vec(args.iter().map(OsString::from).collect()))
163    }
164}