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); 52] = [
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 ("release.config.js", "semantic-release", Kind::ReleaseConfig),
105 (
106 "release.config.cjs",
107 "semantic-release",
108 Kind::ReleaseConfig,
109 ),
110 (
111 "release.config.mjs",
112 "semantic-release",
113 Kind::ReleaseConfig,
114 ),
115 (
116 ".github/workflows/semantic-release.yml",
117 "semantic-release",
118 Kind::Workflow,
119 ),
120 (".goreleaser.yaml", "goreleaser", Kind::ReleaseConfig),
121 (".goreleaser.yml", "goreleaser", Kind::ReleaseConfig),
122 ("goreleaser.yaml", "goreleaser", Kind::ReleaseConfig),
123 ("goreleaser.yml", "goreleaser", Kind::ReleaseConfig),
124 (".config/goreleaser.yaml", "goreleaser", Kind::ReleaseConfig),
125 (".config/goreleaser.yml", "goreleaser", Kind::ReleaseConfig),
126 (
127 ".github/workflows/goreleaser.yml",
128 "goreleaser",
129 Kind::Workflow,
130 ),
131 ("release-plz.toml", "release-plz", Kind::ReleaseConfig),
132 (".release-plz.toml", "release-plz", Kind::ReleaseConfig),
133 (
134 ".github/workflows/release-plz.yml",
135 "release-plz",
136 Kind::Workflow,
137 ),
138 ("cliff.toml", "git-cliff", Kind::ReleaseConfig),
139 ("dist-workspace.toml", "cargo-dist", Kind::ReleaseConfig),
140 (
141 ".github/workflows/release.yml",
142 "cargo-dist",
143 Kind::Workflow,
144 ),
145 (".changeset/config.json", "changesets", Kind::ReleaseConfig),
146 (
147 ".github/workflows/changesets.yml",
148 "changesets",
149 Kind::Workflow,
150 ),
151 (".bumpversion.cfg", "bump-my-version", Kind::ReleaseConfig),
152 (".bumpversion.toml", "bump-my-version", Kind::ReleaseConfig),
153 (".cz.toml", "commitizen", Kind::ReleaseConfig),
154 (".cz.json", "commitizen", Kind::ReleaseConfig),
155 (".cz.yaml", "commitizen", Kind::ReleaseConfig),
156 (".versionrc", "standard-version", Kind::ReleaseConfig),
157 (".versionrc.json", "standard-version", Kind::ReleaseConfig),
158 (
159 ".github/release-drafter.yml",
160 "release-drafter",
161 Kind::ReleaseConfig,
162 ),
163 (
164 ".github/workflows/release-drafter.yml",
165 "release-drafter",
166 Kind::Workflow,
167 ),
168 ("commitlint.config.js", "commitlint", Kind::Hook),
169 ("commitlint.config.cjs", "commitlint", Kind::Hook),
170 (".commitlintrc", "commitlint", Kind::Hook),
171 (".commitlintrc.json", "commitlint", Kind::Hook),
172 (".commitlintrc.yml", "commitlint", Kind::Hook),
173 (".husky/commit-msg", "husky", Kind::Hook),
174 (
175 ".github/workflows/publish.yml",
176 "hand-rolled",
177 Kind::Workflow,
178 ),
179 (
180 ".github/workflows/publish.yaml",
181 "hand-rolled",
182 Kind::Workflow,
183 ),
184 (
185 ".github/workflows/release.yaml",
186 "hand-rolled",
187 Kind::Workflow,
188 ),
189 ("release.sh", "hand-rolled", Kind::BumpScript),
190 ("bump.sh", "hand-rolled", Kind::BumpScript),
191 ("scripts/release.sh", "hand-rolled", Kind::BumpScript),
192 ("scripts/bump-version.sh", "hand-rolled", Kind::BumpScript),
193];
194
195pub const ROOT_FILES: [&str; 4] = ["CHANGELOG.md", "README.md", "LICENSE", "AGENTS.md"];
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
200pub struct Entry {
201 pub path: String,
203 pub tool: &'static str,
205 pub kind: Kind,
207 pub placement: Placement,
209 pub sha256: String,
211}
212
213#[must_use]
216pub fn classify(path: &str) -> Option<(&'static str, Kind, Placement)> {
217 if ROOT_FILES.contains(&path) {
218 return Some(("project", Kind::Root, Placement::InPlace));
219 }
220 PREDECESSORS
221 .iter()
222 .find(|(known, _, _)| *known == path)
223 .map(|(_, tool, kind)| (*tool, *kind, Placement::Archived))
224}
225
226#[must_use]
229pub fn listed<'a>(paths: &[&'a str]) -> Vec<&'a str> {
230 PREDECESSORS
231 .iter()
232 .map(|(known, _, _)| *known)
233 .chain(ROOT_FILES)
234 .filter_map(|known| paths.iter().find(|path| **path == known).copied())
235 .collect()
236}
237
238pub fn scan(target: &Utf8Path) -> Result<Vec<Entry>, RkError> {
244 let present: Vec<&str> = PREDECESSORS
245 .iter()
246 .map(|(path, _, _)| *path)
247 .chain(ROOT_FILES)
248 .filter(|path| {
249 let full = target.join(path);
250 full.is_file() && !full.is_symlink()
251 })
252 .collect();
253 let mut out = Vec::new();
254 for path in listed(&present) {
255 let bytes = std::fs::read(target.join(path))?;
256 let (tool, kind, placement) =
257 classify(path).unwrap_or(("project", Kind::Root, Placement::InPlace));
258 out.push(Entry {
259 path: path.to_owned(),
260 tool,
261 kind,
262 placement,
263 sha256: Digest::of(&bytes).to_string(),
264 });
265 }
266 Ok(out)
267}
268
269#[cfg(test)]
270mod tests {
271 use super::{Kind, PREDECESSORS, Placement, ROOT_FILES, classify, listed};
272
273 #[test]
274 fn every_predecessor_path_is_listed_once_and_no_root_file_is_archived() {
275 let mut seen = std::collections::BTreeSet::new();
276 for (path, _, kind) in PREDECESSORS {
277 assert!(seen.insert(path), "{path} is listed twice");
278 assert_ne!(kind, Kind::Root, "{path}");
279 assert!(!ROOT_FILES.contains(&path), "{path}");
280 }
281 for root in ROOT_FILES {
282 assert_eq!(
283 classify(root),
284 Some(("project", Kind::Root, Placement::InPlace))
285 );
286 }
287 assert_eq!(
288 classify("release-please-config.json"),
289 Some(("release-please", Kind::ReleaseConfig, Placement::Archived))
290 );
291 assert_eq!(classify("src/main.rs"), None);
292 }
293
294 #[test]
295 fn the_listing_keeps_the_table_order() {
296 let paths = [
297 "README.md",
298 "cliff.toml",
299 "release-please-config.json",
300 "src/lib.rs",
301 ];
302 assert_eq!(
303 listed(&paths),
304 ["release-please-config.json", "cliff.toml", "README.md"]
305 );
306 }
307}