1#![deny(missing_docs)]
3use breezyshim::branch::Branch;
4use breezyshim::dirty_tracker::DirtyTreeTracker;
5use breezyshim::error::Error;
6use breezyshim::tree::{PyTree, Tree, TreeChange, WorkingTree};
7use breezyshim::workingtree::PyWorkingTree;
8use breezyshim::workspace::reset_tree_with_dirty_tracker;
9
10pub mod abstract_control;
11pub mod changelog;
12pub mod config;
13pub mod control;
14pub mod debcargo;
15pub mod debcommit;
16pub mod debhelper;
17pub mod detect_gbp_dch;
18pub mod editor;
19pub mod lintian;
20pub mod maintscripts;
21pub mod patches;
22pub mod publish;
23pub mod relations;
24pub mod release_info;
25pub mod rules;
26pub mod vcs;
27pub mod vendor;
28pub mod versions;
29
30pub const DEFAULT_BUILDER: &str = "sbuild --no-clean-source";
33
34#[derive(Debug)]
35pub enum ApplyError<R, E> {
37 CallbackError(E),
39 BrzError(Error),
41 NoChanges(R),
43}
44
45impl<R, E> From<Error> for ApplyError<R, E> {
46 fn from(e: Error) -> Self {
47 ApplyError::BrzError(e)
48 }
49}
50
51pub fn apply_or_revert<R, E, T, U>(
70 local_tree: &T,
71 subpath: &std::path::Path,
72 basis_tree: &U,
73 dirty_tracker: Option<&mut DirtyTreeTracker>,
74 applier: impl FnOnce(&std::path::Path) -> Result<R, E>,
75) -> Result<(R, Vec<TreeChange>, Option<Vec<std::path::PathBuf>>), ApplyError<R, E>>
76where
77 T: PyWorkingTree + breezyshim::tree::PyMutableTree,
78 U: PyTree,
79{
80 let r = match applier(local_tree.abspath(subpath).unwrap().as_path()) {
81 Ok(r) => r,
82 Err(e) => {
83 reset_tree_with_dirty_tracker(
84 local_tree,
85 Some(basis_tree),
86 Some(subpath),
87 dirty_tracker,
88 )
89 .unwrap();
90 return Err(ApplyError::CallbackError(e));
91 }
92 };
93
94 let specific_files = if let Some(relpaths) = dirty_tracker.and_then(|x| x.relpaths()) {
95 let mut relpaths: Vec<_> = relpaths.into_iter().collect();
96 relpaths.sort();
97 local_tree.add(
100 relpaths
101 .iter()
102 .filter_map(|p| {
103 if local_tree.has_filename(p) && local_tree.is_ignored(p).is_some() {
104 Some(p.as_path())
105 } else {
106 None
107 }
108 })
109 .collect::<Vec<_>>()
110 .as_slice(),
111 )?;
112 let specific_files = relpaths
113 .into_iter()
114 .filter(|p| local_tree.is_versioned(p))
115 .collect::<Vec<_>>();
116 if specific_files.is_empty() {
117 return Err(ApplyError::NoChanges(r));
118 }
119 Some(specific_files)
120 } else {
121 local_tree.smart_add(&[local_tree.abspath(subpath).unwrap().as_path()])?;
122 if subpath.as_os_str().is_empty() {
123 None
124 } else {
125 Some(vec![subpath.to_path_buf()])
126 }
127 };
128
129 if local_tree.supports_setting_file_ids() {
130 let local_lock = local_tree.lock_read().unwrap();
131 let basis_lock = basis_tree.lock_read().unwrap();
132 breezyshim::rename_map::guess_renames(basis_tree, local_tree).unwrap();
133 std::mem::drop(basis_lock);
134 std::mem::drop(local_lock);
135 }
136
137 let specific_files_ref = specific_files
138 .as_ref()
139 .map(|fs| fs.iter().map(|p| p.as_path()).collect::<Vec<_>>());
140
141 let changes = local_tree
142 .iter_changes(
143 basis_tree,
144 specific_files_ref.as_deref(),
145 Some(false),
146 Some(true),
147 )?
148 .collect::<Result<Vec<_>, _>>()?;
149
150 if local_tree.get_parent_ids()?.len() <= 1 && changes.is_empty() {
151 return Err(ApplyError::NoChanges(r));
152 }
153
154 Ok((r, changes, specific_files))
155}
156
157pub enum ChangelogError {
159 NotDebianPackage(std::path::PathBuf),
161}
162
163impl std::fmt::Display for ChangelogError {
164 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
165 match self {
166 ChangelogError::NotDebianPackage(path) => {
167 write!(f, "Not a Debian package: {}", path.display())
168 }
169 }
170 }
171}
172
173pub fn add_changelog_entry<T: WorkingTree>(
180 working_tree: &T,
181 changelog_path: &std::path::Path,
182 entry: &[&str],
183) -> Result<(), crate::editor::EditorError> {
184 use crate::editor::{Editor, MutableTreeEdit};
185 let mut cl =
186 working_tree.edit_file::<debian_changelog::ChangeLog>(changelog_path, false, true)?;
187
188 cl.try_auto_add_change(
189 entry,
190 debian_changelog::get_maintainer().unwrap(),
191 Some(chrono::Utc::now().fixed_offset()),
192 None,
193 )
194 .unwrap();
195
196 cl.commit()?;
197
198 Ok(())
199}
200
201#[derive(
202 Clone,
203 Copy,
204 PartialEq,
205 Eq,
206 Debug,
207 Default,
208 PartialOrd,
209 Ord,
210 serde::Serialize,
211 serde::Deserialize,
212)]
213pub enum Certainty {
215 #[serde(rename = "possible")]
216 Possible,
218 #[serde(rename = "likely")]
219 Likely,
221 #[serde(rename = "confident")]
222 Confident,
224 #[default]
225 #[serde(rename = "certain")]
226 Certain,
228}
229
230impl std::str::FromStr for Certainty {
231 type Err = String;
232
233 fn from_str(value: &str) -> Result<Self, Self::Err> {
234 match value {
235 "certain" => Ok(Certainty::Certain),
236 "confident" => Ok(Certainty::Confident),
237 "likely" => Ok(Certainty::Likely),
238 "possible" => Ok(Certainty::Possible),
239 _ => Err(format!("Invalid certainty: {}", value)),
240 }
241 }
242}
243
244impl std::fmt::Display for Certainty {
245 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246 match self {
247 Certainty::Certain => write!(f, "certain"),
248 Certainty::Confident => write!(f, "confident"),
249 Certainty::Likely => write!(f, "likely"),
250 Certainty::Possible => write!(f, "possible"),
251 }
252 }
253}
254
255pub fn certainty_sufficient(
266 actual_certainty: Certainty,
267 minimum_certainty: Option<Certainty>,
268) -> bool {
269 if let Some(minimum_certainty) = minimum_certainty {
270 actual_certainty >= minimum_certainty
271 } else {
272 true
273 }
274}
275
276pub fn min_certainty(certainties: &[Certainty]) -> Option<Certainty> {
278 certainties.iter().min().cloned()
279}
280
281pub fn get_committer(working_tree: &dyn PyWorkingTree) -> String {
283 if let Some(committer) = breezyshim::git::get_committer(working_tree) {
284 return committer;
285 }
286
287 let config = working_tree.branch().get_config_stack();
288
289 config
290 .get("email")
291 .unwrap()
292 .map(|x| x.to_string())
293 .unwrap_or_default()
294}
295
296pub fn control_file_present(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
307 for name in [
308 "debian/control",
309 "debian/control.in",
310 "control",
311 "control.in",
312 "debian/debcargo.toml",
313 ] {
314 let name = subpath.join(name);
315 if tree.has_filename(name.as_path()) {
316 return true;
317 }
318 }
319 false
320}
321
322pub fn is_debcargo_package(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
324 tree.has_filename(subpath.join("debian/debcargo.toml").as_path())
325}
326
327pub fn control_files_in_root(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
329 let debian_path = subpath.join("debian");
330 if tree.has_filename(debian_path.as_path()) {
331 return false;
332 }
333
334 let control_path = subpath.join("control");
335 if tree.has_filename(control_path.as_path()) {
336 return true;
337 }
338
339 tree.has_filename(subpath.join("control.in").as_path())
340}
341
342pub fn parseaddr(input: &str) -> Option<(Option<String>, Option<String>)> {
344 if let Some((_whole, name, addr)) =
345 lazy_regex::regex_captures!(r"(?:(?P<name>[^<]*)\s*<)?(?P<addr>[^<>]*)>?", input)
346 {
347 let name = match name.trim() {
348 "" => None,
349 x => Some(x.to_string()),
350 };
351 let addr = match addr.trim() {
352 "" => None,
353 x => Some(x.to_string()),
354 };
355
356 return Some((name, addr));
357 } else if let Some((_whole, addr)) = lazy_regex::regex_captures!(r"(?P<addr>[^<>]*)", input) {
358 let addr = Some(addr.trim().to_string());
359
360 return Some((None, addr));
361 } else if input.is_empty() {
362 return None;
363 } else if !input.contains('<') {
364 return Some((None, Some(input.to_string())));
365 }
366 None
367}
368
369pub fn gbp_dch(path: &std::path::Path) -> Result<(), std::io::Error> {
371 let mut cmd = std::process::Command::new("gbp");
372 cmd.arg("dch").arg("--ignore-branch");
373 cmd.current_dir(path);
374 let status = cmd.status()?;
375 if !status.success() {
376 return Err(std::io::Error::other(format!("gbp dch failed: {}", status)));
377 }
378 Ok(())
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use serial_test::serial;
385
386 #[test]
387 fn test_parseaddr() {
388 assert_eq!(
389 parseaddr("foo <bar@example.com>").unwrap(),
390 (Some("foo".to_string()), Some("bar@example.com".to_string()))
391 );
392 assert_eq!(parseaddr("foo").unwrap(), (None, Some("foo".to_string())));
393 }
394
395 #[serial]
396 #[test]
397 fn test_git_env() {
398 let td = tempfile::tempdir().unwrap();
399 let cd = breezyshim::controldir::create_standalone_workingtree(td.path(), "git").unwrap();
400
401 let old_name = std::env::var("GIT_COMMITTER_NAME").ok();
402 let old_email = std::env::var("GIT_COMMITTER_EMAIL").ok();
403
404 std::env::set_var("GIT_COMMITTER_NAME", "Some Git Committer");
405 std::env::set_var("GIT_COMMITTER_EMAIL", "committer@example.com");
406
407 let committer = get_committer(&cd);
408
409 if let Some(old_name) = old_name {
410 std::env::set_var("GIT_COMMITTER_NAME", old_name);
411 } else {
412 std::env::remove_var("GIT_COMMITTER_NAME");
413 }
414
415 if let Some(old_email) = old_email {
416 std::env::set_var("GIT_COMMITTER_EMAIL", old_email);
417 } else {
418 std::env::remove_var("GIT_COMMITTER_EMAIL");
419 }
420
421 assert_eq!("Some Git Committer <committer@example.com>", committer);
422 }
423
424 #[serial]
425 #[test]
426 fn test_git_config() {
427 let td = tempfile::tempdir().unwrap();
428 let cd = breezyshim::controldir::create_standalone_workingtree(td.path(), "git").unwrap();
429
430 std::fs::write(
431 td.path().join(".git/config"),
432 b"[user]\nname = Some Git Committer\nemail = other@example.com",
433 )
434 .unwrap();
435
436 assert_eq!(get_committer(&cd), "Some Git Committer <other@example.com>");
437 }
438
439 #[test]
440 fn test_min_certainty() {
441 assert_eq!(None, min_certainty(&[]));
442 assert_eq!(
443 Some(Certainty::Certain),
444 min_certainty(&[Certainty::Certain])
445 );
446 assert_eq!(
447 Some(Certainty::Possible),
448 min_certainty(&[Certainty::Possible])
449 );
450 assert_eq!(
451 Some(Certainty::Possible),
452 min_certainty(&[Certainty::Possible, Certainty::Certain])
453 );
454 assert_eq!(
455 Some(Certainty::Likely),
456 min_certainty(&[Certainty::Likely, Certainty::Certain])
457 );
458 assert_eq!(
459 Some(Certainty::Possible),
460 min_certainty(&[Certainty::Likely, Certainty::Certain, Certainty::Possible])
461 );
462 }
463}