Skip to main content

tagit_workspace/
lib.rs

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}