1use crate::release_info::{suite_to_distribution, Vendor};
3use breezyshim::branch::Branch;
4use breezyshim::commit::PyCommitReporter;
5use breezyshim::error::Error as BrzError;
6use breezyshim::tree::{Kind, Path, PyTree, Tree, WorkingTree};
7use breezyshim::workingtree::PyWorkingTree;
8use breezyshim::RevisionId;
9use debian_changelog::ChangeLog;
10
11#[derive(Debug)]
12pub enum Error {
14 UnreleasedChanges(std::path::PathBuf),
16
17 ChangelogError(debian_changelog::Error),
19
20 BrzError(breezyshim::error::Error),
22}
23
24impl std::fmt::Display for Error {
25 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
26 match self {
27 Error::UnreleasedChanges(path) => write!(f, "Unreleased changes in {}", path.display()),
28 Error::ChangelogError(e) => write!(f, "{}", e),
29 Error::BrzError(e) => write!(f, "{}", e),
30 }
31 }
32}
33
34impl From<breezyshim::error::Error> for Error {
35 fn from(e: breezyshim::error::Error) -> Self {
36 Error::BrzError(e)
37 }
38}
39
40impl From<debian_changelog::Error> for Error {
41 fn from(e: debian_changelog::Error) -> Self {
42 Error::ChangelogError(e)
43 }
44}
45
46impl std::error::Error for Error {}
47
48pub fn debcommit_release(
50 tree: &dyn PyWorkingTree,
51 committer: Option<&str>,
52 subpath: Option<&std::path::Path>,
53 message: Option<&str>,
54 vendor: Option<Vendor>,
55) -> Result<String, Error> {
56 let subpath = subpath.unwrap_or_else(|| std::path::Path::new(""));
57 let cl_path = subpath.join("debian/changelog");
58 let (message, vendor) = if let (Some(message), Some(vendor)) = (message, vendor) {
59 (message.to_string(), vendor)
60 } else {
61 let f = tree.get_file(&cl_path)?;
62 let cl = ChangeLog::read(f)?;
63 let entry = cl.iter().next().unwrap();
64 let message = if let Some(message) = message {
65 message.to_string()
66 } else {
67 format!(
68 "releasing package {} version {}",
69 entry.package().unwrap(),
70 entry.version().unwrap()
71 )
72 };
73 let vendor = vendor.unwrap_or_else(|| {
74 suite_to_distribution(
75 entry
76 .distributions()
77 .as_ref()
78 .and_then(|d| d.first())
79 .unwrap(),
80 )
81 .unwrap()
82 });
83 (message, vendor)
84 };
85 let tag_name = if let Ok(tag_name) =
86 breezyshim::debian::tree_debian_tag_name(tree, &tree.branch(), Some(subpath), Some(vendor))
87 {
88 tag_name
89 } else {
90 return Err(Error::UnreleasedChanges(cl_path));
91 };
92
93 let mut builder = tree.build_commit().message(&message);
94
95 if let Some(committer) = committer {
96 builder = builder.committer(committer);
97 }
98
99 let revid = builder.commit()?;
100 tree.branch().tags().unwrap().set_tag(&tag_name, &revid)?;
101 Ok(tag_name)
102}
103
104pub fn changelog_changes(
106 tree: &dyn Tree,
107 basis_tree: &dyn PyTree,
108 cl_path: &Path,
109) -> Result<Option<Vec<String>>, BrzError> {
110 let mut changes = vec![];
111 for change in tree.iter_changes(basis_tree, Some(&[cl_path]), None, None)? {
112 let change = change?;
113 let paths = change.path;
114 let changed_content = change.changed_content;
115 let versioned = change.versioned;
116 let kind = change.kind;
117 if !changed_content {
119 return Ok(None);
120 }
121 if !versioned.1.unwrap_or(false) {
123 return Ok(None);
124 }
125 if kind.0 != Some(Kind::File) || kind.1 != Some(Kind::File) {
127 return Ok(None);
128 }
129
130 let old_text = basis_tree.get_file_lines(&paths.0.unwrap())?;
131 let new_text = tree.get_file_lines(&paths.1.unwrap())?;
132 changes.extend(new_changelog_entries(&old_text, &new_text));
133 }
134 Ok(Some(changes))
135}
136
137pub fn strip_changelog_message(changes: &[&str]) -> Vec<String> {
150 if changes.is_empty() {
151 return vec![];
152 }
153 let mut changes = changes.to_vec();
154 while changes.last() == Some(&"") {
155 changes.pop();
156 }
157 while changes.first() == Some(&"") {
158 changes.remove(0);
159 }
160
161 let changes = changes
162 .into_iter()
163 .map(|l| lazy_regex::regex_replace!(r" |\t", l, |_| ""))
164 .collect::<Vec<_>>();
165
166 let leader_re = lazy_regex::regex!(r"^[ \t]*[*+-] ");
167 let leader_changes = changes
168 .iter()
169 .filter(|line| leader_re.is_match(line))
170 .collect::<Vec<_>>();
171
172 if leader_changes.len() == 1 {
173 changes
174 .iter()
175 .map(|line| leader_re.replace(line, "").trim_start().to_string())
176 .collect()
177 } else {
178 changes.into_iter().map(|l| l.to_string()).collect()
179 }
180}
181
182pub fn changelog_commit_message(
184 tree: &dyn Tree,
185 basis_tree: &dyn PyTree,
186 path: &Path,
187) -> Result<String, BrzError> {
188 let changes = changelog_changes(tree, basis_tree, path)?;
189 let changes = changes.unwrap_or_default();
190
191 Ok(strip_changelog_message(
192 changes
193 .iter()
194 .map(|s| s.as_str())
195 .collect::<Vec<_>>()
196 .as_slice(),
197 )
198 .concat())
199}
200
201pub fn debcommit(
213 tree: &dyn WorkingTree,
214 committer: Option<&str>,
215 subpath: &Path,
216 paths: Option<&[&Path]>,
217 reporter: Option<&dyn PyCommitReporter>,
218 message: Option<&str>,
219) -> Result<RevisionId, BrzError> {
220 let message = message.map_or_else(
221 || {
222 changelog_commit_message(
223 tree,
224 &tree.basis_tree().unwrap(),
225 &subpath.join("debian/changelog"),
226 )
227 .unwrap()
228 },
229 |m| m.to_string(),
230 );
231 let specific_files = if let Some(paths) = paths {
232 Some(paths.iter().map(|p| subpath.join(p)).collect())
233 } else if !subpath.to_str().unwrap().is_empty() {
234 Some(vec![subpath.to_path_buf()])
235 } else {
236 None
237 };
238
239 let mut builder = tree.build_commit().message(&message);
240
241 if let Some(reporter) = reporter {
242 builder = builder.reporter(reporter);
243 }
244
245 if let Some(committer) = committer {
246 builder = builder.committer(committer);
247 }
248
249 if let Some(specific_files) = specific_files {
250 builder = builder.specific_files(
251 specific_files
252 .iter()
253 .map(|p| p.as_path())
254 .collect::<Vec<_>>()
255 .as_slice(),
256 );
257 }
258
259 builder.commit()
260}
261
262pub fn new_changelog_entries(old_text: &[Vec<u8>], new_text: &[Vec<u8>]) -> Vec<String> {
264 let mut sm = difflib::sequencematcher::SequenceMatcher::new(old_text, new_text);
265 let mut changes = vec![];
266 for group in sm.get_grouped_opcodes(0) {
267 let (j1, j2) = (group[0].second_start, group.last().unwrap().second_end);
268 for line in new_text[j1..j2].iter() {
269 if line.starts_with(b" ") {
270 changes.push(String::from_utf8_lossy(line).to_string());
272 }
273 }
274 }
275 changes
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 mod strip_changelog_message {
282 use super::*;
283
284 #[test]
285 fn test_empty() {
286 assert_eq!(strip_changelog_message(&[]), Vec::<String>::new());
287 }
288
289 #[test]
290 fn test_empty_changes() {
291 assert_eq!(strip_changelog_message(&[""]), Vec::<String>::new());
292 }
293
294 #[test]
295 fn test_removes_leading_whitespace() {
296 assert_eq!(
297 strip_changelog_message(&["foo", " bar", "\tbaz", " bang"]),
298 vec!["foo", "bar", "baz", " bang"],
299 );
300 }
301
302 #[test]
303 fn test_removes_star_if_one() {
304 assert_eq!(strip_changelog_message(&[" * foo"]), ["foo"]);
305 assert_eq!(strip_changelog_message(&["\t* foo"]), ["foo"]);
306 assert_eq!(strip_changelog_message(&[" + foo"]), ["foo"]);
307 assert_eq!(strip_changelog_message(&[" - foo"]), ["foo"]);
308 assert_eq!(strip_changelog_message(&[" * foo"]), ["foo"]);
309 assert_eq!(
310 strip_changelog_message(&[" * foo", " bar"]),
311 ["foo", "bar"]
312 );
313 }
314
315 #[test]
316 fn test_leaves_start_if_multiple() {
317 assert_eq!(
318 strip_changelog_message(&[" * foo", " * bar"]),
319 ["* foo", "* bar"]
320 );
321 assert_eq!(
322 strip_changelog_message(&[" * foo", " + bar"]),
323 ["* foo", "+ bar"]
324 );
325 assert_eq!(
326 strip_changelog_message(&[" * foo", " bar", " * baz"]),
327 ["* foo", "bar", "* baz"],
328 );
329 }
330 }
331}