1use 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)]
9pub enum TemplateType {
11 M4,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct GeneratedFile {
18 pub template_path: Option<PathBuf>,
20
21 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)]
38pub struct FormattingUnpreservable {
40 original_contents: Option<Vec<u8>>,
42
43 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 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
101fn 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
124pub 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
143pub 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
181pub 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
207pub 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)]
237pub enum EditorError<E: std::error::Error> {
239 GeneratedFile(PathBuf, GeneratedFile),
241
242 TemplateError(PathBuf, String),
244
245 FormattingUnpreservable(PathBuf, FormattingUnpreservable),
247
248 IoError(std::io::Error),
250
251 BrzError(BrzError),
253
254 Utf8Error(std::str::Utf8Error),
256
257 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")]
302fn 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
335fn 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 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
393pub 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
439pub 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
489pub trait Marshallable {
491 type Error: std::error::Error + Send + Sync + 'static;
493
494 fn from_bytes(content: &[u8]) -> Result<Self, Self::Error>
496 where
497 Self: Sized;
498
499 fn empty() -> Self;
501
502 fn to_bytes(&self) -> Option<Vec<u8>>;
504
505 fn snapshot(&self) -> Self;
507
508 fn has_changed(&self, other: &Self) -> bool;
510}
511
512pub trait Editor<P: Marshallable>:
514 std::ops::Deref<Target = P> + std::ops::DerefMut<Target = P>
515{
516 fn orig_content(&self) -> Option<&[u8]>;
518
519 fn updated_content(&self) -> Option<Vec<u8>>;
521
522 fn rewritten_content(&self) -> Option<&[u8]>;
524
525 fn has_changed(&self) -> bool {
527 self.updated_content().as_deref() != self.rewritten_content()
528 }
529
530 fn is_generated(&self) -> bool;
532
533 fn commit(&mut self) -> Result<Vec<std::path::PathBuf>, EditorError<P::Error>>;
538
539 fn revert(&mut self) -> Result<(), EditorError<P::Error>>;
544}
545
546pub trait MutableTreeEdit {
548 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
568pub 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 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 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 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
699pub 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 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 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 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 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 editor.inc_data();
1552 assert_eq!(editor.get_data(), Some(2));
1553 assert!(editor.has_changed());
1554
1555 editor.revert().unwrap();
1557 assert_eq!(editor.get_data(), Some(1));
1558 assert!(!editor.has_changed());
1559
1560 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 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 editor.inc_data();
1581 assert_eq!(editor.get_data(), Some(3));
1582 assert!(editor.has_changed());
1583
1584 editor.revert().unwrap();
1586 assert_eq!(editor.get_data(), Some(2));
1587 assert!(!editor.has_changed());
1588
1589 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 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 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 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 editor.inc_data();
1646 assert_eq!(editor.get_data(), Some(2));
1647 assert!(editor.has_changed());
1648
1649 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 editor.inc_data();
1673 assert_eq!(editor.get_data(), Some(2));
1674 editor.commit().unwrap();
1675 assert!(!editor.has_changed());
1676
1677 editor.inc_data();
1679 assert_eq!(editor.get_data(), Some(3));
1680
1681 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 std::fs::write(
1695 &watch_path,
1696 "version=4\nhttps://example.com/downloads example-(.*)\\.tar\\.gz\n",
1697 )
1698 .unwrap();
1699
1700 let mut editor =
1702 FsEditor::<debian_watch::WatchFile>::new(&watch_path, false, false).unwrap();
1703
1704 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 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 let changed_files = editor.commit().unwrap();
1732 assert_eq!(changed_files, vec![watch_path.clone()]);
1733 assert!(!editor.has_changed());
1734
1735 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 std::fs::write(&test_path, "not a number").unwrap();
1748
1749 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 tree.put_file_bytes_non_atomic(std::path::Path::new("test"), b"invalid content")
1764 .unwrap();
1765
1766 let result = tree.edit_file::<TestMarshall>(std::path::Path::new("test"), false, false);
1768 assert!(matches!(result, Err(EditorError::MarshallingError(_))));
1769 }
1770}