Skip to main content

wtg_cli/
release_filter.rs

1//! Release filtering configuration.
2//!
3//! This module provides the `ReleaseFilter` enum which controls which tags/releases
4//! are considered when finding releases for a commit.
5
6use crate::git::TagInfo;
7
8/// Controls which tags/releases are considered when finding releases.
9#[derive(Debug, Clone, Default)]
10pub enum ReleaseFilter {
11    /// All tags are considered (default behavior).
12    #[default]
13    Unrestricted,
14    /// Filter out pre-release versions (nightlies, RCs, etc.).
15    SkipPrereleases,
16    /// Limit to one specific tag by name.
17    Specific(String),
18}
19
20impl ReleaseFilter {
21    /// Filter a list of `TagInfo` candidates.
22    ///
23    /// Returns a new vector containing only tags that pass the filter.
24    #[must_use]
25    pub fn filter_tags(&self, tags: Vec<TagInfo>) -> Vec<TagInfo> {
26        match self {
27            Self::Unrestricted => tags,
28            Self::SkipPrereleases => tags
29                .into_iter()
30                .filter(|t| {
31                    // Keep tags that are not semver (can't determine pre-release status)
32                    // or are semver but not pre-releases
33                    t.semver_info
34                        .as_ref()
35                        .is_none_or(|s| s.pre_release.is_none())
36                })
37                .collect(),
38            Self::Specific(name) => tags.into_iter().filter(|t| t.name == *name).collect(),
39        }
40    }
41
42    /// Check if this is a specific release filter and return the tag name if so.
43    #[must_use]
44    pub fn specific_tag(&self) -> Option<&str> {
45        match self {
46            Self::Specific(name) => Some(name),
47            _ => None,
48        }
49    }
50
51    /// Returns true if pre-releases should be skipped.
52    #[must_use]
53    pub const fn skips_prereleases(&self) -> bool {
54        matches!(self, Self::SkipPrereleases)
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::semver::parse_semver;
62    use chrono::Utc;
63
64    fn make_tag(name: &str) -> TagInfo {
65        TagInfo {
66            name: name.to_string(),
67            commit_hash: "abc123".to_string(),
68            semver_info: parse_semver(name),
69            created_at: Utc::now(),
70            is_release: false,
71            release_name: None,
72            release_url: None,
73            published_at: None,
74            tag_url: None,
75        }
76    }
77
78    #[test]
79    fn unrestricted_keeps_all_tags() {
80        let tags = vec![
81            make_tag("v1.0.0"),
82            make_tag("v1.1.0-beta.1"),
83            make_tag("v2.0.0-rc.1"),
84            make_tag("release-2024"),
85        ];
86        let filter = ReleaseFilter::Unrestricted;
87        let result = filter.filter_tags(tags);
88        assert_eq!(result.len(), 4);
89    }
90
91    #[test]
92    fn skip_prereleases_filters_correctly() {
93        let tags = vec![
94            make_tag("v1.0.0"),
95            make_tag("v1.1.0-beta.1"),
96            make_tag("v2.0.0-rc.1"),
97            make_tag("release-2024"), // non-semver, kept
98        ];
99        let filter = ReleaseFilter::SkipPrereleases;
100        let result = filter.filter_tags(tags);
101        assert_eq!(result.len(), 2);
102        assert!(result.iter().any(|t| t.name == "v1.0.0"));
103        assert!(result.iter().any(|t| t.name == "release-2024"));
104    }
105
106    #[test]
107    fn specific_filters_to_one_tag() {
108        let tags = vec![make_tag("v1.0.0"), make_tag("v1.1.0"), make_tag("v2.0.0")];
109        let filter = ReleaseFilter::Specific("v1.1.0".to_string());
110        let result = filter.filter_tags(tags);
111        assert_eq!(result.len(), 1);
112        assert_eq!(result[0].name, "v1.1.0");
113    }
114
115    #[test]
116    fn specific_returns_empty_if_not_found() {
117        let tags = vec![make_tag("v1.0.0"), make_tag("v2.0.0")];
118        let filter = ReleaseFilter::Specific("v99.0.0".to_string());
119        let result = filter.filter_tags(tags);
120        assert!(result.is_empty());
121    }
122
123    #[test]
124    fn specific_tag_returns_name() {
125        let filter = ReleaseFilter::Specific("v1.0.0".to_string());
126        assert_eq!(filter.specific_tag(), Some("v1.0.0"));
127
128        let filter = ReleaseFilter::Unrestricted;
129        assert_eq!(filter.specific_tag(), None);
130
131        let filter = ReleaseFilter::SkipPrereleases;
132        assert_eq!(filter.specific_tag(), None);
133    }
134
135    #[test]
136    fn skips_prereleases_helper() {
137        assert!(!ReleaseFilter::Unrestricted.skips_prereleases());
138        assert!(ReleaseFilter::SkipPrereleases.skips_prereleases());
139        assert!(!ReleaseFilter::Specific("v1.0.0".to_string()).skips_prereleases());
140    }
141}