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}