release_kit/cli/profile_flags.rs
1//! The flags every verb that resolves a target configuration takes: the
2//! project profile and the Git workflow, as `rk init`, `rk adopt`,
3//! `rk stage`, and `rk profile` read them.
4//!
5//! The canonical names are the long forms below. Three older spellings
6//! stay as hidden aliases, so an existing command line keeps working:
7//! `--tech` for one `--technology`, `--style` for `--release-style`, and
8//! `--workflow` for `--checkout-mode`, whose older values `worktree` and
9//! `branches` the parser still reads.
10
11use clap::Args;
12
13/// The profile and Git workflow answers an invocation states.
14#[derive(Debug, Clone, Default, Args)]
15pub struct ProfileFlags {
16 /// A technology present in the project; repeat it for several. A
17 /// supplied list replaces the configured one whole. Defaults to
18 /// detection from the version files.
19 #[arg(long = "technology", alias = "tech", value_name = "NAME")]
20 pub technology: Vec<String>,
21
22 /// The forge hosting the project: github or gitlab. Defaults to
23 /// detection from the target's git remote; a project with no forge
24 /// resolves to none.
25 #[arg(long)]
26 pub forge: Option<String>,
27
28 /// The project path on the forge, substituted into the rendered files
29 /// and recorded as the landing parameter. Defaults to detection from
30 /// the target's git remote.
31 #[arg(long)]
32 pub repo: Option<String>,
33
34 /// The release intent: automatic, where release-kit drives the
35 /// release; external, where the project releases through a process
36 /// release-kit does not drive; or none. Defaults to the observation's
37 /// proposal: automatic with the one release-bearing technology, none
38 /// where there is none.
39 #[arg(long, value_name = "MODE")]
40 pub release_mode: Option<String>,
41
42 /// The technology that states the version and takes the bot, for an
43 /// automatic release. Required where more than one release-bearing
44 /// technology is present.
45 #[arg(long, value_name = "NAME")]
46 pub release_driver: Option<String>,
47
48 /// The release style of an automatic release: trunk (the bot's
49 /// request carries auto-merge from creation) or lines (every request
50 /// waits for a human's merge).
51 #[arg(long, alias = "style", value_name = "STYLE")]
52 pub release_style: Option<String>,
53
54 /// The one permanent branch. Defaults to the configuration, the
55 /// record, and then master.
56 #[arg(long, value_name = "BRANCH")]
57 pub trunk: Option<String>,
58
59 /// Where a topic branch opens: linked-worktree (every code-changing
60 /// branch in its own linked worktree, the main checkout commits
61 /// nothing) or main-worktree (the original working tree switches to
62 /// it). Recorded as a Git workflow parameter and rendered into the
63 /// landed blocks.
64 #[arg(long, alias = "workflow", value_name = "MODE")]
65 pub checkout_mode: Option<String>,
66
67 /// Which authority moves an implementation onto the trunk: local
68 /// (the checkout squashes and records it, through `rk integrate`) or
69 /// forge (a pull request or merge request does, behind its required
70 /// check). Recorded as a Git workflow parameter and rendered into the
71 /// landed blocks. Defaults to local.
72 #[arg(long, value_name = "MODE")]
73 pub integration: Option<String>,
74
75 /// How the commits of a branch reach the trunk: squash (the branch
76 /// arrives as one commit) or rebase (the branch is rebased on the
77 /// trunk tip and the trunk fast-forwards to it, every commit keeping
78 /// its own message). Recorded as a Git workflow parameter. Defaults
79 /// to squash.
80 #[arg(long, value_name = "FORM")]
81 pub merge_form: Option<String>,
82
83 /// The check the release gate believes: the context the trunk ruleset
84 /// requires under forge integration, and the context the rendered
85 /// release gate judges under local integration. GitHub only.
86 #[arg(long, value_name = "NAME")]
87 pub required_check: Option<String>,
88
89 /// The workflow whose completion wakes the release gate under local
90 /// integration, named by its `name:` key rather than its filename.
91 /// GitHub only.
92 #[arg(long, value_name = "NAME")]
93 pub required_workflow: Option<String>,
94
95 /// The command an automatic bash release runs to produce
96 /// `dist/*.tar.gz` and its `.sha256`. Recorded as a release parameter.
97 /// Defaults to `make dist`.
98 #[arg(long, value_name = "COMMAND")]
99 pub build_command: Option<String>,
100
101 /// The packages the release image installs before it runs the build
102 /// command, separated by spaces. Recorded as a release parameter.
103 /// Defaults to `make`.
104 #[arg(long, value_name = "PACKAGES")]
105 pub build_packages: Option<String>,
106}
107
108impl ProfileFlags {
109 /// Whether no flag answered anything.
110 #[must_use]
111 pub const fn is_empty(&self) -> bool {
112 self.technology.is_empty()
113 && self.forge.is_none()
114 && self.repo.is_none()
115 && self.release_mode.is_none()
116 && self.release_driver.is_none()
117 && self.release_style.is_none()
118 && self.trunk.is_none()
119 && self.checkout_mode.is_none()
120 && self.integration.is_none()
121 && self.merge_form.is_none()
122 && self.required_check.is_none()
123 && self.required_workflow.is_none()
124 && self.build_command.is_none()
125 && self.build_packages.is_none()
126 }
127
128 /// The parsed answers, as the resolution takes them.
129 ///
130 /// # Errors
131 ///
132 /// Returns [`crate::error::RkError::Usage`] for a value outside its
133 /// vocabulary.
134 pub fn inputs(&self) -> Result<crate::profile::Inputs<'_>, crate::error::RkError> {
135 use crate::landing::{CheckoutMode, Integration, MergeForm, Style};
136 use crate::profile::ReleaseMode;
137 Ok(crate::profile::Inputs {
138 technologies: &self.technology,
139 forge: self.forge.as_deref(),
140 repo: self.repo.as_deref(),
141 release_mode: self
142 .release_mode
143 .as_deref()
144 .map(ReleaseMode::parse)
145 .transpose()?,
146 release_driver: self.release_driver.as_deref(),
147 style: self
148 .release_style
149 .as_deref()
150 .map(Style::parse)
151 .transpose()?,
152 trunk: self.trunk.as_deref(),
153 checkout_mode: self
154 .checkout_mode
155 .as_deref()
156 .map(CheckoutMode::parse)
157 .transpose()?,
158 integration: self
159 .integration
160 .as_deref()
161 .map(Integration::parse)
162 .transpose()?,
163 merge_form: self
164 .merge_form
165 .as_deref()
166 .map(MergeForm::parse)
167 .transpose()?,
168 required_check: self.required_check.as_deref(),
169 required_workflow: self.required_workflow.as_deref(),
170 build_command: self.build_command.as_deref(),
171 build_packages: self.build_packages.as_deref(),
172 ..crate::profile::Inputs::default()
173 })
174 }
175}
176
177/// The preset flag's name, as an operator types it.
178pub const WORKFLOW_ONLY: &str = "--workflow-only";
179
180/// Expand `--workflow-only` over the answers an invocation states.
181///
182/// The preset sets four answers: no release, local integration, linked
183/// worktrees, and the workflow guards. It records nothing of its own, so
184/// the expansion is the four answers and nothing else. A flag that states
185/// one of them the same way agrees with it; a flag that states another
186/// answer, or an answer only a release takes, refuses before any write,
187/// naming the flag and the preset. A flag the preset does not set, such
188/// as `--merge-form`, passes through.
189///
190/// # Errors
191///
192/// A [`crate::diagnostic::Reason::PresetContradiction`] refusal naming
193/// every contradicting flag.
194///
195/// SATISFIES project-profile:the-workflow-only-preset-expands-to-recorded-answers
196pub fn workflow_only(
197 mut inputs: crate::profile::Inputs<'_>,
198) -> Result<crate::profile::Inputs<'_>, crate::error::RkError> {
199 use crate::landing::{CheckoutMode, Integration};
200 use crate::profile::ReleaseMode;
201 let mut contradicting: Vec<String> = Vec::new();
202 if let Some(mode) = inputs
203 .release_mode
204 .filter(|mode| *mode != ReleaseMode::None)
205 {
206 contradicting.push(format!(
207 "--release-mode {} (the preset sets profile.release.mode = \"none\")",
208 mode.as_str()
209 ));
210 }
211 if let Some(driver) = inputs.release_driver {
212 contradicting.push(format!(
213 "--release-driver {driver} (an automatic release alone takes a driver)"
214 ));
215 }
216 if let Some(command) = inputs.build_command {
217 contradicting.push(format!(
218 "--build-command {command} (an automatic bash release alone builds)"
219 ));
220 }
221 if let Some(packages) = inputs.build_packages {
222 contradicting.push(format!(
223 "--build-packages {packages} (an automatic bash release alone builds)"
224 ));
225 }
226 if let Some(style) = inputs.style {
227 contradicting.push(format!(
228 "--release-style {} (an automatic release alone takes a style)",
229 style.as_str()
230 ));
231 }
232 if let Some(integration) = inputs
233 .integration
234 .filter(|integration| *integration != Integration::Local)
235 {
236 contradicting.push(format!(
237 "--integration {} (the preset sets git.integration = \"local\")",
238 integration.as_str()
239 ));
240 }
241 if let Some(mode) = inputs
242 .checkout_mode
243 .filter(|mode| *mode != CheckoutMode::LinkedWorktree)
244 {
245 contradicting.push(format!(
246 "--checkout-mode {} (the preset sets git.checkout_mode = \"linked-worktree\")",
247 mode.as_str()
248 ));
249 }
250 if inputs.workflow_guards == Some(false) {
251 contradicting.push(
252 "--workflow-guards off (the preset sets capabilities.workflow_guards = true)"
253 .to_owned(),
254 );
255 }
256 if !contradicting.is_empty() {
257 return Err(crate::error::RkError::refusal(
258 crate::diagnostic::Diagnostic::new(
259 crate::diagnostic::Reason::PresetContradiction,
260 format!(
261 "{} contradict{} {WORKFLOW_ONLY}; nothing was written",
262 contradicting.join(", "),
263 if contradicting.len() == 1 { "s" } else { "" }
264 ),
265 )
266 .expected(format!(
267 "{WORKFLOW_ONLY} alone, or beside a flag it does not set, such as --merge-form"
268 ))
269 .action(format!(
270 "drop {WORKFLOW_ONLY} and state each answer by its own flag, or drop the contradicting flag"
271 ))
272 .target_state("unchanged"),
273 ));
274 }
275 inputs.release_mode = Some(ReleaseMode::None);
276 inputs.integration = Some(Integration::Local);
277 inputs.checkout_mode = Some(CheckoutMode::LinkedWorktree);
278 inputs.workflow_guards = Some(true);
279 Ok(inputs)
280}