Skip to main content

debian_workbench/
control.rs

1//! Tools for working with Debian control files.
2use crate::editor::{Editor, EditorError, FsEditor, GeneratedFile};
3use crate::relations::{ensure_relation, is_relation_implied};
4use deb822_lossless::Paragraph;
5use debian_control::lossless::relations::Relations;
6use std::ops::{Deref, DerefMut};
7use std::path::{Path, PathBuf};
8
9/// Format a description based on summary and long description lines.
10pub fn format_description(summary: &str, long_description: Vec<&str>) -> String {
11    let mut ret = summary.to_string() + "\n";
12    for line in long_description {
13        ret.push(' ');
14        ret.push_str(line);
15        ret.push('\n');
16    }
17    ret
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Copy)]
21/// The type of a control file template.
22pub enum TemplateType {
23    /// A rule in the debian/rules file that generates the control file.
24    Rules,
25
26    /// Generated by gnome-pkg-tools.
27    Gnome,
28
29    /// Generated by pg_buildext.
30    Postgresql,
31
32    /// Generated by a set of files in the debian/control.in directory.
33    Directory,
34
35    /// Generated by cdbs.
36    Cdbs,
37
38    /// Generated by debcargo.
39    Debcargo,
40}
41
42/// Error type for template expansion operations
43#[derive(Debug)]
44pub enum TemplateExpansionError {
45    /// The expansion failed with an error message
46    Failed(String),
47    /// The expand command is missing
48    ExpandCommandMissing(String),
49    /// Unknown templating type encountered
50    UnknownTemplating(PathBuf, Option<PathBuf>),
51    /// A change conflict occurred
52    Conflict(ChangeConflict),
53}
54
55impl From<ChangeConflict> for TemplateExpansionError {
56    fn from(e: ChangeConflict) -> Self {
57        TemplateExpansionError::Conflict(e)
58    }
59}
60
61impl std::fmt::Display for TemplateExpansionError {
62    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
63        match self {
64            TemplateExpansionError::Failed(s) => write!(f, "Failed: {}", s),
65            TemplateExpansionError::ExpandCommandMissing(s) => {
66                write!(f, "Command not found: {}", s)
67            }
68            TemplateExpansionError::UnknownTemplating(p1, p2) => {
69                if let Some(p2) = p2 {
70                    write!(
71                        f,
72                        "Unknown templating: {} -> {}",
73                        p1.display(),
74                        p2.display()
75                    )
76                } else {
77                    write!(f, "Unknown templating: {}", p1.display())
78                }
79            }
80            TemplateExpansionError::Conflict(c) => write!(f, "Conflict: {}", c),
81        }
82    }
83}
84
85impl std::error::Error for TemplateExpansionError {}
86
87/// Run the dh_gnome_clean command.
88///
89/// This needs to do some post-hoc cleaning, since dh_gnome_clean writes various debhelper log
90/// files that should not be checked in.
91///
92/// # Arguments
93/// * `path` - Path to run dh_gnome_clean in
94///
95/// # Errors
96/// Returns an error if:
97/// - Pre-existing .debhelper.log files are found
98/// - No changelog file exists
99/// - The dh_gnome_clean command is not found
100/// - The command fails to execute
101pub fn dh_gnome_clean(path: &std::path::Path) -> Result<(), TemplateExpansionError> {
102    for entry in std::fs::read_dir(path.join("debian")).unwrap().flatten() {
103        if entry
104            .file_name()
105            .to_string_lossy()
106            .ends_with(".debhelper.log")
107        {
108            return Err(TemplateExpansionError::Failed(
109                "pre-existing .debhelper.log files".to_string(),
110            ));
111        }
112    }
113
114    if !path.join("debian/changelog").exists() {
115        return Err(TemplateExpansionError::Failed(
116            "no changelog file".to_string(),
117        ));
118    }
119
120    let result = std::process::Command::new("dh_gnome_clean")
121        .current_dir(path)
122        .output();
123
124    match result {
125        Ok(output) => {
126            if !output.status.success() {
127                let stderr = String::from_utf8_lossy(&output.stderr);
128                return Err(TemplateExpansionError::Failed(stderr.to_string()));
129            }
130        }
131        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
132            return Err(TemplateExpansionError::ExpandCommandMissing(
133                "dh_gnome_clean".to_string(),
134            ));
135        }
136        Err(e) => {
137            return Err(TemplateExpansionError::Failed(e.to_string()));
138        }
139    }
140
141    for entry in std::fs::read_dir(path.join("debian")).unwrap().flatten() {
142        if entry
143            .file_name()
144            .to_string_lossy()
145            .ends_with(".debhelper.log")
146        {
147            std::fs::remove_file(entry.path()).unwrap();
148        }
149    }
150
151    Ok(())
152}
153
154/// Run the 'pg_buildext updatecontrol' command.
155///
156/// # Arguments
157/// * `path` - Path to run pg_buildext updatecontrol in
158///
159/// # Errors
160/// Returns an error if:
161/// - The pg_buildext command is not found
162/// - The command fails to execute
163pub fn pg_buildext_updatecontrol(path: &std::path::Path) -> Result<(), TemplateExpansionError> {
164    let result = std::process::Command::new("pg_buildext")
165        .arg("updatecontrol")
166        .current_dir(path)
167        .output();
168
169    match result {
170        Ok(output) => {
171            if !output.status.success() {
172                let stderr = String::from_utf8_lossy(&output.stderr);
173                return Err(TemplateExpansionError::Failed(stderr.to_string()));
174            }
175        }
176        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
177            return Err(TemplateExpansionError::ExpandCommandMissing(
178                "pg_buildext".to_string(),
179            ));
180        }
181        Err(e) => {
182            return Err(TemplateExpansionError::Failed(e.to_string()));
183        }
184    }
185    Ok(())
186}
187
188/// Expand a control template.
189///
190/// # Arguments
191/// * `template_path` - Path to the control template
192/// * `path` - Path to the control file
193/// * `template_type` - Type of the template
194///
195/// # Returns
196/// Ok if the template was successfully expanded
197fn expand_control_template(
198    template_path: &std::path::Path,
199    path: &std::path::Path,
200    template_type: TemplateType,
201) -> Result<(), TemplateExpansionError> {
202    let package_root = path.parent().unwrap().parent().unwrap();
203    match template_type {
204        TemplateType::Rules => {
205            let path_time = match std::fs::metadata(path) {
206                Ok(metadata) => Some(metadata.modified().unwrap()),
207                Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
208                Err(e) => panic!("Failed to get mtime of {}: {}", path.display(), e),
209            };
210            while let Ok(metadata) = std::fs::metadata(template_path) {
211                if Some(metadata.modified().unwrap()) == path_time {
212                    // Wait until mtime has changed, so that make knows to regenerate.
213                    filetime::set_file_mtime(template_path, filetime::FileTime::now()).unwrap();
214                } else {
215                    break;
216                }
217            }
218            let result = std::process::Command::new("./debian/rules")
219                .arg("debian/control")
220                .current_dir(package_root)
221                .output();
222
223            match result {
224                Ok(output) => {
225                    if !output.status.success() {
226                        let stderr = String::from_utf8_lossy(&output.stderr);
227                        Err(TemplateExpansionError::Failed(format!(
228                            "Exit code {} running ./debian/rules debian/control: {}",
229                            output.status, stderr
230                        )))
231                    } else {
232                        Ok(())
233                    }
234                }
235                Err(e) => Err(TemplateExpansionError::Failed(format!(
236                    "Failed to run ./debian/rules debian/control: {}",
237                    e
238                ))),
239            }
240        }
241        TemplateType::Gnome => dh_gnome_clean(package_root),
242        TemplateType::Postgresql => pg_buildext_updatecontrol(package_root),
243        TemplateType::Cdbs => unreachable!(),
244        TemplateType::Debcargo => unreachable!(),
245        TemplateType::Directory => Err(TemplateExpansionError::UnknownTemplating(
246            path.to_path_buf(),
247            Some(template_path.to_path_buf()),
248        )),
249    }
250}
251
252#[derive(Debug, Clone)]
253struct Deb822Changes(
254    std::collections::HashMap<(String, String), Vec<(String, Option<String>, Option<String>)>>,
255);
256
257impl Deb822Changes {
258    fn new() -> Self {
259        Self(std::collections::HashMap::new())
260    }
261
262    fn insert(
263        &mut self,
264        para_key: (String, String),
265        field: String,
266        old_value: Option<String>,
267        new_value: Option<String>,
268    ) {
269        self.0
270            .entry(para_key)
271            .or_default()
272            .push((field, old_value, new_value));
273    }
274
275    #[allow(dead_code)]
276    fn normalized(&self) -> Vec<((&str, &str), Vec<(&str, Option<&str>, Option<&str>)>)> {
277        let mut ret: Vec<_> = self
278            .0
279            .iter()
280            .map(|(k, v)| {
281                ((k.0.as_str(), k.1.as_str()), {
282                    let mut v: Vec<_> = v
283                        .iter()
284                        .map(|(f, o, n)| (f.as_str(), o.as_deref(), n.as_deref()))
285                        .collect();
286                    v.sort();
287                    v
288                })
289            })
290            .collect();
291        ret.sort_by_key(|(k, _)| *k);
292        ret
293    }
294}
295
296// Update a control file template based on changes to the file itself.
297//
298// # Arguments
299// * `template_path` - Path to the control template
300// * `path` - Path to the control file
301// * `changes` - Changes to apply
302// * `expand_template` - Whether to expand the template after updating it
303//
304// # Returns
305// Ok if the template was successfully updated
306fn update_control_template(
307    template_path: &std::path::Path,
308    template_type: TemplateType,
309    path: &std::path::Path,
310    changes: Deb822Changes,
311    expand_template: bool,
312) -> Result<bool, TemplateExpansionError> {
313    if template_type == TemplateType::Directory {
314        // We can't handle these yet
315        return Err(TemplateExpansionError::UnknownTemplating(
316            path.to_path_buf(),
317            Some(template_path.to_path_buf()),
318        ));
319    }
320
321    let mut template_editor =
322        FsEditor::<deb822_lossless::Deb822>::new(template_path, true, false).unwrap();
323
324    let resolve_conflict = match template_type {
325        TemplateType::Cdbs => Some(resolve_cdbs_template as ResolveDeb822Conflict),
326        _ => None,
327    };
328
329    apply_changes(&mut template_editor, changes.clone(), resolve_conflict)?;
330
331    if !template_editor.has_changed() {
332        // A bit odd, since there were changes to the output file. Anyway.
333        return Ok(false);
334    }
335
336    match template_editor.commit() {
337        Ok(_) => {}
338        Err(e) => return Err(TemplateExpansionError::Failed(e.to_string())),
339    }
340
341    if expand_template {
342        match template_type {
343            TemplateType::Cdbs => {
344                let mut editor =
345                    FsEditor::<deb822_lossless::Deb822>::new(path, true, false).unwrap();
346                apply_changes(&mut editor, changes, None)?;
347                match editor.commit() {
348                    Ok(_) => {}
349                    Err(e) => return Err(TemplateExpansionError::Failed(e.to_string())),
350                }
351            }
352            _ => {
353                expand_control_template(template_path, path, template_type)?;
354            }
355        }
356    }
357
358    Ok(true)
359}
360
361#[derive(Debug, PartialEq, Eq)]
362/// A change conflict.
363pub struct ChangeConflict {
364    /// Paragraph key, i.e. ("Source", "foo")
365    pub para_key: (String, String),
366    /// Field that conflicted
367    pub field: String,
368    /// Old value in the control file
369    pub actual_old_value: Option<String>,
370    /// Old value in the template
371    pub template_old_value: Option<String>,
372    /// New value in the control file
373    pub actual_new_value: Option<String>,
374}
375
376impl std::fmt::Display for ChangeConflict {
377    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
378        write!(
379            f,
380            "{}/{}: {} -> {} (template: {})",
381            self.para_key.0,
382            self.para_key.1,
383            self.actual_old_value.as_deref().unwrap_or(""),
384            self.actual_new_value.as_deref().unwrap_or(""),
385            self.template_old_value.as_deref().unwrap_or("")
386        )
387    }
388}
389
390impl std::error::Error for ChangeConflict {}
391
392type ResolveDeb822Conflict = fn(
393    para_key: (&str, &str),
394    field: &str,
395    actual_old_value: Option<&str>,
396    template_old_value: Option<&str>,
397    actual_new_value: Option<&str>,
398) -> Result<Option<String>, ChangeConflict>;
399
400fn resolve_cdbs_template(
401    para_key: (&str, &str),
402    field: &str,
403    actual_old_value: Option<&str>,
404    template_old_value: Option<&str>,
405    actual_new_value: Option<&str>,
406) -> Result<Option<String>, ChangeConflict> {
407    if para_key.0 == "Source"
408        && field == "Build-Depends"
409        && template_old_value.is_some()
410        && actual_old_value.is_some()
411        && actual_new_value.is_some()
412    {
413        if actual_new_value
414            .unwrap()
415            .contains(actual_old_value.unwrap())
416        {
417            // We're simply adding to the existing list
418            return Ok(Some(
419                actual_new_value
420                    .unwrap()
421                    .replace(actual_old_value.unwrap(), template_old_value.unwrap()),
422            ));
423        } else {
424            let old_rels: Relations = actual_old_value.unwrap().parse().unwrap();
425            let new_rels: Relations = actual_new_value.unwrap().parse().unwrap();
426            let template_old_value = template_old_value.unwrap();
427            let (mut ret, errors) = Relations::parse_relaxed(template_old_value, true);
428            if !errors.is_empty() {
429                log::debug!("Errors parsing template value: {:?}", errors);
430            }
431            for v in new_rels.entries() {
432                if old_rels.entries().any(|r| is_relation_implied(&v, &r)) {
433                    continue;
434                }
435                ensure_relation(&mut ret, v);
436            }
437            return Ok(Some(ret.to_string()));
438        }
439    }
440    Err(ChangeConflict {
441        para_key: (para_key.0.to_string(), para_key.1.to_string()),
442        field: field.to_string(),
443        actual_old_value: actual_old_value.map(|v| v.to_string()),
444        template_old_value: template_old_value.map(|v| v.to_string()),
445        actual_new_value: actual_new_value.map(|s| s.to_string()),
446    })
447}
448
449/// Guess the type for a control template.
450///
451/// # Arguments
452/// * `template_path` - Path to the control template
453/// * `debian_path` - Path to the debian directory
454///
455/// # Returns
456/// Template type; None if unknown
457pub fn guess_template_type(
458    template_path: &std::path::Path,
459    debian_path: Option<&std::path::Path>,
460) -> Option<TemplateType> {
461    // TODO(jelmer): This should use a proper make file parser of some sort..
462    if let Some(debian_path) = debian_path {
463        match std::fs::read(debian_path.join("rules")) {
464            Ok(file) => {
465                for line in file.split(|&c| c == b'\n') {
466                    if line.starts_with(b"debian/control:") {
467                        return Some(TemplateType::Rules);
468                    }
469                    if line.starts_with(b"debian/%: debian/%.in") {
470                        return Some(TemplateType::Rules);
471                    }
472                    if line.starts_with(b"include /usr/share/blends-dev/rules") {
473                        return Some(TemplateType::Rules);
474                    }
475                }
476            }
477            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
478            Err(e) => panic!(
479                "Failed to read {}: {}",
480                debian_path.join("rules").display(),
481                e
482            ),
483        }
484    }
485    match std::fs::read(template_path) {
486        Ok(template) => {
487            let template_str = std::str::from_utf8(&template).unwrap();
488            if template_str.contains("@GNOME_TEAM@") {
489                return Some(TemplateType::Gnome);
490            }
491            if template_str.contains("PGVERSION") {
492                return Some(TemplateType::Postgresql);
493            }
494            if template_str.contains("@cdbs@") {
495                return Some(TemplateType::Cdbs);
496            }
497
498            let control = debian_control::Control::read_relaxed(std::io::Cursor::new(&template))
499                .unwrap()
500                .0;
501
502            let build_depends = control.source().and_then(|s| s.build_depends());
503
504            if build_depends.iter().any(|d| {
505                d.entries()
506                    .any(|e| e.relations().any(|r| r.name() == "gnome-pkg-tools"))
507            }) {
508                return Some(TemplateType::Gnome);
509            }
510
511            if build_depends.iter().any(|d| {
512                d.entries()
513                    .any(|e| e.relations().any(|r| r.name() == "cdbs"))
514            }) {
515                return Some(TemplateType::Cdbs);
516            }
517        }
518        Err(_) if template_path.is_dir() => {
519            return Some(TemplateType::Directory);
520        }
521        Err(e) => panic!("Failed to read {}: {}", template_path.display(), e),
522    }
523    if let Some(debian_path) = debian_path {
524        if debian_path.join("debcargo.toml").exists() {
525            return Some(TemplateType::Debcargo);
526        }
527    }
528    None
529}
530
531/// Apply a set of changes to this deb822 instance.
532///
533/// # Arguments
534/// * `changes` - Changes to apply
535/// * `resolve_conflict` - Callback to resolve conflicts
536fn apply_changes(
537    deb822: &mut deb822_lossless::Deb822,
538    mut changes: Deb822Changes,
539    resolve_conflict: Option<ResolveDeb822Conflict>,
540) -> Result<(), ChangeConflict> {
541    fn default_resolve_conflict(
542        para_key: (&str, &str),
543        field: &str,
544        actual_old_value: Option<&str>,
545        template_old_value: Option<&str>,
546        actual_new_value: Option<&str>,
547    ) -> Result<Option<String>, ChangeConflict> {
548        Err(ChangeConflict {
549            para_key: (para_key.0.to_string(), para_key.1.to_string()),
550            field: field.to_string(),
551            actual_old_value: actual_old_value.map(|v| v.to_string()),
552            template_old_value: template_old_value.map(|v| v.to_string()),
553            actual_new_value: actual_new_value.map(|s| s.to_string()),
554        })
555    }
556
557    let resolve_conflict = resolve_conflict.unwrap_or(default_resolve_conflict);
558
559    for mut paragraph in deb822.paragraphs() {
560        let items: Vec<_> = paragraph.items().collect();
561        for item in items {
562            for (key, old_value, mut new_value) in changes.0.remove(&item).unwrap_or_default() {
563                if paragraph.get(&key) != old_value {
564                    new_value = resolve_conflict(
565                        (&item.0, &item.1),
566                        &key,
567                        old_value.as_deref(),
568                        paragraph.get(&key).as_deref(),
569                        new_value.as_deref(),
570                    )?;
571                }
572                if let Some(new_value) = new_value.as_ref() {
573                    paragraph.set(&key, new_value);
574                } else {
575                    paragraph.remove(&key);
576                }
577            }
578        }
579    }
580    // Add any new paragraphs that weren't processed earlier
581    for (key, p) in changes.0.drain() {
582        let mut paragraph = deb822.add_paragraph();
583        for (field, old_value, mut new_value) in p {
584            if old_value.is_some() {
585                new_value = resolve_conflict(
586                    (&key.0, &key.1),
587                    &field,
588                    old_value.as_deref(),
589                    paragraph.get(&field).as_deref(),
590                    new_value.as_deref(),
591                )?;
592            }
593            if let Some(new_value) = new_value {
594                paragraph.set(&field, &new_value);
595            }
596        }
597    }
598    Ok(())
599}
600
601fn find_template_path(path: &Path) -> Option<PathBuf> {
602    for ext in &["in", "m4"] {
603        let template_path = path.with_extension(ext);
604        if template_path.exists() {
605            return Some(template_path);
606        }
607    }
608    None
609}
610
611/// An editor for a control file that may be generated from a template.
612///
613/// This editor will automatically expand the template if it does not exist.
614/// It will also automatically update the template if the control file is changed.
615///
616/// # Example
617///
618/// ```rust
619/// use std::path::Path;
620/// use debian_workbench::control::TemplatedControlEditor;
621/// let td = tempfile::tempdir().unwrap();
622/// let mut editor = TemplatedControlEditor::create(td.path().join("control")).unwrap();
623/// editor.add_source("foo").set_architecture(Some("all"));
624/// editor.commit().unwrap();
625/// ```
626pub struct TemplatedControlEditor {
627    /// The primary editor for the control file.
628    primary: FsEditor<debian_control::Control>,
629    /// The template that was used to generate the control file.
630    template: Option<Template>,
631    /// Path to the control file.
632    path: PathBuf,
633    /// Whether the control file itself should not be written to disk.
634    template_only: bool,
635}
636
637impl Deref for TemplatedControlEditor {
638    type Target = debian_control::Control;
639
640    fn deref(&self) -> &Self::Target {
641        &self.primary
642    }
643}
644
645impl DerefMut for TemplatedControlEditor {
646    fn deref_mut(&mut self) -> &mut Self::Target {
647        &mut self.primary
648    }
649}
650
651impl TemplatedControlEditor {
652    /// Create a new control file editor.
653    pub fn create<P: AsRef<Path>>(
654        control_path: P,
655    ) -> Result<Self, EditorError<deb822_lossless::Error>> {
656        if control_path.as_ref().exists() {
657            return Err(EditorError::IoError(std::io::Error::new(
658                std::io::ErrorKind::AlreadyExists,
659                "Control file already exists",
660            )));
661        }
662        Self::new(control_path, true)
663    }
664
665    /// Return the type of the template used to generate the control file.
666    pub fn template_type(&self) -> Option<TemplateType> {
667        self.template.as_ref().map(|t| t.template_type)
668    }
669
670    /// Normalize field spacing in the control file.
671    ///
672    /// For template-based control files with deb822-style templates (CDBS, Directory),
673    /// this will normalize both the template file and the primary control file.
674    /// For non-deb822 templates (Rules, Gnome, Postgresql, etc.), this returns an error
675    /// since those control files are generated and shouldn't be normalized.
676    /// For files without templates, it normalizes the control file directly.
677    ///
678    /// # Returns
679    /// An error if a template exists but cannot be normalized, or if the template
680    /// is not a deb822-style template.
681    pub fn normalize_field_spacing(&mut self) -> Result<(), EditorError<deb822_lossless::Error>> {
682        let Some(template) = &self.template else {
683            // No template: normalize the control file directly
684            self.primary.as_mut_deb822().normalize_field_spacing();
685            return Ok(());
686        };
687
688        // Check if this is a deb822-style template
689        let is_deb822_template = matches!(
690            template.template_type,
691            TemplateType::Cdbs | TemplateType::Directory
692        );
693
694        if !is_deb822_template {
695            // Non-deb822 template: cannot normalize generated control files
696            return Err(EditorError::GeneratedFile(
697                self.path.clone(),
698                GeneratedFile {
699                    template_path: Some(template.template_path.clone()),
700                    template_type: None,
701                },
702            ));
703        }
704
705        // For deb822-style templates: normalize the template file
706        let mut template_editor =
707            FsEditor::<deb822_lossless::Deb822>::new(&template.template_path, true, false)?;
708        template_editor.normalize_field_spacing();
709        template_editor.commit()?;
710
711        // Also normalize the primary control file
712        self.primary.as_mut_deb822().normalize_field_spacing();
713        Ok(())
714    }
715
716    /// Open an existing control file.
717    pub fn open<P: AsRef<Path>>(
718        control_path: P,
719    ) -> Result<Self, EditorError<deb822_lossless::Error>> {
720        Self::new(control_path, false)
721    }
722
723    /// Create a new control file editor.
724    pub fn new<P: AsRef<Path>>(
725        control_path: P,
726        allow_missing: bool,
727    ) -> Result<Self, EditorError<deb822_lossless::Error>> {
728        let path = control_path.as_ref();
729        let (template, template_only) = if !path.exists() {
730            if let Some(template) = Template::find(path) {
731                match template.expand() {
732                    Ok(_) => {}
733                    Err(e) => {
734                        return Err(EditorError::TemplateError(
735                            template.template_path,
736                            e.to_string(),
737                        ))
738                    }
739                }
740                (Some(template), true)
741            } else if !allow_missing {
742                return Err(EditorError::IoError(std::io::Error::new(
743                    std::io::ErrorKind::NotFound,
744                    "No control file or template found",
745                )));
746            } else {
747                (None, false)
748            }
749        } else {
750            (Template::find(path), false)
751        };
752        let primary = FsEditor::<debian_control::Control>::new(path, false, false)?;
753        Ok(Self {
754            path: path.to_path_buf(),
755            primary,
756            template_only,
757            template,
758        })
759    }
760
761    /// Return a dictionary describing the changes since the base.
762    ///
763    /// # Returns
764    /// A dictionary mapping tuples of (kind, name) to list of (field_name, old_value, new_value)
765    fn changes(&self) -> Deb822Changes {
766        let orig = deb822_lossless::Deb822::read_relaxed(self.primary.orig_content().unwrap())
767            .unwrap()
768            .0;
769        let mut changes = Deb822Changes::new();
770
771        fn by_key(
772            ps: impl Iterator<Item = Paragraph>,
773        ) -> std::collections::HashMap<(String, String), Paragraph> {
774            let mut ret = std::collections::HashMap::new();
775            for p in ps {
776                if let Some(s) = p.get("Source") {
777                    ret.insert(("Source".to_string(), s), p);
778                } else if let Some(s) = p.get("Package") {
779                    ret.insert(("Package".to_string(), s), p);
780                } else {
781                    let k = p.items().next().unwrap();
782                    ret.insert(k, p);
783                }
784            }
785            ret
786        }
787
788        let orig_by_key = by_key(orig.paragraphs());
789        let new_by_key = by_key(self.as_deb822().paragraphs());
790        let keys = orig_by_key
791            .keys()
792            .chain(new_by_key.keys())
793            .collect::<std::collections::HashSet<_>>();
794        for key in keys {
795            let old = orig_by_key.get(key);
796            let new = new_by_key.get(key);
797            if old == new {
798                continue;
799            }
800            let fields = std::collections::HashSet::<String>::from_iter(
801                old.iter()
802                    .flat_map(|p| p.keys())
803                    .chain(new.iter().flat_map(|p| p.keys())),
804            );
805            for field in &fields {
806                let old_val = old.and_then(|x| x.get(field));
807                let new_val = new.and_then(|x| x.get(field));
808                if old_val != new_val {
809                    changes.insert(key.clone(), field.to_string(), old_val, new_val);
810                }
811            }
812        }
813        changes
814    }
815
816    /// Commit the changes to the control file and template.
817    pub fn commit(&mut self) -> Result<Vec<PathBuf>, EditorError<deb822_lossless::Error>> {
818        let mut changed_files: Vec<PathBuf> = vec![];
819        if self.template_only {
820            // Remove the control file if it exists.
821            match std::fs::remove_file(self.path.as_path()) {
822                Ok(_) => {}
823                Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
824                Err(e) => return Err(EditorError::IoError(e)),
825            }
826
827            changed_files.push(self.path.clone());
828
829            let template = self
830                .template
831                .as_ref()
832                .expect("template_only implies template");
833
834            // Update the template
835            let changed = match template.update(self.changes(), false) {
836                Ok(changed) => changed,
837                Err(e) => {
838                    return Err(EditorError::TemplateError(
839                        template.template_path.clone(),
840                        e.to_string(),
841                    ))
842                }
843            };
844            if changed {
845                changed_files.push(template.template_path.clone());
846            }
847        } else {
848            match self.primary.commit() {
849                Ok(files) => {
850                    changed_files.extend(files.iter().map(|p| p.to_path_buf()));
851                }
852                Err(EditorError::GeneratedFile(
853                    p,
854                    GeneratedFile {
855                        template_path: tp,
856                        template_type: tt,
857                    },
858                )) => {
859                    if tp.is_none() {
860                        return Err(EditorError::GeneratedFile(
861                            p,
862                            GeneratedFile {
863                                template_path: tp,
864                                template_type: tt,
865                            },
866                        ));
867                    }
868                    let template = if let Some(template) = self.template.as_ref() {
869                        template
870                    } else {
871                        return Err(EditorError::IoError(std::io::Error::new(
872                            std::io::ErrorKind::NotFound,
873                            "No control file or template found",
874                        )));
875                    };
876                    let changes = self.changes();
877                    let changed = match template.update(changes, true) {
878                        Ok(changed) => changed,
879                        Err(e) => {
880                            return Err(EditorError::TemplateError(tp.unwrap(), e.to_string()))
881                        }
882                    };
883                    changed_files = if changed {
884                        vec![tp.as_ref().unwrap().to_path_buf(), p]
885                    } else {
886                        vec![]
887                    };
888                }
889                Err(EditorError::IoError(e)) if e.kind() == std::io::ErrorKind::NotFound => {
890                    let template = if let Some(p) = self.template.as_ref() {
891                        p
892                    } else {
893                        return Err(EditorError::IoError(std::io::Error::new(
894                            std::io::ErrorKind::NotFound,
895                            "No control file or template found",
896                        )));
897                    };
898                    let changed = match template.update(self.changes(), !self.template_only) {
899                        Ok(changed) => changed,
900                        Err(e) => {
901                            return Err(EditorError::TemplateError(
902                                template.template_path.clone(),
903                                e.to_string(),
904                            ))
905                        }
906                    };
907                    if changed {
908                        changed_files.push(template.template_path.clone());
909                        changed_files.push(self.path.clone());
910                    }
911                }
912                Err(e) => return Err(e),
913            }
914        }
915
916        Ok(changed_files)
917    }
918}
919
920struct Template {
921    path: PathBuf,
922    template_path: PathBuf,
923    template_type: TemplateType,
924}
925
926impl Template {
927    fn find(path: &Path) -> Option<Self> {
928        let template_path = find_template_path(path)?;
929        let template_type = guess_template_type(&template_path, Some(path.parent().unwrap()))?;
930        Some(Self {
931            path: path.to_path_buf(),
932            template_path,
933            template_type,
934        })
935    }
936
937    fn expand(&self) -> Result<(), TemplateExpansionError> {
938        expand_control_template(&self.template_path, &self.path, self.template_type)
939    }
940
941    fn update(&self, changes: Deb822Changes, expand: bool) -> Result<bool, TemplateExpansionError> {
942        update_control_template(
943            &self.template_path,
944            self.template_type,
945            &self.path,
946            changes,
947            expand,
948        )
949    }
950}
951
952impl Editor<debian_control::Control> for TemplatedControlEditor {
953    fn orig_content(&self) -> Option<&[u8]> {
954        self.primary.orig_content()
955    }
956
957    fn updated_content(&self) -> Option<Vec<u8>> {
958        self.primary.updated_content()
959    }
960
961    fn rewritten_content(&self) -> Option<&[u8]> {
962        self.primary.rewritten_content()
963    }
964
965    fn is_generated(&self) -> bool {
966        self.primary.is_generated()
967    }
968
969    fn commit(&mut self) -> Result<Vec<std::path::PathBuf>, EditorError<deb822_lossless::Error>> {
970        TemplatedControlEditor::commit(self)
971    }
972
973    fn revert(&mut self) -> Result<(), EditorError<deb822_lossless::Error>> {
974        self.primary.revert()
975    }
976}
977
978#[cfg(test)]
979mod tests {
980    use super::*;
981
982    #[test]
983    fn test_format_description() {
984        let summary = "Summary";
985        let long_description = vec!["Long", "Description"];
986        let expected = "Summary\n Long\n Description\n";
987        assert_eq!(format_description(summary, long_description), expected);
988    }
989
990    #[test]
991    fn test_resolve_cdbs_conflicts() {
992        let val = resolve_cdbs_template(
993            ("Source", "libnetsds-perl"),
994            "Build-Depends",
995            Some("debhelper (>= 6), foo"),
996            Some("@cdbs@, debhelper (>= 9)"),
997            Some("debhelper (>= 10), foo"),
998        )
999        .unwrap();
1000
1001        assert_eq!(val, Some("@cdbs@, debhelper (>= 10)".to_string()));
1002
1003        let val = resolve_cdbs_template(
1004            ("Source", "libnetsds-perl"),
1005            "Build-Depends",
1006            Some("debhelper (>= 6), foo"),
1007            Some("@cdbs@, foo"),
1008            Some("debhelper (>= 10), foo"),
1009        )
1010        .unwrap();
1011        assert_eq!(val, Some("@cdbs@, debhelper (>= 10), foo".to_string()));
1012        let val = resolve_cdbs_template(
1013            ("Source", "libnetsds-perl"),
1014            "Build-Depends",
1015            Some("debhelper (>= 6), foo"),
1016            Some("@cdbs@, debhelper (>= 9)"),
1017            Some("debhelper (>= 10), foo"),
1018        )
1019        .unwrap();
1020        assert_eq!(val, Some("@cdbs@, debhelper (>= 10)".to_string()));
1021    }
1022
1023    #[test]
1024    fn test_normalize_field_spacing_without_template() {
1025        // Test normalize_field_spacing on a control file without a template
1026        let td = tempfile::tempdir().unwrap();
1027        let control_path = td.path().join("debian").join("control");
1028        std::fs::create_dir_all(control_path.parent().unwrap()).unwrap();
1029        std::fs::write(
1030            &control_path,
1031            b"Source: test\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n",
1032        )
1033        .unwrap();
1034
1035        let mut editor = TemplatedControlEditor::open(&control_path).unwrap();
1036
1037        // Check the original spacing
1038        let original = editor.as_deb822().to_string();
1039        assert_eq!(
1040            original,
1041            "Source: test\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n"
1042        );
1043
1044        // Normalize field spacing using the new method
1045        editor.normalize_field_spacing().unwrap();
1046
1047        // Check if normalization worked
1048        let normalized = editor.as_deb822().to_string();
1049        assert_eq!(
1050            normalized,
1051            "Source: test\nRecommends: foo\n\nPackage: test\nArchitecture: all\n"
1052        );
1053    }
1054
1055    #[test]
1056    fn test_normalize_field_spacing_with_non_deb822_template() {
1057        // Test that normalize_field_spacing returns an error for non-deb822 templates
1058        let td = tempfile::tempdir().unwrap();
1059        let debian_path = td.path().join("debian");
1060        std::fs::create_dir_all(&debian_path).unwrap();
1061
1062        let control_in_path = debian_path.join("control.in");
1063        let control_path = debian_path.join("control");
1064        let rules_path = debian_path.join("rules");
1065
1066        // Create a Rules-style template
1067        std::fs::write(
1068            &control_in_path,
1069            b"Source: test\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n",
1070        )
1071        .unwrap();
1072
1073        // Create a rules file that generates control
1074        std::fs::write(
1075            &rules_path,
1076            b"#!/usr/bin/make -f\n\ndebian/control: debian/control.in\n\tcp $< $@\n",
1077        )
1078        .unwrap();
1079
1080        // Create a generated control file
1081        std::fs::write(
1082            &control_path,
1083            b"Source: test\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n",
1084        )
1085        .unwrap();
1086
1087        let mut editor = TemplatedControlEditor::open(&control_path).unwrap();
1088        assert_eq!(editor.template_type(), Some(TemplateType::Rules));
1089
1090        // Normalize field spacing should fail for non-deb822 templates
1091        let result = editor.normalize_field_spacing();
1092        assert!(result.is_err());
1093
1094        // Verify it's a GeneratedFile error
1095        match result {
1096            Err(EditorError::GeneratedFile(_, _)) => {
1097                // Expected error type
1098            }
1099            _ => panic!("Expected GeneratedFile error"),
1100        }
1101    }
1102
1103    #[test]
1104    fn test_normalize_field_spacing_with_cdbs_template() {
1105        // Test normalize_field_spacing with a CDBS template
1106        // Note: We can't actually expand CDBS templates in tests, so we test that
1107        // the template file itself gets normalized
1108        let td = tempfile::tempdir().unwrap();
1109        let debian_path = td.path().join("debian");
1110        std::fs::create_dir_all(&debian_path).unwrap();
1111
1112        let control_in_path = debian_path.join("control.in");
1113        let control_path = debian_path.join("control");
1114
1115        // Create a CDBS-style template with double spacing
1116        std::fs::write(
1117            &control_in_path,
1118            b"Source: test\nBuild-Depends:  @cdbs@\nRecommends:  ${cdbs:Recommends}\n\nPackage: test\nArchitecture: all\n",
1119        )
1120        .unwrap();
1121
1122        // Create a control file (pretending it was generated from the template)
1123        std::fs::write(
1124            &control_path,
1125            b"Source: test\nBuild-Depends: debhelper\nRecommends:  foo\n\nPackage: test\nArchitecture: all\n",
1126        )
1127        .unwrap();
1128
1129        let mut editor = TemplatedControlEditor::open(&control_path).unwrap();
1130        assert_eq!(editor.template_type(), Some(TemplateType::Cdbs));
1131
1132        // Verify original template has double spacing
1133        let original_template = std::fs::read_to_string(&control_in_path).unwrap();
1134        assert_eq!(
1135            original_template,
1136            "Source: test\nBuild-Depends:  @cdbs@\nRecommends:  ${cdbs:Recommends}\n\nPackage: test\nArchitecture: all\n"
1137        );
1138
1139        // Normalize field spacing - this should normalize both template and primary
1140        editor.normalize_field_spacing().unwrap();
1141
1142        // Check that the template was normalized
1143        let template_content = std::fs::read_to_string(&control_in_path).unwrap();
1144        assert_eq!(
1145            template_content,
1146            "Source: test\nBuild-Depends: @cdbs@\nRecommends: ${cdbs:Recommends}\n\nPackage: test\nArchitecture: all\n"
1147        );
1148
1149        // Check that the primary control file was also normalized
1150        let control_content = editor.as_deb822().to_string();
1151        assert_eq!(
1152            control_content,
1153            "Source: test\nBuild-Depends: debhelper\nRecommends: foo\n\nPackage: test\nArchitecture: all\n"
1154        );
1155    }
1156
1157    mod guess_template_type {
1158
1159        #[test]
1160        fn test_rules_generates_control() {
1161            let td = tempfile::tempdir().unwrap();
1162            std::fs::create_dir(td.path().join("debian")).unwrap();
1163            std::fs::write(
1164                td.path().join("debian/rules"),
1165                r#"%:
1166	dh $@
1167
1168debian/control: debian/control.in
1169	cp $@ $<
1170"#,
1171            )
1172            .unwrap();
1173            assert_eq!(
1174                super::guess_template_type(
1175                    &td.path().join("debian/control.in"),
1176                    Some(&td.path().join("debian"))
1177                ),
1178                Some(super::TemplateType::Rules)
1179            );
1180        }
1181
1182        #[test]
1183        fn test_rules_generates_control_percent() {
1184            let td = tempfile::tempdir().unwrap();
1185            std::fs::create_dir(td.path().join("debian")).unwrap();
1186            std::fs::write(
1187                td.path().join("debian/rules"),
1188                r#"%:
1189	dh $@
1190
1191debian/%: debian/%.in
1192	cp $@ $<
1193"#,
1194            )
1195            .unwrap();
1196            assert_eq!(
1197                super::guess_template_type(
1198                    &td.path().join("debian/control.in"),
1199                    Some(&td.path().join("debian"))
1200                ),
1201                Some(super::TemplateType::Rules)
1202            );
1203        }
1204
1205        #[test]
1206        fn test_rules_generates_control_blends() {
1207            let td = tempfile::tempdir().unwrap();
1208            std::fs::create_dir(td.path().join("debian")).unwrap();
1209            std::fs::write(
1210                td.path().join("debian/rules"),
1211                r#"%:
1212	dh $@
1213
1214include /usr/share/blends-dev/rules
1215"#,
1216            )
1217            .unwrap();
1218            assert_eq!(
1219                super::guess_template_type(
1220                    &td.path().join("debian/control.stub"),
1221                    Some(&td.path().join("debian"))
1222                ),
1223                Some(super::TemplateType::Rules)
1224            );
1225        }
1226
1227        #[test]
1228        fn test_empty_template() {
1229            let td = tempfile::tempdir().unwrap();
1230            std::fs::create_dir(td.path().join("debian")).unwrap();
1231            // No paragraph
1232            std::fs::write(td.path().join("debian/control.in"), "").unwrap();
1233
1234            assert_eq!(
1235                None,
1236                super::guess_template_type(
1237                    &td.path().join("debian/control.in"),
1238                    Some(&td.path().join("debian"))
1239                )
1240            );
1241        }
1242
1243        #[test]
1244        fn test_build_depends_cdbs() {
1245            let td = tempfile::tempdir().unwrap();
1246            std::fs::create_dir(td.path().join("debian")).unwrap();
1247            std::fs::write(
1248                td.path().join("debian/control.in"),
1249                r#"Source: blah
1250Build-Depends: cdbs
1251Vcs-Git: file://
1252
1253Package: bar
1254"#,
1255            )
1256            .unwrap();
1257            assert_eq!(
1258                Some(super::TemplateType::Cdbs),
1259                super::guess_template_type(
1260                    &td.path().join("debian/control.in"),
1261                    Some(&td.path().join("debian"))
1262                )
1263            );
1264        }
1265
1266        #[test]
1267        fn test_no_build_depends() {
1268            let td = tempfile::tempdir().unwrap();
1269            std::fs::create_dir(td.path().join("debian")).unwrap();
1270            std::fs::write(
1271                td.path().join("debian/control.in"),
1272                r#"Source: blah
1273Vcs-Git: file://
1274
1275Package: bar
1276"#,
1277            )
1278            .unwrap();
1279            assert_eq!(
1280                None,
1281                super::guess_template_type(
1282                    &td.path().join("debian/control.in"),
1283                    Some(&td.path().join("debian"))
1284                )
1285            );
1286        }
1287
1288        #[test]
1289        fn test_gnome() {
1290            let td = tempfile::tempdir().unwrap();
1291            std::fs::create_dir(td.path().join("debian")).unwrap();
1292            std::fs::write(
1293                td.path().join("debian/control.in"),
1294                r#"Foo @GNOME_TEAM@
1295"#,
1296            )
1297            .unwrap();
1298            assert_eq!(
1299                Some(super::TemplateType::Gnome),
1300                super::guess_template_type(
1301                    &td.path().join("debian/control.in"),
1302                    Some(&td.path().join("debian"))
1303                )
1304            );
1305        }
1306
1307        #[test]
1308        fn test_gnome_build_depends() {
1309            let td = tempfile::tempdir().unwrap();
1310            std::fs::create_dir(td.path().join("debian")).unwrap();
1311            std::fs::write(
1312                td.path().join("debian/control.in"),
1313                r#"Source: blah
1314Build-Depends: gnome-pkg-tools, libc6-dev
1315"#,
1316            )
1317            .unwrap();
1318            assert_eq!(
1319                Some(super::TemplateType::Gnome),
1320                super::guess_template_type(
1321                    &td.path().join("debian/control.in"),
1322                    Some(&td.path().join("debian"))
1323                )
1324            );
1325        }
1326
1327        #[test]
1328        fn test_cdbs() {
1329            let td = tempfile::tempdir().unwrap();
1330            std::fs::create_dir(td.path().join("debian")).unwrap();
1331            std::fs::write(
1332                td.path().join("debian/control.in"),
1333                r#"Source: blah
1334Build-Depends: debhelper, cdbs
1335"#,
1336            )
1337            .unwrap();
1338            assert_eq!(
1339                Some(super::TemplateType::Cdbs),
1340                super::guess_template_type(
1341                    &td.path().join("debian/control.in"),
1342                    Some(&td.path().join("debian"))
1343                )
1344            );
1345        }
1346
1347        #[test]
1348        fn test_multiple_paragraphs() {
1349            let td = tempfile::tempdir().unwrap();
1350            std::fs::create_dir(td.path().join("debian")).unwrap();
1351            std::fs::write(
1352                td.path().join("debian/control.in"),
1353                r#"Source: blah
1354Build-Depends: debhelper, cdbs
1355
1356Package: foo
1357"#,
1358            )
1359            .unwrap();
1360            assert_eq!(
1361                Some(super::TemplateType::Cdbs),
1362                super::guess_template_type(
1363                    &td.path().join("debian/control.in"),
1364                    Some(&td.path().join("debian"))
1365                )
1366            );
1367        }
1368
1369        #[test]
1370        fn test_directory() {
1371            let td = tempfile::tempdir().unwrap();
1372            std::fs::create_dir(td.path().join("debian")).unwrap();
1373            std::fs::create_dir(td.path().join("debian/control.in")).unwrap();
1374            assert_eq!(
1375                Some(super::TemplateType::Directory),
1376                super::guess_template_type(
1377                    &td.path().join("debian/control.in"),
1378                    Some(&td.path().join("debian"))
1379                )
1380            );
1381        }
1382
1383        #[test]
1384        fn test_debcargo() {
1385            let td = tempfile::tempdir().unwrap();
1386            std::fs::create_dir(td.path().join("debian")).unwrap();
1387            std::fs::write(
1388                td.path().join("debian/control.in"),
1389                r#"Source: blah
1390Build-Depends: bar
1391"#,
1392            )
1393            .unwrap();
1394            std::fs::write(
1395                td.path().join("debian/debcargo.toml"),
1396                r#"maintainer = Joe Example <joe@example.com>
1397"#,
1398            )
1399            .unwrap();
1400            assert_eq!(
1401                Some(super::TemplateType::Debcargo),
1402                super::guess_template_type(
1403                    &td.path().join("debian/control.in"),
1404                    Some(&td.path().join("debian"))
1405                )
1406            );
1407        }
1408    }
1409
1410    #[test]
1411    fn test_postgresql() {
1412        let td = tempfile::tempdir().unwrap();
1413        std::fs::create_dir(td.path().join("debian")).unwrap();
1414        std::fs::write(
1415            td.path().join("debian/control.in"),
1416            r#"Source: blah
1417Build-Depends: bar, postgresql
1418
1419Package: foo-PGVERSION
1420"#,
1421        )
1422        .unwrap();
1423        assert_eq!(
1424            Some(super::TemplateType::Postgresql),
1425            super::guess_template_type(
1426                &td.path().join("debian/control.in"),
1427                Some(&td.path().join("debian"))
1428            )
1429        );
1430    }
1431
1432    #[test]
1433    fn test_apply_changes() {
1434        let mut deb822: deb822_lossless::Deb822 = r#"Source: blah
1435Build-Depends: debhelper (>= 6), foo
1436
1437Package: bar
1438"#
1439        .parse()
1440        .unwrap();
1441
1442        let mut changes = Deb822Changes(std::collections::HashMap::new());
1443        changes.0.insert(
1444            ("Source".to_string(), "blah".to_string()),
1445            vec![(
1446                "Build-Depends".to_string(),
1447                Some("debhelper (>= 6), foo".to_string()),
1448                Some("debhelper (>= 10), foo".to_string()),
1449            )],
1450        );
1451
1452        super::apply_changes(&mut deb822, changes, None).unwrap();
1453
1454        assert_eq!(
1455            deb822.to_string(),
1456            r#"Source: blah
1457Build-Depends: debhelper (>= 10), foo
1458
1459Package: bar
1460"#
1461        );
1462    }
1463
1464    #[test]
1465    fn test_apply_changes_new_paragraph() {
1466        let mut deb822: deb822_lossless::Deb822 = r#"Source: blah
1467Build-Depends: debhelper (>= 6), foo
1468
1469Package: bar
1470"#
1471        .parse()
1472        .unwrap();
1473
1474        let mut changes = Deb822Changes(std::collections::HashMap::new());
1475        changes.0.insert(
1476            ("Source".to_string(), "blah".to_string()),
1477            vec![(
1478                "Build-Depends".to_string(),
1479                Some("debhelper (>= 6), foo".to_string()),
1480                Some("debhelper (>= 10), foo".to_string()),
1481            )],
1482        );
1483        changes.0.insert(
1484            ("Package".to_string(), "blah2".to_string()),
1485            vec![
1486                ("Package".to_string(), None, Some("blah2".to_string())),
1487                (
1488                    "Description".to_string(),
1489                    None,
1490                    Some("Some package".to_string()),
1491                ),
1492            ],
1493        );
1494
1495        super::apply_changes(&mut deb822, changes, None).unwrap();
1496
1497        assert_eq!(
1498            deb822.to_string(),
1499            r#"Source: blah
1500Build-Depends: debhelper (>= 10), foo
1501
1502Package: bar
1503
1504Package: blah2
1505Description: Some package
1506"#
1507        );
1508    }
1509
1510    #[test]
1511    fn test_apply_changes_conflict() {
1512        let mut deb822: deb822_lossless::Deb822 = r#"Source: blah
1513Build-Depends: debhelper (>= 6), foo
1514
1515Package: bar
1516"#
1517        .parse()
1518        .unwrap();
1519
1520        let mut changes = Deb822Changes(std::collections::HashMap::new());
1521        changes.0.insert(
1522            ("Source".to_string(), "blah".to_string()),
1523            vec![(
1524                "Build-Depends".to_string(),
1525                Some("debhelper (>= 7), foo".to_string()),
1526                Some("debhelper (>= 10), foo".to_string()),
1527            )],
1528        );
1529
1530        let result = super::apply_changes(&mut deb822, changes, None);
1531        assert!(result.is_err());
1532        let err = result.unwrap_err();
1533        assert_eq!(
1534            err,
1535            ChangeConflict {
1536                para_key: ("Source".to_string(), "blah".to_string()),
1537                field: "Build-Depends".to_string(),
1538                actual_old_value: Some("debhelper (>= 7), foo".to_string()),
1539                template_old_value: Some("debhelper (>= 6), foo".to_string()),
1540                actual_new_value: Some("debhelper (>= 10), foo".to_string()),
1541            }
1542        );
1543    }
1544
1545    #[test]
1546    fn test_apply_changes_resolve_conflict() {
1547        let mut deb822: deb822_lossless::Deb822 = r#"Source: blah
1548Build-Depends: debhelper (>= 6), foo
1549
1550Package: bar
1551"#
1552        .parse()
1553        .unwrap();
1554
1555        let mut changes = Deb822Changes(std::collections::HashMap::new());
1556        changes.0.insert(
1557            ("Source".to_string(), "blah".to_string()),
1558            vec![(
1559                "Build-Depends".to_string(),
1560                Some("debhelper (>= 7), foo".to_string()),
1561                Some("debhelper (>= 10), foo".to_string()),
1562            )],
1563        );
1564
1565        let result = super::apply_changes(&mut deb822, changes, Some(|_, _, _, _, _| Ok(None)));
1566        assert!(result.is_ok());
1567        assert_eq!(
1568            deb822.to_string(),
1569            r#"Source: blah
1570
1571Package: bar
1572"#
1573        );
1574    }
1575
1576    mod control_editor {
1577        #[test]
1578        fn test_do_not_edit() {
1579            let td = tempfile::tempdir().unwrap();
1580            std::fs::create_dir(td.path().join("debian")).unwrap();
1581            std::fs::write(
1582                td.path().join("debian/control"),
1583                r#"# DO NOT EDIT
1584# This file was generated by blah
1585
1586Source: blah
1587Testsuite: autopkgtest
1588
1589"#,
1590            )
1591            .unwrap();
1592            let mut editor =
1593                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1594            editor.source().unwrap().set_name("foo");
1595            let changes = editor.changes();
1596            assert_eq!(
1597                changes.normalized(),
1598                vec![
1599                    (
1600                        ("Source", "blah"),
1601                        vec![
1602                            ("Source", Some("blah"), None),
1603                            ("Testsuite", Some("autopkgtest"), None),
1604                        ]
1605                    ),
1606                    (
1607                        ("Source", "foo"),
1608                        vec![
1609                            ("Source", None, Some("foo")),
1610                            ("Testsuite", None, Some("autopkgtest"))
1611                        ]
1612                    )
1613                ]
1614            );
1615            assert!(matches!(
1616                editor.commit().unwrap_err(),
1617                super::EditorError::GeneratedFile(_, _)
1618            ));
1619        }
1620
1621        #[test]
1622        fn test_add_binary() {
1623            let td = tempfile::tempdir().unwrap();
1624            std::fs::create_dir(td.path().join("debian")).unwrap();
1625            std::fs::write(
1626                td.path().join("debian/control"),
1627                r#"Source: blah
1628Testsuite: autopkgtest
1629
1630Package: blah
1631Description: Some description
1632 And there are more lines
1633 And more lines
1634"#,
1635            )
1636            .unwrap();
1637            let mut editor =
1638                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1639            let mut binary = editor.add_binary("foo");
1640            binary.set_description(Some("A new package foo"));
1641            let paths = editor.commit().unwrap();
1642            assert_eq!(paths.len(), 1);
1643        }
1644
1645        #[test]
1646        fn test_list_binaries() {
1647            let td = tempfile::tempdir().unwrap();
1648            std::fs::create_dir(td.path().join("debian")).unwrap();
1649            std::fs::write(
1650                td.path().join("debian/control"),
1651                r#"Source: blah
1652Testsuite: autopkgtest
1653
1654Package: blah
1655Description: Some description
1656 And there are more lines
1657 And more lines
1658"#,
1659            )
1660            .unwrap();
1661            let mut editor =
1662                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1663            let binaries = editor.binaries().collect::<Vec<_>>();
1664            assert_eq!(binaries.len(), 1);
1665            assert_eq!(binaries[0].name().as_deref(), Some("blah"));
1666            assert_eq!(editor.commit().unwrap(), Vec::<&std::path::Path>::new());
1667        }
1668
1669        #[test]
1670        fn test_no_source() {
1671            let td = tempfile::tempdir().unwrap();
1672            std::fs::create_dir(td.path().join("debian")).unwrap();
1673            std::fs::write(
1674                td.path().join("debian/control"),
1675                r#"Package: blah
1676Testsuite: autopkgtest
1677
1678Package: bar
1679"#,
1680            )
1681            .unwrap();
1682            let editor =
1683                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1684            assert!(editor.source().is_none());
1685        }
1686
1687        #[test]
1688        fn test_create() {
1689            let td = tempfile::tempdir().unwrap();
1690            std::fs::create_dir(td.path().join("debian")).unwrap();
1691            let mut editor =
1692                super::TemplatedControlEditor::create(td.path().join("debian/control")).unwrap();
1693            editor.add_source("foo");
1694            assert_eq!(
1695                r#"Source: foo
1696"#,
1697                editor.as_deb822().to_string()
1698            );
1699        }
1700
1701        #[test]
1702        fn test_do_not_edit_no_change() {
1703            let td = tempfile::tempdir().unwrap();
1704            std::fs::create_dir(td.path().join("debian")).unwrap();
1705            std::fs::write(
1706                td.path().join("debian/control"),
1707                r#"# DO NOT EDIT
1708# This file was generated by blah
1709
1710Source: blah
1711Testsuite: autopkgtest
1712
1713"#,
1714            )
1715            .unwrap();
1716            let mut editor =
1717                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1718            assert_eq!(editor.commit().unwrap(), Vec::<&std::path::Path>::new());
1719        }
1720
1721        #[test]
1722        fn test_unpreservable() {
1723            let td = tempfile::tempdir().unwrap();
1724            std::fs::create_dir(td.path().join("debian")).unwrap();
1725            std::fs::write(
1726                td.path().join("debian/control"),
1727                r#"Source: blah
1728# A comment
1729Testsuite: autopkgtest
1730
1731"#,
1732            )
1733            .unwrap();
1734            let mut editor =
1735                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1736            editor
1737                .source()
1738                .unwrap()
1739                .as_mut_deb822()
1740                .set("NewField", "New Field");
1741
1742            editor.commit().unwrap();
1743
1744            assert_eq!(
1745                r#"Source: blah
1746# A comment
1747Testsuite: autopkgtest
1748NewField: New Field
1749
1750"#,
1751                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1752            );
1753        }
1754
1755        #[test]
1756        fn test_modify_source() {
1757            let td = tempfile::tempdir().unwrap();
1758            std::fs::create_dir(td.path().join("debian")).unwrap();
1759            std::fs::write(
1760                td.path().join("debian/control"),
1761                r#"Source: blah
1762Testsuite: autopkgtest
1763"#,
1764            )
1765            .unwrap();
1766
1767            let mut editor =
1768                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1769            editor
1770                .source()
1771                .unwrap()
1772                .as_mut_deb822()
1773                .set("XS-Vcs-Git", "git://github.com/example/example");
1774
1775            editor.commit().unwrap();
1776
1777            assert_eq!(
1778                r#"Source: blah
1779Testsuite: autopkgtest
1780XS-Vcs-Git: git://github.com/example/example
1781"#,
1782                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1783            );
1784        }
1785
1786        /*
1787                #[test]
1788                fn test_wrap_and_sort() {
1789                    let td = tempfile::tempdir().unwrap();
1790                    std::fs::create_dir(td.path().join("debian")).unwrap();
1791
1792                    std::fs::write(
1793                        td.path().join("debian/control"),
1794                        r#"Source: blah
1795        Testsuite: autopkgtest
1796        Depends: package3, package2
1797
1798        Package: libblah
1799        Section: extra
1800        "#,
1801                    )
1802                    .unwrap();
1803
1804                    let mut editor =
1805                        super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1806                    editor.wrap_and_sort(trailing_comma = true).unwrap();
1807
1808                    editor.commit().unwrap();
1809
1810                    assert_eq!(
1811                        r#"Source: blah
1812        Testsuite: autopkgtest
1813        Depends: package2, package3,
1814
1815        Package: libblah
1816        Section: extra
1817        "#,
1818                        std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1819                    );
1820                }
1821
1822                #[test]
1823                fn test_sort_binaries() {
1824                    let td = tempfile::tempdir().unwrap();
1825                    std::fs::create_dir(td.path().join("debian")).unwrap();
1826                    std::fs::write(
1827                        td.path().join("debian/control"),
1828                        r#"Source: blah
1829        Source: blah
1830        Testsuite: autopkgtest
1831        Depends: package3, package2
1832
1833        Package: libfoo
1834        Section: web
1835
1836        Package: libblah
1837        Section: extra
1838        "#,
1839                    )
1840                    .unwrap();
1841
1842                    let mut editor =
1843                        super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1844
1845                    editor.sort_binary_packages().unwrap();
1846
1847                    editor.commit().unwrap();
1848
1849                    assert_eq!(
1850                        r#"Source: blah
1851        Testsuite: autopkgtest
1852        Depends: package3, package2
1853
1854        Package: libblah
1855        Section: extra
1856
1857        Package: libfoo
1858        Section: web
1859        "#,
1860                        std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1861                    );
1862                }
1863
1864            */
1865
1866        #[test]
1867        fn test_modify_binary() {
1868            let td = tempfile::tempdir().unwrap();
1869            std::fs::create_dir(td.path().join("debian")).unwrap();
1870            std::fs::write(
1871                td.path().join("debian/control"),
1872                r#"Source: blah
1873Testsuite: autopkgtest
1874
1875Package: libblah
1876Section: extra
1877"#,
1878            )
1879            .unwrap();
1880
1881            let mut editor =
1882                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1883            let mut binary = editor
1884                .binaries()
1885                .find(|b| b.name().as_deref() == Some("libblah"))
1886                .unwrap();
1887            binary.set_architecture(Some("all"));
1888
1889            editor.commit().unwrap();
1890
1891            assert_eq!(
1892                r#"Source: blah
1893Testsuite: autopkgtest
1894
1895Package: libblah
1896Architecture: all
1897Section: extra
1898"#,
1899                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1900            );
1901        }
1902
1903        #[test]
1904        fn test_doesnt_strip_whitespace() {
1905            let td = tempfile::tempdir().unwrap();
1906            std::fs::create_dir(td.path().join("debian")).unwrap();
1907            std::fs::write(
1908                td.path().join("debian/control"),
1909                r#"Source: blah
1910Testsuite: autopkgtest
1911
1912"#,
1913            )
1914            .unwrap();
1915            let mut editor =
1916                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1917            editor.commit().unwrap();
1918
1919            assert_eq!(
1920                r#"Source: blah
1921Testsuite: autopkgtest
1922
1923"#,
1924                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
1925            );
1926        }
1927
1928        #[cfg(unix)]
1929        #[test]
1930        fn test_update_template() {
1931            use std::os::unix::fs::PermissionsExt;
1932            let td = tempfile::tempdir().unwrap();
1933            std::fs::create_dir(td.path().join("debian")).unwrap();
1934            std::fs::write(
1935                td.path().join("debian/control"),
1936                r#"# DO NOT EDIT
1937# This file was generated by blah
1938
1939Source: blah
1940Testsuite: autopkgtest
1941Uploaders: Jelmer Vernooij <jelmer@jelmer.uk>
1942
1943"#,
1944            )
1945            .unwrap();
1946            std::fs::write(
1947                td.path().join("debian/control.in"),
1948                r#"Source: blah
1949Testsuite: autopkgtest
1950Uploaders: @lintian-brush-test@
1951
1952"#,
1953            )
1954            .unwrap();
1955            std::fs::write(
1956                td.path().join("debian/rules"),
1957                r#"#!/usr/bin/make -f
1958
1959debian/control: debian/control.in
1960	sed -e 's/@lintian-brush-test@/testvalue/' < $< > $@
1961"#,
1962            )
1963            .unwrap();
1964            // Make debian/rules executable
1965            std::fs::set_permissions(
1966                td.path().join("debian/rules"),
1967                std::fs::Permissions::from_mode(0o755),
1968            )
1969            .unwrap();
1970
1971            let mut editor =
1972                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
1973            editor
1974                .source()
1975                .unwrap()
1976                .as_mut_deb822()
1977                .set("Testsuite", "autopkgtest8");
1978
1979            assert_eq!(
1980                editor.commit().unwrap(),
1981                vec![
1982                    td.path().join("debian/control.in"),
1983                    td.path().join("debian/control")
1984                ]
1985            );
1986
1987            assert_eq!(
1988                r#"Source: blah
1989Testsuite: autopkgtest8
1990Uploaders: @lintian-brush-test@
1991
1992"#,
1993                std::fs::read_to_string(td.path().join("debian/control.in")).unwrap()
1994            );
1995
1996            assert_eq!(
1997                r#"Source: blah
1998Testsuite: autopkgtest8
1999Uploaders: testvalue
2000
2001"#,
2002                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
2003            );
2004        }
2005
2006        #[cfg(unix)]
2007        #[test]
2008        fn test_update_template_only() {
2009            use std::os::unix::fs::PermissionsExt;
2010            let td = tempfile::tempdir().unwrap();
2011            std::fs::create_dir(td.path().join("debian")).unwrap();
2012            std::fs::write(
2013                td.path().join("debian/control.in"),
2014                r#"Source: blah
2015Testsuite: autopkgtest
2016Uploaders: @lintian-brush-test@
2017
2018"#,
2019            )
2020            .unwrap();
2021            std::fs::write(
2022                td.path().join("debian/rules"),
2023                r#"#!/usr/bin/make -f
2024
2025debian/control: debian/control.in
2026	sed -e 's/@lintian-brush-test@/testvalue/' < $< > $@
2027"#,
2028            )
2029            .unwrap();
2030
2031            std::fs::set_permissions(
2032                td.path().join("debian/rules"),
2033                std::fs::Permissions::from_mode(0o755),
2034            )
2035            .unwrap();
2036
2037            let mut editor =
2038                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2039            editor
2040                .source()
2041                .unwrap()
2042                .as_mut_deb822()
2043                .set("Testsuite", "autopkgtest8");
2044
2045            editor.commit().unwrap();
2046
2047            assert_eq!(
2048                r#"Source: blah
2049Testsuite: autopkgtest8
2050Uploaders: @lintian-brush-test@
2051
2052"#,
2053                std::fs::read_to_string(td.path().join("debian/control.in")).unwrap()
2054            );
2055
2056            assert!(!td.path().join("debian/control").exists());
2057        }
2058
2059        #[cfg(unix)]
2060        #[test]
2061        fn test_update_template_invalid_tokens() {
2062            use std::os::unix::fs::PermissionsExt;
2063            let td = tempfile::tempdir().unwrap();
2064            std::fs::create_dir(td.path().join("debian")).unwrap();
2065            std::fs::write(
2066                td.path().join("debian/control"),
2067                r#"# DO NOT EDIT
2068# This file was generated by blah
2069
2070Source: blah
2071Testsuite: autopkgtest
2072Uploaders: Jelmer Vernooij <jelmer@jelmer.uk>
2073"#,
2074            )
2075            .unwrap();
2076            std::fs::write(
2077                td.path().join("debian/control.in"),
2078                r#"Source: blah
2079Testsuite: autopkgtest
2080@OTHERSTUFF@
2081"#,
2082            )
2083            .unwrap();
2084
2085            std::fs::write(
2086                td.path().join("debian/rules"),
2087                r#"#!/usr/bin/make -f
2088
2089debian/control: debian/control.in
2090	sed -e 's/@OTHERSTUFF@/Vcs-Git: example.com/' < $< > $@
2091"#,
2092            )
2093            .unwrap();
2094
2095            std::fs::set_permissions(
2096                td.path().join("debian/rules"),
2097                std::fs::Permissions::from_mode(0o755),
2098            )
2099            .unwrap();
2100
2101            let mut editor =
2102                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2103            editor
2104                .source()
2105                .unwrap()
2106                .as_mut_deb822()
2107                .set("Testsuite", "autopkgtest8");
2108            editor.commit().unwrap();
2109
2110            assert_eq!(
2111                r#"Source: blah
2112Testsuite: autopkgtest8
2113@OTHERSTUFF@
2114"#,
2115                std::fs::read_to_string(td.path().join("debian/control.in")).unwrap()
2116            );
2117
2118            assert_eq!(
2119                r#"Source: blah
2120Testsuite: autopkgtest8
2121Vcs-Git: example.com
2122"#,
2123                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
2124            );
2125        }
2126
2127        #[test]
2128        fn test_update_cdbs_template() {
2129            let td = tempfile::tempdir().unwrap();
2130            std::fs::create_dir(td.path().join("debian")).unwrap();
2131
2132            std::fs::write(
2133                td.path().join("debian/control"),
2134                r#"Source: blah
2135Testsuite: autopkgtest
2136Build-Depends: some-foo, libc6
2137
2138"#,
2139            )
2140            .unwrap();
2141
2142            std::fs::write(
2143                td.path().join("debian/control.in"),
2144                r#"Source: blah
2145Testsuite: autopkgtest
2146Build-Depends: @cdbs@, libc6
2147
2148"#,
2149            )
2150            .unwrap();
2151
2152            let mut editor =
2153                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2154
2155            editor
2156                .source()
2157                .unwrap()
2158                .as_mut_deb822()
2159                .set("Build-Depends", "some-foo, libc6, some-bar");
2160
2161            assert_eq!(
2162                editor
2163                    .source()
2164                    .unwrap()
2165                    .build_depends()
2166                    .unwrap()
2167                    .to_string(),
2168                "some-foo, libc6, some-bar".to_string()
2169            );
2170
2171            assert_eq!(Some(super::TemplateType::Cdbs), editor.template_type());
2172
2173            assert_eq!(
2174                editor.commit().unwrap(),
2175                vec![
2176                    td.path().join("debian/control.in"),
2177                    td.path().join("debian/control")
2178                ]
2179            );
2180
2181            assert_eq!(
2182                r#"Source: blah
2183Testsuite: autopkgtest
2184Build-Depends: @cdbs@, libc6, some-bar
2185
2186"#,
2187                std::fs::read_to_string(td.path().join("debian/control.in")).unwrap()
2188            );
2189
2190            assert_eq!(
2191                r#"Source: blah
2192Testsuite: autopkgtest
2193Build-Depends: some-foo, libc6, some-bar
2194
2195"#,
2196                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
2197            );
2198        }
2199
2200        #[test]
2201        #[ignore = "Not implemented yet"]
2202        fn test_description_stays_last() {
2203            let td = tempfile::tempdir().unwrap();
2204            std::fs::create_dir(td.path().join("debian")).unwrap();
2205            std::fs::write(
2206                td.path().join("debian/control"),
2207                r#"Source: blah
2208Testsuite: autopkgtest
2209
2210Package: libblah
2211Section: extra
2212Description: foo
2213 bar
2214
2215"#,
2216            )
2217            .unwrap();
2218
2219            let mut editor =
2220                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2221            editor
2222                .binaries()
2223                .find(|b| b.name().as_deref() == Some("libblah"))
2224                .unwrap()
2225                .set_architecture(Some("all"));
2226
2227            editor.commit().unwrap();
2228
2229            assert_eq!(
2230                r#"Source: blah
2231Testsuite: autopkgtest
2232
2233Package: libblah
2234Section: extra
2235Architecture: all
2236Description: foo
2237 bar
2238"#,
2239                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
2240            );
2241        }
2242
2243        #[test]
2244        fn test_no_new_heading_whitespace() {
2245            let td = tempfile::tempdir().unwrap();
2246            std::fs::create_dir(td.path().join("debian")).unwrap();
2247            std::fs::write(
2248                td.path().join("debian/control"),
2249                r#"Source: blah
2250Build-Depends:
2251 debhelper-compat (= 11),
2252 uuid-dev
2253
2254"#,
2255            )
2256            .unwrap();
2257
2258            let mut editor =
2259                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2260            editor
2261                .source()
2262                .unwrap()
2263                .as_mut_deb822()
2264                .set("Build-Depends", "debhelper-compat (= 12),\nuuid-dev");
2265
2266            editor.commit().unwrap();
2267
2268            assert_eq!(
2269                r#"Source: blah
2270Build-Depends:
2271 debhelper-compat (= 12),
2272 uuid-dev
2273
2274"#,
2275                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
2276            );
2277        }
2278
2279        #[test]
2280        fn test_control_clone_behavior() {
2281            use std::io::Cursor;
2282
2283            let content = b"Source: test\nMaintainer: Joe <joe@example.com>\n";
2284            let (control1, _) =
2285                debian_control::Control::read_relaxed(Cursor::new(content)).unwrap();
2286            let control2 = control1.clone();
2287
2288            println!(
2289                "control1 name before: {:?}",
2290                control1.source().unwrap().name()
2291            );
2292            println!(
2293                "control2 name before: {:?}",
2294                control2.source().unwrap().name()
2295            );
2296
2297            // Modify control2
2298            control2.source().unwrap().set_name("changed");
2299
2300            println!(
2301                "control1 name after: {:?}",
2302                control1.source().unwrap().name()
2303            );
2304            println!(
2305                "control2 name after: {:?}",
2306                control2.source().unwrap().name()
2307            );
2308
2309            // If clone is deep, control1 should still be "test"
2310            // If clone is shallow, both will be "changed"
2311        }
2312
2313        #[test]
2314        fn test_revert_before_commit() {
2315            use crate::editor::Editor;
2316
2317            let td = tempfile::tempdir().unwrap();
2318            std::fs::create_dir(td.path().join("debian")).unwrap();
2319            std::fs::write(
2320                td.path().join("debian/control"),
2321                r#"Source: blah
2322Maintainer: Joe Developer <joe@example.com>
2323
2324Package: blah
2325Architecture: any
2326Description: Some package
2327"#,
2328            )
2329            .unwrap();
2330
2331            let mut editor =
2332                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2333
2334            // Verify original state
2335            assert_eq!(editor.source().unwrap().name(), Some("blah".to_string()));
2336            assert!(!editor.has_changed());
2337
2338            // Make a change
2339            editor.source().unwrap().set_name("newname");
2340            assert_eq!(editor.source().unwrap().name(), Some("newname".to_string()));
2341            assert!(editor.has_changed());
2342
2343            // Revert the change
2344            editor.revert().unwrap();
2345            assert_eq!(editor.source().unwrap().name(), Some("blah".to_string()));
2346            assert!(!editor.has_changed());
2347
2348            // File should not have changed
2349            assert_eq!(
2350                r#"Source: blah
2351Maintainer: Joe Developer <joe@example.com>
2352
2353Package: blah
2354Architecture: any
2355Description: Some package
2356"#,
2357                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
2358            );
2359        }
2360
2361        #[test]
2362        fn test_revert_after_commit() {
2363            use crate::editor::Editor;
2364
2365            let td = tempfile::tempdir().unwrap();
2366            std::fs::create_dir(td.path().join("debian")).unwrap();
2367            std::fs::write(
2368                td.path().join("debian/control"),
2369                r#"Source: blah
2370Maintainer: Joe Developer <joe@example.com>
2371
2372Package: blah
2373Architecture: any
2374Description: Some package
2375"#,
2376            )
2377            .unwrap();
2378
2379            let mut editor =
2380                super::TemplatedControlEditor::open(td.path().join("debian/control")).unwrap();
2381
2382            // Make a change and commit
2383            editor.source().unwrap().set_name("newname");
2384            assert!(editor.has_changed());
2385            editor.commit().unwrap();
2386            assert!(!editor.has_changed());
2387            assert_eq!(editor.source().unwrap().name(), Some("newname".to_string()));
2388
2389            // Verify the file was updated
2390            let content = std::fs::read_to_string(td.path().join("debian/control")).unwrap();
2391            assert_eq!(
2392                r#"Source: newname
2393Maintainer: Joe Developer <joe@example.com>
2394
2395Package: blah
2396Architecture: any
2397Description: Some package
2398"#,
2399                content
2400            );
2401
2402            // Make another change
2403            editor.source().unwrap().set_name("thirdname");
2404            assert_eq!(
2405                editor.source().unwrap().name(),
2406                Some("thirdname".to_string())
2407            );
2408            assert!(editor.has_changed());
2409
2410            // Revert should go back to committed state (newname, not original blah)
2411            editor.revert().unwrap();
2412            assert_eq!(editor.source().unwrap().name(), Some("newname".to_string()));
2413            assert!(!editor.has_changed());
2414
2415            // File should still have committed content
2416            assert_eq!(
2417                r#"Source: newname
2418Maintainer: Joe Developer <joe@example.com>
2419
2420Package: blah
2421Architecture: any
2422Description: Some package
2423"#,
2424                std::fs::read_to_string(td.path().join("debian/control")).unwrap()
2425            );
2426        }
2427    }
2428}