1use std::{
2 fmt::{Debug, Display},
3 path::{Path, PathBuf},
4 process::{Command, Stdio},
5};
6
7use anyhow::bail;
8#[doc(hidden)]
9pub use inventory;
10use owo_colors::OwoColorize;
11use semver::Version;
12use tagit_cfg::TagitCfg;
13use tagit_core::out;
14use tagit_sub_core::TAGIT_DIR;
15
16pub fn with_workspace_entries(
17 dry_run: bool,
18 check_committed: bool,
19 mut f: impl FnMut(WorkspaceEntry<'_>) -> anyhow::Result<()>,
20) -> anyhow::Result<()> {
21 with_workspaces(|workspace| with_workspace(workspace, dry_run, check_committed, &mut f))
22}
23
24pub fn with_workspace(
25 workspace: &dyn TagitWorkspace,
26 dry_run: bool,
27 check_committed: bool,
28 mut f: impl FnMut(WorkspaceEntry<'_>) -> anyhow::Result<()>,
29) -> anyhow::Result<()> {
30 let mut uncommitted: usize = 0;
31 for package in workspace.members() {
32 let is_subtree = package
33 .manifest_path()
34 .components()
35 .any(|part| part.as_os_str().to_str() == Some(TAGIT_DIR));
36 if is_subtree {
37 continue;
38 }
39 let TagitCfg {
40 skip, skip_retag, ..
41 } = package.cfg()?;
42 if skip {
43 continue;
44 }
45 out!("found package", "{package}");
46 let path = package.manifest_path();
47 if check_committed {
48 let manifest_tracked = Command::new("git")
49 .arg("ls-files")
50 .arg("--error-unmatch")
51 .arg(path)
52 .stdout(Stdio::null())
53 .status()?
54 .success();
55 if !manifest_tracked {
56 uncommitted += 1;
57 if !dry_run {
58 bail!("untracked manifest")
59 } else {
60 out!("untracked", "{}", path.display().purple())
61 }
62 }
63 let manifest_commited = Command::new("git")
64 .arg("diff-index")
65 .arg("--quiet")
66 .arg("HEAD")
67 .arg(path)
68 .status()?
69 .success();
70 if !manifest_commited {
71 uncommitted += 1;
72 if !dry_run {
73 bail!("uncommitted manifest")
74 } else {
75 out!("uncommitted", "{}", path.display().purple())
76 }
77 }
78 }
79 let name = package.name();
80 let is_root = workspace.root_manifest() == package.manifest_path();
81 let tag_prefix = if is_root {
82 "".to_string()
83 } else {
84 format!("{name}/")
85 };
86 let version = package.version();
87 let root = package.root();
88 let tag_prefix = &tag_prefix;
89 let paths = &*package.paths()?;
90 f(WorkspaceEntry {
91 version,
92 root,
93 name,
94 tag_prefix,
95 skip_retag,
96 paths,
97 })?;
98 }
99 if dry_run {
100 if uncommitted == 0 {
101 println!("{} {}", "dry run".yellow(), "ok".green());
102 } else {
103 println!(
104 "{} encountered {} untracked/uncommitted manifest(s)",
105 "dry run".yellow(),
106 uncommitted.red(),
107 )
108 }
109 } else {
110 assert_eq!(uncommitted, 0);
111 }
112 Ok(())
113}
114
115#[non_exhaustive]
116pub struct WorkspaceEntry<'a> {
117 pub version: &'a Version,
118 pub root: &'a Path,
119 pub name: &'a str,
120 pub tag_prefix: &'a str,
121 pub skip_retag: bool,
122 pub paths: &'a [PathBuf],
123}
124
125pub trait TagitPackage: Display {
126 fn manifest_path(&self) -> &Path;
127 fn cfg(&self) -> anyhow::Result<TagitCfg>;
128 fn name(&self) -> &str;
129 fn version(&self) -> &Version;
130 fn root(&self) -> &Path;
131 fn paths(&self) -> anyhow::Result<Vec<PathBuf>> {
132 Ok(Vec::new())
133 }
134}
135
136pub trait TagitWorkspace {
137 fn members(&self) -> Vec<&dyn TagitPackage>;
138 fn root_manifest(&self) -> &Path;
139}
140
141pub trait TagitWorkspaceProvider: 'static + Send + Sync + Debug {
142 fn with_workspace(
143 &self,
144 f: &mut dyn FnMut(&dyn TagitWorkspace) -> anyhow::Result<()>,
145 ) -> anyhow::Result<()>;
146}
147
148#[derive(Debug, Clone)]
149#[doc(hidden)]
150pub struct WorkspaceProvider(&'static dyn TagitWorkspaceProvider);
151
152impl WorkspaceProvider {
153 #[doc(hidden)]
154 pub const fn new(provider: &'static impl TagitWorkspaceProvider) -> Self {
155 Self(provider)
156 }
157}
158
159pub fn with_workspaces(
160 mut f: impl FnMut(&dyn TagitWorkspace) -> anyhow::Result<()>,
161) -> anyhow::Result<()> {
162 for provider in inventory::iter::<WorkspaceProvider> {
163 provider.0.with_workspace(&mut f)?;
164 }
165 Ok(())
166}
167
168inventory::collect!(WorkspaceProvider);
169
170#[macro_export]
171macro_rules! submit {
172 ($provider:expr) => {
173 $crate::inventory::submit! {
174 $crate::WorkspaceProvider::new(&$provider)
175 }
176 };
177}