Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
216 linesCodeBlameRaw
1//! The command reference in the docs, `apps/docs/src/content/docs/reference/cli.md`,
2//! is written from the command tree itself. A test fails when it is out of
3//! date; regenerate it with
4//!
5//! ```sh
6//! G1T_WRITE_DOCS=1 cargo test -p g1t cli_reference
7//! ```
8
9use clap::{Arg, ArgAction, Command};
10use std::fmt::Write as _;
11
12const HEAD: &str = "---
13title: CLI commands
14description: Every g1t command with its arguments and flags, written from the CLI's own definitions.
15---
16
17<!-- Generated from crates/g1t by `G1T_WRITE_DOCS=1 cargo test -p g1t cli_reference`. Do not edit by hand. -->
18
19Every command of the [g1t CLI](/guides/cli/), with its arguments and flags.
20`g1t <command> --help` prints the same at a terminal. Each command that talks
21to the API names the operations it calls, which link to the
22[API reference](/reference/api/); [`g1t api`](#g1t-api) calls any other.
23
24## Flags every command takes
25
26| Flag | |
27| --- | --- |
28";
29
30/// Help text as a sentence: clap drops the full stop doc comments end with.
31fn sentence(text: &str) -> String {
32 let text = text.trim();
33 if text.is_empty() || text.ends_with(['.', ':', '?', '!']) {
34 text.to_owned()
35 } else {
36 format!("{text}.")
37 }
38}
39
40fn slug(text: &str) -> String {
41 let mut out = String::new();
42 for c in text.to_lowercase().chars() {
43 if c.is_ascii_alphanumeric() {
44 out.push(c);
45 } else if !out.ends_with('-') {
46 out.push('-');
47 }
48 }
49 out.trim_matches('-').to_owned()
50}
51
52fn cell(text: &str) -> String {
53 text.replace('|', "\\|").replace('\n', " ")
54}
55
56fn flag(arg: &Arg) -> String {
57 let value = arg
58 .get_value_names()
59 .map(|v| {
60 v.iter()
61 .map(|n| n.to_string())
62 .collect::<Vec<_>>()
63 .join(" ")
64 })
65 .unwrap_or_else(|| arg.get_id().to_string().to_uppercase());
66 let takes_value = matches!(arg.get_action(), ArgAction::Set | ArgAction::Append);
67 let optional = arg.get_num_args().is_some_and(|n| n.min_values() == 0);
68 let mut name = match (arg.get_short(), arg.get_long()) {
69 (Some(s), Some(l)) => format!("-{s}, --{l}"),
70 (None, Some(l)) => format!("--{l}"),
71 (Some(s), None) => format!("-{s}"),
72 (None, None) => {
73 return if arg.is_required_set() {
74 format!("<{value}>")
75 } else {
76 format!("[{value}]")
77 };
78 }
79 };
80 if takes_value {
81 name.push_str(&if optional {
82 format!(" [{value}]")
83 } else {
84 format!(" <{value}>")
85 });
86 }
87 name
88}
89
90fn describe(arg: &Arg) -> String {
91 let mut text = sentence(&arg.get_help().map(|h| h.to_string()).unwrap_or_default());
92 let values: Vec<String> = arg
93 .get_possible_values()
94 .iter()
95 .filter(|v| !v.is_hide_set())
96 .map(|v| format!("`{}`", v.get_name()))
97 .collect();
98 if !values.is_empty() && !text.contains(&values.join(", ")) {
99 let _ = write!(
100 text,
101 "{}One of {}.",
102 if text.is_empty() { "" } else { " " },
103 values.join(", ")
104 );
105 }
106 let defaults: Vec<String> = arg
107 .get_default_values()
108 .iter()
109 .map(|v| v.to_string_lossy().into_owned())
110 .collect();
111 if !defaults.is_empty() && !matches!(arg.get_action(), ArgAction::SetTrue) {
112 let _ = write!(text, " Default: `{}`.", defaults.join(","));
113 }
114 if arg.is_last_set() {
115 text = format!("After `--`. {text}");
116 }
117 cell(text.trim())
118}
119
120fn command(out: &mut String, cmd: &Command, path: &str, depth: usize) {
121 if cmd.is_hide_set() || cmd.get_name() == "help" {
122 return;
123 }
124 let subs: Vec<&Command> = cmd
125 .get_subcommands()
126 .filter(|c| !c.is_hide_set() && c.get_name() != "help")
127 .collect();
128 let heading = "#".repeat(depth.min(4));
129 let _ = writeln!(out, "{heading} {path}\n");
130 if let Some(about) = cmd.get_about() {
131 let _ = writeln!(out, "{}\n", sentence(&about.to_string()));
132 }
133 if !subs.is_empty() {
134 for sub in &subs {
135 command(out, sub, &format!("{path} {}", sub.get_name()), depth + 1);
136 }
137 return;
138 }
139 let usage = cmd.clone().render_usage().to_string();
140 let usage = usage.trim_start_matches("Usage: ").trim();
141 let _ = writeln!(out, "```text\n{usage}\n```\n");
142 let args: Vec<&Arg> = cmd
143 .get_arguments()
144 .filter(|a| {
145 !a.is_hide_set()
146 && !a.is_global_set()
147 && !matches!(a.get_id().as_str(), "help" | "version")
148 })
149 .collect();
150 if !args.is_empty() {
151 let _ = writeln!(out, "| Argument or flag | |\n| --- | --- |");
152 for arg in args {
153 let _ = writeln!(out, "| `{}` | {} |", cell(&flag(arg)), describe(arg));
154 }
155 let _ = writeln!(out);
156 }
157 if let Some(after) = cmd.get_after_help() {
158 let _ = writeln!(out, "```text\n{}\n```\n", after.to_string().trim());
159 }
160 let key = path.trim_start_matches("g1t ");
161 if let Some(m) = crate::mapping::COMMANDS
162 .iter()
163 .find(|m| m.command == key)
164 .filter(|m| !m.ops.is_empty())
165 {
166 let links: Vec<String> = m
167 .ops
168 .iter()
169 .map(|id| {
170 let tag = crate::ops::find(id).map(|op| op.tag).unwrap_or("");
171 format!("[`{id}`](/reference/api/{}/{}/)", slug(tag), slug(id))
172 })
173 .collect();
174 let _ = writeln!(out, "Calls {}.\n", links.join(", "));
175 }
176}
177
178pub fn reference() -> String {
179 let mut root = <crate::cli::Cli as clap::CommandFactory>::command();
180 root.build();
181 let mut out = HEAD.to_owned();
182 for arg in root
183 .get_arguments()
184 .filter(|a| a.is_global_set() && !a.is_hide_set())
185 {
186 let _ = writeln!(out, "| `{}` | {} |", cell(&flag(arg)), describe(arg));
187 }
188 let _ = writeln!(out);
189 for sub in root.get_subcommands() {
190 command(&mut out, sub, &format!("g1t {}", sub.get_name()), 2);
191 }
192 out.trim_end().to_owned() + "\n"
193}
194
195#[cfg(test)]
196mod tests {
197 #[test]
198 fn cli_reference_is_current() {
199 let path = concat!(
200 env!("CARGO_MANIFEST_DIR"),
201 "/../../apps/docs/src/content/docs/reference/cli.md"
202 );
203 let want = super::reference();
204 if std::env::var_os("G1T_WRITE_DOCS").is_some() {
205 std::fs::write(path, &want).unwrap();
206 return;
207 }
208 let have = std::fs::read_to_string(path)
209 .unwrap_or_default()
210 .replace("\r\n", "\n");
211 assert!(
212 have == want,
213 "apps/docs/src/content/docs/reference/cli.md is out of date: run G1T_WRITE_DOCS=1 cargo test -p g1t cli_reference"
214 );
215 }
216}