patchkit 0.2.4

A library for parsing and manipulating patch files
Documentation
#[cfg(test)]
mod tests {
    use crate::edit;
    use crate::edit::DiffFormat;
    use rowan::ast::AstNode;

    #[test]
    fn test_parse_context_diff() {
        let input = r#"*** a/file.txt	2023-01-01 00:00:00
--- b/file.txt	2023-01-02 00:00:00
***************
*** 1,3 ****
  line 1
! line 2
  line 3
--- 1,3 ----
  line 1
! line 2 modified
  line 3
"#;
        let parsed = edit::parse(input);
        assert!(parsed.ok());

        let patch = parsed.tree();
        assert_eq!(patch.detect_format(), Some(DiffFormat::Context));

        let mut files = patch.context_diff_files();
        let file = files.next().unwrap();
        assert!(files.next().is_none());

        // Check file headers
        let old_file = file.old_file().unwrap();
        assert!(old_file.path().unwrap().text().contains("a/file.txt"));

        let new_file = file.new_file().unwrap();
        assert!(new_file.path().unwrap().text().contains("b/file.txt"));

        // Check hunk
        let mut hunks = file.hunks();
        let hunk = hunks.next().unwrap();
        assert!(hunks.next().is_none());

        // Check sections
        let old_section = hunk.old_section().unwrap();
        let _new_section = hunk.new_section().unwrap();

        // The sections should have context and change lines
        assert!(old_section
            .syntax()
            .children()
            .any(|n| n.kind() == crate::edit::lex::SyntaxKind::CONTEXT_LINE));
        assert!(old_section
            .syntax()
            .children()
            .any(|n| n.kind() == crate::edit::lex::SyntaxKind::CONTEXT_CHANGE_LINE));
    }

    #[test]
    fn test_parse_ed_diff() {
        let input = r#"2c
line 2 modified
.
5a
new line after 5
.
3d
"#;
        let parsed = edit::parse(input);
        assert!(parsed.ok());

        let patch = parsed.tree();
        assert_eq!(patch.detect_format(), Some(DiffFormat::Ed));

        let commands: Vec<_> = patch.ed_commands().collect();
        assert_eq!(commands.len(), 3);

        // First command: change
        let change = commands[0].as_change().unwrap();
        let (start, end) = change.line_numbers();
        assert_eq!(start, Some(2));
        assert_eq!(end, None); // Single line
        let content: Vec<_> = change.content_lines().collect();
        assert_eq!(content.len(), 1);

        // Second command: add
        let add = commands[1].as_add().unwrap();
        let (start, _) = add.line_numbers();
        assert_eq!(start, Some(5));

        // Third command: delete
        let delete = commands[2].as_delete().unwrap();
        let (start, _) = delete.line_numbers();
        assert_eq!(start, Some(3));
    }

    #[test]
    fn test_parse_normal_diff() {
        let input = r#"2c2
< line 2
---
> line 2 modified
5a6,7
> new line 6
> new line 7
8,9d10
< old line 8
< old line 9
"#;
        let parsed = edit::parse(input);
        assert!(parsed.ok());

        let patch = parsed.tree();
        assert_eq!(patch.detect_format(), Some(DiffFormat::Normal));

        let hunks: Vec<_> = patch.normal_hunks().collect();
        assert_eq!(hunks.len(), 3);

        // First hunk: change
        let hunk1 = &hunks[0];
        assert!(hunk1.old_lines().is_some());
        assert!(hunk1.new_lines().is_some());

        // Second hunk: add
        let hunk2 = &hunks[1];
        assert!(hunk2.old_lines().is_none()); // No old lines for add
        assert!(hunk2.new_lines().is_some());

        // Third hunk: delete
        let hunk3 = &hunks[2];
        assert!(hunk3.old_lines().is_some());
        assert!(hunk3.new_lines().is_none()); // No new lines for delete
    }

    #[test]
    fn test_mixed_formats_in_one_file() {
        // This should parse each format separately
        let input = r#"--- a/unified.txt
+++ b/unified.txt
@@ -1,1 +1,1 @@
-old
+new

*** a/context.txt
--- b/context.txt
***************
*** 1 ****
! old
--- 1 ----
! new

2c
new content
.

5c5
< old line
---
> new line
"#;
        let parsed = edit::parse(input);
        assert!(parsed.ok());

        let patch = parsed.tree();

        // Should have detected multiple formats
        // Note: The iterators traverse the entire tree, not just direct children
        // This means they might count nodes at different levels
        let patch_files = patch.patch_files().count();
        let context_diff_files = patch.context_diff_files().count();
        let ed_commands = patch.ed_commands().count();
        let normal_hunks = patch.normal_hunks().count();

        // The parser successfully parses the mixed format input, but the exact
        // node structure may vary. The important thing is that all formats
        // are recognized and parsed without errors.

        // The mixed format test is challenging because:
        // 1. Real-world patches rarely mix formats like this
        // 2. The parser may interpret ambiguous sections differently
        // 3. Some sections might be parsed as generic patch files

        // The key success criteria is that it parses without errors
        assert!(parsed.ok());

        // And that it found multiple distinct sections
        let total_sections = patch_files + context_diff_files + ed_commands + normal_hunks;
        assert!(total_sections >= 3); // Should parse at least 3 different sections
    }

    #[test]
    fn test_context_diff_with_additions_deletions() {
        let input = r#"*** a/file.txt
--- b/file.txt
***************
*** 1,5 ****
  line 1
- line 2
  line 3
  line 4
  line 5
--- 1,6 ----
  line 1
  line 3
+ line 3.5
  line 4
  line 5
+ line 6
"#;
        let parsed = edit::parse(input);
        assert!(parsed.ok());

        let patch = parsed.tree();
        let file = patch.context_diff_files().next().unwrap();
        let hunk = file.hunks().next().unwrap();

        // Check old section has a delete line
        let old_section = hunk.old_section().unwrap();
        assert!(old_section
            .syntax()
            .children()
            .any(|n| n.kind() == crate::edit::lex::SyntaxKind::DELETE_LINE));

        // Check new section has add lines
        let new_section = hunk.new_section().unwrap();
        assert!(new_section
            .syntax()
            .children()
            .any(|n| n.kind() == crate::edit::lex::SyntaxKind::ADD_LINE));
    }

    #[test]
    fn test_ed_diff_with_ranges() {
        let input = r#"3,5c
line 3 new
line 4 new
line 5 new
.
10,12d
"#;
        let parsed = edit::parse(input);
        assert!(parsed.ok());

        let patch = parsed.tree();
        let commands: Vec<_> = patch.ed_commands().collect();
        assert_eq!(commands.len(), 2);

        // First command: change range
        let change = commands[0].as_change().unwrap();
        let (start, end) = change.line_numbers();
        assert_eq!(start, Some(3));
        assert_eq!(end, Some(5));

        // Second command: delete range
        let delete = commands[1].as_delete().unwrap();
        let (start, end) = delete.line_numbers();
        assert_eq!(start, Some(10));
        assert_eq!(end, Some(12));
    }

    #[test]
    fn test_normal_diff_all_operations() {
        let input = r#"0a1,2
> new line 1
> new line 2
3,4c5,6
< old line 3
< old line 4
---
> new line 5
> new line 6
7,8d6
< deleted line 7
< deleted line 8
"#;
        let parsed = edit::parse(input);
        assert!(parsed.ok());

        let patch = parsed.tree();
        let hunks: Vec<_> = patch.normal_hunks().collect();
        assert_eq!(hunks.len(), 3);

        // All hunks should have commands
        for hunk in &hunks {
            assert!(hunk.command().is_some());
        }
    }

    #[test]
    fn test_lossless_roundtrip_all_formats() {
        let inputs = vec![
            // Unified diff
            r#"--- a/file.txt	2023-01-01
+++ b/file.txt	2023-01-02
@@ -1,3 +1,3 @@
 line 1
-line 2
+line 2 modified
 line 3
"#,
            // Context diff
            r#"*** a/file.txt	2023-01-01
--- b/file.txt	2023-01-02
***************
*** 1,3 ****
  line 1
! line 2
  line 3
--- 1,3 ----
  line 1
! line 2 modified
  line 3
"#,
            // Ed diff
            r#"2c
line 2 modified
.
"#,
            // Normal diff
            r#"2c2
< line 2
---
> line 2 modified
"#,
        ];

        for input in inputs {
            let parsed = edit::parse(input);
            assert!(parsed.ok());

            // Get the syntax node and convert back to text
            let syntax = parsed.syntax_node();
            let output = syntax.text().to_string();

            // Should preserve the original input exactly
            assert_eq!(input, output);
        }
    }

    #[test]
    fn test_format_detection_accuracy() {
        // Test that we correctly identify each format
        assert_eq!(
            edit::parse("--- a/file\n+++ b/file\n@@ -1 +1 @@\n-old\n+new\n")
                .tree()
                .detect_format(),
            Some(DiffFormat::Unified)
        );

        assert_eq!(
            edit::parse(
                "*** a/file\n--- b/file\n***************\n*** 1 ****\n! old\n--- 1 ----\n! new\n"
            )
            .tree()
            .detect_format(),
            Some(DiffFormat::Context)
        );

        assert_eq!(
            edit::parse("1c\nnew line\n.\n").tree().detect_format(),
            Some(DiffFormat::Ed)
        );

        assert_eq!(
            edit::parse("1c1\n< old\n---\n> new\n")
                .tree()
                .detect_format(),
            Some(DiffFormat::Normal)
        );
    }
}