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