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