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")]
183impl From<pyo3::PyErr> for ChangelogError {
184 fn from(e: pyo3::PyErr) -> Self {
185 use pyo3::import_exception;
186
187 import_exception!(breezy.transport, NoSuchFile);
188
189 pyo3::Python::with_gil(|py| {
190 if e.is_instance_of::<NoSuchFile>(py) {
191 return ChangelogError::NotDebianPackage(
192 e.into_value(py)
193 .bind(py)
194 .getattr("path")
195 .unwrap()
196 .extract()
197 .unwrap(),
198 );
199 } else {
200 ChangelogError::Python(e)
201 }
202 })
203 }
204}
205
206pub fn add_changelog_entry(
213 working_tree: &WorkingTree,
214 changelog_path: &std::path::Path,
215 entry: &[&str],
216) -> Result<(), crate::editor::EditorError> {
217 use crate::editor::{Editor, MutableTreeEdit};
218 let mut cl =
219 working_tree.edit_file::<debian_changelog::ChangeLog>(changelog_path, false, true)?;
220
221 cl.auto_add_change(
222 entry,
223 debian_changelog::get_maintainer().unwrap(),
224 None,
225 None,
226 );
227
228 cl.commit()?;
229
230 Ok(())
231}
232
233#[derive(
234 Clone,
235 Copy,
236 PartialEq,
237 Eq,
238 Debug,
239 Default,
240 PartialOrd,
241 Ord,
242 serde::Serialize,
243 serde::Deserialize,
244)]
245pub enum Certainty {
247 #[serde(rename = "possible")]
248 Possible,
250 #[serde(rename = "likely")]
251 Likely,
253 #[serde(rename = "confident")]
254 Confident,
256 #[default]
257 #[serde(rename = "certain")]
258 Certain,
260}
261
262impl std::str::FromStr for Certainty {
263 type Err = String;
264
265 fn from_str(value: &str) -> Result<Self, Self::Err> {
266 match value {
267 "certain" => Ok(Certainty::Certain),
268 "confident" => Ok(Certainty::Confident),
269 "likely" => Ok(Certainty::Likely),
270 "possible" => Ok(Certainty::Possible),
271 _ => Err(format!("Invalid certainty: {}", value)),
272 }
273 }
274}
275
276impl std::fmt::Display for Certainty {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 match self {
279 Certainty::Certain => write!(f, "certain"),
280 Certainty::Confident => write!(f, "confident"),
281 Certainty::Likely => write!(f, "likely"),
282 Certainty::Possible => write!(f, "possible"),
283 }
284 }
285}
286
287#[cfg(feature = "python")]
288impl pyo3::FromPyObject<'_> for Certainty {
289 fn extract_bound(ob: &pyo3::Bound<pyo3::PyAny>) -> pyo3::PyResult<Self> {
290 use std::str::FromStr;
291 let s = ob.extract::<String>()?;
292 Certainty::from_str(&s).map_err(pyo3::exceptions::PyValueError::new_err)
293 }
294}
295
296#[cfg(feature = "python")]
297impl pyo3::ToPyObject for Certainty {
298 fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject {
299 self.to_string().to_object(py)
300 }
301}
302
303pub fn certainty_sufficient(
314 actual_certainty: Certainty,
315 minimum_certainty: Option<Certainty>,
316) -> bool {
317 if let Some(minimum_certainty) = minimum_certainty {
318 actual_certainty >= minimum_certainty
319 } else {
320 true
321 }
322}
323
324pub fn min_certainty(certainties: &[Certainty]) -> Option<Certainty> {
326 certainties.iter().min().cloned()
327}
328
329#[cfg(feature = "python")]
330fn get_git_committer(working_tree: &WorkingTree) -> Option<String> {
331 pyo3::prepare_freethreaded_python();
332 pyo3::Python::with_gil(|py| {
333 let repo = working_tree.branch().repository();
334 let git = match repo.to_object(py).getattr(py, "_git") {
335 Ok(x) => Some(x),
336 Err(e) if e.is_instance_of::<pyo3::exceptions::PyAttributeError>(py) => None,
337 Err(e) => {
338 return Err(e);
339 }
340 };
341
342 if let Some(git) = git {
343 let cs = git.call_method0(py, "get_config_stack")?;
344
345 let mut user = std::env::var("GIT_COMMITTER_NAME").ok();
346 let mut email = std::env::var("GIT_COMMITTER_EMAIL").ok();
347 if user.is_none() {
348 match cs.call_method1(py, "get", (("user",), "name")) {
349 Ok(x) => {
350 user = Some(
351 std::str::from_utf8(x.extract::<&[u8]>(py)?)
352 .unwrap()
353 .to_string(),
354 );
355 }
356 Err(e) if e.is_instance_of::<pyo3::exceptions::PyKeyError>(py) => {
357 }
359 Err(e) => {
360 return Err(e);
361 }
362 };
363 }
364 if email.is_none() {
365 match cs.call_method1(py, "get", (("user",), "email")) {
366 Ok(x) => {
367 email = 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
382 if let (Some(user), Some(email)) = (user, email) {
383 return Ok(Some(format!("{} <{}>", user, email)));
384 }
385
386 let gs = breezyshim::config::global_stack().unwrap();
387
388 Ok(gs
389 .get("email")?
390 .map(|email| email.extract::<String>(py).unwrap()))
391 } else {
392 Ok(None)
393 }
394 })
395 .unwrap()
396}
397
398pub fn get_committer(working_tree: &WorkingTree) -> String {
400 #[cfg(feature = "python")]
401 if let Some(committer) = get_git_committer(working_tree) {
402 return committer;
403 }
404
405 let config = working_tree.branch().get_config_stack();
406
407 config
408 .get("email")
409 .unwrap()
410 .map(|x| x.to_string())
411 .unwrap_or_default()
412}
413
414pub fn control_file_present(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
425 for name in [
426 "debian/control",
427 "debian/control.in",
428 "control",
429 "control.in",
430 "debian/debcargo.toml",
431 ] {
432 let name = subpath.join(name);
433 if tree.has_filename(name.as_path()) {
434 return true;
435 }
436 }
437 false
438}
439
440pub fn is_debcargo_package(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
442 tree.has_filename(subpath.join("debian/debcargo.toml").as_path())
443}
444
445pub fn control_files_in_root(tree: &dyn Tree, subpath: &std::path::Path) -> bool {
447 let debian_path = subpath.join("debian");
448 if tree.has_filename(debian_path.as_path()) {
449 return false;
450 }
451
452 let control_path = subpath.join("control");
453 if tree.has_filename(control_path.as_path()) {
454 return true;
455 }
456
457 tree.has_filename(subpath.join("control.in").as_path())
458}
459
460pub fn parseaddr(input: &str) -> Option<(Option<String>, Option<String>)> {
462 if let Some((_whole, name, addr)) =
463 lazy_regex::regex_captures!(r"(?:(?P<name>[^<]*)\s*<)?(?P<addr>[^<>]*)>?", input)
464 {
465 let name = match name.trim() {
466 "" => None,
467 x => Some(x.to_string()),
468 };
469 let addr = match addr.trim() {
470 "" => None,
471 x => Some(x.to_string()),
472 };
473
474 return Some((name, addr));
475 } else if let Some((_whole, addr)) = lazy_regex::regex_captures!(r"(?P<addr>[^<>]*)", input) {
476 let addr = Some(addr.trim().to_string());
477
478 return Some((None, addr));
479 } else if input.is_empty() {
480 return None;
481 } else if !input.contains('<') {
482 return Some((None, Some(input.to_string())));
483 }
484 None
485}
486
487pub fn gbp_dch(path: &std::path::Path) -> Result<(), std::io::Error> {
489 let mut cmd = std::process::Command::new("gbp");
490 cmd.arg("dch").arg("--ignore-branch");
491 cmd.current_dir(path);
492 let status = cmd.status()?;
493 if !status.success() {
494 return Err(std::io::Error::new(
495 std::io::ErrorKind::Other,
496 format!("gbp dch failed: {}", status),
497 ));
498 }
499 Ok(())
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505 use serial_test::serial;
506
507 #[test]
508 fn test_parseaddr() {
509 assert_eq!(
510 parseaddr("foo <bar@example.com>").unwrap(),
511 (Some("foo".to_string()), Some("bar@example.com".to_string()))
512 );
513 assert_eq!(parseaddr("foo").unwrap(), (None, Some("foo".to_string())));
514 }
515
516 #[cfg(feature = "python")]
517 #[serial]
518 #[test]
519 fn test_git_env() {
520 let td = tempfile::tempdir().unwrap();
521 let cd = breezyshim::controldir::create_standalone_workingtree(td.path(), "git").unwrap();
522
523 let old_name = std::env::var("GIT_COMMITTER_NAME").ok();
524 let old_email = std::env::var("GIT_COMMITTER_EMAIL").ok();
525
526 std::env::set_var("GIT_COMMITTER_NAME", "Some Git Committer");
527 std::env::set_var("GIT_COMMITTER_EMAIL", "committer@example.com");
528
529 let committer = get_committer(&cd);
530
531 if let Some(old_name) = old_name {
532 std::env::set_var("GIT_COMMITTER_NAME", old_name);
533 } else {
534 std::env::remove_var("GIT_COMMITTER_NAME");
535 }
536
537 if let Some(old_email) = old_email {
538 std::env::set_var("GIT_COMMITTER_EMAIL", old_email);
539 } else {
540 std::env::remove_var("GIT_COMMITTER_EMAIL");
541 }
542
543 assert_eq!("Some Git Committer <committer@example.com>", committer);
544 }
545
546 #[serial]
547 #[test]
548 fn test_git_config() {
549 let td = tempfile::tempdir().unwrap();
550 let cd = breezyshim::controldir::create_standalone_workingtree(td.path(), "git").unwrap();
551
552 std::fs::write(
553 td.path().join(".git/config"),
554 b"[user]\nname = Some Git Committer\nemail = other@example.com",
555 )
556 .unwrap();
557
558 assert_eq!(get_committer(&cd), "Some Git Committer <other@example.com>");
559 }
560
561 #[test]
562 fn test_min_certainty() {
563 assert_eq!(None, min_certainty(&[]));
564 assert_eq!(
565 Some(Certainty::Certain),
566 min_certainty(&[Certainty::Certain])
567 );
568 assert_eq!(
569 Some(Certainty::Possible),
570 min_certainty(&[Certainty::Possible])
571 );
572 assert_eq!(
573 Some(Certainty::Possible),
574 min_certainty(&[Certainty::Possible, Certainty::Certain])
575 );
576 assert_eq!(
577 Some(Certainty::Likely),
578 min_certainty(&[Certainty::Likely, Certainty::Certain])
579 );
580 assert_eq!(
581 Some(Certainty::Possible),
582 min_certainty(&[Certainty::Likely, Certainty::Certain, Certainty::Possible])
583 );
584 }
585}