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