Skip to main content

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 trunk's
49    /// request merges on the operator's order) or lines (the project keeps
50    /// release lines, and every request 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 trunk ruleset requires before a release request
84    /// merges. GitHub only.
85    #[arg(long, value_name = "NAME")]
86    pub required_check: Option<String>,
87
88    /// The packages the release job of an automatic bash release installs
89    /// before it runs the verify command, separated by spaces. Recorded as
90    /// a release parameter. Defaults to `make`.
91    #[arg(long, value_name = "PACKAGES")]
92    pub build_packages: Option<String>,
93}
94
95impl ProfileFlags {
96    /// Whether no flag answered anything.
97    #[must_use]
98    pub const fn is_empty(&self) -> bool {
99        self.technology.is_empty()
100            && self.forge.is_none()
101            && self.repo.is_none()
102            && self.release_mode.is_none()
103            && self.release_driver.is_none()
104            && self.release_style.is_none()
105            && self.trunk.is_none()
106            && self.checkout_mode.is_none()
107            && self.integration.is_none()
108            && self.merge_form.is_none()
109            && self.required_check.is_none()
110            && self.build_packages.is_none()
111    }
112
113    /// The parsed answers, as the resolution takes them.
114    ///
115    /// # Errors
116    ///
117    /// Returns [`crate::error::RkError::Usage`] for a value outside its
118    /// vocabulary.
119    pub fn inputs(&self) -> Result<crate::profile::Inputs<'_>, crate::error::RkError> {
120        use crate::landing::{CheckoutMode, Integration, MergeForm, Style};
121        use crate::profile::ReleaseMode;
122        Ok(crate::profile::Inputs {
123            technologies: &self.technology,
124            forge: self.forge.as_deref(),
125            repo: self.repo.as_deref(),
126            release_mode: self
127                .release_mode
128                .as_deref()
129                .map(ReleaseMode::parse)
130                .transpose()?,
131            release_driver: self.release_driver.as_deref(),
132            style: self
133                .release_style
134                .as_deref()
135                .map(Style::parse)
136                .transpose()?,
137            trunk: self.trunk.as_deref(),
138            checkout_mode: self
139                .checkout_mode
140                .as_deref()
141                .map(CheckoutMode::parse)
142                .transpose()?,
143            integration: self
144                .integration
145                .as_deref()
146                .map(Integration::parse)
147                .transpose()?,
148            merge_form: self
149                .merge_form
150                .as_deref()
151                .map(MergeForm::parse)
152                .transpose()?,
153            required_check: self.required_check.as_deref(),
154            build_packages: self.build_packages.as_deref(),
155            ..crate::profile::Inputs::default()
156        })
157    }
158}
159
160/// The preset flag's name, as an operator types it.
161pub const WORKFLOW_ONLY: &str = "--workflow-only";
162
163/// Expand `--workflow-only` over the answers an invocation states.
164///
165/// The preset sets four answers: no release, local integration, linked
166/// worktrees, and the workflow guards. It records nothing of its own, so
167/// the expansion is the four answers and nothing else. A flag that states
168/// one of them the same way agrees with it; a flag that states another
169/// answer, or an answer only a release takes, refuses before any write,
170/// naming the flag and the preset. A flag the preset does not set, such
171/// as `--merge-form`, passes through.
172///
173/// # Errors
174///
175/// A [`crate::diagnostic::Reason::PresetContradiction`] refusal naming
176/// every contradicting flag.
177///
178/// SATISFIES project-profile:the-workflow-only-preset-expands-to-recorded-answers
179pub fn workflow_only(
180    mut inputs: crate::profile::Inputs<'_>,
181) -> Result<crate::profile::Inputs<'_>, crate::error::RkError> {
182    use crate::landing::{CheckoutMode, Integration};
183    use crate::profile::ReleaseMode;
184    let mut contradicting: Vec<String> = Vec::new();
185    if let Some(mode) = inputs
186        .release_mode
187        .filter(|mode| *mode != ReleaseMode::None)
188    {
189        contradicting.push(format!(
190            "--release-mode {} (the preset sets profile.release.mode = \"none\")",
191            mode.as_str()
192        ));
193    }
194    if let Some(driver) = inputs.release_driver {
195        contradicting.push(format!(
196            "--release-driver {driver} (an automatic release alone takes a driver)"
197        ));
198    }
199    if let Some(packages) = inputs.build_packages {
200        contradicting.push(format!(
201            "--build-packages {packages} (an automatic bash release alone builds)"
202        ));
203    }
204    if let Some(style) = inputs.style {
205        contradicting.push(format!(
206            "--release-style {} (an automatic release alone takes a style)",
207            style.as_str()
208        ));
209    }
210    if let Some(integration) = inputs
211        .integration
212        .filter(|integration| *integration != Integration::Local)
213    {
214        contradicting.push(format!(
215            "--integration {} (the preset sets git.integration = \"local\")",
216            integration.as_str()
217        ));
218    }
219    if let Some(mode) = inputs
220        .checkout_mode
221        .filter(|mode| *mode != CheckoutMode::LinkedWorktree)
222    {
223        contradicting.push(format!(
224            "--checkout-mode {} (the preset sets git.checkout_mode = \"linked-worktree\")",
225            mode.as_str()
226        ));
227    }
228    if inputs.workflow_guards == Some(false) {
229        contradicting.push(
230            "--workflow-guards off (the preset sets capabilities.workflow_guards = true)"
231                .to_owned(),
232        );
233    }
234    if !contradicting.is_empty() {
235        return Err(crate::error::RkError::refusal(
236            crate::diagnostic::Diagnostic::new(
237                crate::diagnostic::Reason::PresetContradiction,
238                format!(
239                    "{} contradict{} {WORKFLOW_ONLY}; nothing was written",
240                    contradicting.join(", "),
241                    if contradicting.len() == 1 { "s" } else { "" }
242                ),
243            )
244            .expected(format!(
245                "{WORKFLOW_ONLY} alone, or beside a flag it does not set, such as --merge-form"
246            ))
247            .action(format!(
248                "drop {WORKFLOW_ONLY} and state each answer by its own flag, or drop the contradicting flag"
249            ))
250            .target_state("unchanged"),
251        ));
252    }
253    inputs.release_mode = Some(ReleaseMode::None);
254    inputs.integration = Some(Integration::Local);
255    inputs.checkout_mode = Some(CheckoutMode::LinkedWorktree);
256    inputs.workflow_guards = Some(true);
257    Ok(inputs)
258}