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