Grcov report - cli_entry.rs

1

    use std::collections::HashSet;

2

    use std::path::Path;

3

    use std::str::FromStr;

4

    use std::sync::{

5

      Arc,

6

      Mutex,

7

    };

8

9

    use crate::secrets::Secrets;

10

    use anyhow::Ok;

11

    use clap::{

12

      crate_authors,

13

      CommandFactory,

14

      Parser,

15

      Subcommand,

16

    };

17

    use clap_complete::Shell;

18

    use console::style;

19

    use mk_lib::schema::{

20

      ExecutionStack,

21

      Task,

22

      TaskContext,

23

      TaskRoot,

24

    };

25

    use mk_lib::version::get_version_digits;

26

    use once_cell::sync::Lazy;

27

    use prettytable::format::consts;

28

    use prettytable::{

29

      row,

30

      Table,

31

    };

32

33

    static VERSION: Lazy<String> = Lazy::new(get_version_digits);

34

35

    /// The CLI arguments

36

    #[derive(Debug, Parser)]

37

    #[command(

38

      version = VERSION.as_str(),

39

      about,

40

      long_about = "mk is a powerful and flexible task runner designed to help you automate and manage your tasks efficiently. It supports running commands both locally and inside containers, making it versatile for various environments and use cases. Running tasks in containers is a first-class citizen, ensuring seamless integration with containerized workflows.",

41

      arg_required_else_help = true,

42

      author = crate_authors!("\n"),

43

      propagate_version = true,

44

    )]

45

    struct Args {

46

      #[arg(

47

        short,

48

        long,

49

        help = "Config file to source",

50

        env = "MK_CONFIG",

51

        default_value = "tasks.yaml"

52

      )]

53

      config: String,

54

55

      // Waiting for the dynamic completion to be implemented

56

      // Tracking can be found here:

57

      // - https://github.com/clap-rs/clap/issues/3166

58

      // - https://github.com/clap-rs/clap/issues/1232

59

      //

60

      // Usually, this would call `mk list --plain` or `mk list --json` to capture

61

      // the available tasks and use them as completions.

62

      #[arg(help = "The task name to run", value_hint = clap::ValueHint::Other)]

63

      task_name: Option<String>,

64

65

      #[command(subcommand)]

66

      command: Option<Command>,

67

    }

68

69

    /// The available subcommands

70

    #[derive(Debug, Subcommand)]

71

    enum Command {

72

      #[command(visible_aliases = ["r"], arg_required_else_help = true, about = "Run specific tasks")]

73

      Run {

74

        #[arg(required = true, help = "The task name to run", value_hint = clap::ValueHint::Other)]

75

        task_name: String,

76

      },

77

      #[command(visible_aliases = ["ls"], about = "List all available tasks")]

78

      List {

79

        #[arg(short, long, help = "Show list that does not include headers")]

80

        plain: bool,

81

82

        #[arg(short, long, help = "Show list in JSON format", conflicts_with = "plain")]

83

        json: bool,

84

      },

85

      #[command(visible_aliases = ["comp"], about = "Generate shell completions")]

86

      Completion {

87

        #[arg(required = true, help = "The shell to generate completions for")]

88

        shell: String,

89

      },

90

      #[command(visible_aliases = ["s"], arg_required_else_help = true, about = "Access stored secrets")]

91

      Secrets(Secrets),

92

    }

93

94

    /// The CLI entry

95

    pub(super) struct CliEntry {

96

      args: Args,

97

      task_root: Arc<TaskRoot>,

98

      execution_stack: ExecutionStack,

99

    }

100

101

    impl CliEntry {

102

      /// Create a new CLI entry

103
294

      pub fn new() -> anyhow::Result<Self> {

104
294

        let args = Args::parse();

105
294

        log::trace!("Config: {}", args.config);

106

107
294

        assert!(!args.config.is_empty());

108

109
294

        let config = Path::new(&args.config);

110
294

        if !config.exists() {

111
147

          anyhow::bail!("Config file does not exist");

112
147

        }

113

114
147

        let task_root = Arc::new(TaskRoot::from_file(&args.config)?);

115
147

        let execution_stack = Arc::new(Mutex::new(HashSet::new()));

116
147

        Ok(Self {

117
147

          args,

118
147

          task_root,

119
147

          execution_stack,

120
147

        })

121
294

      }

122

123

      /// Run the CLI entry

124
147

      pub fn run(&self) -> anyhow::Result<()> {

125
84

        match &self.args.command {

126
63

          Some(Command::Run { task_name }) => {

127
63

            self.run_task(task_name)?;

128

          },

129
21

          Some(Command::List { plain, json }) => {

130
21

            self.print_available_tasks(*plain, *json)?;

131

          },

132

          Some(Command::Completion { shell }) => {

133

            self.write_completions(shell)?;

134

          },

135

          Some(Command::Secrets(secrets)) => {

136

            secrets.execute()?;

137

          },

138

          None => {

139
63

            if let Some(task_name) = &self.args.task_name {

140
63

              self.run_task(task_name)?;

141

            } else {

142

              anyhow::bail!("No subcommand or task name provided. Use `--help` flag for more information.");

143

            }

144

          },

145

        }

146

147
84

        Ok(())

148
147

      }

149

150

      /// Run the specified tasks

151
126

      fn run_task(&self, task_name: &str) -> anyhow::Result<()> {

152
126

        assert!(!task_name.is_empty());

153

154
126

        let task = self

155
126

          .task_root

156
126

          .tasks

157
126

          .get(task_name)

158
126

          .ok_or_else(|| anyhow::anyhow!("Task not found"))?;

159

160
63

        log::trace!("Task: {:?}", task);

161

162

        // Scope the lock to the task execution

163

        {

164
63

          let mut stack = self

165
63

            .execution_stack

166
63

            .lock()

167
63

            .map_err(|e| anyhow::anyhow!("Failed to lock execution stack - {}", e))?;

168

169
63

          stack.insert(task_name.to_string());

170
63

        }

171
63

172
63

        let mut context = TaskContext::new(self.task_root.clone(), self.execution_stack.clone());

173
63

        task.run(&mut context)?;

174

175

        // Don't carry over the execution stack to the next task

176

        {

177
63

          let mut stack = self

178
63

            .execution_stack

179
63

            .lock()

180
63

            .map_err(|e| anyhow::anyhow!("Failed to lock execution stack - {}", e))?;

181

182
63

          stack.clear();

183
63

        }

184
63

185
63

        Ok(())

186
126

      }

187

188

      /// Print all available tasks

189
21

      fn print_available_tasks(&self, plain: bool, json: bool) -> anyhow::Result<()> {

190
21

        if json {

191

          let tasks: Vec<_> = self

192

            .task_root

193

            .tasks

194

            .iter()

195

            .map(|(name, task)| {

196

              if let Task::Task(task) = task {

197

                serde_json::json!({

198

                  "name": name,

199

                  "description": task.description,

200

                })

201

              } else {

202

                serde_json::json!({

203

                  "name": name,

204

                  "description": "No description provided",

205

                })

206

              }

207

            })

208

            .collect();

209

          println!("{}", serde_json::to_string_pretty(&tasks)?);

210

        } else {

211
21

          let mut table = Table::new();

212
21

          if !plain {

213
21

            table.set_titles(row![Fbb->"Task", Fbb->"Description"]);

214
21

215
21

            let msg = style("Available tasks:").bold().cyan();

216
21

            println!();

217
21

            println!("{msg}");

218
21

            println!();

219
21

          }

220
21

          table.set_format(*consts::FORMAT_CLEAN);

221

222
21

          for (task_name, task) in &self.task_root.tasks {

223
21

            if let Task::Task(task) = task {

224
21

              table.add_row(row![b->&task_name, Fg->&task.description]);

225
21

            } else {

226

              table.add_row(row![b->&task_name, Fg->"No description provided"]);

227

            }

228

          }

229

230
21

          table.printstd();

231

        }

232

233
21

        Ok(())

234
21

      }

235

236

      fn write_completions(&self, shell: &str) -> anyhow::Result<()> {

237

        let shell = Shell::from_str(shell).map_err(|e| anyhow::anyhow!("Invalid shell - {}", e))?;

238

239

        let mut app = Args::command();

240

        clap_complete::generate(shell, &mut app, "mk", &mut std::io::stdout().lock());

241

242

        Ok(())

243

      }

244

    }