1use camino::Utf8Path;
12use serde::{Deserialize, Serialize};
13
14use crate::digest::Digest;
15use crate::error::RkError;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "kebab-case")]
20pub enum Kind {
21 Workflow,
23 ReleaseConfig,
25 BumpScript,
27 Hook,
29 Manifest,
31 Root,
33}
34
35impl Kind {
36 #[must_use]
38 pub const fn as_str(self) -> &'static str {
39 match self {
40 Self::Workflow => "workflow",
41 Self::ReleaseConfig => "release-config",
42 Self::BumpScript => "bump-script",
43 Self::Hook => "hook",
44 Self::Manifest => "manifest",
45 Self::Root => "root",
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "kebab-case")]
53pub enum Placement {
54 Archived,
56 InPlace,
58}
59
60impl Placement {
61 #[must_use]
63 pub const fn as_str(self) -> &'static str {
64 match self {
65 Self::Archived => "archived",
66 Self::InPlace => "in-place",
67 }
68 }
69}
70
71pub const PREDECESSORS: [(&str, &str, Kind); 54] = [
74 (
78 "release-please-config.json",
79 "release-please",
80 Kind::ReleaseConfig,
81 ),
82 (
83 ".release-please-manifest.json",
84 "release-please",
85 Kind::Manifest,
86 ),
87 (
88 ".github/workflows/release-please.yml",
89 "release-please",
90 Kind::Workflow,
91 ),
92 (
93 ".github/workflows/release-please.yaml",
94 "release-please",
95 Kind::Workflow,
96 ),
97 (".releaserc", "semantic-release", Kind::ReleaseConfig),
98 (".releaserc.json", "semantic-release", Kind::ReleaseConfig),
99 (".releaserc.yaml", "semantic-release", Kind::ReleaseConfig),
100 (".releaserc.yml", "semantic-release", Kind::ReleaseConfig),
101 (".releaserc.js", "semantic-release", Kind::ReleaseConfig),
102 (".releaserc.cjs", "semantic-release", Kind::ReleaseConfig),
103 (".releaserc.mjs", "semantic-release", Kind::ReleaseConfig),
104 (".releaserc.ts", "semantic-release", Kind::ReleaseConfig),
105 ("release.config.js", "semantic-release", Kind::ReleaseConfig),
106 (
107 "release.config.cjs",
108 "semantic-release",
109 Kind::ReleaseConfig,
110 ),
111 (
112 "release.config.mjs",
113 "semantic-release",
114 Kind::ReleaseConfig,
115 ),
116 ("release.config.ts", "semantic-release", Kind::ReleaseConfig),
117 (
118 ".github/workflows/semantic-release.yml",
119 "semantic-release",
120 Kind::Workflow,
121 ),
122 (".goreleaser.yaml", "goreleaser", Kind::ReleaseConfig),
123 (".goreleaser.yml", "goreleaser", Kind::ReleaseConfig),
124 ("goreleaser.yaml", "goreleaser", Kind::ReleaseConfig),
125 ("goreleaser.yml", "goreleaser", Kind::ReleaseConfig),
126 (".config/goreleaser.yaml", "goreleaser", Kind::ReleaseConfig),
127 (".config/goreleaser.yml", "goreleaser", Kind::ReleaseConfig),
128 (
129 ".github/workflows/goreleaser.yml",
130 "goreleaser",
131 Kind::Workflow,
132 ),
133 ("release-plz.toml", "release-plz", Kind::ReleaseConfig),
134 (".release-plz.toml", "release-plz", Kind::ReleaseConfig),
135 (
136 ".github/workflows/release-plz.yml",
137 "release-plz",
138 Kind::Workflow,
139 ),
140 ("cliff.toml", "git-cliff", Kind::ReleaseConfig),
141 ("dist-workspace.toml", "cargo-dist", Kind::ReleaseConfig),
142 (
143 ".github/workflows/release.yml",
144 "cargo-dist",
145 Kind::Workflow,
146 ),
147 (".changeset/config.json", "changesets", Kind::ReleaseConfig),
148 (
149 ".github/workflows/changesets.yml",
150 "changesets",
151 Kind::Workflow,
152 ),
153 (".bumpversion.cfg", "bump-my-version", Kind::ReleaseConfig),
154 (".bumpversion.toml", "bump-my-version", Kind::ReleaseConfig),
155 (".cz.toml", "commitizen", Kind::ReleaseConfig),
156 (".cz.json", "commitizen", Kind::ReleaseConfig),
157 (".cz.yaml", "commitizen", Kind::ReleaseConfig),
158 (".versionrc", "standard-version", Kind::ReleaseConfig),
159 (".versionrc.json", "standard-version", Kind::ReleaseConfig),
160 (
161 ".github/release-drafter.yml",
162 "release-drafter",
163 Kind::ReleaseConfig,
164 ),
165 (
166 ".github/workflows/release-drafter.yml",
167 "release-drafter",
168 Kind::Workflow,
169 ),
170 ("commitlint.config.js", "commitlint", Kind::Hook),
171 ("commitlint.config.cjs", "commitlint", Kind::Hook),
172 (".commitlintrc", "commitlint", Kind::Hook),
173 (".commitlintrc.json", "commitlint", Kind::Hook),
174 (".commitlintrc.yml", "commitlint", Kind::Hook),
175 (".husky/commit-msg", "husky", Kind::Hook),
176 (
177 ".github/workflows/publish.yml",
178 "hand-rolled",
179 Kind::Workflow,
180 ),
181 (
182 ".github/workflows/publish.yaml",
183 "hand-rolled",
184 Kind::Workflow,
185 ),
186 (
187 ".github/workflows/release.yaml",
188 "hand-rolled",
189 Kind::Workflow,
190 ),
191 ("release.sh", "hand-rolled", Kind::BumpScript),
192 ("bump.sh", "hand-rolled", Kind::BumpScript),
193 ("scripts/release.sh", "hand-rolled", Kind::BumpScript),
194 ("scripts/bump-version.sh", "hand-rolled", Kind::BumpScript),
195];
196
197pub const SEMANTIC_RELEASE: &str = "semantic-release";
199
200pub const GORELEASER: &str = "goreleaser";
202
203pub fn release_configs(tool: &'static str) -> impl Iterator<Item = &'static str> {
209 PREDECESSORS
210 .iter()
211 .filter(move |(_, owner, kind)| *owner == tool && *kind == Kind::ReleaseConfig)
212 .map(|(path, _, _)| *path)
213}
214
215pub const ROOT_FILES: [&str; 4] = ["CHANGELOG.md", "README.md", "LICENSE", "AGENTS.md"];
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
220pub struct Entry {
221 pub path: String,
223 pub tool: &'static str,
225 pub kind: Kind,
227 pub placement: Placement,
229 pub sha256: String,
231}
232
233#[must_use]
236pub fn classify(path: &str) -> Option<(&'static str, Kind, Placement)> {
237 if ROOT_FILES.contains(&path) {
238 return Some(("project", Kind::Root, Placement::InPlace));
239 }
240 PREDECESSORS
241 .iter()
242 .find(|(known, _, _)| *known == path)
243 .map(|(_, tool, kind)| (*tool, *kind, Placement::Archived))
244}
245
246#[must_use]
249pub fn listed<'a>(paths: &[&'a str]) -> Vec<&'a str> {
250 PREDECESSORS
251 .iter()
252 .map(|(known, _, _)| *known)
253 .chain(ROOT_FILES)
254 .filter_map(|known| paths.iter().find(|path| **path == known).copied())
255 .collect()
256}
257
258pub fn scan(target: &Utf8Path) -> Result<Vec<Entry>, RkError> {
264 let present: Vec<&str> = PREDECESSORS
265 .iter()
266 .map(|(path, _, _)| *path)
267 .chain(ROOT_FILES)
268 .filter(|path| {
269 let full = target.join(path);
270 full.is_file() && !full.is_symlink()
271 })
272 .collect();
273 let mut out = Vec::new();
274 for path in listed(&present) {
275 let bytes = std::fs::read(target.join(path))?;
276 let (tool, kind, placement) =
277 classify(path).unwrap_or(("project", Kind::Root, Placement::InPlace));
278 out.push(Entry {
279 path: path.to_owned(),
280 tool,
281 kind,
282 placement,
283 sha256: Digest::of(&bytes).to_string(),
284 });
285 }
286 Ok(out)
287}
288
289#[cfg(test)]
290mod tests {
291 use super::{Kind, PREDECESSORS, Placement, ROOT_FILES, classify, listed};
292
293 #[test]
294 fn every_predecessor_path_is_listed_once_and_no_root_file_is_archived() {
295 let mut seen = std::collections::BTreeSet::new();
296 for (path, _, kind) in PREDECESSORS {
297 assert!(seen.insert(path), "{path} is listed twice");
298 assert_ne!(kind, Kind::Root, "{path}");
299 assert!(!ROOT_FILES.contains(&path), "{path}");
300 }
301 for root in ROOT_FILES {
302 assert_eq!(
303 classify(root),
304 Some(("project", Kind::Root, Placement::InPlace))
305 );
306 }
307 assert_eq!(
308 classify("release-please-config.json"),
309 Some(("release-please", Kind::ReleaseConfig, Placement::Archived))
310 );
311 assert_eq!(classify("src/main.rs"), None);
312 }
313
314 #[test]
315 fn the_listing_keeps_the_table_order() {
316 let paths = [
317 "README.md",
318 "cliff.toml",
319 "release-please-config.json",
320 "src/lib.rs",
321 ];
322 assert_eq!(
323 listed(&paths),
324 ["release-please-config.json", "cliff.toml", "README.md"]
325 );
326 }
327}