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::attach(|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.try_auto_add_change(
233 entry,
234 debian_changelog::get_maintainer().unwrap(),
235 Some(chrono::Utc::now().fixed_offset()),
236 None,
237 )
238 .unwrap();
239
240 cl.commit()?;
241
242 Ok(())
243}
244
245#[derive(
246 Clone,
247 Copy,
248 PartialEq,
249 Eq,
250 Debug,
251 Default,
252 PartialOrd,
253 Ord,
254 serde::Serialize,
255 serde::Deserialize,
256)]
257pub enum Certainty {
259 #[serde(rename = "possible")]
260 Possible,
262 #[serde(rename = "likely")]
263 Likely,
265 #[serde(rename = "confident")]
266 Confident,
268 #[default]
269 #[serde(rename = "certain")]
270 Certain,
272}
273
274impl std::str::FromStr for Certainty {
275 type Err = String;
276
277 fn from_str(value: &str) -> Result<Self, Self::Err> {
278 match value {
279 "certain" => Ok(Certainty::Certain),
280 "confident" => Ok(Certainty::Confident),
281 "likely" => Ok(Certainty::Likely),
282 "possible" => Ok(Certainty::Possible),
283 _ => Err(format!("Invalid certainty: {}", value)),
284 }
285 }
286}
287
288impl std::fmt::Display for Certainty {
289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290 match self {
291 Certainty::Certain => write!(f, "certain"),
292 Certainty::Confident => write!(f, "confident"),
293 Certainty::Likely => write!(f, "likely"),
294 Certainty::Possible => write!(f, "possible"),
295 }
296 }
297}
298
299#[cfg(feature = "python")]
300impl pyo3::FromPyObject<'_, '_> for Certainty {
301 type Error = pyo3::PyErr;
302
303 fn extract(ob: pyo3::Borrowed<'_, '_, pyo3::PyAny>) -> Result<Self, Self::Error> {
304 use std::str::FromStr;
305 let s = ob.extract::<String>()?;
306 Certainty::from_str(&s).map_err(pyo3::exceptions::PyValueError::new_err)
307 }
308}
309
310#[cfg(feature = "python")]
311impl<'py> pyo3::IntoPyObject<'py> for Certainty {
312 type Target = pyo3::types::PyString;
313
314 type Output = pyo3::Bound<'py, Self::Target>;
315
316 type Error = pyo3::PyErr;
317
318 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
319 let s = self.to_string();
320 Ok(pyo3::types::PyString::new(py, &s))
321 }
322}
323
324pub fn certainty_sufficient(
335 actual_certainty: Certainty,
336 minimum_certainty: Option<Certainty>,
337) -> bool {
338 if let Some(minimum_certainty) = minimum_certainty {
339 actual_certainty >= minimum_certainty
340 } else {
341 true
342 }
343}
344
345pub fn min_certainty(certainties: &[Certainty]) -> Option<Certainty> {
347 certainties.iter().min().cloned()
348}
349
350#[cfg(feature = "python")]
351fn get_git_committer(working_tree: &dyn PyWorkingTree) -> Option<String> {
352 pyo3::Python::attach(|py| {
353 let repo = working_tree.branch().repository();
354 let git = match repo.to_object(py).getattr(py, "_git") {
355 Ok(x) => Some(x),
356 Err(e) if e.is_instance_of::<pyo3::exceptions::PyAttributeError>(py) => None,
357 Err(e) => {
358 return Err(e);
359 }
360 };
361
362 if let Some(git) = git {
363 let cs = git.call_method0(py, "get_config_stack")?;
364
365 let mut user = std::env::var("GIT_COMMITTER_NAME").ok();
366 let mut email = std::env::var("GIT_COMMITTER_EMAIL").ok();
367 if user.is_none() {
368 match cs.call_method1(py, "get", (("user",), "name")) {
369 Ok(x) => {
370 user = 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 if email.is_none() {
385 match cs.call_method1(py, "get", (("user",), "email")) {
386 Ok(x) => {
387 email = Some(
388 std::str::from_utf8(x.extract::<&[u8]>(py)?)
389 .unwrap()
390 .to_string(),
391 );
392 }
393 Err(e) if e.is_instance_of::<pyo3::exceptions::PyKeyError>(py) => {
394 }
396 Err(e) => {
397 return Err(e);
398 }
399 };
400 }
401
402 if let (Some(user), Some(email)) = (user, email) {
403 return Ok(Some(format!("{} <{}>", user, email)));
404 }
405
406 let gs = breezyshim::config::global_stack().unwrap();
407
408 Ok(gs
409 .get("email")?
410 .map(|email| email.extract::<String>(py).unwrap()))
411 } else {
412 Ok(None)
413 }
414 })
415 .unwrap()
416}
417
418pub fn get_committer(working_tree: &dyn PyWorkingTree) -> String {
420 #[cfg(feature = "python")]
421 if let Some(committer) = get_git_committer(working_tree) {
422 return committer;
423 }
424
425 let config = working_tree.branch().get_config_stack();
426
427 config
428 .get("email")
429 .unwrap()
430 .map(|x| x.to_string())
431 .unwrap_or_default()
432}
433
434pub fn control_file_present(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
445 for name in [
446 "debian/control",
447 "debian/control.in",
448 "control",
449 "control.in",
450 "debian/debcargo.toml",
451 ] {
452 let name = subpath.join(name);
453 if tree.has_filename(name.as_path()) {
454 return true;
455 }
456 }
457 false
458}
459
460pub fn is_debcargo_package(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
462 tree.has_filename(subpath.join("debian/debcargo.toml").as_path())
463}
464
465pub fn control_files_in_root(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
467 let debian_path = subpath.join("debian");
468 if tree.has_filename(debian_path.as_path()) {
469 return false;
470 }
471
472 let control_path = subpath.join("control");
473 if tree.has_filename(control_path.as_path()) {
474 return true;
475 }
476
477 tree.has_filename(subpath.join("control.in").as_path())
478}
479
480pub fn parseaddr(input: &str) -> Option<(Option<String>, Option<String>)> {
482 if let Some((_whole, name, addr)) =
483 lazy_regex::regex_captures!(r"(?:(?P<name>[^<]*)\s*<)?(?P<addr>[^<>]*)>?", input)
484 {
485 let name = match name.trim() {
486 "" => None,
487 x => Some(x.to_string()),
488 };
489 let addr = match addr.trim() {
490 "" => None,
491 x => Some(x.to_string()),
492 };
493
494 return Some((name, addr));
495 } else if let Some((_whole, addr)) = lazy_regex::regex_captures!(r"(?P<addr>[^<>]*)", input) {
496 let addr = Some(addr.trim().to_string());
497
498 return Some((None, addr));
499 } else if input.is_empty() {
500 return None;
501 } else if !input.contains('<') {
502 return Some((None, Some(input.to_string())));
503 }
504 None
505}
506
507pub fn gbp_dch(path: &std::path::Path) -> Result<(), std::io::Error> {
509 let mut cmd = std::process::Command::new("gbp");
510 cmd.arg("dch").arg("--ignore-branch");
511 cmd.current_dir(path);
512 let status = cmd.status()?;
513 if !status.success() {
514 return Err(std::io::Error::other(format!("gbp dch failed: {}", status)));
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}