Skip to main content

debian_workbench/
editor.rs

1//! Editing files
2use breezyshim::error::Error as BrzError;
3use breezyshim::tree::MutableTree;
4use std::borrow::Cow;
5use std::io::BufRead;
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9/// The type of template used to generate a file.
10pub enum TemplateType {
11    /// M4 template
12    M4,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16/// Error indicating that a file is generated from another file.
17pub struct GeneratedFile {
18    /// The path to the template file
19    pub template_path: Option<PathBuf>,
20
21    /// The type of template
22    pub template_type: Option<TemplateType>,
23}
24
25impl std::fmt::Display for GeneratedFile {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        write!(f, "File is generated")?;
28        if let Some(template_path) = &self.template_path {
29            write!(f, " from {}", template_path.display())?;
30        }
31        Ok(())
32    }
33}
34
35impl std::error::Error for GeneratedFile {}
36
37#[derive(Clone, PartialEq, Eq)]
38/// Error indicating that formatting could not be preserved.
39pub struct FormattingUnpreservable {
40    /// The original contents of the file
41    original_contents: Option<Vec<u8>>,
42
43    /// The contents rewritten with our parser/serializer
44    rewritten_contents: Option<Vec<u8>>,
45}
46
47impl std::fmt::Debug for FormattingUnpreservable {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        f.debug_struct("FormattingUnpreservable")
50            .field(
51                "original_contents",
52                &self
53                    .original_contents
54                    .as_deref()
55                    .map(|x| std::str::from_utf8(x)),
56            )
57            .field(
58                "rewritten_contents",
59                &self
60                    .rewritten_contents
61                    .as_deref()
62                    .map(|x| std::str::from_utf8(x)),
63            )
64            .finish()
65    }
66}
67
68impl std::fmt::Display for FormattingUnpreservable {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        write!(f, "Unable to preserve formatting",)
71    }
72}
73
74impl std::error::Error for FormattingUnpreservable {}
75
76impl FormattingUnpreservable {
77    /// Get a unified diff of the original and rewritten contents.
78    pub fn diff(&self) -> Vec<String> {
79        let original_lines = std::str::from_utf8(self.original_contents.as_deref().unwrap_or(b""))
80            .unwrap()
81            .split_inclusive('\n')
82            .collect::<Vec<_>>();
83        let rewritten_lines =
84            std::str::from_utf8(self.rewritten_contents.as_deref().unwrap_or(b""))
85                .unwrap()
86                .split_inclusive('\n')
87                .collect::<Vec<_>>();
88
89        difflib::unified_diff(
90            original_lines.as_slice(),
91            rewritten_lines.as_slice(),
92            "original",
93            "rewritten",
94            "",
95            "",
96            3,
97        )
98    }
99}
100
101/// Check that formatting can be preserved.
102///
103/// # Arguments
104/// * `rewritten_text` - The rewritten file contents
105/// * `text` - The original file contents
106/// * `allow_reformatting` - Whether to allow reformatting
107fn check_preserve_formatting(
108    rewritten_text: Option<&[u8]>,
109    text: Option<&[u8]>,
110    allow_reformatting: bool,
111) -> Result<(), FormattingUnpreservable> {
112    if allow_reformatting {
113        return Ok(());
114    }
115    if rewritten_text == text {
116        return Ok(());
117    }
118    Err(FormattingUnpreservable {
119        original_contents: text.map(|x| x.to_vec()),
120        rewritten_contents: rewritten_text.map(|x| x.to_vec()),
121    })
122}
123
124/// Number of lines to scan for generated file indicators.
125pub const DO_NOT_EDIT_SCAN_LINES: usize = 20;
126
127fn check_generated_contents(bufread: &mut dyn BufRead) -> Result<(), GeneratedFile> {
128    for l in bufread.lines().take(DO_NOT_EDIT_SCAN_LINES) {
129        let l = if let Ok(l) = l { l } else { continue };
130        if l.contains("DO NOT EDIT")
131            || l.contains("Do not edit!")
132            || l.contains("This file is autogenerated")
133        {
134            return Err(GeneratedFile {
135                template_path: None,
136                template_type: None,
137            });
138        }
139    }
140    Ok(())
141}
142
143/// List of extensions for template files.
144pub const GENERATED_EXTENSIONS: &[&str] = &["in", "m4", "stub"];
145
146fn check_template_exists(path: &std::path::Path) -> Option<(PathBuf, Option<TemplateType>)> {
147    for ext in GENERATED_EXTENSIONS {
148        let template_path = path.with_extension(ext);
149        if template_path.exists() {
150            return Some((
151                template_path,
152                match ext {
153                    &"m4" => Some(TemplateType::M4),
154                    _ => None,
155                },
156            ));
157        }
158    }
159    None
160}
161
162fn tree_check_template_exists(
163    tree: &dyn MutableTree,
164    path: &std::path::Path,
165) -> Option<(PathBuf, Option<TemplateType>)> {
166    for ext in GENERATED_EXTENSIONS {
167        let template_path = path.with_extension(ext);
168        if tree.has_filename(&template_path) {
169            return Some((
170                template_path,
171                match ext {
172                    &"m4" => Some(TemplateType::M4),
173                    _ => None,
174                },
175            ));
176        }
177    }
178    None
179}
180
181/// Check if a file is generated from another file.
182///
183/// # Arguments
184/// * `path` - Path to the file to check
185///
186/// # Errors
187/// * `GeneratedFile` - when a generated file is found
188pub fn check_generated_file(path: &std::path::Path) -> Result<(), GeneratedFile> {
189    if let Some((template_path, template_type)) = check_template_exists(path) {
190        return Err(GeneratedFile {
191            template_path: Some(template_path),
192            template_type,
193        });
194    }
195
196    match std::fs::File::open(path) {
197        Ok(f) => {
198            let mut buf = std::io::BufReader::new(f);
199            check_generated_contents(&mut buf)?;
200        }
201        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
202        Err(e) => panic!("Error reading file: {}", e),
203    }
204    Ok(())
205}
206
207/// Check if a file is generated from another file.
208///
209/// # Arguments
210/// * `path` - Path to the file to check
211///
212/// # Errors
213/// * `GeneratedFile` - when a generated file is found
214pub fn tree_check_generated_file(
215    tree: &dyn MutableTree,
216    path: &std::path::Path,
217) -> Result<(), GeneratedFile> {
218    if let Some((template_path, template_type)) = tree_check_template_exists(tree, path) {
219        return Err(GeneratedFile {
220            template_path: Some(template_path),
221            template_type,
222        });
223    }
224
225    match tree.get_file(path) {
226        Ok(f) => {
227            let mut buf = std::io::BufReader::new(f);
228            check_generated_contents(&mut buf)?;
229        }
230        Err(BrzError::NoSuchFile(..)) => {}
231        Err(e) => panic!("Error reading file: {}", e),
232    }
233    Ok(())
234}
235
236#[derive(Debug)]
237/// Error that can occur when editing a file.
238pub enum EditorError<E: std::error::Error> {
239    /// One of the files is generated from another file, and we were unable to edit it.
240    GeneratedFile(PathBuf, GeneratedFile),
241
242    /// Error in a template file
243    TemplateError(PathBuf, String),
244
245    /// Unable to preserve formatting in a file.
246    FormattingUnpreservable(PathBuf, FormattingUnpreservable),
247
248    /// I/O error
249    IoError(std::io::Error),
250
251    /// Breezy error
252    BrzError(BrzError),
253
254    /// UTF-8 encoding error
255    Utf8Error(std::str::Utf8Error),
256
257    /// Error parsing or marshalling file contents
258    MarshallingError(E),
259}
260
261impl<E: std::error::Error> From<BrzError> for EditorError<E> {
262    fn from(e: BrzError) -> Self {
263        EditorError::BrzError(e)
264    }
265}
266
267impl<E: std::error::Error> std::fmt::Display for EditorError<E> {
268    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269        match self {
270            EditorError::GeneratedFile(p, _e) => {
271                write!(f, "File {} is generated from another file", p.display())
272            }
273            EditorError::FormattingUnpreservable(p, _e) => {
274                write!(f, "Unable to preserve formatting in {}", p.display())
275            }
276            EditorError::IoError(e) => write!(f, "I/O error: {}", e),
277            EditorError::BrzError(e) => write!(f, "Breezy error: {}", e),
278            EditorError::TemplateError(p, e) => {
279                write!(f, "Error in template {}: {}", p.display(), e)
280            }
281            EditorError::Utf8Error(e) => write!(f, "UTF-8 error: {}", e),
282            EditorError::MarshallingError(e) => write!(f, "Marshalling error: {}", e),
283        }
284    }
285}
286
287impl<E: std::error::Error> std::error::Error for EditorError<E> {}
288
289impl<E: std::error::Error> From<std::io::Error> for EditorError<E> {
290    fn from(e: std::io::Error) -> Self {
291        EditorError::IoError(e)
292    }
293}
294
295impl<E: std::error::Error> From<std::str::Utf8Error> for EditorError<E> {
296    fn from(e: std::str::Utf8Error) -> Self {
297        EditorError::Utf8Error(e)
298    }
299}
300
301#[cfg(feature = "merge3")]
302/// Update a file with a three-way merge.
303fn update_with_merge3(
304    original_contents: &[u8],
305    rewritten_contents: &[u8],
306    updated_contents: &[u8],
307) -> Option<Vec<u8>> {
308    let rewritten_lines = rewritten_contents
309        .split_inclusive(|&x| x == b'\n')
310        .collect::<Vec<_>>();
311    let original_lines = original_contents
312        .split_inclusive(|&x| x == b'\n')
313        .collect::<Vec<_>>();
314    let updated_lines = updated_contents
315        .split_inclusive(|&x| x == b'\n')
316        .collect::<Vec<_>>();
317    let m3 = merge3::Merge3::new(
318        rewritten_lines.as_slice(),
319        original_lines.as_slice(),
320        updated_lines.as_slice(),
321    );
322    if m3
323        .merge_regions()
324        .iter()
325        .any(|x| matches!(x, merge3::MergeRegion::Conflict { .. }))
326    {
327        return None;
328    }
329    Some(
330        m3.merge_lines(false, &merge3::StandardMarkers::default())
331            .concat(),
332    )
333}
334
335/// Reformat a file.
336///
337/// # Arguments
338/// * `original_contents` - The original contents of the file
339/// * `rewritten_contents` - The contents rewritten with our parser/serializer
340/// * `updated_contents` - Updated contents rewritten with our parser/serializer after changes were
341///  made
342///  * `allow_reformatting` - Whether to allow reformatting of the file
343///
344///  # Returns
345///  A tuple with the new contents and a boolean indicating whether the file was changed`
346fn reformat_file<'a>(
347    original_contents: Option<&'a [u8]>,
348    rewritten_contents: Option<&'a [u8]>,
349    updated_contents: Option<&'a [u8]>,
350    allow_reformatting: bool,
351) -> Result<(Option<Cow<'a, [u8]>>, bool), FormattingUnpreservable> {
352    if updated_contents == rewritten_contents || updated_contents == original_contents {
353        return Ok((updated_contents.map(Cow::Borrowed), false));
354    }
355    #[allow(unused_mut)]
356    let mut updated_contents = updated_contents.map(std::borrow::Cow::Borrowed);
357    match check_preserve_formatting(rewritten_contents, original_contents, allow_reformatting) {
358        Ok(()) => {}
359        Err(e) => {
360            if rewritten_contents.is_none()
361                || original_contents.is_none()
362                || updated_contents.is_none()
363            {
364                return Err(e);
365            }
366            #[cfg(feature = "merge3")]
367            {
368                // Run three way merge
369                log::debug!("Unable to preserve formatting; falling back to merge3");
370                updated_contents = Some(std::borrow::Cow::Owned(
371                    if let Some(lines) = update_with_merge3(
372                        original_contents.unwrap(),
373                        rewritten_contents.unwrap(),
374                        updated_contents.unwrap().as_ref(),
375                    ) {
376                        lines
377                    } else {
378                        return Err(e);
379                    },
380                ));
381            }
382            #[cfg(not(feature = "merge3"))]
383            {
384                log::debug!("Unable to preserve formatting; merge3 feature not enabled");
385                return Err(e);
386            }
387        }
388    }
389
390    Ok((updated_contents, true))
391}
392
393/// Edit a formatted file.
394///
395/// # Arguments
396/// * `path` - Path to the file
397/// * `original_contents` - The original contents of the file
398/// * `rewritten_contents` - The contents rewritten with our parser/serializer
399/// * `updated_contents` - Updated contents rewritten with our parser/serializer after changes were
400///   made
401/// * `allow_generated` - Do not raise `GeneratedFile` when encountering a generated file
402/// * `allow_reformatting` - Whether to allow reformatting of the file
403///
404/// # Returns
405/// `true` if the file was changed, `false` otherwise
406pub fn edit_formatted_file<E: std::error::Error>(
407    path: &std::path::Path,
408    original_contents: Option<&[u8]>,
409    rewritten_contents: Option<&[u8]>,
410    updated_contents: Option<&[u8]>,
411    allow_generated: bool,
412    allow_reformatting: bool,
413) -> Result<bool, EditorError<E>> {
414    if original_contents == updated_contents {
415        return Ok(false);
416    }
417    let (updated_contents, changed) = reformat_file(
418        original_contents,
419        rewritten_contents,
420        updated_contents,
421        allow_reformatting,
422    )
423    .map_err(|e| EditorError::FormattingUnpreservable(path.to_path_buf(), e))?;
424    if changed && !allow_generated {
425        check_generated_file(path)
426            .map_err(|e| EditorError::GeneratedFile(path.to_path_buf(), e))?;
427    }
428
429    if changed {
430        if let Some(updated_contents) = updated_contents {
431            std::fs::write(path, updated_contents)?;
432        } else {
433            std::fs::remove_file(path)?;
434        }
435    }
436    Ok(changed)
437}
438
439/// Edit a formatted file in a tree.
440///
441/// # Arguments
442/// * `tree` - The tree to edit
443/// * `path` - Path to the file
444/// * `original_contents` - The original contents of the file
445/// * `rewritten_contents` - The contents rewritten with our parser/serializer
446/// * `updated_contents` - Updated contents rewritten with our parser/serializer after changes were
447///   made
448/// * `allow_generated` - Do not raise `GeneratedFile` when encountering a generated file
449/// * `allow_reformatting` - Whether to allow reformatting of the file
450///
451/// # Returns
452/// `true` if the file was changed, `false` otherwise
453pub fn tree_edit_formatted_file<E: std::error::Error>(
454    tree: &dyn MutableTree,
455    path: &std::path::Path,
456    original_contents: Option<&[u8]>,
457    rewritten_contents: Option<&[u8]>,
458    updated_contents: Option<&[u8]>,
459    allow_generated: bool,
460    allow_reformatting: bool,
461) -> Result<bool, EditorError<E>> {
462    assert!(path.is_relative());
463    if original_contents == updated_contents {
464        return Ok(false);
465    }
466    if !allow_generated {
467        tree_check_generated_file(tree, path)
468            .map_err(|e| EditorError::GeneratedFile(path.to_path_buf(), e))?;
469    }
470
471    let (updated_contents, changed) = reformat_file(
472        original_contents,
473        rewritten_contents,
474        updated_contents,
475        allow_reformatting,
476    )
477    .map_err(|e| EditorError::FormattingUnpreservable(path.to_path_buf(), e))?;
478    if changed {
479        if let Some(updated_contents) = updated_contents {
480            tree.put_file_bytes_non_atomic(path, updated_contents.as_ref())?;
481            tree.add(&[path])?;
482        } else if tree.has_filename(path) {
483            tree.remove(&[path])?;
484        }
485    }
486    Ok(changed)
487}
488
489/// A trait for types that can be edited
490pub trait Marshallable {
491    /// The error type returned when parsing fails
492    type Error: std::error::Error + Send + Sync + 'static;
493
494    /// Parse the contents of a file
495    fn from_bytes(content: &[u8]) -> Result<Self, Self::Error>
496    where
497        Self: Sized;
498
499    /// Create an empty instance
500    fn empty() -> Self;
501
502    /// Serialize the contents of a file
503    fn to_bytes(&self) -> Option<Vec<u8>>;
504
505    /// Create a snapshot of the current state
506    fn snapshot(&self) -> Self;
507
508    /// Check if the current state has changed compared to another state
509    fn has_changed(&self, other: &Self) -> bool;
510}
511
512/// An editor for a file
513pub trait Editor<P: Marshallable>:
514    std::ops::Deref<Target = P> + std::ops::DerefMut<Target = P>
515{
516    /// The original content, if any - without reformatting
517    fn orig_content(&self) -> Option<&[u8]>;
518
519    /// The updated content, if any
520    fn updated_content(&self) -> Option<Vec<u8>>;
521
522    /// The original content, but rewritten with our parser/serializer
523    fn rewritten_content(&self) -> Option<&[u8]>;
524
525    /// Whether the file has changed
526    fn has_changed(&self) -> bool {
527        self.updated_content().as_deref() != self.rewritten_content()
528    }
529
530    /// Check if the file is generated
531    fn is_generated(&self) -> bool;
532
533    /// Commit the changes
534    ///
535    /// # Returns
536    /// A list of paths that were changed
537    fn commit(&mut self) -> Result<Vec<std::path::PathBuf>, EditorError<P::Error>>;
538
539    /// Revert the changes to the original state
540    ///
541    /// # Errors
542    /// Returns an error if reverting fails (e.g., I/O error, parsing error)
543    fn revert(&mut self) -> Result<(), EditorError<P::Error>>;
544}
545
546/// Allow calling .edit_file("debian/control") on a tree
547pub trait MutableTreeEdit {
548    /// Edit a file in a tree
549    fn edit_file<P: Marshallable>(
550        &self,
551        path: &std::path::Path,
552        allow_generated: bool,
553        allow_reformatting: bool,
554    ) -> Result<TreeEditor<'_, P>, EditorError<P::Error>>;
555}
556
557impl<T: MutableTree> MutableTreeEdit for T {
558    fn edit_file<P: Marshallable>(
559        &self,
560        path: &std::path::Path,
561        allow_generated: bool,
562        allow_reformatting: bool,
563    ) -> Result<TreeEditor<'_, P>, EditorError<P::Error>> {
564        TreeEditor::new(self, path, allow_generated, allow_reformatting)
565    }
566}
567
568/// An editor for a file in a breezy tree
569pub struct TreeEditor<'a, P: Marshallable> {
570    tree: &'a dyn MutableTree,
571    path: PathBuf,
572    orig_content: Option<Vec<u8>>,
573    rewritten_content: Option<Vec<u8>>,
574    allow_generated: bool,
575    allow_reformatting: bool,
576    parsed: Option<P>,
577    parsed_base: Option<P>,
578}
579
580impl<P: Marshallable> std::ops::Deref for TreeEditor<'_, P> {
581    type Target = P;
582
583    fn deref(&self) -> &Self::Target {
584        self.parsed.as_ref().unwrap()
585    }
586}
587
588impl<P: Marshallable> std::ops::DerefMut for TreeEditor<'_, P> {
589    fn deref_mut(&mut self) -> &mut Self::Target {
590        self.parsed.as_mut().unwrap()
591    }
592}
593
594impl<'a, P: Marshallable> TreeEditor<'a, P> {
595    /// Create a new editor, with preferences being read from the environment
596    pub fn from_env(
597        tree: &'a dyn MutableTree,
598        path: &std::path::Path,
599        allow_generated: bool,
600        allow_reformatting: Option<bool>,
601    ) -> Result<Self, EditorError<P::Error>> {
602        let allow_reformatting = allow_reformatting.unwrap_or_else(|| {
603            std::env::var("REFORMATTING").unwrap_or("disallow".to_string()) == "allow"
604        });
605
606        Self::new(tree, path, allow_generated, allow_reformatting)
607    }
608
609    /// Read the file contents and parse them
610    fn read(&mut self) -> Result<(), EditorError<P::Error>> {
611        self.orig_content = match self.tree.get_file_text(&self.path) {
612            Ok(c) => Some(c),
613            Err(BrzError::NoSuchFile(..)) => None,
614            Err(e) => return Err(e.into()),
615        };
616        self.parsed = match self.orig_content.as_deref() {
617            Some(content) => Some(P::from_bytes(content).map_err(EditorError::MarshallingError)?),
618            None => Some(P::empty()),
619        };
620        self.parsed_base = self.parsed.as_ref().map(|p| p.snapshot());
621        self.rewritten_content = self.parsed.as_ref().unwrap().to_bytes();
622        Ok(())
623    }
624
625    /// Create a new editor
626    pub fn new(
627        tree: &'a dyn MutableTree,
628        path: &std::path::Path,
629        allow_generated: bool,
630        allow_reformatting: bool,
631    ) -> Result<Self, EditorError<P::Error>> {
632        assert!(path.is_relative());
633        let mut ret = Self {
634            tree,
635            path: path.to_path_buf(),
636            orig_content: None,
637            rewritten_content: None,
638            allow_generated,
639            allow_reformatting,
640            parsed: None,
641            parsed_base: None,
642        };
643        ret.read()?;
644        Ok(ret)
645    }
646}
647
648impl<P: Marshallable> Editor<P> for TreeEditor<'_, P> {
649    fn orig_content(&self) -> Option<&[u8]> {
650        self.orig_content.as_deref()
651    }
652
653    fn updated_content(&self) -> Option<Vec<u8>> {
654        self.parsed.as_ref().unwrap().to_bytes()
655    }
656
657    fn rewritten_content(&self) -> Option<&[u8]> {
658        self.rewritten_content.as_deref()
659    }
660
661    fn commit(&mut self) -> Result<Vec<std::path::PathBuf>, EditorError<P::Error>> {
662        let updated_content = self.updated_content();
663
664        let changed = tree_edit_formatted_file(
665            self.tree,
666            &self.path,
667            self.orig_content.as_deref(),
668            self.rewritten_content.as_deref(),
669            updated_content.as_deref(),
670            self.allow_generated,
671            self.allow_reformatting,
672        )?;
673        if changed {
674            self.parsed_base = self.parsed.as_ref().map(|p| p.snapshot());
675            self.orig_content = updated_content.clone();
676            self.rewritten_content = updated_content;
677            Ok(vec![self.path.clone()])
678        } else {
679            Ok(vec![])
680        }
681    }
682
683    fn is_generated(&self) -> bool {
684        tree_check_generated_file(self.tree, &self.path).is_ok() || {
685            let mut buf =
686                std::io::BufReader::new(std::io::Cursor::new(self.orig_content().unwrap()));
687            check_generated_contents(&mut buf).is_err()
688        }
689    }
690
691    fn revert(&mut self) -> Result<(), EditorError<P::Error>> {
692        if let Some(base) = &self.parsed_base {
693            self.parsed = Some(base.snapshot());
694        }
695        Ok(())
696    }
697}
698
699/// An editor for a file
700pub struct FsEditor<P: Marshallable> {
701    path: PathBuf,
702    orig_content: Option<Vec<u8>>,
703    rewritten_content: Option<Vec<u8>>,
704    allow_generated: bool,
705    allow_reformatting: bool,
706    parsed: Option<P>,
707    parsed_base: Option<P>,
708}
709
710impl<M: Marshallable> std::ops::Deref for FsEditor<M> {
711    type Target = M;
712
713    fn deref(&self) -> &Self::Target {
714        self.parsed.as_ref().unwrap()
715    }
716}
717
718impl<M: Marshallable> std::ops::DerefMut for FsEditor<M> {
719    fn deref_mut(&mut self) -> &mut Self::Target {
720        self.parsed.as_mut().unwrap()
721    }
722}
723
724impl<P: Marshallable> FsEditor<P> {
725    /// Create a new editor, with preferences being read from the environment
726    pub fn from_env(
727        path: &std::path::Path,
728        allow_generated: bool,
729        allow_reformatting: Option<bool>,
730    ) -> Result<Self, EditorError<P::Error>> {
731        let allow_reformatting = allow_reformatting.unwrap_or_else(|| {
732            std::env::var("REFORMATTING").unwrap_or("disallow".to_string()) == "allow"
733        });
734
735        Self::new(path, allow_generated, allow_reformatting)
736    }
737
738    /// Read the file contents and parse them
739    fn read(&mut self) -> Result<(), EditorError<P::Error>> {
740        self.orig_content = match std::fs::read(&self.path) {
741            Ok(c) => Some(c),
742            Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
743            Err(e) => return Err(e.into()),
744        };
745        self.parsed = match self.orig_content.as_deref() {
746            Some(content) => Some(P::from_bytes(content).map_err(EditorError::MarshallingError)?),
747            None => Some(P::empty()),
748        };
749        self.parsed_base = self.parsed.as_ref().map(|p| p.snapshot());
750        self.rewritten_content = self.parsed.as_ref().unwrap().to_bytes();
751        Ok(())
752    }
753
754    /// Create a new editor
755    pub fn new(
756        path: &std::path::Path,
757        allow_generated: bool,
758        allow_reformatting: bool,
759    ) -> Result<Self, EditorError<P::Error>> {
760        let mut ret = Self {
761            path: path.to_path_buf(),
762            orig_content: None,
763            rewritten_content: None,
764            allow_generated,
765            allow_reformatting,
766            parsed: None,
767            parsed_base: None,
768        };
769        ret.read()?;
770        Ok(ret)
771    }
772}
773
774impl<P: Marshallable> Editor<P> for FsEditor<P> {
775    fn orig_content(&self) -> Option<&[u8]> {
776        self.orig_content.as_deref()
777    }
778
779    fn updated_content(&self) -> Option<Vec<u8>> {
780        self.parsed.as_ref().unwrap().to_bytes()
781    }
782
783    fn rewritten_content(&self) -> Option<&[u8]> {
784        self.rewritten_content.as_deref()
785    }
786
787    fn is_generated(&self) -> bool {
788        check_template_exists(&self.path).is_some() || {
789            let mut buf =
790                std::io::BufReader::new(std::io::Cursor::new(self.orig_content().unwrap()));
791            check_generated_contents(&mut buf).is_err()
792        }
793    }
794
795    fn commit(&mut self) -> Result<Vec<std::path::PathBuf>, EditorError<P::Error>> {
796        let updated_content = self.updated_content();
797
798        let changed = edit_formatted_file(
799            &self.path,
800            self.orig_content.as_deref(),
801            self.rewritten_content.as_deref(),
802            updated_content.as_deref(),
803            self.allow_generated,
804            self.allow_reformatting,
805        )?;
806        if changed {
807            self.parsed_base = self.parsed.as_ref().map(|p| p.snapshot());
808            self.orig_content = updated_content.clone();
809            self.rewritten_content = updated_content;
810            Ok(vec![self.path.clone()])
811        } else {
812            Ok(vec![])
813        }
814    }
815
816    fn revert(&mut self) -> Result<(), EditorError<P::Error>> {
817        if let Some(base) = &self.parsed_base {
818            self.parsed = Some(base.snapshot());
819        }
820        Ok(())
821    }
822}
823
824impl Marshallable for debian_control::Control {
825    type Error = deb822_lossless::Error;
826
827    fn from_bytes(content: &[u8]) -> Result<Self, Self::Error> {
828        debian_control::Control::read_relaxed(std::io::Cursor::new(content))
829            .map(|(control, _)| control)
830    }
831
832    fn empty() -> Self {
833        debian_control::Control::new()
834    }
835
836    fn to_bytes(&self) -> Option<Vec<u8>> {
837        self.source()?;
838        Some(self.to_string().into_bytes())
839    }
840
841    fn snapshot(&self) -> Self {
842        debian_control::Control::snapshot(self)
843    }
844
845    fn has_changed(&self, other: &Self) -> bool {
846        self != other
847    }
848}
849
850impl Marshallable for debian_changelog::ChangeLog {
851    type Error = debian_changelog::Error;
852
853    fn from_bytes(content: &[u8]) -> Result<Self, Self::Error> {
854        debian_changelog::ChangeLog::read_relaxed(std::io::Cursor::new(content))
855    }
856
857    fn empty() -> Self {
858        debian_changelog::ChangeLog::new()
859    }
860
861    fn to_bytes(&self) -> Option<Vec<u8>> {
862        Some(self.to_string().into_bytes())
863    }
864
865    fn snapshot(&self) -> Self {
866        self.clone()
867    }
868
869    fn has_changed(&self, other: &Self) -> bool {
870        self != other
871    }
872}
873
874impl Marshallable for debian_copyright::lossless::Copyright {
875    type Error = debian_copyright::lossless::Error;
876
877    fn from_bytes(content: &[u8]) -> Result<Self, Self::Error> {
878        let s = std::str::from_utf8(content).unwrap();
879        debian_copyright::lossless::Copyright::from_str_relaxed(s).map(|(copyright, _)| copyright)
880    }
881
882    fn empty() -> Self {
883        debian_copyright::lossless::Copyright::new()
884    }
885
886    fn to_bytes(&self) -> Option<Vec<u8>> {
887        Some(self.to_string().into_bytes())
888    }
889
890    fn snapshot(&self) -> Self {
891        self.clone()
892    }
893
894    fn has_changed(&self, other: &Self) -> bool {
895        self != other
896    }
897}
898
899impl Marshallable for makefile_lossless::Makefile {
900    type Error = makefile_lossless::Error;
901
902    fn from_bytes(content: &[u8]) -> Result<Self, Self::Error> {
903        makefile_lossless::Makefile::read_relaxed(std::io::Cursor::new(content))
904    }
905
906    fn empty() -> Self {
907        makefile_lossless::Makefile::new()
908    }
909
910    fn to_bytes(&self) -> Option<Vec<u8>> {
911        Some(self.to_string().into_bytes())
912    }
913
914    fn snapshot(&self) -> Self {
915        self.clone()
916    }
917
918    fn has_changed(&self, other: &Self) -> bool {
919        self != other
920    }
921}
922
923impl Marshallable for deb822_lossless::Deb822 {
924    type Error = deb822_lossless::Error;
925
926    fn from_bytes(content: &[u8]) -> Result<Self, Self::Error> {
927        deb822_lossless::Deb822::read_relaxed(std::io::Cursor::new(content))
928            .map(|(deb822, _)| deb822)
929            .map_err(deb822_lossless::Error::from)
930    }
931
932    fn empty() -> Self {
933        deb822_lossless::Deb822::new()
934    }
935
936    fn to_bytes(&self) -> Option<Vec<u8>> {
937        Some(self.to_string().into_bytes())
938    }
939
940    fn snapshot(&self) -> Self {
941        self.clone()
942    }
943
944    fn has_changed(&self, other: &Self) -> bool {
945        self != other
946    }
947}
948
949impl Marshallable for crate::maintscripts::Maintscript {
950    type Error = crate::maintscripts::ParseError;
951
952    fn from_bytes(content: &[u8]) -> Result<Self, Self::Error> {
953        use std::str::FromStr;
954        let content = std::str::from_utf8(content).unwrap();
955        crate::maintscripts::Maintscript::from_str(content)
956    }
957
958    fn empty() -> Self {
959        crate::maintscripts::Maintscript::new()
960    }
961
962    fn to_bytes(&self) -> Option<Vec<u8>> {
963        if self.is_empty() {
964            None
965        } else {
966            Some(self.to_string().into_bytes())
967        }
968    }
969
970    fn snapshot(&self) -> Self {
971        self.clone()
972    }
973
974    fn has_changed(&self, other: &Self) -> bool {
975        self != other
976    }
977}
978
979impl Marshallable for debian_watch::WatchFile {
980    type Error = std::convert::Infallible;
981
982    fn from_bytes(content: &[u8]) -> Result<Self, Self::Error> {
983        let content = std::str::from_utf8(content).unwrap();
984        Ok(debian_watch::WatchFile::from_str_relaxed(content))
985    }
986
987    fn empty() -> Self {
988        debian_watch::WatchFile::new(Some(4))
989    }
990
991    fn to_bytes(&self) -> Option<Vec<u8>> {
992        Some(self.to_string().into_bytes())
993    }
994
995    fn snapshot(&self) -> Self {
996        self.clone()
997    }
998
999    fn has_changed(&self, other: &Self) -> bool {
1000        self != other
1001    }
1002}
1003
1004#[cfg(test)]
1005mod tests {
1006    use super::*;
1007    #[test]
1008    fn test_formatting_same() {
1009        assert_eq!(
1010            Ok(()),
1011            check_preserve_formatting(Some(b"FOO  "), Some(b"FOO  "), false)
1012        );
1013    }
1014
1015    #[test]
1016    fn test_formatting_different() {
1017        assert_eq!(
1018            Err(FormattingUnpreservable {
1019                original_contents: Some("FOO \n".as_bytes().to_vec()),
1020                rewritten_contents: Some("FOO  \n".as_bytes().to_vec()),
1021            }),
1022            check_preserve_formatting(Some(b"FOO  \n"), Some(b"FOO \n"), false)
1023        );
1024    }
1025
1026    #[test]
1027    fn test_diff() {
1028        let e = FormattingUnpreservable {
1029            original_contents: Some(b"FOO X\n".to_vec()),
1030            rewritten_contents: Some(b"FOO  X\n".to_vec()),
1031        };
1032        assert_eq!(
1033            e.diff(),
1034            vec![
1035                "--- original\t\n",
1036                "+++ rewritten\t\n",
1037                "@@ -1 +1 @@\n",
1038                "-FOO X\n",
1039                "+FOO  X\n",
1040            ]
1041        );
1042    }
1043
1044    #[test]
1045    fn test_reformatting_allowed() {
1046        assert_eq!(
1047            Ok(()),
1048            check_preserve_formatting(Some(b"FOO  "), Some(b"FOO "), true)
1049        );
1050    }
1051
1052    #[test]
1053    fn test_generated_control_file() {
1054        let td = tempfile::tempdir().unwrap();
1055        std::fs::create_dir(td.path().join("debian")).unwrap();
1056        std::fs::write(td.path().join("debian/control.in"), "Source: blah\n").unwrap();
1057        assert_eq!(
1058            Err(GeneratedFile {
1059                template_path: Some(td.path().join("debian/control.in")),
1060                template_type: None,
1061            }),
1062            check_generated_file(&td.path().join("debian/control"))
1063        );
1064    }
1065
1066    #[test]
1067    fn test_generated_file_missing() {
1068        let td = tempfile::tempdir().unwrap();
1069        std::fs::create_dir(td.path().join("debian")).unwrap();
1070        assert_eq!(
1071            Ok(()),
1072            check_generated_file(&td.path().join("debian/control"))
1073        );
1074    }
1075
1076    #[test]
1077    fn test_do_not_edit() {
1078        let td = tempfile::tempdir().unwrap();
1079        std::fs::create_dir(td.path().join("debian")).unwrap();
1080        std::fs::write(
1081            td.path().join("debian/control"),
1082            "# DO NOT EDIT\nSource: blah\n",
1083        )
1084        .unwrap();
1085        assert_eq!(
1086            Err(GeneratedFile {
1087                template_path: None,
1088                template_type: None,
1089            }),
1090            check_generated_file(&td.path().join("debian/control"))
1091        );
1092    }
1093
1094    #[test]
1095    fn test_do_not_edit_after_header() {
1096        // check_generated_file() only checks the first 20 lines.
1097        let td = tempfile::tempdir().unwrap();
1098        std::fs::create_dir(td.path().join("debian")).unwrap();
1099        std::fs::write(
1100            td.path().join("debian/control"),
1101            "\n".repeat(50) + "# DO NOT EDIT\nSource: blah\n",
1102        )
1103        .unwrap();
1104        assert_eq!(
1105            Ok(()),
1106            check_generated_file(&td.path().join("debian/control"))
1107        );
1108    }
1109
1110    #[test]
1111    fn test_unchanged() {
1112        let td = tempfile::tempdir().unwrap();
1113        std::fs::write(td.path().join("a"), "some content\n").unwrap();
1114        assert!(!edit_formatted_file::<std::convert::Infallible>(
1115            &td.path().join("a"),
1116            Some("some content\n".as_bytes()),
1117            Some("some content reformatted\n".as_bytes()),
1118            Some("some content\n".as_bytes()),
1119            false,
1120            false
1121        )
1122        .unwrap());
1123        assert!(!edit_formatted_file::<std::convert::Infallible>(
1124            &td.path().join("a"),
1125            Some("some content\n".as_bytes()),
1126            Some("some content\n".as_bytes()),
1127            Some("some content\n".as_bytes()),
1128            false,
1129            false
1130        )
1131        .unwrap());
1132        assert!(!edit_formatted_file::<std::convert::Infallible>(
1133            &td.path().join("a"),
1134            Some("some content\n".as_bytes()),
1135            Some("some content reformatted\n".as_bytes()),
1136            Some("some content reformatted\n".as_bytes()),
1137            false,
1138            false
1139        )
1140        .unwrap());
1141    }
1142
1143    #[test]
1144    fn test_changed() {
1145        let td = tempfile::tempdir().unwrap();
1146        std::fs::write(td.path().join("a"), "some content\n").unwrap();
1147        assert!(edit_formatted_file::<std::convert::Infallible>(
1148            &td.path().join("a"),
1149            Some("some content\n".as_bytes()),
1150            Some("some content\n".as_bytes()),
1151            Some("new content\n".as_bytes()),
1152            false,
1153            false
1154        )
1155        .unwrap());
1156        assert_eq!(
1157            "new content\n",
1158            std::fs::read_to_string(td.path().join("a")).unwrap()
1159        );
1160    }
1161
1162    #[test]
1163    fn test_unformattable() {
1164        let td = tempfile::tempdir().unwrap();
1165        assert!(matches!(
1166            edit_formatted_file::<std::convert::Infallible>(
1167                &td.path().join("a"),
1168                Some(b"some content\n"),
1169                Some(b"reformatted content\n"),
1170                Some(b"new content\n"),
1171                false,
1172                false
1173            )
1174            .unwrap_err(),
1175            EditorError::FormattingUnpreservable(_, FormattingUnpreservable { .. })
1176        ));
1177    }
1178
1179    #[derive(Clone, PartialEq)]
1180    struct TestMarshall {
1181        data: Option<usize>,
1182    }
1183
1184    impl TestMarshall {
1185        fn get_data(&self) -> Option<usize> {
1186            self.data
1187        }
1188
1189        fn unset_data(&mut self) {
1190            self.data = None;
1191        }
1192
1193        fn inc_data(&mut self) {
1194            match &mut self.data {
1195                Some(x) => *x += 1,
1196                None => self.data = Some(1),
1197            }
1198        }
1199    }
1200
1201    impl Marshallable for TestMarshall {
1202        type Error = std::num::ParseIntError;
1203
1204        fn from_bytes(content: &[u8]) -> Result<Self, Self::Error> {
1205            let s = std::str::from_utf8(content).unwrap();
1206            let data = s.parse()?;
1207            Ok(Self { data: Some(data) })
1208        }
1209
1210        fn empty() -> Self {
1211            Self { data: None }
1212        }
1213
1214        fn to_bytes(&self) -> Option<Vec<u8>> {
1215            self.data.map(|x| x.to_string().into_bytes())
1216        }
1217
1218        fn snapshot(&self) -> Self {
1219            self.clone()
1220        }
1221
1222        fn has_changed(&self, other: &Self) -> bool {
1223            self != other
1224        }
1225    }
1226
1227    #[test]
1228    fn test_edit_create_file() {
1229        let td = tempfile::tempdir().unwrap();
1230
1231        let mut editor = FsEditor::<TestMarshall>::new(&td.path().join("a"), false, false).unwrap();
1232        assert!(!editor.has_changed());
1233        editor.inc_data();
1234        assert_eq!(editor.get_data(), Some(1));
1235        assert!(editor.has_changed());
1236        assert_eq!(editor.commit().unwrap(), vec![td.path().join("a")]);
1237        assert_eq!(editor.get_data(), Some(1));
1238
1239        assert_eq!("1", std::fs::read_to_string(td.path().join("a")).unwrap());
1240    }
1241
1242    #[test]
1243    fn test_edit_create_no_changes() {
1244        let td = tempfile::tempdir().unwrap();
1245
1246        let mut editor = FsEditor::<TestMarshall>::new(&td.path().join("a"), false, false).unwrap();
1247        assert!(!editor.has_changed());
1248        assert_eq!(editor.commit().unwrap(), Vec::<std::path::PathBuf>::new());
1249        assert_eq!(editor.get_data(), None);
1250        assert!(!td.path().join("a").exists());
1251    }
1252
1253    #[test]
1254    fn test_edit_change() {
1255        let td = tempfile::tempdir().unwrap();
1256        std::fs::write(td.path().join("a"), "1").unwrap();
1257
1258        let mut editor = FsEditor::<TestMarshall>::new(&td.path().join("a"), false, false).unwrap();
1259        assert!(!editor.has_changed());
1260        editor.inc_data();
1261        assert_eq!(editor.get_data(), Some(2));
1262        assert!(editor.has_changed());
1263        assert_eq!(editor.commit().unwrap(), vec![td.path().join("a")]);
1264        assert_eq!(editor.get_data(), Some(2));
1265
1266        assert_eq!("2", std::fs::read_to_string(td.path().join("a")).unwrap());
1267    }
1268
1269    #[test]
1270    fn test_edit_delete() {
1271        let td = tempfile::tempdir().unwrap();
1272        std::fs::write(td.path().join("a"), "1").unwrap();
1273
1274        let mut editor = FsEditor::<TestMarshall>::new(&td.path().join("a"), false, false).unwrap();
1275        assert!(!editor.has_changed());
1276        editor.unset_data();
1277        assert_eq!(editor.get_data(), None);
1278        assert!(editor.has_changed());
1279        assert_eq!(editor.commit().unwrap(), vec![td.path().join("a")]);
1280        assert_eq!(editor.get_data(), None);
1281
1282        assert!(!td.path().join("a").exists());
1283    }
1284
1285    #[test]
1286    fn test_tree_editor_edit() {
1287        use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat};
1288        let tempdir = tempfile::tempdir().unwrap();
1289
1290        let tree =
1291            create_standalone_workingtree(tempdir.path(), &ControlDirFormat::default()).unwrap();
1292
1293        let mut editor = tree
1294            .edit_file::<TestMarshall>(std::path::Path::new("a"), false, false)
1295            .unwrap();
1296
1297        assert!(!editor.has_changed());
1298        editor.inc_data();
1299        assert_eq!(editor.get_data(), Some(1));
1300        assert!(editor.has_changed());
1301        assert_eq!(editor.commit().unwrap(), vec![std::path::Path::new("a")]);
1302
1303        assert_eq!(
1304            "1",
1305            std::fs::read_to_string(tempdir.path().join("a")).unwrap()
1306        );
1307    }
1308
1309    #[test]
1310    fn test_tree_edit_control() {
1311        use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat};
1312        let tempdir = tempfile::tempdir().unwrap();
1313
1314        let tree =
1315            create_standalone_workingtree(tempdir.path(), &ControlDirFormat::default()).unwrap();
1316
1317        tree.mkdir(std::path::Path::new("debian")).unwrap();
1318
1319        let mut editor = tree
1320            .edit_file::<debian_control::Control>(
1321                std::path::Path::new("debian/control"),
1322                false,
1323                false,
1324            )
1325            .unwrap();
1326
1327        assert!(!editor.has_changed());
1328        let mut source = editor.add_source("blah");
1329        source.set_homepage(&"https://example.com".parse().unwrap());
1330        assert!(editor.has_changed());
1331        assert_eq!(
1332            editor.commit().unwrap(),
1333            vec![std::path::Path::new("debian/control")]
1334        );
1335
1336        assert_eq!(
1337            "Source: blah\nHomepage: https://example.com/\n",
1338            std::fs::read_to_string(tempdir.path().join("debian/control")).unwrap()
1339        );
1340    }
1341
1342    #[derive(Clone, PartialEq, Debug)]
1343    struct LossyFormat {
1344        lines: Vec<String>,
1345    }
1346
1347    impl Marshallable for LossyFormat {
1348        type Error = std::convert::Infallible;
1349
1350        fn from_bytes(content: &[u8]) -> Result<Self, Self::Error> {
1351            let s = std::str::from_utf8(content).unwrap();
1352            Ok(LossyFormat {
1353                lines: s
1354                    .lines()
1355                    .filter(|l| !l.starts_with('#'))
1356                    .map(|l| l.to_string())
1357                    .collect(),
1358            })
1359        }
1360
1361        fn empty() -> Self {
1362            LossyFormat { lines: vec![] }
1363        }
1364
1365        fn to_bytes(&self) -> Option<Vec<u8>> {
1366            Some(self.lines.join("\n").into_bytes())
1367        }
1368
1369        fn snapshot(&self) -> Self {
1370            self.clone()
1371        }
1372
1373        fn has_changed(&self, other: &Self) -> bool {
1374            self != other
1375        }
1376    }
1377
1378    #[test]
1379    fn test_merge3() {
1380        let td = tempfile::tempdir().unwrap();
1381        std::fs::write(
1382            td.path().join("test.txt"),
1383            "line1\nline2\nline3\n# comment\nline4\n",
1384        )
1385        .unwrap();
1386
1387        let mut editor =
1388            super::FsEditor::<LossyFormat>::new(&td.path().join("test.txt"), false, false).unwrap();
1389        editor.lines.insert(0, "line0".to_string());
1390
1391        #[cfg(feature = "merge3")]
1392        {
1393            editor.commit().unwrap();
1394            assert_eq!(
1395                "line0\nline1\nline2\nline3\n# comment\nline4\n",
1396                std::fs::read_to_string(td.path().join("test.txt")).unwrap()
1397            );
1398        }
1399        #[cfg(not(feature = "merge3"))]
1400        {
1401            let result = editor.commit();
1402            let updated_content = std::fs::read_to_string(td.path().join("test.txt")).unwrap();
1403            assert!(result.is_err(), "{:?}", updated_content);
1404            assert!(
1405                matches!(
1406                    result.as_ref().unwrap_err(),
1407                    super::EditorError::FormattingUnpreservable(_, _)
1408                ),
1409                "{:?} {:?}",
1410                result,
1411                updated_content
1412            );
1413            assert_eq!("line1\nline2\nline3\n# comment\nline4\n", updated_content);
1414        }
1415    }
1416
1417    #[test]
1418    fn test_reformat_file_preserved() {
1419        let (updated_content, changed) = reformat_file(
1420            Some(b"original\n"),
1421            Some(b"original\n"),
1422            Some(b"updated\n"),
1423            false,
1424        )
1425        .unwrap();
1426        assert_eq!(updated_content, Some(Cow::Borrowed(&b"updated\n"[..])));
1427        assert!(changed);
1428    }
1429
1430    #[test]
1431    fn test_reformat_file_not_preserved_allowed() {
1432        let (updated_content, changed) = reformat_file(
1433            Some(b"original\n#comment\n"),
1434            Some(b"original\n"),
1435            Some(b"updated\n"),
1436            true,
1437        )
1438        .unwrap();
1439        assert_eq!(updated_content, Some(Cow::Borrowed(&b"updated\n"[..])));
1440        assert!(changed);
1441    }
1442
1443    #[test]
1444    fn test_reformat_file_not_preserved_not_allowed() {
1445        let err = reformat_file(
1446            Some(b"original\n#comment\n"),
1447            Some(b"original\n"),
1448            Some(b"updated\n"),
1449            false,
1450        )
1451        .unwrap_err();
1452        assert!(matches!(err, FormattingUnpreservable { .. }));
1453    }
1454
1455    #[test]
1456    fn test_reformat_file_not_preserved_merge3() {
1457        let r = reformat_file(
1458            Some(b"original\noriginal 2\noriginal 3\n#comment\noriginal 4\n"),
1459            Some(b"original\noriginal 2\noriginal 3\noriginal 4\n"),
1460            Some(b"updated\noriginal 2\noriginal 3\n#comment\noriginal 4\n"),
1461            false,
1462        );
1463        #[cfg(feature = "merge3")]
1464        {
1465            let (updated_content, changed) = r.unwrap();
1466            let updated = std::str::from_utf8(updated_content.as_ref().unwrap().as_ref()).unwrap();
1467            assert_eq!(
1468                updated,
1469                "updated\noriginal 2\noriginal 3\n#comment\noriginal 4\n"
1470            );
1471            assert!(changed);
1472        }
1473        #[cfg(not(feature = "merge3"))]
1474        {
1475            assert!(matches!(r.unwrap_err(), FormattingUnpreservable { .. }));
1476        }
1477    }
1478
1479    #[test]
1480    fn test_edit_formatted_file_preservable() {
1481        let td = tempfile::tempdir().unwrap();
1482        std::fs::write(td.path().join("a"), "some content\n").unwrap();
1483        assert!(edit_formatted_file::<std::convert::Infallible>(
1484            &td.path().join("a"),
1485            Some("some content\n".as_bytes()),
1486            Some("some content\n".as_bytes()),
1487            Some("new content\n".as_bytes()),
1488            false,
1489            false
1490        )
1491        .unwrap());
1492        assert_eq!(
1493            "new content\n",
1494            std::fs::read_to_string(td.path().join("a")).unwrap()
1495        );
1496    }
1497
1498    #[test]
1499    fn test_edit_formatted_file_not_preservable() {
1500        let td = tempfile::tempdir().unwrap();
1501        std::fs::write(td.path().join("a"), "some content\n#extra\n").unwrap();
1502        assert!(matches!(
1503            edit_formatted_file::<std::convert::Infallible>(
1504                &td.path().join("a"),
1505                Some("some content\n#extra\n".as_bytes()),
1506                Some("some content\n".as_bytes()),
1507                Some("new content\n".as_bytes()),
1508                false,
1509                false
1510            )
1511            .unwrap_err(),
1512            EditorError::FormattingUnpreservable(_, FormattingUnpreservable { .. })
1513        ));
1514
1515        assert_eq!(
1516            "some content\n#extra\n",
1517            std::fs::read_to_string(td.path().join("a")).unwrap()
1518        );
1519    }
1520
1521    #[test]
1522    fn test_edit_formatted_file_not_preservable_allowed() {
1523        let td = tempfile::tempdir().unwrap();
1524        std::fs::write(td.path().join("a"), "some content\n").unwrap();
1525        assert!(edit_formatted_file::<std::convert::Infallible>(
1526            &td.path().join("a"),
1527            Some("some content\n#extra\n".as_bytes()),
1528            Some("some content\n".as_bytes()),
1529            Some("new content\n".as_bytes()),
1530            false,
1531            true
1532        )
1533        .is_ok());
1534
1535        assert_eq!(
1536            "new content\n",
1537            std::fs::read_to_string(td.path().join("a")).unwrap()
1538        );
1539    }
1540
1541    #[test]
1542    fn test_revert_before_commit() {
1543        let td = tempfile::tempdir().unwrap();
1544        std::fs::write(td.path().join("a"), "1").unwrap();
1545
1546        let mut editor = FsEditor::<TestMarshall>::new(&td.path().join("a"), false, false).unwrap();
1547        assert_eq!(editor.get_data(), Some(1));
1548        assert!(!editor.has_changed());
1549
1550        // Make a change
1551        editor.inc_data();
1552        assert_eq!(editor.get_data(), Some(2));
1553        assert!(editor.has_changed());
1554
1555        // Revert the change
1556        editor.revert().unwrap();
1557        assert_eq!(editor.get_data(), Some(1));
1558        assert!(!editor.has_changed());
1559
1560        // File should still have original content
1561        assert_eq!("1", std::fs::read_to_string(td.path().join("a")).unwrap());
1562    }
1563
1564    #[test]
1565    fn test_revert_after_commit() {
1566        let td = tempfile::tempdir().unwrap();
1567        std::fs::write(td.path().join("a"), "1").unwrap();
1568
1569        let mut editor = FsEditor::<TestMarshall>::new(&td.path().join("a"), false, false).unwrap();
1570        assert_eq!(editor.get_data(), Some(1));
1571
1572        // Make a change and commit
1573        editor.inc_data();
1574        assert_eq!(editor.get_data(), Some(2));
1575        editor.commit().unwrap();
1576        assert_eq!("2", std::fs::read_to_string(td.path().join("a")).unwrap());
1577        assert!(!editor.has_changed());
1578
1579        // Make another change
1580        editor.inc_data();
1581        assert_eq!(editor.get_data(), Some(3));
1582        assert!(editor.has_changed());
1583
1584        // Revert should go back to committed state (2, not original 1)
1585        editor.revert().unwrap();
1586        assert_eq!(editor.get_data(), Some(2));
1587        assert!(!editor.has_changed());
1588
1589        // File should still have committed content
1590        assert_eq!("2", std::fs::read_to_string(td.path().join("a")).unwrap());
1591    }
1592
1593    #[test]
1594    fn test_multiple_commit_revert_cycles() {
1595        let td = tempfile::tempdir().unwrap();
1596        std::fs::write(td.path().join("a"), "1").unwrap();
1597
1598        let mut editor = FsEditor::<TestMarshall>::new(&td.path().join("a"), false, false).unwrap();
1599        assert_eq!(editor.get_data(), Some(1));
1600
1601        // First cycle: change, commit
1602        editor.inc_data();
1603        assert_eq!(editor.get_data(), Some(2));
1604        editor.commit().unwrap();
1605        assert_eq!("2", std::fs::read_to_string(td.path().join("a")).unwrap());
1606
1607        // Second cycle: change, revert, change again, commit
1608        editor.inc_data();
1609        assert_eq!(editor.get_data(), Some(3));
1610        editor.revert().unwrap();
1611        assert_eq!(editor.get_data(), Some(2));
1612
1613        editor.inc_data();
1614        editor.inc_data();
1615        assert_eq!(editor.get_data(), Some(4));
1616        editor.commit().unwrap();
1617        assert_eq!("4", std::fs::read_to_string(td.path().join("a")).unwrap());
1618
1619        // Third cycle: change, revert
1620        editor.inc_data();
1621        assert_eq!(editor.get_data(), Some(5));
1622        editor.revert().unwrap();
1623        assert_eq!(editor.get_data(), Some(4));
1624        assert!(!editor.has_changed());
1625        assert_eq!("4", std::fs::read_to_string(td.path().join("a")).unwrap());
1626    }
1627
1628    #[test]
1629    fn test_tree_editor_revert_before_commit() {
1630        use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat};
1631        let tempdir = tempfile::tempdir().unwrap();
1632
1633        let tree =
1634            create_standalone_workingtree(tempdir.path(), &ControlDirFormat::default()).unwrap();
1635
1636        tree.put_file_bytes_non_atomic(std::path::Path::new("a"), b"1")
1637            .unwrap();
1638
1639        let mut editor = tree
1640            .edit_file::<TestMarshall>(std::path::Path::new("a"), false, false)
1641            .unwrap();
1642        assert_eq!(editor.get_data(), Some(1));
1643
1644        // Make a change
1645        editor.inc_data();
1646        assert_eq!(editor.get_data(), Some(2));
1647        assert!(editor.has_changed());
1648
1649        // Revert the change
1650        editor.revert().unwrap();
1651        assert_eq!(editor.get_data(), Some(1));
1652        assert!(!editor.has_changed());
1653    }
1654
1655    #[test]
1656    fn test_tree_editor_revert_after_commit() {
1657        use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat};
1658        let tempdir = tempfile::tempdir().unwrap();
1659
1660        let tree =
1661            create_standalone_workingtree(tempdir.path(), &ControlDirFormat::default()).unwrap();
1662
1663        tree.put_file_bytes_non_atomic(std::path::Path::new("a"), b"1")
1664            .unwrap();
1665
1666        let mut editor = tree
1667            .edit_file::<TestMarshall>(std::path::Path::new("a"), false, false)
1668            .unwrap();
1669        assert_eq!(editor.get_data(), Some(1));
1670
1671        // Make a change and commit
1672        editor.inc_data();
1673        assert_eq!(editor.get_data(), Some(2));
1674        editor.commit().unwrap();
1675        assert!(!editor.has_changed());
1676
1677        // Make another change
1678        editor.inc_data();
1679        assert_eq!(editor.get_data(), Some(3));
1680
1681        // Revert should go back to committed state (2, not original 1)
1682        editor.revert().unwrap();
1683        assert_eq!(editor.get_data(), Some(2));
1684        assert!(!editor.has_changed());
1685    }
1686
1687    #[test]
1688    fn test_watchfile_editor() {
1689        let td = tempfile::tempdir().unwrap();
1690        let watch_path = td.path().join("debian/watch");
1691        std::fs::create_dir_all(watch_path.parent().unwrap()).unwrap();
1692
1693        // Create a basic watch file
1694        std::fs::write(
1695            &watch_path,
1696            "version=4\nhttps://example.com/downloads example-(.*)\\.tar\\.gz\n",
1697        )
1698        .unwrap();
1699
1700        // Test that we can read and edit the watch file
1701        let mut editor =
1702            FsEditor::<debian_watch::WatchFile>::new(&watch_path, false, false).unwrap();
1703
1704        // Verify we can read the watch file
1705        assert_eq!(editor.version(), 4);
1706        let entries: Vec<_> = editor.entries().collect();
1707        assert_eq!(entries.len(), 1);
1708        assert_eq!(entries[0].url(), "https://example.com/downloads");
1709        assert_eq!(
1710            entries[0].matching_pattern(),
1711            Some("example-(.*)\\.tar\\.gz".to_string())
1712        );
1713
1714        // Make a change
1715        let entry = debian_watch::EntryBuilder::new("https://example.com/other")
1716            .matching_pattern("other-(.*)\\.tar\\.gz")
1717            .build();
1718        editor.add_entry(entry);
1719
1720        let entries: Vec<_> = editor.entries().collect();
1721        assert_eq!(entries.len(), 2);
1722        assert_eq!(entries[0].url(), "https://example.com/downloads");
1723        assert_eq!(entries[1].url(), "https://example.com/other");
1724        assert_eq!(
1725            entries[1].matching_pattern(),
1726            Some("other-(.*)\\.tar\\.gz".to_string())
1727        );
1728        assert!(editor.has_changed());
1729
1730        // Commit the changes
1731        let changed_files = editor.commit().unwrap();
1732        assert_eq!(changed_files, vec![watch_path.clone()]);
1733        assert!(!editor.has_changed());
1734
1735        // Verify the file was updated
1736        let content = std::fs::read_to_string(&watch_path).unwrap();
1737        let expected = "version=4\nhttps://example.com/downloads example-(.*)\\.tar\\.gz\nhttps://example.com/other other-(.*)\\.tar\\.gz\n";
1738        assert_eq!(content, expected);
1739    }
1740
1741    #[test]
1742    fn test_marshalling_error_handling() {
1743        let td = tempfile::tempdir().unwrap();
1744        let test_path = td.path().join("test");
1745
1746        // Write invalid content that will fail to parse as an integer
1747        std::fs::write(&test_path, "not a number").unwrap();
1748
1749        // Attempt to create an editor should fail with a marshalling error
1750        let result = FsEditor::<TestMarshall>::new(&test_path, false, false);
1751        assert!(matches!(result, Err(EditorError::MarshallingError(_))));
1752    }
1753
1754    #[test]
1755    fn test_tree_editor_marshalling_error() {
1756        use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat};
1757        let tempdir = tempfile::tempdir().unwrap();
1758
1759        let tree =
1760            create_standalone_workingtree(tempdir.path(), &ControlDirFormat::default()).unwrap();
1761
1762        // Put invalid content in the tree
1763        tree.put_file_bytes_non_atomic(std::path::Path::new("test"), b"invalid content")
1764            .unwrap();
1765
1766        // Attempt to create an editor should fail with a marshalling error
1767        let result = tree.edit_file::<TestMarshall>(std::path::Path::new("test"), false, false);
1768        assert!(matches!(result, Err(EditorError::MarshallingError(_))));
1769    }
1770}