opencrates 3.0.1

Enterprise-grade AI-powered Rust development companion with comprehensive automation, monitoring, and deployment capabilities
use std::path::Path;
use std::path::PathBuf;

use thiserror::Error;

/// A parsed patch hunk, representing one file operation.
#[derive(Debug, PartialEq)]
pub enum Hunk {
    AddFile {
        path: PathBuf,
        contents: String,
    },
    DeleteFile {
        path: PathBuf,
    },
    UpdateFile {
        path: PathBuf,
        move_path: Option<PathBuf>,
        chunks: Vec<UpdateFileChunk>,
    },
}

impl Hunk {
    #[must_use]
    pub fn resolve_path(&self, cwd: &Path) -> PathBuf {
        let path = match self {
            Hunk::AddFile { path, .. } => path,
            Hunk::DeleteFile { path } => path,
            Hunk::UpdateFile { path, .. } => path,
        };

        if path.is_absolute() {
            path.clone()
        } else {
            cwd.join(path)
        }
    }
}

/// A chunk of changes within an `UpdateFile` hunk.
#[derive(Debug, PartialEq)]
pub struct UpdateFileChunk {
    /// Optional context line immediately preceding the changes.
    pub change_context: Option<String>,
    /// Lines to be removed from the original file.
    pub old_lines: Vec<String>,
    /// Lines to be added to the file.
    pub new_lines: Vec<String>,
    /// True if the chunk ends with `*** End of File`.
    pub is_end_of_file: bool,
}

/// Errors that can occur during patch parsing.
#[derive(Debug, Error, PartialEq)]
pub enum ParseError {
    #[error("Invalid patch format: {0}")]
    InvalidPatchError(String),
    #[error("Invalid hunk at line {line_number}: {message}")]
    InvalidHunkError { message: String, line_number: usize },
}

use ParseError::{InvalidHunkError, InvalidPatchError};

/// Parses the given patch string into a vector of `Hunk`s.
pub fn parse_patch(patch: &str) -> Result<Vec<Hunk>, ParseError> {
    let mut lines = patch.lines().peekable();

    // The patch must start with `*** Begin Patch`.
    match lines.next() {
        Some("*** Begin Patch") => {}
        _ => {
            return Err(InvalidPatchError(
                "Expected `*** Begin Patch` on the first line".to_string(),
            ))
        }
    }

    let mut hunks: Vec<Hunk> = Vec::new();

    while let Some(line) = lines.next() {
        if line == "*** End Patch" {
            // End of patch found.
            if lines.next().is_some() {
                // There should be no content after `*** End Patch`.
                return Err(InvalidPatchError(
                    "Unexpected content after `*** End Patch`".to_string(),
                ));
            }
            return Ok(hunks);
        }

        let mut parts = line.splitn(2, ": ");
        let directive = parts.next().unwrap_or("");
        let path_str = parts.next().unwrap_or("").trim();

        if path_str.is_empty() {
            return Err(InvalidHunkError {
                message: "Missing path for file operation".to_string(),
                line_number: 0,
            });
        }

        let file_path = PathBuf::from(path_str);

        match directive {
            "*** Add File" => {
                let mut contents = String::new();
                while let Some(peek_line) = lines.peek() {
                    if peek_line.starts_with("*** ") {
                        break;
                    }
                    let content_line = lines.next().unwrap();
                    if let Some(text) = content_line.strip_prefix('+') {
                        contents.push_str(text);
                        contents.push('\n');
                    } else {
                        return Err(InvalidHunkError {
                            message: "Expected line to start with '+'".to_string(),
                            line_number: 0,
                        });
                    }
                }
                hunks.push(Hunk::AddFile { path: file_path, contents });
            }
            "*** Delete File" => {
                hunks.push(Hunk::DeleteFile { path: file_path });
            }
            "*** Update File" => {
                let mut move_path: Option<PathBuf> = None;
                if let Some(peek_line) = lines.peek() {
                    if peek_line.starts_with("*** Move to: ") {
                        let move_line = lines.next().unwrap();
                        let mut move_parts = move_line.splitn(2, ": ");
                        move_parts.next(); // Skip "*** Move to"
                        move_path = move_parts.next().map(|s| PathBuf::from(s.trim()));
                    }
                }

                let mut chunks: Vec<UpdateFileChunk> = Vec::new();
                while let Some(peek_line) = lines.peek() {
                    if *peek_line == "*** End of File" {
                        let _ = lines.next(); // Consume the line.
                        if let Some(last_chunk) = chunks.last_mut() {
                            last_chunk.is_end_of_file = true;
                        }
                        break;
                    }
                    if !peek_line.starts_with("@@") {
                        break;
                    }

                    lines.next(); // Consume "@@"
                    let mut change_context: Option<String> = None;
                    let mut old_lines: Vec<String> = Vec::new();
                    let mut new_lines: Vec<String> = Vec::new();

                    while let Some(chunk_line) = lines.peek() {
                        if chunk_line.starts_with("*** ") || chunk_line.starts_with("@@") {
                            break;
                        }

                        let line = lines.next().unwrap();
                        if let Some(stripped) = line.strip_prefix(' ') {
                            // Context line for seeking.
                            change_context = Some(stripped.to_string());
                        } else if let Some(stripped) = line.strip_prefix('-') {
                            old_lines.push(stripped.to_string());
                        } else if let Some(stripped) = line.strip_prefix('+') {
                            new_lines.push(stripped.to_string());
                        } else {
                            // Invalid line in hunk.
                            return Err(InvalidHunkError {
                                message: "Invalid line in update chunk. Must start with ' ', '+', or '-'.".to_string(),
                                line_number: 0,
                            });
                        }
                    }
                    chunks.push(UpdateFileChunk {
                        change_context,
                        old_lines,
                        new_lines,
                        is_end_of_file: false,
                    });
                }
                hunks.push(Hunk::UpdateFile {
                    path: file_path,
                    move_path,
                    chunks,
                });
            }
            _ => {
                return Err(InvalidHunkError {
                    message: "Unknown file operation directive".to_string(),
                    line_number: 0,
                })
            }
        }
    }

    // If we reach here, `*** End Patch` was not found.
    Err(InvalidPatchError(
        "Expected `*** End Patch` at the end of the patch".to_string(),
    ))
}