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 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 vcs;
31pub mod vendor;
32pub mod versions;
33
34pub const DEFAULT_BUILDER: &str = "sbuild --no-clean-source";
37
38#[derive(Debug)]
39pub enum ApplyError<R, E> {
41 CallbackError(E),
43 BrzError(Error),
45 NoChanges(R),
47}
48
49impl<R, E> From<Error> for ApplyError<R, E> {
50 fn from(e: Error) -> Self {
51 ApplyError::BrzError(e)
52 }
53}
54
55pub fn apply_or_revert<R, E, T, U>(
74 local_tree: &T,
75 subpath: &std::path::Path,
76 basis_tree: &U,
77 dirty_tracker: Option<&mut DirtyTreeTracker>,
78 applier: impl FnOnce(&std::path::Path) -> Result<R, E>,
79) -> Result<(R, Vec<TreeChange>, Option<Vec<std::path::PathBuf>>), ApplyError<R, E>>
80where
81 T: PyWorkingTree + breezyshim::tree::PyMutableTree,
82 U: PyTree,
83{
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::attach(|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<T: WorkingTree>(
216 working_tree: &T,
217 changelog_path: &std::path::Path,
218 entry: &[&str],
219) -> Result<(), crate::editor::EditorError<debian_changelog::Error>> {
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.try_auto_add_change(
225 entry,
226 debian_changelog::get_maintainer().unwrap(),
227 Some(chrono::Utc::now().fixed_offset()),
228 None,
229 )
230 .unwrap();
231
232 cl.commit()?;
233
234 Ok(())
235}
236
237#[derive(
238 Clone,
239 Copy,
240 PartialEq,
241 Eq,
242 Debug,
243 Default,
244 PartialOrd,
245 Ord,
246 serde::Serialize,
247 serde::Deserialize,
248)]
249pub enum Certainty {
251 #[serde(rename = "possible")]
252 Possible,
254 #[serde(rename = "likely")]
255 Likely,
257 #[serde(rename = "confident")]
258 Confident,
260 #[default]
261 #[serde(rename = "certain")]
262 Certain,
264}
265
266impl std::str::FromStr for Certainty {
267 type Err = String;
268
269 fn from_str(value: &str) -> Result<Self, Self::Err> {
270 match value {
271 "certain" => Ok(Certainty::Certain),
272 "confident" => Ok(Certainty::Confident),
273 "likely" => Ok(Certainty::Likely),
274 "possible" => Ok(Certainty::Possible),
275 _ => Err(format!("Invalid certainty: {}", value)),
276 }
277 }
278}
279
280impl std::fmt::Display for Certainty {
281 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282 match self {
283 Certainty::Certain => write!(f, "certain"),
284 Certainty::Confident => write!(f, "confident"),
285 Certainty::Likely => write!(f, "likely"),
286 Certainty::Possible => write!(f, "possible"),
287 }
288 }
289}
290
291#[cfg(feature = "python")]
292impl pyo3::FromPyObject<'_, '_> for Certainty {
293 type Error = pyo3::PyErr;
294
295 fn extract(ob: pyo3::Borrowed<'_, '_, pyo3::PyAny>) -> Result<Self, Self::Error> {
296 use std::str::FromStr;
297 let s = ob.extract::<String>()?;
298 Certainty::from_str(&s).map_err(pyo3::exceptions::PyValueError::new_err)
299 }
300}
301
302#[cfg(feature = "python")]
303impl<'py> pyo3::IntoPyObject<'py> for Certainty {
304 type Target = pyo3::types::PyString;
305
306 type Output = pyo3::Bound<'py, Self::Target>;
307
308 type Error = pyo3::PyErr;
309
310 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
311 let s = self.to_string();
312 Ok(pyo3::types::PyString::new(py, &s))
313 }
314}
315
316pub fn certainty_sufficient(
327 actual_certainty: Certainty,
328 minimum_certainty: Option<Certainty>,
329) -> bool {
330 if let Some(minimum_certainty) = minimum_certainty {
331 actual_certainty >= minimum_certainty
332 } else {
333 true
334 }
335}
336
337pub fn min_certainty(certainties: &[Certainty]) -> Option<Certainty> {
339 certainties.iter().min().cloned()
340}
341
342#[cfg(feature = "python")]
343fn get_git_committer(working_tree: &dyn PyWorkingTree) -> Option<String> {
344 pyo3::Python::attach(|py| {
345 let repo = working_tree.branch().repository();
346 let git = match repo.to_object(py).getattr(py, "_git") {
347 Ok(x) => Some(x),
348 Err(e) if e.is_instance_of::<pyo3::exceptions::PyAttributeError>(py) => None,
349 Err(e) => {
350 return Err(e);
351 }
352 };
353
354 if let Some(git) = git {
355 let cs = git.call_method0(py, "get_config_stack")?;
356
357 let mut user = std::env::var("GIT_COMMITTER_NAME").ok();
358 let mut email = std::env::var("GIT_COMMITTER_EMAIL").ok();
359 if user.is_none() {
360 match cs.call_method1(py, "get", (("user",), "name")) {
361 Ok(x) => {
362 user = Some(
363 std::str::from_utf8(x.extract::<&[u8]>(py)?)
364 .unwrap()
365 .to_string(),
366 );
367 }
368 Err(e) if e.is_instance_of::<pyo3::exceptions::PyKeyError>(py) => {
369 }
371 Err(e) => {
372 return Err(e);
373 }
374 };
375 }
376 if email.is_none() {
377 match cs.call_method1(py, "get", (("user",), "email")) {
378 Ok(x) => {
379 email = Some(
380 std::str::from_utf8(x.extract::<&[u8]>(py)?)
381 .unwrap()
382 .to_string(),
383 );
384 }
385 Err(e) if e.is_instance_of::<pyo3::exceptions::PyKeyError>(py) => {
386 }
388 Err(e) => {
389 return Err(e);
390 }
391 };
392 }
393
394 if let (Some(user), Some(email)) = (user, email) {
395 return Ok(Some(format!("{} <{}>", user, email)));
396 }
397
398 let gs = breezyshim::config::global_stack().unwrap();
399
400 Ok(gs
401 .get("email")?
402 .map(|email| email.extract::<String>(py).unwrap()))
403 } else {
404 Ok(None)
405 }
406 })
407 .unwrap()
408}
409
410pub fn get_committer(working_tree: &dyn PyWorkingTree) -> String {
412 #[cfg(feature = "python")]
413 if let Some(committer) = get_git_committer(working_tree) {
414 return committer;
415 }
416
417 let config = working_tree.branch().get_config_stack();
418
419 config
420 .get("email")
421 .unwrap()
422 .map(|x| x.to_string())
423 .unwrap_or_default()
424}
425
426pub fn control_file_present(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
437 for name in [
438 "debian/control",
439 "debian/control.in",
440 "control",
441 "control.in",
442 "debian/debcargo.toml",
443 ] {
444 let name = subpath.join(name);
445 if tree.has_filename(name.as_path()) {
446 return true;
447 }
448 }
449 false
450}
451
452pub fn is_debcargo_package(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
454 tree.has_filename(subpath.join("debian/debcargo.toml").as_path())
455}
456
457pub fn control_files_in_root(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
459 let debian_path = subpath.join("debian");
460 if tree.has_filename(debian_path.as_path()) {
461 return false;
462 }
463
464 let control_path = subpath.join("control");
465 if tree.has_filename(control_path.as_path()) {
466 return true;
467 }
468
469 tree.has_filename(subpath.join("control.in").as_path())
470}
471
472pub fn parseaddr(input: &str) -> Option<(Option<String>, Option<String>)> {
474 if let Some((_whole, name, addr)) =
475 lazy_regex::regex_captures!(r"(?:(?P<name>[^<]*)\s*<)?(?P<addr>[^<>]*)>?", input)
476 {
477 let name = match name.trim() {
478 "" => None,
479 x => Some(x.to_string()),
480 };
481 let addr = match addr.trim() {
482 "" => None,
483 x => Some(x.to_string()),
484 };
485
486 return Some((name, addr));
487 } else if let Some((_whole, addr)) = lazy_regex::regex_captures!(r"(?P<addr>[^<>]*)", input) {
488 let addr = Some(addr.trim().to_string());
489
490 return Some((None, addr));
491 } else if input.is_empty() {
492 return None;
493 } else if !input.contains('<') {
494 return Some((None, Some(input.to_string())));
495 }
496 None
497}
498
499pub fn gbp_dch(path: &std::path::Path) -> Result<(), std::io::Error> {
501 let mut cmd = std::process::Command::new("gbp");
502 cmd.arg("dch").arg("--ignore-branch");
503 cmd.current_dir(path);
504 let status = cmd.status()?;
505 if !status.success() {
506 return Err(std::io::Error::other(format!("gbp dch failed: {}", status)));
507 }
508 Ok(())
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514 use serial_test::serial;
515
516 #[test]
517 fn test_parseaddr() {
518 assert_eq!(
519 parseaddr("foo <bar@example.com>").unwrap(),
520 (Some("foo".to_string()), Some("bar@example.com".to_string()))
521 );
522 assert_eq!(parseaddr("foo").unwrap(), (None, Some("foo".to_string())));
523 }
524
525 #[cfg(feature = "python")]
526 #[serial]
527 #[test]
528 fn test_git_env() {
529 let td = tempfile::tempdir().unwrap();
530 let cd = breezyshim::controldir::create_standalone_workingtree(td.path(), "git").unwrap();
531
532 let old_name = std::env::var("GIT_COMMITTER_NAME").ok();
533 let old_email = std::env::var("GIT_COMMITTER_EMAIL").ok();
534
535 std::env::set_var("GIT_COMMITTER_NAME", "Some Git Committer");
536 std::env::set_var("GIT_COMMITTER_EMAIL", "committer@example.com");
537
538 let committer = get_committer(&cd);
539
540 if let Some(old_name) = old_name {
541 std::env::set_var("GIT_COMMITTER_NAME", old_name);
542 } else {
543 std::env::remove_var("GIT_COMMITTER_NAME");
544 }
545
546 if let Some(old_email) = old_email {
547 std::env::set_var("GIT_COMMITTER_EMAIL", old_email);
548 } else {
549 std::env::remove_var("GIT_COMMITTER_EMAIL");
550 }
551
552 assert_eq!("Some Git Committer <committer@example.com>", committer);
553 }
554
555 #[serial]
556 #[test]
557 fn test_git_config() {
558 let td = tempfile::tempdir().unwrap();
559 let cd = breezyshim::controldir::create_standalone_workingtree(td.path(), "git").unwrap();
560
561 std::fs::write(
562 td.path().join(".git/config"),
563 b"[user]\nname = Some Git Committer\nemail = other@example.com",
564 )
565 .unwrap();
566
567 assert_eq!(get_committer(&cd), "Some Git Committer <other@example.com>");
568 }
569
570 #[test]
571 fn test_min_certainty() {
572 assert_eq!(None, min_certainty(&[]));
573 assert_eq!(
574 Some(Certainty::Certain),
575 min_certainty(&[Certainty::Certain])
576 );
577 assert_eq!(
578 Some(Certainty::Possible),
579 min_certainty(&[Certainty::Possible])
580 );
581 assert_eq!(
582 Some(Certainty::Possible),
583 min_certainty(&[Certainty::Possible, Certainty::Certain])
584 );
585 assert_eq!(
586 Some(Certainty::Likely),
587 min_certainty(&[Certainty::Likely, Certainty::Certain])
588 );
589 assert_eq!(
590 Some(Certainty::Possible),
591 min_certainty(&[Certainty::Likely, Certainty::Certain, Certainty::Possible])
592 );
593 }
594}