disperse/
lib.rs

1pub mod cargo;
2pub mod config;
3pub mod custom;
4pub mod github;
5pub mod launchpad;
6pub mod manpage;
7pub mod news_file;
8pub mod project_config;
9pub mod python;
10pub mod version;
11use breezyshim::branch::Branch;
12use breezyshim::repository::Repository;
13use breezyshim::workingtree::WorkingTree;
14use log::warn;
15use std::path::{Path, PathBuf};
16
17pub use version::Version;
18
19pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Status {
23    Final,
24    Dev,
25}
26
27#[cfg(feature = "pyo3")]
28impl pyo3::FromPyObject<'_> for Status {
29    fn extract_bound(ob: &pyo3::Bound<pyo3::PyAny>) -> pyo3::PyResult<Self> {
30        use pyo3::prelude::*;
31        let s = ob.extract::<String>()?;
32        s.parse()
33            .map_err(pyo3::PyErr::new::<pyo3::exceptions::PyValueError, _>)
34    }
35}
36
37#[cfg(feature = "pyo3")]
38impl<'py> pyo3::IntoPyObject<'py> for Status {
39    type Target = pyo3::types::PyString;
40    type Output = pyo3::Bound<'py, Self::Target>;
41    type Error = std::convert::Infallible;
42
43    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
44        Ok(pyo3::types::PyString::new(py, &self.to_string()))
45    }
46}
47
48#[cfg(feature = "pyo3")]
49impl<'py> pyo3::IntoPyObject<'py> for &Status {
50    type Target = pyo3::types::PyString;
51    type Output = pyo3::Bound<'py, Self::Target>;
52    type Error = std::convert::Infallible;
53
54    fn into_pyobject(self, py: pyo3::Python<'py>) -> Result<Self::Output, Self::Error> {
55        Ok(pyo3::types::PyString::new(py, &self.to_string()))
56    }
57}
58
59impl ToString for Status {
60    fn to_string(&self) -> String {
61        match self {
62            Status::Final => "final".to_string(),
63            Status::Dev => "dev".to_string(),
64        }
65    }
66}
67
68impl std::str::FromStr for Status {
69    type Err = String;
70    fn from_str(s: &str) -> Result<Self, Self::Err> {
71        match s {
72            "final" => Ok(Status::Final),
73            "dev" => Ok(Status::Dev),
74            _ => Err(format!("invalid status: {}", s)),
75        }
76    }
77}
78
79pub fn check_new_revisions(
80    branch: &dyn Branch,
81    news_file_path: Option<&std::path::Path>,
82) -> std::result::Result<bool, Box<dyn std::error::Error>> {
83    let tags = branch.tags().unwrap().get_reverse_tag_dict()?;
84    let lock = branch.lock_read();
85    let repository = branch.repository();
86    let graph = repository.get_graph();
87    let from_revid = graph
88        .iter_lefthand_ancestry(&branch.last_revision(), None)?
89        .find_map(|revid| {
90            let revid = revid.ok()?;
91            if tags.contains_key(&revid) {
92                Some(revid)
93            } else {
94                None
95            }
96        });
97
98    log::debug!(
99        "Checking revisions between {} and {}",
100        branch.last_revision(),
101        from_revid
102            .as_ref()
103            .map(|r| r.to_string())
104            .unwrap_or_else(|| "null".to_string())
105    );
106
107    if from_revid == Some(branch.last_revision()) {
108        return Ok(false);
109    }
110
111    let from_tree = from_revid
112        .map(|r| repository.revision_tree(&r))
113        .unwrap_or(repository.revision_tree(&breezyshim::revisionid::RevisionId::null()))?;
114
115    let last_tree = branch.basis_tree()?;
116    let mut delta = breezyshim::intertree::get(&from_tree, &last_tree).compare();
117    if let Some(news_file_path) = news_file_path {
118        for (i, m) in delta.modified.iter().enumerate() {
119            if (m.path.0.as_deref(), m.path.1.as_deref())
120                == (Some(news_file_path), Some(news_file_path))
121            {
122                delta.modified.remove(i);
123                break;
124            }
125        }
126    }
127    std::mem::drop(lock);
128    Ok(delta.has_changed())
129}
130
131pub fn find_last_version_in_tags(
132    branch: &dyn breezyshim::branch::Branch,
133    tag_name: &str,
134) -> Result<(Option<Version>, Option<Status>), Box<dyn std::error::Error>> {
135    let rev_tag_dict = branch.tags()?.get_reverse_tag_dict()?;
136    let graph = branch.repository().get_graph();
137
138    let (revid, tags) = graph
139        .iter_lefthand_ancestry(&branch.last_revision(), None)?
140        .find_map(|r| {
141            let revid = r.ok()?;
142            rev_tag_dict.get(&revid).map(|tags| (revid, tags))
143        })
144        .unwrap();
145
146    for tag in tags {
147        let release = match crate::version::unexpand_tag(tag_name, tag) {
148            Ok(release) => release,
149            Err(_) => continue,
150        };
151        let status = if revid == branch.last_revision() {
152            Status::Final
153        } else {
154            Status::Dev
155        };
156        return Ok((Some(release), Some(status)));
157    }
158
159    warn!("Unable to find any tags matching {}", tag_name);
160    Ok((None, None))
161}
162
163pub fn find_last_version_in_files(
164    tree: &dyn WorkingTree,
165    cfg: &project_config::ProjectConfig,
166) -> Result<Option<(crate::version::Version, Option<Status>)>, Box<dyn std::error::Error>> {
167    if tree.has_filename(Path::new("Cargo.toml")) {
168        log::debug!("Reading version from Cargo.toml");
169        return Ok(Some((cargo::find_version(tree)?, None)));
170    }
171    if tree.has_filename(Path::new("pyproject.toml")) {
172        log::debug!("Reading version from pyproject.toml");
173        if let Some(version) = python::find_version_in_pyproject_toml(tree)? {
174            return Ok(Some((version, None)));
175        }
176        if python::pyproject_uses_hatch_vcs(tree)? {
177            let version = if let Some(version) = python::find_hatch_vcs_version(tree) {
178                version
179            } else {
180                unimplemented!("hatch in use but unable to find hatch vcs version");
181            };
182            return Ok(Some((version, None)));
183        }
184    }
185    for update_cfg in cfg.update_version.as_ref().unwrap_or(&Vec::new()) {
186        let path = &update_cfg.path;
187        let new_line = &update_cfg.new_line;
188        log::debug!("Reading version from {}", path.display());
189        let f = tree.get_file(path).unwrap();
190        use std::io::BufRead;
191        let buf = std::io::BufReader::new(f);
192        let lines = buf.lines().map(|l| l.unwrap()).collect::<Vec<_>>();
193        let (v, s) = custom::reverse_version(
194            new_line.as_str(),
195            lines
196                .iter()
197                .map(|l| l.as_str())
198                .collect::<Vec<_>>()
199                .as_slice(),
200        );
201        if let Some(v) = v {
202            return Ok(Some((v, s)));
203        }
204    }
205    Ok(None)
206}
207
208#[derive(Debug)]
209pub enum FindPendingVersionError {
210    OddPendingVersion(String),
211    NoUnreleasedChanges,
212    Other(Box<dyn std::error::Error>),
213    NotFound,
214}
215
216impl std::fmt::Display for FindPendingVersionError {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        match self {
219            Self::OddPendingVersion(e) => {
220                write!(f, "Odd pending version: {}", e)
221            }
222            Self::NotFound => {
223                write!(f, "No pending version found")
224            }
225            Self::Other(e) => {
226                write!(f, "Other error: {}", e)
227            }
228            Self::NoUnreleasedChanges => {
229                write!(f, "No unreleased changes")
230            }
231        }
232    }
233}
234
235impl std::error::Error for FindPendingVersionError {}
236
237pub fn find_pending_version(
238    tree: &dyn breezyshim::tree::Tree,
239    cfg: &project_config::ProjectConfig,
240) -> Result<Version, FindPendingVersionError> {
241    if let Some(news_file) = cfg.news_file.as_ref() {
242        match news_file::tree_news_find_pending(tree, news_file) {
243            Ok(Some(version)) => Ok(version.parse().unwrap()),
244            Ok(None) => Err(FindPendingVersionError::NoUnreleasedChanges),
245            Err(news_file::Error::OddVersion(e)) => {
246                Err(FindPendingVersionError::OddPendingVersion(e))
247            }
248            Err(news_file::Error::PendingExists { .. }) => {
249                unreachable!();
250            }
251            Err(e) => Err(FindPendingVersionError::Other(Box::new(e))),
252        }
253    } else {
254        Err(FindPendingVersionError::NotFound)
255    }
256}
257
258pub fn drop_segment_parameters(u: &url::Url) -> url::Url {
259    breezyshim::urlutils::split_segment_parameters(
260        &u.as_str().trim_end_matches('/').parse().unwrap(),
261    )
262    .0
263}
264
265#[test]
266fn test_drop_segment_parameters() {
267    assert_eq!(
268        drop_segment_parameters(&"https://example.com/foo/bar,baz=quux".parse().unwrap()),
269        "https://example.com/foo/bar".parse().unwrap()
270    );
271    assert_eq!(
272        drop_segment_parameters(&"https://example.com/foo/bar,baz=quux#frag".parse().unwrap()),
273        "https://example.com/foo/bar".parse().unwrap()
274    );
275    assert_eq!(
276        drop_segment_parameters(
277            &"https://example.com/foo/bar,baz=quux#frag?frag2"
278                .parse()
279                .unwrap()
280        ),
281        "https://example.com/foo/bar".parse().unwrap()
282    );
283}
284
285pub fn iter_glob<'a>(
286    local_tree: &'a dyn WorkingTree,
287    pattern: &str,
288) -> impl Iterator<Item = PathBuf> + 'a {
289    let abspath = local_tree.basedir();
290
291    glob::glob(format!("{}/{}", abspath.to_str().unwrap(), pattern).as_str())
292        .unwrap()
293        .filter_map(|e| e.ok())
294        .map(|path| local_tree.relpath(path.as_path()).unwrap())
295        .filter(|p| !local_tree.is_control_filename(p))
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn test_iter_glob() {
304        let td = tempfile::tempdir().unwrap();
305        let local_tree = breezyshim::controldir::create_standalone_workingtree(
306            td.path(),
307            &breezyshim::controldir::ControlDirFormat::default(),
308        )
309        .unwrap();
310        std::fs::write(local_tree.basedir().join("foo"), "").unwrap();
311        std::fs::write(local_tree.basedir().join("bar"), "").unwrap();
312        assert_eq!(
313            iter_glob(&local_tree, "*").collect::<Vec<_>>(),
314            vec![PathBuf::from("bar"), PathBuf::from("foo")]
315        );
316        assert_eq!(
317            iter_glob(&local_tree, "foo").collect::<Vec<_>>(),
318            vec![PathBuf::from("foo")]
319        );
320        assert_eq!(
321            iter_glob(&local_tree, "bar").collect::<Vec<_>>(),
322            vec![PathBuf::from("bar")]
323        );
324        assert_eq!(
325            iter_glob(&local_tree, "baz").collect::<Vec<_>>(),
326            Vec::<PathBuf>::new()
327        );
328        assert_eq!(
329            iter_glob(&local_tree, "*o").collect::<Vec<_>>(),
330            vec![PathBuf::from("foo")]
331        );
332    }
333}