1use crate::app;
2use crate::core::{self, error::MonorailError};
3
4use clap::builder::ArgPredicate;
5use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command};
6use once_cell::sync::OnceCell;
7use serde::Serialize;
8use std::io::Write;
9use std::result::Result;
10use std::str::FromStr;
11use std::{env, path};
12use tokio::runtime::Runtime;
13
14pub const CMD_MONORAIL: &str = "monorail";
15pub const CMD_CONFIG: &str = "config";
16pub const CMD_CHECKPOINT: &str = "checkpoint";
17pub const CMD_DELETE: &str = "delete";
18pub const CMD_UPDATE: &str = "update";
19pub const CMD_SHOW: &str = "show";
20pub const CMD_TARGET: &str = "target";
21pub const CMD_RUN: &str = "run";
22pub const CMD_ANALYZE: &str = "analyze";
23pub const CMD_RESULT: &str = "result";
24pub const CMD_LOG: &str = "log";
25pub const CMD_TAIL: &str = "tail";
26pub const CMD_OUT: &str = "out";
27pub const CMD_RENDER: &str = "render";
28pub const CMD_GENERATE: &str = "generate";
29
30pub const ARG_GIT_PATH: &str = "git-path";
31pub const ARG_BEGIN: &str = "begin";
32pub const ARG_END: &str = "end";
33pub const ARG_CONFIG_FILE: &str = "config-file";
34pub const ARG_OUTPUT_FORMAT: &str = "output-format";
35pub const ARG_PENDING: &str = "pending";
36pub const ARG_COMMANDS: &str = "commands";
37pub const ARG_ARGS: &str = "args";
38pub const ARG_ARGMAPS: &str = "argmaps";
39pub const ARG_TARGETS: &str = "targets";
40pub const ARG_CHANGES: &str = "changes";
41pub const ARG_CHANGE_TARGETS: &str = "change-targets";
42pub const ARG_TARGET_GROUPS: &str = "target-groups";
43pub const ARG_SEQUENCES: &str = "sequences";
44pub const ARG_ALL: &str = "all";
45pub const ARG_VERBOSE: &str = "verbose";
46pub const ARG_STDERR: &str = "stderr";
47pub const ARG_STDOUT: &str = "stdout";
48pub const ARG_ID: &str = "id";
49pub const ARG_DEPS: &str = "deps";
50pub const ARG_FAIL_ON_UNDEFINED: &str = "fail-on-undefined";
51pub const ARG_NO_BASE_ARGMAPS: &str = "no-base-argmaps";
52pub const ARG_FORMAT: &str = "format";
53pub const ARG_TYPE: &str = "type";
54pub const ARG_OUTPUT_FILE: &str = "output-file";
55
56pub const VAL_JSON: &str = "json";
57
58static DEFAULT_CONFIG_PATH: OnceCell<String> = OnceCell::new();
60
61fn default_config_path() -> &'static str {
62 DEFAULT_CONFIG_PATH.get_or_init(|| {
63 env::current_dir()
64 .unwrap()
65 .join("Monorail.json")
66 .display()
67 .to_string()
68 })
69}
70
71pub fn build() -> clap::Command {
72 let arg_git_path = Arg::new(ARG_GIT_PATH)
73 .long(ARG_GIT_PATH)
74 .help("Absolute path to a `git` binary to use for certain operations. If unspecified, default value uses PATH to resolve.")
75 .num_args(1)
76 .default_value("git");
77 let arg_begin = Arg::new(ARG_BEGIN)
78 .short('b')
79 .long(ARG_BEGIN)
80 .help("Start of the interval to consider for changes; if not provided, the checkpoint (or start of history, if no checkpoint exists) is used")
81 .num_args(1)
82 .required(false);
83 let arg_end = Arg::new(ARG_END)
84 .short('e')
85 .long(ARG_END)
86 .help(
87 "End of the interval to consider for changes; if not provided, end of history is used",
88 )
89 .num_args(1)
90 .required(false);
91 let arg_log_id = Arg::new(ARG_ID)
92 .long(ARG_ID)
93 .short('i')
94 .required(false)
95 .value_parser(clap::value_parser!(usize))
96 .num_args(1)
97 .help("Result id to query; if not provided, the most recent will be used");
98 let arg_log_command = Arg::new(ARG_COMMANDS)
99 .short('c')
100 .long(ARG_COMMANDS)
101 .required(false)
102 .num_args(1..)
103 .value_delimiter(' ')
104 .action(ArgAction::Append)
105 .help("A list commands for which to include logs");
106 let arg_log_target = Arg::new(ARG_TARGETS)
107 .short('t')
108 .long(ARG_TARGETS)
109 .num_args(1..)
110 .value_delimiter(' ')
111 .required(false)
112 .action(ArgAction::Append)
113 .help("A list of targets for which to include logs");
114
115 let arg_log_stderr = Arg::new(ARG_STDERR)
116 .long(ARG_STDERR)
117 .help("Include stderr logs")
118 .action(ArgAction::SetTrue);
119
120 let arg_log_stdout = Arg::new(ARG_STDOUT)
121 .long(ARG_STDOUT)
122 .help("Include stdout logs")
123 .action(ArgAction::SetTrue);
124
125 Command::new(CMD_MONORAIL)
126 .version(env!("CARGO_PKG_VERSION"))
127 .author("Patrick Nordahl <plnordahl@gmail.com>")
128 .about("A tool for effective polyglot, multi-project monorepo development.")
129 .arg_required_else_help(true)
130 .arg(
131 Arg::new(ARG_CONFIG_FILE)
132 .short('f')
133 .long(ARG_CONFIG_FILE)
134 .help("Absolute path to a configuration file")
135 .num_args(1)
136 .default_value(default_config_path()),
137 )
138 .arg(
139 Arg::new(ARG_OUTPUT_FORMAT)
140 .short('o')
141 .long(ARG_OUTPUT_FORMAT)
142 .help("Format to use for program output")
143 .value_parser([VAL_JSON])
144 .default_value(VAL_JSON)
145 .num_args(1),
146 )
147 .arg(
148 Arg::new(ARG_VERBOSE)
149 .short('v')
150 .long(ARG_VERBOSE)
151 .help("Emit additional workflow, result, and error logging. Specifying this flag multiple times (up to 3) increases the verbosity.")
152 .action(ArgAction::Count),
153 )
154 .subcommand(
155 Command::new(CMD_CONFIG)
156 .arg_required_else_help(true)
157 .about("Parse and display configuration")
158 .subcommand(
159 Command::new(CMD_SHOW)
160 .about("Show configuration, including runtime default values"))
161 .subcommand(
162 Command::new(CMD_GENERATE)
163 .about("Generate a configuration file from, and linked to, a source file")
164 .after_help("This command accepts a valid JSON configuration file over stdin that includes a top-level 'source' object, validating and synchronizing the source file and generated files.")))
165 .subcommand(Command::new(CMD_CHECKPOINT)
166 .about("Show, update, or delete the tracking checkpoint")
167 .subcommand(
168 Command::new(CMD_UPDATE)
169 .about("Update the tracking checkpoint")
170 .after_help(r#"This command updates the tracking checkpoint with data appropriate for the configured vcs."#)
171 .arg(arg_git_path.clone())
172 .arg(
173 Arg::new(ARG_PENDING)
174 .short('p')
175 .long(ARG_PENDING)
176 .help("Add pending changes to the checkpoint. E.g. for git, this means the paths and content checksums of uncommitted and unstaged changes since the last commit.")
177 .action(ArgAction::SetTrue),
178 )
179 .arg(
180 Arg::new(ARG_ID)
181 .long(ARG_ID)
182 .short('i')
183 .required(false)
184 .num_args(1)
185 .help("ID to use for the updated checkpoint. If not provided, this will default to the beginning of history for the current change provider."),
186 )
187 )
188 .subcommand(
189 Command::new(CMD_DELETE)
190 .about("Remove the tracking checkpoint")
191 .after_help(r#"This command removes the tracking checkpoint."#)
192 )
193 .subcommand(
194 Command::new(CMD_SHOW)
195 .about("Show the tracking checkpoint")
196 .after_help(r#"This command displays the tracking checkpoint."#)
197 )
198 )
199 .subcommand(Command::new(CMD_TARGET)
200 .about("Display targets and target groups")
201 .arg_required_else_help(true)
202 .subcommand(
203 Command::new(CMD_RENDER)
204 .about("Output a visualization of the target graph.")
205 .after_help("This command parses the target graph and emits it in a visual format for use in other tools, such as .dot.")
206 .arg(
207 Arg::new(ARG_TYPE)
208 .short('t')
209 .long(ARG_TYPE)
210 .help("File type to render")
211 .required(false)
212 .num_args(1)
213 )
214 .arg(
215 Arg::new(ARG_OUTPUT_FILE)
216 .short('f')
217 .long(ARG_OUTPUT_FILE)
218 .help("Filesystem location to render to")
219 .required(false)
220 .num_args(1)
221 )
222 )
223 .subcommand(
224 Command::new(CMD_SHOW)
225 .about("List targets and their properties.")
226 .after_help(r#"This command reads targets from configuration data, and displays their properties."#)
227 .arg(
228 Arg::new(ARG_TARGET_GROUPS)
229 .short('g')
230 .long(ARG_TARGET_GROUPS)
231 .help("Display a representation of the 'depends on' relationship of targets. The array represents a topological ordering of the graph, with each element of the array being a set of targets that do not depend on each other at that position of the ordering.")
232 .action(ArgAction::SetTrue),
233 )
234 .arg(
235 Arg::new(ARG_COMMANDS)
236 .short('c')
237 .long(ARG_COMMANDS)
238 .help("Display information for the commands that are defined for this target.")
239 .action(ArgAction::SetTrue),
240 )
241 .arg(
242 Arg::new(ARG_ARGMAPS)
243 .short('m')
244 .long(ARG_ARGMAPS)
245 .help("Display information for the argmaps that are defined for this target.")
246 .action(ArgAction::SetTrue),
247 )
248 ))
249
250 .subcommand(Command::new(CMD_RUN)
251 .about("Run target commands.")
252 .after_help(r#"
253When --argmaps, and/or --args are provided, keys that appear multiple times will have their respective arrays concatenated in the following order:
254
255 1. All mappings in the `base` argmap, unless disabled with --no-base-argmaps
256 2. All mappings in the files specified by --argmaps, in the order provided
257 3. Each element of the --args array, in the order provided
258
259Refer to --help for more information on these options.
260"#)
261 .arg_required_else_help(true)
262 .arg(
263 Arg::new(ARG_COMMANDS)
264 .short('c')
265 .long(ARG_COMMANDS)
266 .required(false)
267 .num_args(1..)
268 .value_delimiter(' ')
269 .action(ArgAction::Append)
270 .help("A list of commands that will be executed, in the order provided. If one or more sequences are provided, they will be expanded and executed first.")
271 )
272 .arg(
273 Arg::new(ARG_SEQUENCES)
274 .short('s')
275 .long(ARG_SEQUENCES)
276 .required(false)
277 .num_args(1..)
278 .value_delimiter(' ')
279 .action(ArgAction::Append)
280 .help("A list of command sequences that will be expanded and executed in the order provided. If one or more --commands are provided, they will be executed after all sequences.")
281 )
282 .arg(
283 Arg::new(ARG_TARGETS)
284 .short('t')
285 .long(ARG_TARGETS)
286 .num_args(1..)
287 .value_delimiter(' ')
288 .required(false)
289 .action(ArgAction::Append)
290 .help("A list of targets for which commands will be executed. If not provided, target groups will be inferred from the target graph.")
291 )
292 .arg(
293 Arg::new(ARG_ARGS)
294 .short('a')
295 .long(ARG_ARGS)
296 .num_args(1..)
297 .required(false)
298 .action(ArgAction::Append)
299 .help("A list of runtime arguments to be provided when executing one command for a single target.")
300 .long_help("This is a shorthand form of the more expressive '--argmaps', designed for testing and single command + single target use. Providing this flag without specifying exactly one command and one target will result in an error.")
301 )
302 .arg(
303 Arg::new(ARG_ARGMAPS)
304 .long(ARG_ARGMAPS)
305 .short('m')
306 .num_args(1..)
307 .required(false)
308 .action(ArgAction::Append)
309 .help("A list of argmap names to load when executing commands.")
310 .long_help(r#"Each name provided is resolved to a respective {{name}}.json file in the target's argmap directory. Each object is merged from left to right, and keys that appear in multiple mappings have their arrays of arguments concatenated according to the order of the lists provided.
311
312An argmap has the following form:
313
314{
315 "<command>": [
316 "arg1",
317 "arg2",
318 ...
319 "argN"
320 ]
321}
322
323See `monorail run -h` for information on how this interacts with other arg-related options.
324
325"#),
326 )
327 .arg(
328 Arg::new(ARG_DEPS)
329 .long(ARG_DEPS)
330 .help("Include target dependencies")
331 .action(ArgAction::SetTrue),
332 )
333 .arg(
334 Arg::new(ARG_FAIL_ON_UNDEFINED)
335 .long(ARG_FAIL_ON_UNDEFINED)
336 .long_help("Fail commands that are undefined by targets.")
337 .action(ArgAction::SetTrue),
338 )
339 .arg(
340 Arg::new(ARG_NO_BASE_ARGMAPS)
341 .long(ARG_NO_BASE_ARGMAPS)
342 .long_help("Disable loading base argmaps for run targets. By default, all base argmaps are loaded.")
343 .action(ArgAction::SetTrue),
344 )
345 .arg(arg_git_path.clone())
346 .arg(arg_begin.clone())
347 .arg(arg_end.clone())
348 .group(
349 ArgGroup::new("commands_and_or_sequences")
350 .args([ARG_COMMANDS, ARG_SEQUENCES])
351 .required(true)
352 .multiple(true),
353 )
354 )
355 .subcommand(Command::new(CMD_RESULT)
356 .about("Show historical results from runs")
357 .arg_required_else_help(true)
358 .subcommand(Command::new(CMD_SHOW)
359 .about("Show results from `run` invocations")))
360
361 .subcommand(
363 Command::new(CMD_LOG)
364 .about("Show historical or tail real-time logs")
365 .arg_required_else_help(true)
366 .subcommand(
367 Command::new(CMD_SHOW).about("Display run logs")
368 .after_help(r#"This command shows logs for current or historical run invocations."#)
369 .arg(arg_log_id.clone())
370 .arg(arg_log_command.clone())
371 .arg(arg_log_target.clone())
372 .arg(arg_log_stderr.clone())
373 .arg(arg_log_stdout.clone()))
374 .subcommand(
375 Command::new(CMD_TAIL).about("Receive a stream of logs from executed commands")
376 .arg(arg_log_id)
377 .arg(arg_log_command)
378 .arg(arg_log_target)
379 .arg(arg_log_stderr)
380 .arg(arg_log_stdout))
381 )
382 .subcommand(
383 Command::new(CMD_ANALYZE).about("Analyze repository changes and targets")
384 .after_help(r#"This command performs an analysis of historical changes from the checkpoint to end of history in a change provider, as well as pending changes on the filesystem. By default, this command only outputs a list of affected targets."#)
385 .arg(arg_git_path.clone())
386 .arg(arg_begin.clone())
387 .arg(arg_end.clone())
388 .arg(
389 Arg::new(ARG_CHANGES)
390 .long(ARG_CHANGES)
391 .help("Displays detailed information about changes. If no checkpoint is available to guide change detection, this flag is ignored.")
392 .action(ArgAction::SetTrue)
393 .default_value_if(ARG_CHANGE_TARGETS, ArgPredicate::IsPresent, Some("true"))
394 .default_value_if(ARG_ALL, ArgPredicate::IsPresent, Some("true")),
395 )
396 .arg(
397 Arg::new(ARG_CHANGE_TARGETS)
398 .long(ARG_CHANGE_TARGETS)
399 .help("For each change, display the targets affected. If no checkpoint is available to guide change detection, this flag is ignored.")
400 .action(ArgAction::SetTrue)
401 .default_value_if(ARG_ALL, ArgPredicate::IsPresent, Some("true")),
402 )
403 .arg(
404 Arg::new(ARG_TARGET_GROUPS)
405 .long(ARG_TARGET_GROUPS)
406 .help("Display targets grouped according to the dependency graph. If no checkpoint is available to guide change detection, all targets are returned.")
407 .action(ArgAction::SetTrue)
408 .default_value_if(ARG_ALL, ArgPredicate::IsPresent, Some("true")),
409 )
410 .arg(
411 Arg::new(ARG_ALL)
412 .long(ARG_ALL)
413 .help("Display changes, change targets, and targets")
414 .action(ArgAction::SetTrue),
415 ))
416 .subcommand(
417 Command::new(CMD_OUT)
418 .about("Manipulate data in the monorail output directory")
419 .arg_required_else_help(true)
420 .subcommand(
421 Command::new(CMD_DELETE).about("Delete files from the output dir")
422 .after_help(r#""#)
423 .arg(
424 Arg::new(ARG_ALL)
425 .long(ARG_ALL)
426 .help("Delete all subdirectories")
427 .action(ArgAction::SetTrue),
428 )
429 ))
430}
431
432pub const HANDLE_OK: i32 = 0;
433pub const HANDLE_ERR: i32 = 1;
434pub const HANDLE_FATAL: i32 = 2;
435
436#[inline(always)]
437fn get_code(is_err: bool) -> i32 {
438 if is_err {
439 return HANDLE_ERR;
440 }
441 HANDLE_OK
442}
443
444#[derive(Debug)]
445pub struct OutputOptions<'a> {
446 pub format: &'a str,
447}
448
449#[tracing::instrument]
450pub fn handle<'a>(
451 matches: &ArgMatches,
452 output_options: &OutputOptions<'a>,
453) -> Result<i32, MonorailError> {
454 let verbosity = matches.get_one::<u8>(ARG_VERBOSE).unwrap_or(&0);
455 app::setup_tracing(output_options.format, *verbosity)?;
456
457 match matches.get_one::<String>(ARG_CONFIG_FILE) {
458 Some(config_file) => {
459 let config_file_path = path::Path::new(&config_file);
460 if !config_file_path.is_absolute() {
461 return Err(MonorailError::Generic(format!(
462 "Configuration file path '{}' is not absolute",
463 &config_file
464 )));
465 }
466 if let Some(config_matches) = matches.subcommand_matches(CMD_CONFIG) {
468 if config_matches.subcommand_matches(CMD_GENERATE).is_some() {
469 return handle_config_generate(config_file_path, output_options);
470 }
471 }
472 let work_path =
473 path::Path::new(config_file_path)
474 .parent()
475 .ok_or(MonorailError::Generic(format!(
476 "Config file {} has no parent directory",
477 config_file_path.display()
478 )))?;
479 let mut config = core::Config::new(config_file_path)?;
480 config.check(config_file_path, work_path)?;
481 if let Some(config_matches) = matches.subcommand_matches(CMD_CONFIG) {
482 if config_matches.subcommand_matches(CMD_SHOW).is_some() {
483 config.fill();
485 write_result(&Ok(config), output_options)?;
486 return Ok(HANDLE_OK);
487 }
488 }
489 if let Some(checkpoint_matches) = matches.subcommand_matches(CMD_CHECKPOINT) {
490 if let Some(update_matches) = checkpoint_matches.subcommand_matches(CMD_UPDATE) {
491 return handle_checkpoint_update(
492 &config,
493 update_matches,
494 output_options,
495 work_path,
496 );
497 }
498 if checkpoint_matches.subcommand_matches(CMD_DELETE).is_some() {
499 return handle_checkpoint_delete(&config, output_options, work_path);
500 }
501 if checkpoint_matches.subcommand_matches(CMD_SHOW).is_some() {
502 return handle_checkpoint_show(&config, output_options, work_path);
503 }
504 }
505 if let Some(result_matches) = matches.subcommand_matches(CMD_RESULT) {
506 if result_matches.subcommand_matches(CMD_SHOW).is_some() {
507 return handle_result_show(&config, result_matches, output_options, work_path);
508 }
509 }
510 if let Some(target_matches) = matches.subcommand_matches(CMD_TARGET) {
511 if let Some(show_matches) = target_matches.subcommand_matches(CMD_SHOW) {
512 return handle_target_show(
513 &mut config,
514 show_matches,
515 output_options,
516 work_path,
517 );
518 }
519 if let Some(render_matches) = target_matches.subcommand_matches(CMD_RENDER) {
520 return handle_target_render(
521 &config,
522 render_matches,
523 output_options,
524 work_path,
525 );
526 }
527 }
528 if let Some(analyze_matches) = matches.subcommand_matches(CMD_ANALYZE) {
529 return handle_analyze(&config, analyze_matches, output_options, work_path);
530 }
531 if let Some(run_matches) = matches.subcommand_matches(CMD_RUN) {
532 return handle_run(&config, run_matches, output_options, work_path);
533 }
534 if let Some(log_matches) = matches.subcommand_matches(CMD_LOG) {
535 if let Some(tail_matches) = log_matches.subcommand_matches(CMD_TAIL) {
536 return handle_log_tail(&config, tail_matches, output_options);
537 }
538 if let Some(show_matches) = log_matches.subcommand_matches(CMD_SHOW) {
539 return handle_log_show(&config, show_matches, output_options, work_path);
540 }
541 }
542 if let Some(out_matches) = matches.subcommand_matches(CMD_OUT) {
543 if let Some(delete_matches) = out_matches.subcommand_matches(CMD_DELETE) {
544 return handle_out_delete(&config, delete_matches, output_options);
545 }
546 }
547 Err(MonorailError::from("Command not recognized"))
548 }
549 None => Err(MonorailError::from("No configuration specified")),
550 }
551}
552
553fn handle_out_delete<'a>(
554 config: &'a core::Config,
555 matches: &'a ArgMatches,
556 output_options: &OutputOptions<'a>,
557) -> Result<i32, MonorailError> {
558 let rt = Runtime::new()?;
559 let _guard =
560 rt.block_on(core::server::LockServer::new(config.server.lock.clone()).acquire())?;
561 let i = app::out::OutDeleteInput::try_from(matches)?;
562 let res = app::out::out_delete(&config.out_dir, &i);
563 write_result(&res, output_options)?;
564 Ok(get_code(res.is_err()))
565}
566
567fn handle_analyze<'a>(
568 config: &'a core::Config,
569 matches: &'a ArgMatches,
570 output_options: &OutputOptions<'a>,
571 work_path: &'a path::Path,
572) -> Result<i32, MonorailError> {
573 let rt = Runtime::new()?;
574 let i = app::analyze::HandleAnalyzeInput::from(matches);
575 let res = rt.block_on(app::analyze::handle_analyze(config, &i, work_path));
576 write_result(&res, output_options)?;
577 Ok(get_code(res.is_err()))
578}
579
580fn handle_run<'a>(
581 config: &'a core::Config,
582 matches: &'a ArgMatches,
583 output_options: &OutputOptions<'a>,
584 work_path: &'a path::Path,
585) -> Result<i32, MonorailError> {
586 let rt = Runtime::new()?;
587 let _guard =
588 rt.block_on(core::server::LockServer::new(config.server.lock.clone()).acquire())?;
589 let i = app::run::HandleRunInput::try_from(matches).unwrap();
590 let invocation = env::args().skip(1).collect::<Vec<_>>().join(" ");
591 let o = rt.block_on(app::run::handle_run(config, &i, &invocation, work_path))?;
592 let mut code = HANDLE_OK;
593 if o.failed {
594 code = HANDLE_ERR;
595 }
596 write_result(&Ok(o), output_options)?;
597 Ok(code)
598}
599
600fn handle_config_generate(
601 output_file_path: &path::Path,
602 output_options: &OutputOptions<'_>,
603) -> Result<i32, MonorailError> {
604 let res = app::config::config_generate(app::config::ConfigGenerateInput { output_file_path });
605 write_result(&res, output_options)?;
606 Ok(get_code(res.is_err()))
607}
608
609fn handle_checkpoint_update<'a>(
610 config: &'a core::Config,
611 matches: &'a ArgMatches,
612 output_options: &OutputOptions<'a>,
613 work_path: &'a path::Path,
614) -> Result<i32, MonorailError> {
615 let rt = Runtime::new()?;
616 let _guard =
617 rt.block_on(core::server::LockServer::new(config.server.lock.clone()).acquire())?;
618 let i = app::checkpoint::CheckpointUpdateInput::try_from(matches)?;
619 let res = rt.block_on(app::checkpoint::handle_checkpoint_update(
620 config, &i, work_path,
621 ));
622 write_result(&res, output_options)?;
623 Ok(get_code(res.is_err()))
624}
625
626fn handle_checkpoint_show<'a>(
627 config: &'a core::Config,
628 output_options: &OutputOptions<'a>,
629 work_path: &'a path::Path,
630) -> Result<i32, MonorailError> {
631 let rt = Runtime::new()?;
632 let res = rt.block_on(app::checkpoint::handle_checkpoint_show(config, work_path));
633 write_result(&res, output_options)?;
634 Ok(get_code(res.is_err()))
635}
636
637fn handle_checkpoint_delete<'a>(
638 config: &'a core::Config,
639 output_options: &OutputOptions<'a>,
640 work_path: &'a path::Path,
641) -> Result<i32, MonorailError> {
642 let rt = Runtime::new()?;
643 let _guard =
644 rt.block_on(core::server::LockServer::new(config.server.lock.clone()).acquire())?;
645 let res = rt.block_on(app::checkpoint::handle_checkpoint_delete(config, work_path));
646 write_result(&res, output_options)?;
647 Ok(get_code(res.is_err()))
648}
649
650fn handle_log_tail<'a>(
651 config: &'a core::Config,
652 matches: &'a ArgMatches,
653 output_options: &OutputOptions<'a>,
654) -> Result<i32, MonorailError> {
655 let rt = Runtime::new()?;
656 let i = app::log::LogTailInput::try_from(matches)?;
657 match rt.block_on(app::log::log_tail(config, &i)) {
658 Ok(_) => Ok(HANDLE_OK),
659 Err(e) => {
660 write_result(&Err::<(), MonorailError>(e), output_options)?;
661 Ok(HANDLE_ERR)
662 }
663 }
664}
665
666fn handle_log_show<'a>(
667 config: &'a core::Config,
668 matches: &'a ArgMatches,
669 output_options: &OutputOptions<'a>,
670 work_path: &'a path::Path,
671) -> Result<i32, MonorailError> {
672 let i = app::log::LogShowInput::try_from(matches)?;
673 match app::log::log_show(config, &i, work_path) {
674 Ok(_) => Ok(HANDLE_OK),
675 Err(e) => {
676 write_result(&Err::<(), MonorailError>(e), output_options)?;
677 Ok(HANDLE_ERR)
678 }
679 }
680}
681fn handle_target_render<'a>(
682 config: &'a core::Config,
683 matches: &'a ArgMatches,
684 output_options: &OutputOptions<'a>,
685 work_path: &'a path::Path,
686) -> Result<i32, MonorailError> {
687 let output_file = match matches.get_one::<String>(ARG_OUTPUT_FILE) {
688 Some(v) => {
689 let vp = path::Path::new(v);
690 if vp.is_absolute() {
691 vp.to_path_buf()
692 } else {
693 work_path.join(vp)
694 }
695 }
696 None => work_path.join("target.dot"),
697 };
698 let render_type = match matches.get_one::<String>(ARG_TYPE) {
699 Some(v) => app::target::TargetRenderType::from_str(v)?,
700 None => app::target::TargetRenderType::Dot,
701 };
702 let res = app::target::target_render(
703 config,
704 app::target::TargetRenderInput {
705 render_type,
706 output_file,
707 },
708 work_path,
709 );
710 write_result(&res, output_options)?;
711 Ok(get_code(res.is_err()))
712}
713fn handle_target_show<'a>(
714 config: &'a mut core::Config,
715 matches: &'a ArgMatches,
716 output_options: &OutputOptions<'a>,
717 work_path: &'a path::Path,
718) -> Result<i32, MonorailError> {
719 let res = app::target::target_show(
720 config,
721 app::target::TargetShowInput {
722 show_target_groups: matches.get_flag(ARG_TARGET_GROUPS),
723 show_commands: matches.get_flag(ARG_COMMANDS),
724 show_argmaps: matches.get_flag(ARG_ARGMAPS),
725 },
726 work_path,
727 );
728 write_result(&res, output_options)?;
729 Ok(get_code(res.is_err()))
730}
731fn handle_result_show<'a>(
732 config: &'a core::Config,
733 matches: &'a ArgMatches,
734 output_options: &OutputOptions<'a>,
735 work_path: &'a path::Path,
736) -> Result<i32, MonorailError> {
737 let i = app::result::ResultShowInput::try_from(matches)?;
738 let res = app::result::result_show(config, work_path, &i);
739 write_result(&res, output_options)?;
740 Ok(get_code(res.is_err()))
741}
742
743#[derive(Serialize)]
744struct TimestampWrapper<T: Serialize> {
745 timestamp: String,
746 #[serde(flatten)]
747 data: T,
748}
749impl<T: Serialize> TimestampWrapper<T> {
750 fn new(data: T) -> Self {
751 Self {
752 timestamp: chrono::Utc::now().to_rfc3339(),
753 data,
754 }
755 }
756}
757
758pub fn write_result<T>(
759 value: &Result<T, MonorailError>,
760 opts: &OutputOptions<'_>,
761) -> Result<(), MonorailError>
762where
763 T: Serialize,
764{
765 match opts.format {
766 "json" => {
767 match value {
768 Ok(t) => {
769 let mut writer = std::io::stdout();
770 serde_json::to_writer(&mut writer, &TimestampWrapper::new(t))?;
771 writeln!(writer)?;
772 }
773 Err(e) => {
774 let mut writer = std::io::stderr();
775 serde_json::to_writer(&mut writer, &TimestampWrapper::new(e))?;
776 writeln!(writer)?;
777 }
778 }
779 Ok(())
780 }
781 _ => Err(MonorailError::Generic(format!(
782 "Unrecognized output format {}",
783 opts.format
784 ))),
785 }
786}
787
788impl TryFrom<&clap::ArgMatches> for app::result::ResultShowInput {
789 type Error = MonorailError;
790 fn try_from(_cmd: &clap::ArgMatches) -> Result<Self, Self::Error> {
791 Ok(Self {})
792 }
793}
794
795impl<'a> TryFrom<&'a clap::ArgMatches> for app::checkpoint::CheckpointUpdateInput<'a> {
796 type Error = MonorailError;
797 fn try_from(cmd: &'a clap::ArgMatches) -> Result<Self, Self::Error> {
798 Ok(Self {
799 id: cmd.get_one::<String>(ARG_ID).map(|x: &String| x.as_str()),
800 git_opts: core::git::GitOptions {
801 begin: None,
802 end: None,
803 git_path: cmd
804 .get_one::<String>(ARG_GIT_PATH)
805 .ok_or(MonorailError::MissingArg(ARG_GIT_PATH.into()))?,
806 },
807 pending: cmd.get_flag(ARG_PENDING),
808 })
809 }
810}
811impl<'a> TryFrom<&'a clap::ArgMatches> for app::run::HandleRunInput<'a> {
812 type Error = MonorailError;
813 fn try_from(cmd: &'a clap::ArgMatches) -> Result<Self, Self::Error> {
814 Ok(Self {
815 git_opts: core::git::GitOptions {
816 begin: cmd
817 .get_one::<String>(ARG_BEGIN)
818 .map(|x: &String| x.as_str()),
819 end: cmd.get_one::<String>(ARG_END).map(|x: &String| x.as_str()),
820 git_path: cmd
821 .get_one::<String>(ARG_GIT_PATH)
822 .ok_or(MonorailError::MissingArg(ARG_GIT_PATH.into()))?,
823 },
824 commands: cmd
825 .get_many::<String>(ARG_COMMANDS)
826 .ok_or(MonorailError::MissingArg(ARG_COMMANDS.into()))
827 .into_iter()
828 .flatten()
829 .collect(),
830 targets: cmd
831 .get_many::<String>(ARG_TARGETS)
832 .into_iter()
833 .flatten()
834 .collect(),
835 sequences: cmd
836 .get_many::<String>(ARG_SEQUENCES)
837 .into_iter()
838 .flatten()
839 .collect(),
840 args: cmd
841 .get_many::<String>(ARG_ARGS)
842 .into_iter()
843 .flatten()
844 .collect(),
845 argmaps: cmd
846 .get_many::<String>(ARG_ARGMAPS)
847 .into_iter()
848 .flatten()
849 .collect(),
850 include_deps: cmd.get_flag(ARG_DEPS),
851 fail_on_undefined: cmd.get_flag(ARG_FAIL_ON_UNDEFINED),
852 use_base_argmaps: !cmd.get_flag(ARG_NO_BASE_ARGMAPS),
853 })
854 }
855}
856
857impl<'a> From<&'a clap::ArgMatches> for app::analyze::HandleAnalyzeInput<'a> {
858 fn from(cmd: &'a clap::ArgMatches) -> Self {
859 Self {
860 git_opts: core::git::GitOptions {
861 begin: cmd
862 .get_one::<String>(ARG_BEGIN)
863 .map(|x: &String| x.as_str()),
864 end: cmd.get_one::<String>(ARG_END).map(|x: &String| x.as_str()),
865 git_path: cmd.get_one::<String>(ARG_GIT_PATH).unwrap(),
866 },
867 analyze_input: app::analyze::AnalyzeInput {
868 show_changes: cmd.get_flag(ARG_CHANGES),
869 show_change_targets: cmd.get_flag(ARG_CHANGE_TARGETS),
870 show_target_groups: cmd.get_flag(ARG_TARGET_GROUPS),
871 },
872 }
873 }
874}
875
876impl<'a> TryFrom<&'a clap::ArgMatches> for app::log::LogTailInput {
877 type Error = MonorailError;
878 fn try_from(cmd: &'a clap::ArgMatches) -> Result<Self, Self::Error> {
879 Ok(Self {
880 filter_input: core::server::LogFilterInput::try_from(cmd)?,
881 })
882 }
883}
884
885impl<'a> TryFrom<&'a clap::ArgMatches> for core::server::LogFilterInput {
886 type Error = MonorailError;
887 fn try_from(cmd: &'a clap::ArgMatches) -> Result<Self, Self::Error> {
888 Ok(Self {
889 include_stdout: cmd.get_flag(ARG_STDOUT),
890 include_stderr: cmd.get_flag(ARG_STDERR),
891 commands: cmd
892 .get_many::<String>(ARG_COMMANDS)
893 .into_iter()
894 .flatten()
895 .map(|x| x.to_owned())
896 .collect(),
897 targets: cmd
898 .get_many::<String>(ARG_TARGETS)
899 .into_iter()
900 .flatten()
901 .map(|x| x.to_owned())
902 .collect(),
903 })
904 }
905}
906
907impl<'a> TryFrom<&'a clap::ArgMatches> for app::log::LogShowInput<'a> {
908 type Error = MonorailError;
909 fn try_from(cmd: &'a clap::ArgMatches) -> Result<Self, Self::Error> {
910 Ok(Self {
911 filter_input: core::server::LogFilterInput::try_from(cmd)?,
912 id: cmd.get_one::<usize>(ARG_ID),
913 })
914 }
915}
916
917impl<'a> TryFrom<&'a clap::ArgMatches> for app::out::OutDeleteInput {
918 type Error = MonorailError;
919 fn try_from(cmd: &'a clap::ArgMatches) -> Result<Self, Self::Error> {
920 Ok(Self {
921 all: cmd.get_flag(ARG_ALL),
922 })
923 }
924}