1pub mod catalog;
21
22use std::collections::BTreeMap;
23
24use camino::Utf8Path;
25use serde::{Deserialize, Serialize};
26
27use crate::diagnostic::{Diagnostic, Reason};
28use crate::error::RkError;
29use crate::landing::manifest::{self, CheckoutMode, Integration, Provider, Style};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "lowercase")]
34pub enum ReleaseMode {
35 Automatic,
38 External,
41 None,
43}
44
45impl ReleaseMode {
46 #[must_use]
48 pub const fn as_str(self) -> &'static str {
49 match self {
50 Self::Automatic => "automatic",
51 Self::External => "external",
52 Self::None => "none",
53 }
54 }
55
56 pub fn parse(raw: &str) -> Result<Self, RkError> {
62 match raw {
63 "automatic" => Ok(Self::Automatic),
64 "external" => Ok(Self::External),
65 "none" => Ok(Self::None),
66 other => Err(RkError::Usage(format!(
67 "unknown release mode '{other}'; the modes are: automatic, external, none"
68 ))),
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct ReleaseIntent {
79 pub mode: ReleaseMode,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub driver: Option<String>,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub style: Option<Style>,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub line_prefix: Option<String>,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96pub struct ProfileSnapshot {
97 pub technologies: Vec<String>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub forge: Option<String>,
102 pub release: ReleaseIntent,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108pub struct GitWorkflow {
109 pub trunk: String,
111 pub checkout_mode: CheckoutMode,
113 #[serde(default = "crate::landing::manifest::integration_forge")]
116 pub integration: Integration,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121pub struct CapabilityRequests {
122 #[serde(default)]
124 pub nix_packaging: bool,
125 #[serde(default)]
127 pub reporting_policy: bool,
128 #[serde(default)]
130 pub scorecard: bool,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub code_scanning: Option<Provider>,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
138#[serde(rename_all = "lowercase")]
139pub enum Source {
140 Flag,
142 Config,
144 Record,
146 Observation,
148 Default,
150}
151
152impl Source {
153 #[must_use]
159 pub const fn rank(self) -> u8 {
160 match self {
161 Self::Flag => 0,
162 Self::Config => 1,
163 Self::Record => 2,
164 Self::Observation => 3,
165 Self::Default => 4,
166 }
167 }
168
169 #[must_use]
171 pub const fn as_str(self) -> &'static str {
172 match self {
173 Self::Flag => "flag",
174 Self::Config => "config",
175 Self::Record => "record",
176 Self::Observation => "observation",
177 Self::Default => "default",
178 }
179 }
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
185#[serde(rename_all = "kebab-case", tag = "state")]
186pub enum Proposal {
187 None,
189 Automatic {
191 driver: String,
193 },
194 Ambiguous {
197 drivers: Vec<String>,
199 },
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct Params {
208 profile: ProfileSnapshot,
209 git: GitWorkflow,
210 capabilities: CapabilityRequests,
211 repo: String,
212 security_contact: String,
213 security_response: String,
214}
215
216#[derive(Default)]
218pub struct Inputs<'a> {
219 pub technologies: &'a [String],
222 pub forge: Option<&'a str>,
224 pub repo: Option<&'a str>,
226 pub release_mode: Option<ReleaseMode>,
228 pub release_driver: Option<&'a str>,
230 pub style: Option<Style>,
232 pub trunk: Option<&'a str>,
234 pub checkout_mode: Option<CheckoutMode>,
236 pub integration: Option<Integration>,
238 pub nix: Option<bool>,
240 pub reporting_policy: Option<bool>,
242 pub scorecard: Option<bool>,
244 pub code_scanning: Option<Option<Provider>>,
247}
248
249#[derive(Clone, Copy, PartialEq, Eq)]
251pub enum Purpose {
252 Init,
254 Preview,
257 Upgrade,
259 Adopt,
261}
262
263#[derive(Debug, Clone)]
265pub struct Resolved {
266 pub params: Params,
268 pub sources: BTreeMap<&'static str, Source>,
270 pub unknown: Vec<String>,
272 pub proposal: Option<Proposal>,
275}
276
277pub fn canonical_category(raw: &str) -> Result<String, String> {
287 let lowered = raw.trim().to_ascii_lowercase();
288 let shaped = lowered
289 .chars()
290 .next()
291 .is_some_and(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
292 && lowered
293 .chars()
294 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-');
295 if shaped {
296 Ok(lowered)
297 } else {
298 Err(format!(
299 "{raw:?} is not a category name; one is lowercase letters and digits with hyphens inside, as [a-z0-9][a-z0-9-]*"
300 ))
301 }
302}
303
304pub fn canonical_list(key: &str, raw: &[String]) -> Result<Vec<String>, String> {
310 let mut out = Vec::with_capacity(raw.len());
311 for value in raw {
312 let name = canonical_category(value).map_err(|reason| format!("{key}: {reason}"))?;
313 if out.contains(&name) {
314 return Err(format!("{key} names {name} twice; each technology once"));
315 }
316 out.push(name);
317 }
318 out.sort();
319 Ok(out)
320}
321
322impl Params {
323 #[must_use]
326 pub fn from_record(record: &manifest::Manifest) -> Self {
327 Self {
328 profile: record.profile.clone(),
329 git: record.git.clone(),
330 capabilities: record.capabilities.clone(),
331 repo: record.parameters.repo.clone(),
332 security_contact: record.parameters.security_contact.clone(),
333 security_response: record.parameters.security_response.clone(),
334 }
335 }
336
337 pub fn resolve(
345 target: &Utf8Path,
346 flags: &Inputs<'_>,
347 config: Option<&crate::config::Config>,
348 record: Option<&manifest::Manifest>,
349 purpose: Purpose,
350 ) -> Result<Self, RkError> {
351 resolve(target, flags, config, record, purpose).map(|resolved| resolved.params)
352 }
353
354 #[must_use]
356 pub const fn profile(&self) -> &ProfileSnapshot {
357 &self.profile
358 }
359
360 #[must_use]
362 pub const fn git(&self) -> &GitWorkflow {
363 &self.git
364 }
365
366 #[must_use]
368 pub const fn capabilities(&self) -> &CapabilityRequests {
369 &self.capabilities
370 }
371
372 #[must_use]
374 pub fn technologies(&self) -> &[String] {
375 &self.profile.technologies
376 }
377
378 #[must_use]
380 pub fn forge(&self) -> Option<&str> {
381 self.profile.forge.as_deref()
382 }
383
384 #[must_use]
386 pub const fn release_mode(&self) -> ReleaseMode {
387 self.profile.release.mode
388 }
389
390 #[must_use]
392 pub fn driver(&self) -> Option<&str> {
393 self.profile.release.driver.as_deref()
394 }
395
396 #[must_use]
398 pub const fn style(&self) -> Option<Style> {
399 self.profile.release.style
400 }
401
402 #[must_use]
404 pub const fn nix_packaging(&self) -> bool {
405 self.capabilities.nix_packaging
406 }
407
408 #[must_use]
410 pub const fn reporting_policy(&self) -> bool {
411 self.capabilities.reporting_policy
412 }
413
414 #[must_use]
416 pub const fn scorecard(&self) -> bool {
417 self.capabilities.scorecard
418 }
419
420 #[must_use]
422 pub const fn code_scanning(&self) -> Option<Provider> {
423 self.capabilities.code_scanning
424 }
425
426 #[must_use]
429 pub fn repo(&self) -> &str {
430 &self.repo
431 }
432
433 #[must_use]
435 pub const fn checkout_mode(&self) -> CheckoutMode {
436 self.git.checkout_mode
437 }
438
439 #[must_use]
441 pub const fn integration(&self) -> Integration {
442 self.git.integration
443 }
444
445 #[must_use]
447 pub fn trunk(&self) -> &str {
448 &self.git.trunk
449 }
450
451 #[must_use]
454 pub fn line_prefix(&self) -> &str {
455 self.profile
456 .release
457 .line_prefix
458 .as_deref()
459 .unwrap_or(crate::config::LINE_PREFIX_DEFAULT)
460 }
461
462 #[must_use]
465 pub fn security_contact(&self) -> &str {
466 &self.security_contact
467 }
468
469 #[must_use]
471 pub fn security_response(&self) -> &str {
472 &self.security_response
473 }
474
475 #[must_use]
479 pub fn canonical_flags(&self) -> String {
480 let mut out = String::new();
481 for technology in &self.profile.technologies {
482 out.push_str(" --technology ");
483 out.push_str(technology);
484 }
485 if let Some(forge) = &self.profile.forge {
486 out.push_str(" --forge ");
487 out.push_str(forge);
488 }
489 if !self.repo.is_empty() && self.repo != crate::projection::REPO_PLACEHOLDER {
493 out.push_str(" --repo ");
494 out.push_str(&self.repo);
495 }
496 out.push_str(" --release-mode ");
497 out.push_str(self.profile.release.mode.as_str());
498 if let Some(driver) = &self.profile.release.driver {
499 out.push_str(" --release-driver ");
500 out.push_str(driver);
501 }
502 if let Some(style) = self.profile.release.style {
503 out.push_str(" --release-style ");
504 out.push_str(style.as_str());
505 }
506 out.push_str(" --trunk ");
507 out.push_str(&self.git.trunk);
508 out.push_str(" --checkout-mode ");
509 out.push_str(self.git.checkout_mode.as_str());
510 out.push_str(" --integration ");
511 out.push_str(self.git.integration.as_str());
512 out
513 }
514
515 #[must_use]
521 pub fn capability_flags(&self) -> String {
522 let mut out = String::new();
523 if self.capabilities.nix_packaging {
524 out.push_str(" --nix-packaging");
525 }
526 if self.capabilities.reporting_policy {
527 out.push_str(" --reporting-policy");
528 }
529 if self.capabilities.scorecard {
530 out.push_str(" --scorecard");
531 }
532 out.push_str(" --code-scanning ");
539 out.push_str(
540 self.capabilities
541 .code_scanning
542 .map_or("off", Provider::as_str),
543 );
544 out
545 }
546
547 #[must_use]
554 pub fn capability_toggles(&self) -> String {
555 let word = |on: bool| if on { "on" } else { "off" };
556 format!(
557 " --nix-packaging {} --reporting-policy {} --scorecard {} --code-scanning {}",
558 word(self.capabilities.nix_packaging),
559 word(self.capabilities.reporting_policy),
560 word(self.capabilities.scorecard),
561 self.capabilities
562 .code_scanning
563 .map_or("off", Provider::as_str)
564 )
565 }
566}
567
568#[cfg(test)]
569impl Params {
570 pub(crate) fn for_test(repo: &str, style: Option<Style>) -> Self {
575 Self {
576 profile: ProfileSnapshot {
577 technologies: vec!["rust".to_owned()],
578 forge: Some("github".to_owned()),
579 release: ReleaseIntent {
580 mode: ReleaseMode::Automatic,
581 driver: Some("rust".to_owned()),
582 style,
583 line_prefix: Some(crate::config::LINE_PREFIX_DEFAULT.to_owned()),
584 },
585 },
586 git: GitWorkflow {
587 trunk: crate::config::TRUNK_DEFAULT.to_owned(),
588 checkout_mode: CheckoutMode::LinkedWorktree,
589 integration: Integration::Local,
590 },
591 capabilities: CapabilityRequests {
592 nix_packaging: false,
593 reporting_policy: true,
594 scorecard: false,
595 code_scanning: None,
596 },
597 repo: repo.to_owned(),
598 security_contact: String::new(),
599 security_response: crate::config::RESPONSE_DEFAULT.to_owned(),
600 }
601 }
602
603 pub(crate) fn for_test_security(contact: &str, response: &str) -> Self {
605 Self {
606 security_contact: contact.to_owned(),
607 security_response: response.to_owned(),
608 ..Self::for_test("acme/widget", Some(Style::Trunk))
609 }
610 }
611
612 pub(crate) fn for_test_release_less(
615 technologies: &[&str],
616 forge: Option<&str>,
617 mode: ReleaseMode,
618 ) -> Self {
619 let mut params = Self::for_test("acme/widget", None);
620 params.profile.technologies = technologies.iter().map(|t| (*t).to_owned()).collect();
621 params.profile.forge = forge.map(str::to_owned);
622 params.profile.release = ReleaseIntent {
623 mode,
624 driver: None,
625 style: None,
626 line_prefix: None,
627 };
628 params.capabilities.reporting_policy = false;
629 if forge.is_none() {
630 params.repo = String::new();
631 }
632 params
633 }
634
635 pub(crate) fn set_pair_for_test(&mut self, driver: &str, forge: &str) {
637 self.profile.technologies = vec![driver.to_owned()];
638 self.profile.release.driver = Some(driver.to_owned());
639 self.profile.forge = Some(forge.to_owned());
640 }
641
642 pub(crate) const fn set_checkout_mode_for_test(&mut self, mode: CheckoutMode) {
644 self.git.checkout_mode = mode;
645 }
646
647 pub(crate) const fn set_integration_for_test(&mut self, mode: Integration) {
649 self.git.integration = mode;
650 }
651
652 pub(crate) const fn set_nix_for_test(&mut self, nix: bool) {
654 self.capabilities.nix_packaging = nix;
655 }
656
657 pub(crate) const fn set_scorecard_for_test(&mut self, scorecard: bool) {
659 self.capabilities.scorecard = scorecard;
660 }
661
662 pub(crate) const fn set_code_scanning_for_test(&mut self, provider: Option<Provider>) {
664 self.capabilities.code_scanning = provider;
665 }
666}
667
668fn invalid_release(message: impl std::fmt::Display) -> RkError {
671 RkError::Usage(format!(
672 "{message}; an automatic release names a driver among profile.technologies and a style, and an external or none release names neither"
673 ))
674}
675
676fn answered<T>(chain: [(Option<T>, Source); 5]) -> Option<(T, Source)> {
678 chain
679 .into_iter()
680 .find_map(|(value, source)| value.map(|value| (value, source)))
681}
682
683#[allow(
691 clippy::too_many_lines,
692 reason = "the resolution is one precedence walk per field, and splitting it would hide that every field walks the same chain"
693)]
694pub fn resolve(
695 target: &Utf8Path,
696 flags: &Inputs<'_>,
697 config: Option<&crate::config::Config>,
698 record: Option<&manifest::Manifest>,
699 purpose: Purpose,
700) -> Result<Resolved, RkError> {
701 let mut sources: BTreeMap<&'static str, Source> = BTreeMap::new();
702 let observed = crate::detect::observe(target.as_std_path());
703 let known_drivers = catalog::known_drivers();
704
705 let (technologies, source) = answered([
707 (
708 (!flags.technologies.is_empty()).then(|| flags.technologies.to_vec()),
709 Source::Flag,
710 ),
711 (
712 config.and_then(|c| c.profile.technologies.clone()),
713 Source::Config,
714 ),
715 (
716 record.map(|r| r.profile.technologies.clone()),
717 Source::Record,
718 ),
719 (
720 Some(
721 observed
722 .technologies
723 .iter()
724 .map(|t| (*t).to_owned())
725 .collect(),
726 ),
727 Source::Observation,
728 ),
729 (None, Source::Default),
730 ])
731 .unwrap_or_else(|| (Vec::new(), Source::Default));
732 let technologies =
733 canonical_list("profile.technologies", &technologies).map_err(RkError::Usage)?;
734 sources.insert("profile.technologies", source);
735
736 let forge_flag = flags
738 .forge
739 .map(|name| canonical_category(name).map_err(RkError::Usage))
740 .transpose()?;
741 let (forge, source) = answered([
742 (forge_flag.map(Some), Source::Flag),
743 (
744 config
745 .and_then(|c| c.profile.forge.clone())
746 .map(|value| if value.is_empty() { None } else { Some(value) }),
747 Source::Config,
748 ),
749 (record.map(|r| r.profile.forge.clone()), Source::Record),
750 (
751 observed.forge.map(|forge| Some(forge.as_str().to_owned())),
752 Source::Observation,
753 ),
754 (Some(None), Source::Default),
755 ])
756 .unwrap_or((None, Source::Default));
757 let forge = forge
758 .map(|name| canonical_category(&name).map_err(RkError::Usage))
759 .transpose()?;
760 sources.insert("profile.forge", source);
761
762 let (repo, source) = answered([
764 (flags.repo.map(str::to_owned), Source::Flag),
765 (
766 config
767 .map(|c| c.project.repo.clone())
768 .filter(|value| !value.is_empty()),
769 Source::Config,
770 ),
771 (
772 record
773 .map(|r| r.parameters.repo.clone())
774 .filter(|value| !value.is_empty()),
775 Source::Record,
776 ),
777 (observed.repo.clone(), Source::Observation),
778 (None, Source::Default),
779 ])
780 .map_or((None, Source::Default), |(value, source)| {
781 (Some(value), source)
782 });
783 sources.insert("project.repo", source);
784
785 let release_bearing: Vec<String> = technologies
788 .iter()
789 .filter(|name| known_drivers.contains(name))
790 .cloned()
791 .collect();
792 let proposal = match release_bearing.as_slice() {
793 [] => Proposal::None,
794 [one] => Proposal::Automatic {
795 driver: one.clone(),
796 },
797 many => Proposal::Ambiguous {
798 drivers: many.to_vec(),
799 },
800 };
801 let proposed_mode = match &proposal {
808 Proposal::Automatic { .. } | Proposal::Ambiguous { .. } => ReleaseMode::Automatic,
809 Proposal::None => ReleaseMode::None,
810 };
811 let (mode, source) = answered([
812 (flags.release_mode, Source::Flag),
813 (config.and_then(|c| c.profile.release.mode), Source::Config),
814 (record.map(|r| r.profile.release.mode), Source::Record),
815 (Some(proposed_mode), Source::Observation),
816 (None, Source::Default),
817 ])
818 .unwrap_or((ReleaseMode::None, Source::Default));
819 let mode_source = source;
820 sources.insert("profile.release.mode", mode_source);
821 let mode_answered_above = mode_source != Source::Observation;
822 let proposal = (!mode_answered_above).then_some(proposal);
823
824 let driver_flag = flags
828 .release_driver
829 .map(|name| canonical_category(name).map_err(RkError::Usage))
830 .transpose()?;
831 let (driver, driver_source) = answered([
832 (driver_flag, Source::Flag),
833 (
834 config.and_then(|c| c.profile.release.driver.clone()),
835 Source::Config,
836 ),
837 (
838 record.and_then(|r| r.profile.release.driver.clone()),
839 Source::Record,
840 ),
841 (
842 match &proposal {
843 Some(Proposal::Automatic { driver }) if mode == ReleaseMode::Automatic => {
844 Some(driver.clone())
845 }
846 _ => None,
847 },
848 Source::Observation,
849 ),
850 (None, Source::Default),
851 ])
852 .map_or((None, Source::Default), |(value, source)| {
853 (Some(value), source)
854 });
855 let (style, style_source) = answered([
856 (flags.style, Source::Flag),
857 (config.and_then(|c| c.profile.release.style), Source::Config),
858 (record.and_then(|r| r.profile.release.style), Source::Record),
859 (None, Source::Observation),
860 (
861 (mode == ReleaseMode::Automatic && matches!(purpose, Purpose::Init | Purpose::Preview))
862 .then_some(Style::Trunk),
863 Source::Default,
864 ),
865 ])
866 .map_or((None, Source::Default), |(value, source)| {
867 (Some(value), source)
868 });
869 let (line_prefix, prefix_source) = answered([
870 (None, Source::Flag),
871 (
872 config.and_then(|c| c.profile.release.line_prefix.clone()),
873 Source::Config,
874 ),
875 (
876 record.and_then(|r| r.profile.release.line_prefix.clone()),
877 Source::Record,
878 ),
879 (None, Source::Observation),
880 (
881 (mode == ReleaseMode::Automatic).then(|| crate::config::LINE_PREFIX_DEFAULT.to_owned()),
882 Source::Default,
883 ),
884 ])
885 .map_or((None, Source::Default), |(value, source)| {
886 (Some(value), source)
887 });
888
889 let reporting = purpose == Purpose::Preview;
894 let release = match mode {
895 ReleaseMode::Automatic => {
896 if let Some(Proposal::Ambiguous { drivers }) = &proposal
897 && driver.is_none()
898 && !reporting
899 {
900 return Err(RkError::Usage(format!(
901 "the target carries more than one release-bearing technology, {}, and nothing names the driver; pass --release-driver <name>, or set profile.release.driver in {}",
902 drivers.join(" and "),
903 crate::config::CONFIG_PATH
904 )));
905 }
906 if forge.is_none() && !reporting {
907 let message = observed.host.map_or_else(
908 || "no forge detected: the target has no origin remote, and an automatic release needs one".to_owned(),
909 |host| format!("no forge detected: the host {host} is not recognized, and an automatic release needs one"),
910 );
911 return Err(RkError::refusal(
912 Diagnostic::new(Reason::ForgeUndetected, message)
913 .expected("a github.com or gitlab remote, or --forge")
914 .action("pass --forge <github|gitlab>, or --release-mode none for a project that releases nothing"),
915 ));
916 }
917 if driver.is_none() && !reporting {
918 return Err(invalid_release(format!(
919 "profile.release.mode is automatic and no driver is named; pass --release-driver <{}>",
920 known_drivers.join("|")
921 )));
922 }
923 if let Some(driver) = &driver
924 && !technologies.contains(driver)
925 && !reporting
926 {
927 return Err(invalid_release(format!(
928 "profile.release.driver names {driver}, which profile.technologies does not carry ({})",
929 if technologies.is_empty() {
930 "empty".to_owned()
931 } else {
932 technologies.join(", ")
933 }
934 )));
935 }
936 let style = match (style, purpose) {
937 (None, Purpose::Upgrade | Purpose::Adopt) => {
938 return Err(RkError::Usage(
939 "the target carries no style parameter; set profile.release.style in .release-kit/config.toml or pass --release-style <trunk|lines>".into(),
940 ));
941 }
942 (None, _) => Style::Trunk,
943 (Some(style), _) => style,
944 };
945 sources.insert("profile.release.driver", driver_source);
946 sources.insert("profile.release.style", style_source);
947 sources.insert("profile.release.line_prefix", prefix_source);
948 ReleaseIntent {
949 mode,
950 driver,
951 style: Some(style),
952 line_prefix: Some(
953 line_prefix.unwrap_or_else(|| crate::config::LINE_PREFIX_DEFAULT.to_owned()),
954 ),
955 }
956 }
957 ReleaseMode::External | ReleaseMode::None => {
958 for (key, present, value_source) in [
965 ("profile.release.driver", driver.is_some(), driver_source),
966 ("profile.release.style", style.is_some(), style_source),
967 (
968 "profile.release.line_prefix",
969 line_prefix.is_some(),
970 prefix_source,
971 ),
972 ] {
973 if present
974 && matches!(value_source, Source::Flag | Source::Config)
975 && value_source.rank() <= mode_source.rank()
976 {
977 return Err(invalid_release(format!(
978 "{key} is set while profile.release.mode is {}",
979 mode.as_str()
980 )));
981 }
982 }
983 ReleaseIntent {
984 mode,
985 driver: None,
986 style: None,
987 line_prefix: None,
988 }
989 }
990 };
991
992 let adapter_known = forge
996 .as_deref()
997 .is_some_and(|name| crate::detect::Forge::parse(name).is_some());
998 let repo = match (forge.is_some(), adapter_known, repo) {
999 (false, _, _) => String::new(),
1002 (true, false, repo) => repo.unwrap_or_default(),
1003 (true, true, Some(repo)) => repo,
1004 (true, true, None) if purpose == Purpose::Preview => {
1005 crate::projection::REPO_PLACEHOLDER.to_owned()
1006 }
1007 (true, true, None) => return Err(crate::landing::repo_unresolved()),
1008 };
1009
1010 let (trunk, source) = answered([
1012 (flags.trunk.map(str::to_owned), Source::Flag),
1013 (config.and_then(|c| c.git.trunk.clone()), Source::Config),
1014 (record.map(|r| r.git.trunk.clone()), Source::Record),
1015 (None, Source::Observation),
1016 (
1017 Some(crate::config::TRUNK_DEFAULT.to_owned()),
1018 Source::Default,
1019 ),
1020 ])
1021 .unwrap_or_else(|| (crate::config::TRUNK_DEFAULT.to_owned(), Source::Default));
1022 sources.insert("git.trunk", source);
1023 let (checkout_mode, source) = answered([
1024 (flags.checkout_mode, Source::Flag),
1025 (config.and_then(|c| c.git.checkout_mode), Source::Config),
1026 (record.map(|r| r.git.checkout_mode), Source::Record),
1027 (None, Source::Observation),
1028 (
1029 Some(if purpose == Purpose::Adopt {
1030 CheckoutMode::MainWorktree
1031 } else {
1032 CheckoutMode::LinkedWorktree
1033 }),
1034 Source::Default,
1035 ),
1036 ])
1037 .unwrap_or((CheckoutMode::LinkedWorktree, Source::Default));
1038 sources.insert("git.checkout_mode", source);
1039 let (integration, source) = answered([
1044 (flags.integration, Source::Flag),
1045 (config.and_then(|c| c.git.integration), Source::Config),
1046 (record.map(|r| r.git.integration), Source::Record),
1047 (None, Source::Observation),
1048 (
1053 Some(if purpose == Purpose::Adopt {
1054 Integration::Forge
1055 } else {
1056 Integration::Local
1057 }),
1058 Source::Default,
1059 ),
1060 ])
1061 .unwrap_or((Integration::Local, Source::Default));
1062 sources.insert("git.integration", source);
1063
1064 let (nix_packaging, source) = answered([
1066 (flags.nix, Source::Flag),
1067 (
1068 config.and_then(|c| c.capabilities.nix_packaging),
1069 Source::Config,
1070 ),
1071 (record.map(|r| r.capabilities.nix_packaging), Source::Record),
1072 (None, Source::Observation),
1073 (Some(false), Source::Default),
1074 ])
1075 .unwrap_or((false, Source::Default));
1076 sources.insert("capabilities.nix_packaging", source);
1077 let (reporting_policy, source) = answered([
1078 (flags.reporting_policy, Source::Flag),
1079 (
1080 config.and_then(|c| c.capabilities.reporting_policy),
1081 Source::Config,
1082 ),
1083 (
1084 record.map(|r| r.capabilities.reporting_policy),
1085 Source::Record,
1086 ),
1087 (None, Source::Observation),
1088 (
1091 Some(release.mode == ReleaseMode::Automatic),
1092 Source::Default,
1093 ),
1094 ])
1095 .unwrap_or((false, Source::Default));
1096 sources.insert("capabilities.reporting_policy", source);
1097 let (scorecard, source) = answered([
1098 (flags.scorecard, Source::Flag),
1099 (
1100 config.and_then(|c| c.capabilities.scorecard),
1101 Source::Config,
1102 ),
1103 (record.map(|r| r.capabilities.scorecard), Source::Record),
1104 (None, Source::Observation),
1105 (Some(false), Source::Default),
1106 ])
1107 .unwrap_or((false, Source::Default));
1108 sources.insert("capabilities.scorecard", source);
1109 let configured_scanning = config
1110 .and_then(|c| c.capabilities.code_scanning.as_deref())
1111 .map(Provider::parse)
1112 .transpose()?;
1113 let (code_scanning, source) = answered([
1114 (flags.code_scanning, Source::Flag),
1115 (configured_scanning, Source::Config),
1116 (record.map(|r| r.capabilities.code_scanning), Source::Record),
1117 (None, Source::Observation),
1118 (Some(None), Source::Default),
1119 ])
1120 .unwrap_or((None, Source::Default));
1121 sources.insert("capabilities.code_scanning", source);
1122 let (security_contact, source) = answered([
1130 (None, Source::Flag),
1131 (
1132 config.and_then(|c| c.security.contact.clone()),
1133 Source::Config,
1134 ),
1135 (
1136 record.map(|r| r.parameters.security_contact.clone()),
1137 Source::Record,
1138 ),
1139 (None, Source::Observation),
1140 (Some(String::new()), Source::Default),
1141 ])
1142 .unwrap_or((String::new(), Source::Default));
1143 let security_contact =
1144 crate::config::canonical_contact(&security_contact).map_err(crate::config::invalid)?;
1145 sources.insert("security.contact", source);
1146 let (security_response, source) = answered([
1147 (None, Source::Flag),
1148 (
1149 config.and_then(|c| c.security.response.clone()),
1150 Source::Config,
1151 ),
1152 (
1153 record.map(|r| r.parameters.security_response.clone()),
1154 Source::Record,
1155 ),
1156 (None, Source::Observation),
1157 (
1158 Some(crate::config::RESPONSE_DEFAULT.to_owned()),
1159 Source::Default,
1160 ),
1161 ])
1162 .unwrap_or_else(|| (crate::config::RESPONSE_DEFAULT.to_owned(), Source::Default));
1163 let security_response =
1164 crate::config::canonical_response(&security_response).map_err(crate::config::invalid)?;
1165 sources.insert("security.response", source);
1166
1167 let mut unknown: Vec<String> = technologies
1168 .iter()
1169 .filter(|name| !known_drivers.contains(name))
1170 .map(|name| format!("technology {name}"))
1171 .collect();
1172 if let Some(name) = &forge
1173 && crate::detect::Forge::parse(name).is_none()
1174 {
1175 unknown.push(format!("forge {name}"));
1176 }
1177
1178 Ok(Resolved {
1179 params: Params {
1180 profile: ProfileSnapshot {
1181 technologies,
1182 forge,
1183 release,
1184 },
1185 git: GitWorkflow {
1186 trunk,
1187 checkout_mode,
1188 integration,
1189 },
1190 capabilities: CapabilityRequests {
1191 nix_packaging,
1192 reporting_policy,
1193 scorecard,
1194 code_scanning,
1195 },
1196 repo,
1197 security_contact,
1198 security_response,
1199 },
1200 sources,
1201 unknown,
1202 proposal,
1203 })
1204}