1#![deny(missing_docs)]
3use breezyshim::branch::Branch;
4use breezyshim::dirty_tracker::DirtyTreeTracker;
5use breezyshim::error::Error;
6#[cfg(feature = "python")]
7use breezyshim::repository::PyRepository;
8use breezyshim::tree::{PyTree, Tree, TreeChange, WorkingTree};
9use breezyshim::workingtree::PyWorkingTree;
10use breezyshim::workspace::reset_tree_with_dirty_tracker;
11
12pub mod abstract_control;
13pub mod benfile;
14pub mod changelog;
15pub mod config;
16pub mod control;
17pub mod debcargo;
18pub mod debcommit;
19pub mod debhelper;
20pub mod detect_gbp_dch;
21pub mod editor;
22pub mod lintian;
23pub mod maintscripts;
24pub mod patches;
25pub mod publish;
26pub mod relations;
27pub mod release_info;
28pub mod rules;
29pub mod salsa;
30pub mod snapshot;
31pub mod transition;
32#[cfg(feature = "udd")]
33pub mod udd;
34pub mod vcs;
35pub mod vendor;
36pub mod versions;
37#[cfg(feature = "udd")]
38pub mod wnpp;
39
40pub const DEFAULT_BUILDER: &str = "sbuild --no-clean-source";
43
44#[derive(Debug)]
45pub enum ApplyError<R, E> {
47 CallbackError(E),
49 BrzError(Error),
51 NoChanges(R),
53}
54
55impl<R, E> From<Error> for ApplyError<R, E> {
56 fn from(e: Error) -> Self {
57 ApplyError::BrzError(e)
58 }
59}
60
61pub fn apply_or_revert<R, E, T, U>(
80 local_tree: &T,
81 subpath: &std::path::Path,
82 basis_tree: &U,
83 dirty_tracker: Option<&mut DirtyTreeTracker>,
84 applier: impl FnOnce(&std::path::Path) -> Result<R, E>,
85) -> Result<(R, Vec<TreeChange>, Option<Vec<std::path::PathBuf>>), ApplyError<R, E>>
86where
87 T: PyWorkingTree + breezyshim::tree::PyMutableTree,
88 U: PyTree,
89{
90 let r = match applier(local_tree.abspath(subpath).unwrap().as_path()) {
91 Ok(r) => r,
92 Err(e) => {
93 reset_tree_with_dirty_tracker(
94 local_tree,
95 Some(basis_tree),
96 Some(subpath),
97 dirty_tracker,
98 )
99 .unwrap();
100 return Err(ApplyError::CallbackError(e));
101 }
102 };
103
104 let specific_files = if let Some(relpaths) = dirty_tracker.and_then(|x| x.relpaths()) {
105 let mut relpaths: Vec<_> = relpaths.into_iter().collect();
106 relpaths.sort();
107 local_tree.add(
110 relpaths
111 .iter()
112 .filter_map(|p| {
113 if local_tree.has_filename(p) && local_tree.is_ignored(p).is_some() {
114 Some(p.as_path())
115 } else {
116 None
117 }
118 })
119 .collect::<Vec<_>>()
120 .as_slice(),
121 )?;
122 let specific_files = relpaths
123 .into_iter()
124 .filter(|p| local_tree.is_versioned(p))
125 .collect::<Vec<_>>();
126 if specific_files.is_empty() {
127 return Err(ApplyError::NoChanges(r));
128 }
129 Some(specific_files)
130 } else {
131 local_tree.smart_add(&[local_tree.abspath(subpath).unwrap().as_path()])?;
132 if subpath.as_os_str().is_empty() {
133 None
134 } else {
135 Some(vec![subpath.to_path_buf()])
136 }
137 };
138
139 if local_tree.supports_setting_file_ids() {
140 let local_lock = local_tree.lock_read().unwrap();
141 let basis_lock = basis_tree.lock_read().unwrap();
142 breezyshim::rename_map::guess_renames(basis_tree, local_tree).unwrap();
143 std::mem::drop(basis_lock);
144 std::mem::drop(local_lock);
145 }
146
147 let specific_files_ref = specific_files
148 .as_ref()
149 .map(|fs| fs.iter().map(|p| p.as_path()).collect::<Vec<_>>());
150
151 let changes = local_tree
152 .iter_changes(
153 basis_tree,
154 specific_files_ref.as_deref(),
155 Some(false),
156 Some(true),
157 )?
158 .collect::<Result<Vec<_>, _>>()?;
159
160 if local_tree.get_parent_ids()?.len() <= 1 && changes.is_empty() {
161 return Err(ApplyError::NoChanges(r));
162 }
163
164 Ok((r, changes, specific_files))
165}
166
167pub enum ChangelogError {
169 NotDebianPackage(std::path::PathBuf),
171}
172
173impl std::fmt::Display for ChangelogError {
174 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
175 match self {
176 ChangelogError::NotDebianPackage(path) => {
177 write!(f, "Not a Debian package: {}", path.display())
178 }
179 #[cfg(feature = "python")]
180 ChangelogError::Python(e) => write!(f, "{}", e),
181 }
182 }
183}
184
185pub fn add_changelog_entry<T: WorkingTree>(
192 working_tree: &T,
193 changelog_path: &std::path::Path,
194 entry: &[&str],
195) -> Result<(), crate::editor::EditorError> {
196 use crate::editor::{Editor, MutableTreeEdit};
197 let mut cl =
198 working_tree.edit_file::<debian_changelog::ChangeLog>(changelog_path, false, true)?;
199
200 cl.auto_add_change(
201 entry,
202 debian_changelog::get_maintainer().unwrap(),
203 None,
204 None,
205 );
206
207 cl.commit()?;
208
209 Ok(())
210}
211
212#[derive(
213 Clone,
214 Copy,
215 PartialEq,
216 Eq,
217 Debug,
218 Default,
219 PartialOrd,
220 Ord,
221 serde::Serialize,
222 serde::Deserialize,
223)]
224pub enum Certainty {
226 #[serde(rename = "possible")]
227 Possible,
229 #[serde(rename = "likely")]
230 Likely,
232 #[serde(rename = "confident")]
233 Confident,
235 #[default]
236 #[serde(rename = "certain")]
237 Certain,
239}
240
241impl std::str::FromStr for Certainty {
242 type Err = String;
243
244 fn from_str(value: &str) -> Result<Self, Self::Err> {
245 match value {
246 "certain" => Ok(Certainty::Certain),
247 "confident" => Ok(Certainty::Confident),
248 "likely" => Ok(Certainty::Likely),
249 "possible" => Ok(Certainty::Possible),
250 _ => Err(format!("Invalid certainty: {}", value)),
251 }
252 }
253}
254
255impl std::fmt::Display for Certainty {
256 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257 match self {
258 Certainty::Certain => write!(f, "certain"),
259 Certainty::Confident => write!(f, "confident"),
260 Certainty::Likely => write!(f, "likely"),
261 Certainty::Possible => write!(f, "possible"),
262 }
263 }
264}
265
266pub fn certainty_sufficient(
277 actual_certainty: Certainty,
278 minimum_certainty: Option<Certainty>,
279) -> bool {
280 if let Some(minimum_certainty) = minimum_certainty {
281 actual_certainty >= minimum_certainty
282 } else {
283 true
284 }
285}
286
287pub fn min_certainty(certainties: &[Certainty]) -> Option<Certainty> {
289 certainties.iter().min().cloned()
290}
291
292#[cfg(feature = "python")]
293fn get_git_committer(working_tree: &dyn PyWorkingTree) -> Option<String> {
294 pyo3::Python::attach(|py| {
295 let repo = working_tree.branch().repository();
296 let git = match repo.to_object(py).getattr(py, "_git") {
297 Ok(x) => Some(x),
298 Err(e) if e.is_instance_of::<pyo3::exceptions::PyAttributeError>(py) => None,
299 Err(e) => {
300 return Err(e);
301 }
302 };
303
304 if let Some(git) = git {
305 let cs = git.call_method0(py, "get_config_stack")?;
306
307 let mut user = std::env::var("GIT_COMMITTER_NAME").ok();
308 let mut email = std::env::var("GIT_COMMITTER_EMAIL").ok();
309 if user.is_none() {
310 match cs.call_method1(py, "get", (("user",), "name")) {
311 Ok(x) => {
312 user = Some(
313 std::str::from_utf8(x.extract::<&[u8]>(py)?)
314 .unwrap()
315 .to_string(),
316 );
317 }
318 Err(e) if e.is_instance_of::<pyo3::exceptions::PyKeyError>(py) => {
319 }
321 Err(e) => {
322 return Err(e);
323 }
324 };
325 }
326 if email.is_none() {
327 match cs.call_method1(py, "get", (("user",), "email")) {
328 Ok(x) => {
329 email = Some(
330 std::str::from_utf8(x.extract::<&[u8]>(py)?)
331 .unwrap()
332 .to_string(),
333 );
334 }
335 Err(e) if e.is_instance_of::<pyo3::exceptions::PyKeyError>(py) => {
336 }
338 Err(e) => {
339 return Err(e);
340 }
341 };
342 }
343
344 if let (Some(user), Some(email)) = (user, email) {
345 return Ok(Some(format!("{} <{}>", user, email)));
346 }
347
348 let gs = breezyshim::config::global_stack().unwrap();
349
350 Ok(gs
351 .get("email")?
352 .map(|email| email.extract::<String>(py).unwrap()))
353 } else {
354 Ok(None)
355 }
356 })
357 .unwrap()
358}
359
360pub fn get_committer(working_tree: &dyn PyWorkingTree) -> String {
362 #[cfg(feature = "python")]
363 if let Some(committer) = get_git_committer(working_tree) {
364 return committer;
365 }
366
367 let config = working_tree.branch().get_config_stack();
368
369 config
370 .get("email")
371 .unwrap()
372 .map(|x| x.to_string())
373 .unwrap_or_default()
374}
375
376pub fn control_file_present(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
387 for name in [
388 "debian/control",
389 "debian/control.in",
390 "control",
391 "control.in",
392 "debian/debcargo.toml",
393 ] {
394 let name = subpath.join(name);
395 if tree.has_filename(name.as_path()) {
396 return true;
397 }
398 }
399 false
400}
401
402pub fn is_debcargo_package(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
404 tree.has_filename(subpath.join("debian/debcargo.toml").as_path())
405}
406
407pub fn control_files_in_root(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
409 let debian_path = subpath.join("debian");
410 if tree.has_filename(debian_path.as_path()) {
411 return false;
412 }
413
414 let control_path = subpath.join("control");
415 if tree.has_filename(control_path.as_path()) {
416 return true;
417 }
418
419 tree.has_filename(subpath.join("control.in").as_path())
420}
421
422pub fn parseaddr(input: &str) -> Option<(Option<String>, Option<String>)> {
424 if let Some((_whole, name, addr)) =
425 lazy_regex::regex_captures!(r"(?:(?P<name>[^<]*)\s*<)?(?P<addr>[^<>]*)>?", input)
426 {
427 let name = match name.trim() {
428 "" => None,
429 x => Some(x.to_string()),
430 };
431 let addr = match addr.trim() {
432 "" => None,
433 x => Some(x.to_string()),
434 };
435
436 return Some((name, addr));
437 } else if let Some((_whole, addr)) = lazy_regex::regex_captures!(r"(?P<addr>[^<>]*)", input) {
438 let addr = Some(addr.trim().to_string());
439
440 return Some((None, addr));
441 } else if input.is_empty() {
442 return None;
443 } else if !input.contains('<') {
444 return Some((None, Some(input.to_string())));
445 }
446 None
447}
448
449pub fn gbp_dch(path: &std::path::Path) -> Result<(), std::io::Error> {
451 let mut cmd = std::process::Command::new("gbp");
452 cmd.arg("dch").arg("--ignore-branch");
453 cmd.current_dir(path);
454 let status = cmd.status()?;
455 if !status.success() {
456 return Err(std::io::Error::new(
457 std::io::ErrorKind::Other,
458 format!("gbp dch failed: {}", status),
459 ));
460 }
461 Ok(())
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467 use serial_test::serial;
468
469 #[test]
470 fn test_parseaddr() {
471 assert_eq!(
472 parseaddr("foo <bar@example.com>").unwrap(),
473 (Some("foo".to_string()), Some("bar@example.com".to_string()))
474 );
475 assert_eq!(parseaddr("foo").unwrap(), (None, Some("foo".to_string())));
476 }
477
478 #[cfg(feature = "python")]
479 #[serial]
480 #[test]
481 fn test_git_env() {
482 let td = tempfile::tempdir().unwrap();
483 let cd = breezyshim::controldir::create_standalone_workingtree(td.path(), "git").unwrap();
484
485 let old_name = std::env::var("GIT_COMMITTER_NAME").ok();
486 let old_email = std::env::var("GIT_COMMITTER_EMAIL").ok();
487
488 std::env::set_var("GIT_COMMITTER_NAME", "Some Git Committer");
489 std::env::set_var("GIT_COMMITTER_EMAIL", "committer@example.com");
490
491 let committer = get_committer(&cd);
492
493 if let Some(old_name) = old_name {
494 std::env::set_var("GIT_COMMITTER_NAME", old_name);
495 } else {
496 std::env::remove_var("GIT_COMMITTER_NAME");
497 }
498
499 if let Some(old_email) = old_email {
500 std::env::set_var("GIT_COMMITTER_EMAIL", old_email);
501 } else {
502 std::env::remove_var("GIT_COMMITTER_EMAIL");
503 }
504
505 assert_eq!("Some Git Committer <committer@example.com>", committer);
506 }
507
508 #[serial]
509 #[test]
510 fn test_git_config() {
511 let td = tempfile::tempdir().unwrap();
512 let cd = breezyshim::controldir::create_standalone_workingtree(td.path(), "git").unwrap();
513
514 std::fs::write(
515 td.path().join(".git/config"),
516 b"[user]\nname = Some Git Committer\nemail = other@example.com",
517 )
518 .unwrap();
519
520 assert_eq!(get_committer(&cd), "Some Git Committer <other@example.com>");
521 }
522
523 #[test]
524 fn test_min_certainty() {
525 assert_eq!(None, min_certainty(&[]));
526 assert_eq!(
527 Some(Certainty::Certain),
528 min_certainty(&[Certainty::Certain])
529 );
530 assert_eq!(
531 Some(Certainty::Possible),
532 min_certainty(&[Certainty::Possible])
533 );
534 assert_eq!(
535 Some(Certainty::Possible),
536 min_certainty(&[Certainty::Possible, Certainty::Certain])
537 );
538 assert_eq!(
539 Some(Certainty::Likely),
540 min_certainty(&[Certainty::Likely, Certainty::Certain])
541 );
542 assert_eq!(
543 Some(Certainty::Possible),
544 min_certainty(&[Certainty::Likely, Certainty::Certain, Certainty::Possible])
545 );
546 }
547}