Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
959e17b
fixed diff context hunk range identifiers
alirezabeygi803 Jun 5, 2024
d473087
WIP patch files
alirezabeygi803 Jun 7, 2024
54372a3
updated patch_utils
alirezabeygi803 Jun 15, 2024
7f4b3e0
Merge branch 'main' into patch
alirezabeygi803 Jun 15, 2024
f0cc8bd
updated patch_utils
alirezabeygi803 Jun 18, 2024
59850dc
updated patchutil
alirezabeygi803 Jun 23, 2024
18060c4
updated patch
alirezabeygi803 Jun 25, 2024
466d4f6
Merge branch 'main' into patch
alirezabeygi803 Jun 25, 2024
4568537
introduced patch static files for tests
alirezabeygi803 Jun 26, 2024
ed2f340
introduced cli args
alirezabeygi803 Jun 27, 2024
1745419
Merge branch 'main' into patch
alirezabeygi803 Jun 30, 2024
82cc209
code cleanup
alirezabeygi803 Jul 1, 2024
cc40934
Merge branch 'main' into patch
alirezabeygi803 Jul 15, 2024
17ddd5f
using regex instead of if-else
alirezabeygi803 Jul 15, 2024
c78a5b9
updated patch_util
alirezabeygi803 Jul 22, 2024
410d7a1
patch final steps
alirezabeygi803 Jul 29, 2024
d602239
fixed build error
alirezabeygi803 Jul 30, 2024
09ae4da
Merge branch 'main' into patch
alirezabeygi803 Jul 30, 2024
af66c7d
Merge branch 'main' into patch
alirezabeygi803 Aug 11, 2024
8622710
updated Cargo.toml
alirezabeygi803 Aug 11, 2024
fdcced3
fixed Diff Context Unified Date issue
alirezabeygi803 Aug 11, 2024
6ea9675
Merge branch 'main' into patch
alirezabeygi803 Sep 9, 2024
1a43944
updated- in progress
alirezabeygi803 Sep 10, 2024
7981a75
Merge branch 'main' into patch
alirezabeygi803 Sep 10, 2024
8939a3c
Merge branch 'main' into patch
alirezabeygi803 Sep 27, 2024
0eb1ddc
Merge branch 'main' into patch
alirezabeygi803 Oct 14, 2024
594bf93
Merge remote-tracking branch 'origin/main' into patch
alirezabeygi803 Oct 17, 2024
f30310b
ran cargo fmt --all
alirezabeygi803 Oct 17, 2024
320f105
Merge branch 'patch' of https://github.com/alirezabeygi803/posixutils…
alirezabeygi803 Oct 17, 2024
d51eb5d
supporting both DateTime formats
alirezabeygi803 Oct 20, 2024
0cba9d1
Cargo.lock updated
alirezabeygi803 Oct 20, 2024
c058d42
Fixed issues
alirezabeygi803 Oct 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 90 additions & 84 deletions Cargo.lock

Large diffs are not rendered by default.

152 changes: 76 additions & 76 deletions m4/tests/integration_test.rs

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions text/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,7 @@ path = "./tsort.rs"
[[bin]]
name = "wc"
path = "./wc.rs"

[[bin]]
name = "patch"
path = "./patch.rs"
5 changes: 4 additions & 1 deletion text/diff_util/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ use super::constants::UTF8_NOT_ALLOWED_BYTES;
use crate::diff_util::constants::COULD_NOT_UNWRAP_FILENAME;

pub fn system_time_to_rfc2822(system_time: SystemTime) -> String {
Into::<DateTime<Local>>::into(system_time).to_rfc2822()
format!(
"{}",
Into::<DateTime<Local>>::into(system_time).format("%Y-%m-%d %H:%M:%S.%f %z")
)
}

pub fn is_binary(file_path: &PathBuf) -> io::Result<bool> {
Expand Down
210 changes: 210 additions & 0 deletions text/patch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// TODO: Handle no new lines
// TODO: detects loops!
// TODO: handle new ed PatchLine variants
// TODO: Check if patch is in ED or NORMAL format, if yes, output file should be Some(path)
// TODO: Determine patch kind and read to end of patch, do this again until all patches are extracted and pushed to patch list
// TODO: if number fo patches is more than 1, then they should be in either Context copied format or unified format, otherwise it is not possible to handle it
// TODO: If patch file is in context or unified format and file/output-file is not Some(path), extract path of original and modified files, and extract modification-date,
// TODO: Apply patch and if there is modification-date, apply it to the destination file.
// TODO: TaDa, done!
// TODO: Write tests!
//////////////////////////////////////////////////////////////////////////
// TODO : if hunks validation failed, write reject files.
// TODO : update modified_data of new file according to the patch
mod patch_utils;

extern crate clap;

use std::{
io::{self},
path::PathBuf,
};

use clap::Parser;
use patch_utils::{
constants::init,
functions::{context_unified_date_convert, file_exists, if_else, print_error},
hunks::Apply,
patch_file::PatchFile,
patch_file_kind::FileKind,
patch_format::PatchFormat,
patch_options::PatchOptions,
patch_units::PatchUnits,
};

/// patch - convert original file to modified file and vice versa using result of diff
#[derive(Parser, Debug, Clone)]
#[command(author, version, about, long_about)]
struct Args {
/// backup original content of each file
#[arg(short = 'b', long = "backup")]
backup: bool,

#[clap(flatten)]
format: FormatGroup,

/// assume patches were created with old file and new file swapped
#[arg(short = 'R', long = "reverse")]
reverse: bool,

/// change the working directory to dir first
#[arg(short = 'd', long = "dir")]
dir: Option<PathBuf>,

/// make merged if-then-else output using NAME
#[arg(short = 'D', long = "ifdef")]
ifdef: Option<String>,

/// strip NUM leading components from file names.
#[arg(short = 'p', long = "strip")]
num: Option<usize>,

/// output rejects to REJECT_FILE
#[arg(short = 'r', long = "reject-file")]
reject_file: Option<PathBuf>,

/// output patched files to OUTPUT_FILE
#[arg(short = 'o', long = "output")]
output_file: Option<PathBuf>,

/// file to apply patch to, empty for copied context and unified formats
#[arg(short, long)]
file: Option<PathBuf>,

/// read file from PATCHFILE instead of stdin
#[arg(short = 'i', long = "input")]
patchfile: PathBuf,
}

#[derive(Debug, Clone, clap::Args)]
#[group(required = false, multiple = false)]
pub struct FormatGroup {
/// interpret the patch file as a copied context difference
#[clap(short, long)]
context: bool,
/// interpret the patch file as an ed script
#[clap(short, long)]
ed: bool,
/// interpret the patch file as a normal script
#[clap(short, long)]
normal: bool,
/// interpret the patch file as a unified script
#[clap(short, long)]
unified: bool,
}

impl From<Args> for PatchOptions {
fn from(value: Args) -> Self {
let patch_format_options = [
(value.format.ed, PatchFormat::EditScript),
(value.format.normal, PatchFormat::Normal),
(value.format.context, PatchFormat::Context),
(value.format.unified, PatchFormat::Unified),
(true, PatchFormat::None), // to ensure next unwrap works
];

let patch_format = patch_format_options
.iter()
.filter(|option| option.0)
.map(|option| option.1)
.nth(0)
.unwrap();

PatchOptions {
backup: value.backup,
reverse: value.reverse,
file: value.file,
output_file: value.output_file,
strip: value.num,
patch_format,
patch_file: value.patchfile,
reject_file: value.reject_file,
}
}
}

impl Args {
fn verify_patchfile_exists(self) -> io::Result<Self> {
let if_true: fn(&PathBuf) = |_| {};

let if_false: fn(&PathBuf) = |file_name: &PathBuf| {
print_error(format!(
" **** Can't open patch file {} : No such file or directory",
file_name.to_str().expect("Could not unwrap file_name")
));
};

let file_exists = file_exists(&self.patchfile);

if_else(file_exists, if_true, if_false)(&self.patchfile);

match file_exists {
true => Ok(self),
false => Err(io::Error::new(
io::ErrorKind::NotFound,
"Could not find patch file!",
)),
}
}

fn change_dir(self) -> io::Result<Self> {
if let Some(path) = &self.dir {
std::env::set_current_dir(path)?;
}

Ok(self)
}

fn try_verify_format_and_output_file(self) -> io::Result<Self> {
if self.output_file.is_none() && self.file.is_none() {
if self.format.ed || self.format.normal {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"patch: applying patch for ed and normal format without specifying output-file is not possible!"));
}
}

Ok(self)
}
}

fn main() {
patch_operation().expect("Failed!");
}

fn patch_operation() -> io::Result<()> {
init();

let args = Args::parse();

let args = args
.change_dir()?
.verify_patchfile_exists()?
.try_verify_format_and_output_file()?;

let patch_options: PatchOptions = args.into();
let patch_file =
PatchFile::load_file(PathBuf::from(&patch_options.patch_file), FileKind::Patch)?;

let patch = PatchUnits::try_build(&patch_options, &patch_file)?;

if let Some(patch_units) = patch {
let into_hunks = patch_units.into_hunks();

match into_hunks {
Ok(mut into_hunks) => {
for hunks in into_hunks.hunks.iter_mut() {
let result = hunks.apply();
result.expect("Failed to apply hunk!");
}

for _failures in into_hunks.failures.iter_mut() {
// let reject_path =
}
}
Err(_) => todo!(),
}
}

Ok(())
}
131 changes: 131 additions & 0 deletions text/patch_utils/constants/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use regex::Regex;
use std::{collections::HashMap, sync::Once};

use crate::patch_utils::patch_unit::PatchUnitKind;

pub const ORIGINAL_SKIP: usize = 2; // 0 is Hunk separator; 1 is original range

// CONTEXT REGEX STRINGS
pub const CONTEXT_FORMAT_HUNK_SEPARATOR_REGEX: &str = r"^\s*\*{15}\s*$";
pub const CONTEXT_FORMAT_ORIGINAL_RANGE_REGEX: &str = r"^\s*^\*\*\*\s+(\d+|\d+,\d+)\s+\*{4}\s*$";
pub const CONTEXT_FORMAT_MODIFIED_RANGE_REGEX: &str = r"^\s*^-{3}\s+(\d+|\d+,\d+)\s+-{4}\s*$";
pub const CONTEXT_FORMAT_FIRST_LINE_REGEX: &str = r"^\s*\*{3}\s*.+\s+(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{9} [+-]\d{4}|\w{3} \w{3} \d{1,2} \d{2}:\d{2}:\d{2} \d{4})?\s*$";
pub const CONTEXT_FORMAT_SECOND_LINE_REGEX: &str = r"^\s*-{3}\s*.+\s+(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{9} [+-]\d{4}|\w{3} \w{3} \d{1,2} \d{2}:\d{2}:\d{2} \d{4})?\s*$";
pub const CONTEXT_FORMAT_INSERTED_LINE_REGEX: &str = r"^\s*\+\s.*\s*$";
pub const CONTEXT_FORMAT_DELETED_LINE_REGEX: &str = r"^\s*-\s.*\s*$";
pub const CONTEXT_FORMAT_CHANGED_LINE_REGEX: &str = r"^\s*\!\s.*\s*$";
pub const CONTEXT_FORMAT_UNCHANGED_LINE_REGEX: &str = r"^\s*\s\s.*\s*$";

const INITIALIZE_CONTEXT_REGEX_CACHE_ONCE: Once = Once::new();
static mut CONTEXT_REGEX_CACHE: Option<HashMap<ContextRegexKind, Regex>> = None;

const ORDERED_KINDS: &[ContextRegexKind] = &[
ContextRegexKind::FirstLine,
ContextRegexKind::SecondLine,
ContextRegexKind::HunkSeparator,
ContextRegexKind::OriginalRange,
ContextRegexKind::ModifiedRange,
ContextRegexKind::InsertedLine,
ContextRegexKind::DeletedLine,
ContextRegexKind::ChangedLine,
ContextRegexKind::UnchangedLine,
];

pub fn context_match(line: &str) -> PatchUnitKind {
let cache = context_regex_cache();

for kind in ORDERED_KINDS {
if cache[kind].is_match(line) {
return PatchUnitKind::Context(*kind);
}
}

// TODO: NewLine,
// TODO: NoNewLine

PatchUnitKind::Unkonw
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ContextRegexKind {
HunkSeparator,
OriginalRange,
ModifiedRange,
FirstLine,
SecondLine,
InsertedLine,
DeletedLine,
ChangedLine,
UnchangedLine,
}

pub fn initialize_context_regex_cache() {
INITIALIZE_CONTEXT_REGEX_CACHE_ONCE.call_once(|| unsafe {
let mut regex_cache = HashMap::new();

regex_cache.insert(
ContextRegexKind::HunkSeparator,
Regex::new(CONTEXT_FORMAT_HUNK_SEPARATOR_REGEX)
.expect("CONTEXT_FORMAT_HUNK_SEPARATOR_REGEX regex is not correct!"),
);

regex_cache.insert(
ContextRegexKind::OriginalRange,
Regex::new(CONTEXT_FORMAT_ORIGINAL_RANGE_REGEX)
.expect("CONTEXT_FORMAT_ORIGINAL_RANGE_REGEX regex is not correct!"),
);

regex_cache.insert(
ContextRegexKind::ModifiedRange,
Regex::new(CONTEXT_FORMAT_MODIFIED_RANGE_REGEX)
.expect("CONTEXT_FORMAT_MODIFIED_RANGE_REGEX regex is not correct!"),
);

regex_cache.insert(
ContextRegexKind::FirstLine,
Regex::new(CONTEXT_FORMAT_FIRST_LINE_REGEX)
.expect("CONTEXT_FORMAT_FIRST_LINE_REGEX regex is not correct!"),
);

regex_cache.insert(
ContextRegexKind::SecondLine,
Regex::new(CONTEXT_FORMAT_SECOND_LINE_REGEX)
.expect("CONTEXT_FORMAT_SECOND_LINE_REGEX regex is not correct!"),
);

regex_cache.insert(
ContextRegexKind::InsertedLine,
Regex::new(CONTEXT_FORMAT_INSERTED_LINE_REGEX)
.expect("CONTEXT_FORMAT_INSERTED_LINE_REGEX regex is not correct!"),
);

regex_cache.insert(
ContextRegexKind::DeletedLine,
Regex::new(CONTEXT_FORMAT_DELETED_LINE_REGEX)
.expect("CONTEXT_FORMAT_DELETED_LINE_REGEX regex is not correct!"),
);

regex_cache.insert(
ContextRegexKind::ChangedLine,
Regex::new(CONTEXT_FORMAT_CHANGED_LINE_REGEX)
.expect("CONTEXT_FORMAT_CHANGED_LINE_REGEX regex is not correct!"),
);

regex_cache.insert(
ContextRegexKind::UnchangedLine,
Regex::new(CONTEXT_FORMAT_UNCHANGED_LINE_REGEX)
.expect("CONTEXT_FORMAT_UNCHANGED_LINE_REGEX regex is not correct!"),
);

CONTEXT_REGEX_CACHE = Some(regex_cache);
});
}

pub fn context_regex_cache() -> &'static HashMap<ContextRegexKind, Regex> {
#[allow(static_mut_refs)]
if let Some(original_normal_regex_cache) = unsafe { &CONTEXT_REGEX_CACHE } {
return original_normal_regex_cache;
}

panic!("NORMAL_REGEX_CACHE should not be empty!");
Copy link

Copilot AI Apr 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update the panic message to reference 'CONTEXT_REGEX_CACHE' instead of 'NORMAL_REGEX_CACHE' for clarity.

Suggested change
panic!("NORMAL_REGEX_CACHE should not be empty!");
panic!("CONTEXT_REGEX_CACHE should not be empty!");

Copilot uses AI. Check for mistakes.
}
Loading