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