diff --git a/patch.opam b/patch.opam index 6863339..26f6fa5 100644 --- a/patch.opam +++ b/patch.opam @@ -8,7 +8,7 @@ bug-reports: "https://github.com/hannesm/patch/issues" license: "ISC" depends: [ - "ocaml" {>= "4.04.2"} + "ocaml" {>= "4.08"} "dune" {>= "3.0"} "alcotest" {with-test & >= "0.7.0"} "crowbar" {with-test} diff --git a/src/dune b/src/dune index 5ff03f7..73c8ae5 100644 --- a/src/dune +++ b/src/dune @@ -2,9 +2,7 @@ (name patch) (synopsis "Patch purely in OCaml") (public_name patch) - (modules patch patch_lib fname)) - -(ocamllex fname) + (modules patch lib fname)) (executable (name patch_command) diff --git a/src/fname.ml b/src/fname.ml new file mode 100644 index 0000000..99b2c83 --- /dev/null +++ b/src/fname.ml @@ -0,0 +1,123 @@ +type lexer_output = + | Quoted of (string * string) + | Unquoted + | Error of string + +exception Cant_parse_octal + +let ascii_zero = 48 (* Char.code '0' *) +let octal_to_char c1 c2 c3 = + let char_to_digit c = Char.code c - ascii_zero in + try + Char.chr ( + (char_to_digit c1 lsl 6) lor + (char_to_digit c2 lsl 3) lor + char_to_digit c3 + ) + with Invalid_argument _ -> raise Cant_parse_octal + +let lex_quoted_char s len i = + match s.[i] with + | 'a' -> Some ('\007', 2) + | 'b' -> Some ('\b', 2) + | 'f' -> Some ('\012', 2) + | 'n' -> Some ('\n', 2) + | 'r' -> Some ('\r', 2) + | 't' -> Some ('\t', 2) + | 'v' -> Some ('\011', 2) + | '\\' -> Some ('\\', 2) + | '"' -> Some ('"', 2) + | '0'..'3' as c1 when len >= 3 -> + begin match s.[i + 1], s.[i + 2] with + | ('0'..'7' as c2), ('0'..'7' as c3) -> + (try Some (octal_to_char c1 c2 c3, 4) + with Cant_parse_octal -> None) + | _, _ -> None + end + | _ -> None + +let rec lex_quoted_filename buf s len i = + if len > 0 then + match s.[i] with + | '"' -> Quoted (Buffer.contents buf, Lib.String.slice ~start:(i + 1) s) + | '\\' when len > 2 -> + let char_size = + match lex_quoted_char s (len - 1) (i + 1) with + | Some (c, char_size) -> Buffer.add_char buf c; char_size + | None -> Buffer.add_char buf s.[i]; 1 + in + lex_quoted_filename buf s (len - char_size) (i + char_size) + | c -> + Buffer.add_char buf c; + lex_quoted_filename buf s (len - 1) (i + 1) + else + Unquoted + +let lex_filename buf s len = + if len > 0 then + match s.[0] with + | '"' -> lex_quoted_filename buf s (len - 1) 1 + | _ -> Unquoted + else + Error "empty filename" + +let parse_filename ~allow_space s = + match lex_filename (Buffer.create 128) s (String.length s) with + | Quoted x -> Ok x + | Unquoted when not allow_space -> + begin match Lib.String.cut ' ' s with + | None -> Ok (s, "") + | Some x -> Ok x + end + | Unquoted -> Ok (s, "") + | Error msg -> Error msg + +let parse s = + let filename_and_date = + match Lib.String.cut '\t' s with + | None -> + parse_filename ~allow_space:false s + | Some (filename, date) -> + match parse_filename ~allow_space:true filename with + | Ok (filename, "") -> Ok (filename, date) + | Ok _ -> Error "Unexpected character after closing double-quote" + | Error _ as err -> err + in + match filename_and_date with + | Ok (filename, date) -> + if filename = "/dev/null" || + let date = String.trim date in + Lib.String.is_prefix ~prefix:"1970-" date || + Lib.String.is_prefix ~prefix:"1969-" date || + Lib.String.is_suffix ~suffix:" 1970" date || + Lib.String.is_suffix ~suffix:" 1969" date then + (* See https://github.com/hannesm/patch/issues/8 *) + Ok None + else + Ok (Some filename) + | Error _ as err -> err + +let parse_git_header s = + let parse s = + match parse_filename ~allow_space:true s with + | Ok (s, "") -> Ok (Lib.String.cut '/' s) + | Ok _ -> Error "Unexpected character after closing double-quote in header" + | Error _ as err -> err + in + let rec loop s len i = + if i < len then + match s.[i] with + | ' ' | '\t' -> + let a = parse (Lib.String.slice ~stop:i s) in + let b = parse (Lib.String.slice ~start:(i + 1) s) in + begin match a, b with + | Ok (Some ("a", a)), Ok (Some ("b", b)) + when a = (b : string) -> + Some a + | _, _ -> loop s len (i + 1) + end + | _ -> loop s len (i + 1) + else + None + in + loop s (String.length s) 0 diff --git a/src/fname.mli b/src/fname.mli index 27280f1..7640264 100644 --- a/src/fname.mli +++ b/src/fname.mli @@ -3,3 +3,5 @@ val parse : string -> (string option, string) result is equivalent to [/dev/null]. Returns [Error msg] in case of error. *) + +val parse_git_header : string -> string option diff --git a/src/fname.mll b/src/fname.mll deleted file mode 100644 index c92154d..0000000 --- a/src/fname.mll +++ /dev/null @@ -1,74 +0,0 @@ -{ -module String = Patch_lib.String - -type lexer_output = - | Quoted of string - | Unquoted - | Error of string - -exception Cant_parse_octal - -let ascii_zero = Char.code '0' -let octal_to_char c1 c2 c3 = - let char_to_digit c = Char.code c - ascii_zero in - try - Char.chr ( - (char_to_digit c1 lsl 6) lor - (char_to_digit c2 lsl 3) lor - char_to_digit c3 - ) - with Invalid_argument _ -> raise Cant_parse_octal -} - -let octal = ['0'-'7'] - -rule lex_quoted_filename buf = parse - | "\\a" { Buffer.add_char buf '\007'; lex_quoted_filename buf lexbuf } - | "\\b" { Buffer.add_char buf '\b'; lex_quoted_filename buf lexbuf } - | "\\f" { Buffer.add_char buf '\012'; lex_quoted_filename buf lexbuf } - | "\\n" { Buffer.add_char buf '\n'; lex_quoted_filename buf lexbuf } - | "\\r" { Buffer.add_char buf '\r'; lex_quoted_filename buf lexbuf } - | "\\t" { Buffer.add_char buf '\t'; lex_quoted_filename buf lexbuf } - | "\\v" { Buffer.add_char buf '\011'; lex_quoted_filename buf lexbuf } - | "\\\\" { Buffer.add_char buf '\\'; lex_quoted_filename buf lexbuf } - | "\\\"" { Buffer.add_char buf '"'; lex_quoted_filename buf lexbuf } - | '\\' (['0'-'3'] as c1) (octal as c2) (octal as c3) - { - match octal_to_char c1 c2 c3 with - | octal -> - Buffer.add_char buf octal; - lex_quoted_filename buf lexbuf - | exception Cant_parse_octal -> Unquoted - } - | '\\' _ { Unquoted } - | '"' eof { Quoted (Buffer.contents buf) } - | '"' _ { Unquoted } - | _ as c { Buffer.add_char buf c; lex_quoted_filename buf lexbuf } - | eof { Unquoted } - -and lex_filename buf = parse - | '"' { lex_quoted_filename buf lexbuf } - | _ { Unquoted } - | eof { Error "empty filename" } - -{ -let parse s = - let filename, date = - match String.cut '\t' s with - | None -> (s, "") - | Some x -> x - in - if filename = "/dev/null" || - String.is_prefix ~prefix:"1970-" date || - String.is_prefix ~prefix:"1969-" date || - String.is_suffix ~suffix:" 1970" date || - String.is_suffix ~suffix:" 1969" date then - (* See https://github.com/hannesm/patch/issues/8 *) - Ok None - else - let lexbuf = Lexing.from_string filename in - match lex_filename (Buffer.create 128) lexbuf with - | Quoted x -> Ok (Some x) - | Unquoted -> Ok (Some filename) - | Error msg -> Error msg -} diff --git a/src/patch_lib.ml b/src/lib.ml similarity index 87% rename from src/patch_lib.ml rename to src/lib.ml index bcd6e2d..a45eb44 100644 --- a/src/patch_lib.ml +++ b/src/lib.ml @@ -38,14 +38,11 @@ module String = struct in let len = stop - start in String.sub str start len +end - let trim = String.trim - - let get = String.get - - let concat = String.concat - - let length = String.length - - let equal = String.equal +module List = struct + let rec last = function + | [] -> invalid_arg "List.last" + | [x] -> x + | _::xs -> last xs end diff --git a/src/patch_lib.mli b/src/lib.mli similarity index 61% rename from src/patch_lib.mli rename to src/lib.mli index bcaa10f..9a4fa72 100644 --- a/src/patch_lib.mli +++ b/src/lib.mli @@ -4,9 +4,8 @@ module String : sig val cut : char -> string -> (string * string) option val cuts : char -> string -> string list val slice : ?start:int -> ?stop:int -> string -> string - val trim : string -> string - val get : string -> int -> char - val concat : string -> string list -> string - val length : string -> int - val equal : string -> string -> bool +end + +module List : sig + val last : 'a list -> 'a end diff --git a/src/patch.ml b/src/patch.ml index 8857a25..f76ef08 100644 --- a/src/patch.ml +++ b/src/patch.ml @@ -1,5 +1,3 @@ -module String = Patch_lib.String - type hunk = { mine_start : int ; mine_len : int ; @@ -26,58 +24,106 @@ let unified_diff ~mine_no_nl ~their_no_nl hunk = (if their_no_nl then no_nl_str else [])) let pp_hunk ~mine_no_nl ~their_no_nl ppf hunk = - Format.fprintf ppf "@@@@ -%d,%d +%d,%d @@@@\n%s" + Format.fprintf ppf "%@%@ -%d,%d +%d,%d %@%@\n%s\n" hunk.mine_start hunk.mine_len hunk.their_start hunk.their_len (unified_diff ~mine_no_nl ~their_no_nl hunk) -let take data num = - let rec take0 num data acc = - match num, data with - | 0, _ -> List.rev acc - | n, x::xs -> take0 (pred n) xs (x :: acc) - | _ -> invalid_arg "take0 broken" +let list_cut idx l = + let rec aux acc idx = function + | l when idx = 0 -> (List.rev acc, l) + | [] -> invalid_arg "list_cut" + | x::xs -> aux (x :: acc) (idx - 1) xs in - take0 num data [] + aux [] idx l -let drop data num = - let rec drop data num = - match num, data with - | 0, _ -> data - | n, _::xs -> drop xs (pred n) - | _ -> invalid_arg "drop broken" +let rec apply_hunk ~cleanly ~fuzz (last_matched_line, offset, lines) ({mine_start; mine_len; mine; their_start = _; their_len; their} as hunk) = + let mine_start = mine_start + offset in + let patch_match ~search_offset = + let mine_start = mine_start + search_offset in + let prefix, rest = list_cut (Stdlib.max 0 (mine_start - 1)) lines in + let actual_mine, suffix = list_cut mine_len rest in + if actual_mine <> (mine : string list) then + invalid_arg "unequal mine"; + (* TODO: should we check their_len against List.length their? *) + (mine_start + mine_len, offset + (their_len - mine_len), prefix @ their @ suffix) in - try drop data num with - | Invalid_argument _ -> invalid_arg ("drop " ^ string_of_int num ^ " on " ^ string_of_int (List.length data)) - -(* TODO verify that it applies cleanly *) -let apply_hunk old (index, to_build) hunk = - try - let prefix = take (drop old index) (hunk.mine_start - index) in - (hunk.mine_start + hunk.mine_len, to_build @ prefix @ hunk.their) - with - | Invalid_argument _ -> invalid_arg ("apply_hunk " ^ string_of_int index ^ " old len " ^ string_of_int (List.length old) ^ - " hunk start " ^ string_of_int hunk.mine_start ^ " hunk len " ^ string_of_int hunk.mine_len) + try patch_match ~search_offset:0 + with Invalid_argument _ -> + if cleanly then + invalid_arg "apply_hunk" + else + let max_pos_offset = Stdlib.max 0 (List.length lines - Stdlib.max 0 (mine_start - 1) - mine_len) in + let max_neg_offset = mine_start - last_matched_line in + let rec locate search_offset = + let aux search_offset max_offset = + try + if search_offset <= max_offset then + Some (patch_match ~search_offset) + else + None + with Invalid_argument _ -> None + in + if search_offset > max_pos_offset && search_offset > max_neg_offset then + if fuzz < 3 && List.length mine >= 2 && List.length their >= 2 then + let hunk = + if List.hd hunk.mine = (List.hd hunk.their : string) then + { + mine_start = hunk.mine_start + 1; + mine_len = hunk.mine_len - 1; + mine = List.tl hunk.mine; + their_start = hunk.their_start + 1; + their_len = hunk.their_len - 1; + their = List.tl hunk.their; + } + else + hunk + in + let hunk = + if Lib.List.last hunk.mine = (Lib.List.last hunk.their : string) then + { + mine_start = hunk.mine_start; + mine_len = hunk.mine_len - 1; + mine = List.rev (List.tl (List.rev hunk.mine)); + their_start = hunk.their_start; + their_len = hunk.their_len - 1; + their = List.rev (List.tl (List.rev hunk.their)); + } + else + hunk + in + if hunk.mine_len = 0 && hunk.their_len = 0 then + invalid_arg "apply_hunk: equal hunks... why?!" + else if mine_len = (hunk.mine_len : int) && their_len = (hunk.their_len : int) then + invalid_arg "apply_hunk: could not apply fuzz" + else + apply_hunk ~cleanly ~fuzz:(fuzz + 1) (last_matched_line, offset, lines) hunk + else + invalid_arg "apply_hunk" + else + match aux search_offset max_pos_offset with + | Some x -> x + | None -> + match aux (-search_offset) max_neg_offset with + | Some x -> x + | None -> locate (search_offset + 1) + in + locate 1 let to_start_len data = (* input being "?19,23" *) - match String.cut ',' (String.slice ~start:1 data) with - | None when data = "+1" || data = "-1" -> (0, 1) + match Lib.String.cut ',' (Lib.String.slice ~start:1 data) with + | None when data = "+1" || data = "-1" -> (1, 1) | None -> invalid_arg ("start_len broken in " ^ data) - | Some (start, len) -> - let len = int_of_string len - and start = int_of_string start - in - let st = if len = 0 || start = 0 then start else pred start in - (st, len) + | Some (start, len) -> (int_of_string start, int_of_string len) let count_to_sl_sl data = - if String.is_prefix ~prefix:"@@" data then + if Lib.String.is_prefix ~prefix:"@@ -" data then (* input: "@@ -19,23 +19,12 @@ bla" *) (* output: ((19,23), (19, 12)) *) - match List.filter (function "" -> false | _ -> true) (String.cuts '@' data) with + match List.filter (function "" -> false | _ -> true) (Lib.String.cuts '@' data) with | numbers::_ -> let nums = String.trim numbers in - (match String.cut ' ' nums with + (match Lib.String.cut ' ' nums with | None -> invalid_arg "couldn't find space in count" | Some (mine, theirs) -> Some (to_start_len mine, to_start_len theirs)) | _ -> invalid_arg "broken line!" @@ -85,18 +131,24 @@ let count_to_sl_sl data = None let sort_into_bags ~counter:(mine_len, their_len) dir mine their m_nl t_nl str = - if String.length str = 0 then - failwith "invalid patch (empty line)" - else if mine_len = 0 && their_len = 0 && String.get str 0 <> '\\' then + let both data = + if m_nl || t_nl then + failwith "\"no newline at the end of file\" is not at the end of the file"; + if mine_len = 0 || their_len = 0 then + failwith "invalid patch (both size exhausted)"; + let counter = (mine_len - 1, their_len - 1) in + Some (counter, `Both, (data :: mine), (data :: their), m_nl, t_nl) + in + let str_len = String.length str in + if mine_len = 0 && their_len = 0 && (str_len = 0 || str.[0] <> '\\') then None - else match String.get str 0, String.slice ~start:1 str with + else if str_len = 0 then + both "" (* NOTE: this should technically be a parse error but GNU patch accepts that and some patches in opam-repository do use this behaviour *) + else match String.get str 0, Lib.String.slice ~start:1 str with | ' ', data -> - if m_nl || t_nl then - failwith "\"no newline at the end of file\" is not at the end of the file"; - if mine_len = 0 || their_len = 0 then - failwith "invalid patch (both size exhausted)"; - let counter = (mine_len - 1, their_len - 1) in - Some (counter, `Both, (data :: mine), (data :: their), m_nl, t_nl) + both data + | '\t', data -> + both ("\t"^data) (* NOTE: not valid but accepted by GNU patch *) | '+', data -> if t_nl then failwith "\"no newline at the end of file\" is not at the end of the file"; @@ -132,7 +184,11 @@ let to_hunk count data mine_no_nl their_no_nl = | Some ((mine_start, mine_len), (their_start, their_len)) -> let counter = (mine_len, their_len) in let rec step ~counter dir mine their mine_no_nl their_no_nl = function - | [] | [""] -> (List.rev mine, List.rev their, mine_no_nl, their_no_nl, []) + | [] | [""] when counter = (0, 0) -> (List.rev mine, List.rev their, mine_no_nl, their_no_nl, []) + | [""] when counter = (1, 1) -> (List.rev ("" :: mine), List.rev ("" :: their), mine_no_nl, their_no_nl, []) (* GNU patch behaviour *) + | [""] when counter = (2, 2) -> (List.rev ("" :: "" :: mine), List.rev ("" :: "" :: their), mine_no_nl, their_no_nl, []) (* GNU patch behaviour *) + | [""] when counter = (3, 3) -> (List.rev ("" :: "" :: "" :: mine), List.rev ("" :: "" :: "" :: their), mine_no_nl, their_no_nl, []) (* GNU patch behaviour *) + | [] | [""] -> failwith "bad file" | x::xs -> match sort_into_bags ~counter dir mine their mine_no_nl their_no_nl x with | Some (counter, dir, mine, their, mine_no_nl', their_no_nl') -> step ~counter dir mine their mine_no_nl' their_no_nl' xs | None -> (List.rev mine, List.rev their, mine_no_nl, their_no_nl, x :: xs) @@ -157,7 +213,7 @@ let operation_eq a b = match a, b with | Create a, Create b -> String.equal a b | Edit (a, a'), Edit (b, b') | Rename_only (a, a'), Rename_only (b, b') -> String.equal a b && String.equal a' b' - | _ -> false + | Delete _, _ | Create _, _ | Edit _, _ | Rename_only _, _ -> false let no_file = "/dev/null" @@ -213,6 +269,7 @@ let pp_operation ppf op = Format.fprintf ppf "--- %a\n" pp_filename no_file ; Format.fprintf ppf "+++ %a\n" pp_filename name | Rename_only (old_name, new_name) -> + Format.fprintf ppf "diff --git %a %a" pp_filename old_name pp_filename new_name; Format.fprintf ppf "rename from %a\n" pp_filename old_name; Format.fprintf ppf "rename to %a\n" pp_filename new_name @@ -241,16 +298,25 @@ let strip_prefix ~p filename = if p = 0 then filename else - match String.cuts '/' filename with + match Lib.String.cuts '/' filename with | [] -> assert false | x::xs -> (* Per GNU patch's spec: A sequence of one or more adjacent slashes is counted as a single slash. *) - let filename = x :: List.filter (function "" -> false | _ -> true) xs in - String.concat "/" (drop filename p) + let filename' = x :: List.filter (function "" -> false | _ -> true) xs in + let rec drop_up_to n = function + | [] -> assert false + | l when n = 0 -> l + | [_] -> failwith "wrong prefix" + | _::xs -> drop_up_to (n - 1) xs + in + (* GNU patch just drops the max number of slashes when the filename doesn't have enough slashes to satisfy -p *) + match drop_up_to p filename' with + | [] -> assert false + | l -> String.concat "/" l let operation_of_strings ~p mine their = - let mine_fn = String.slice ~start:4 mine - and their_fn = String.slice ~start:4 their in + let mine_fn = Lib.String.slice ~start:4 mine + and their_fn = Lib.String.slice ~start:4 their in match Fname.parse mine_fn, Fname.parse their_fn with | Ok None, Ok (Some b) -> Create (strip_prefix ~p b) | Ok (Some a), Ok None -> Delete (strip_prefix ~p a) @@ -260,17 +326,40 @@ let operation_of_strings ~p mine their = | _, Error msg -> raise (Parse_error {msg; lines = [their]}) let parse_one ~p data = + let open (struct + type mode = Git of string option + end) in + let is_git = function + | Some (Git _) -> true + | None -> false + in (* first locate --- and +++ lines *) - let rec find_start ?hdr = function - | [] -> hdr, [] - | x::y::xs when String.is_prefix ~prefix:"rename from " x && String.is_prefix ~prefix:"rename to " y -> - let hdr = Rename_only (String.slice ~start:12 x, String.slice ~start:10 y) in - find_start ~hdr xs - | x::y::xs when String.is_prefix ~prefix:"--- " x -> + let rec find_start ~mode ~git_action = function + | [] -> git_action, [] + | x::xs when Lib.String.is_prefix ~prefix:"diff --git " x -> + let git_filename = Fname.parse_git_header (Lib.String.slice ~start:11 x) in + begin match mode, git_action with + | (None | Some (Git _)), None -> find_start ~mode:(Some (Git git_filename)) ~git_action:None xs + | None, Some _ -> assert false (* impossible state *) + | Some (Git _), Some git_action -> (Some git_action, x :: xs) + end + | x::y::xs when is_git mode && Lib.String.is_prefix ~prefix:"rename from " x && Lib.String.is_prefix ~prefix:"rename to " y -> + let git_action = Some (Rename_only (Lib.String.slice ~start:12 x, Lib.String.slice ~start:10 y)) in + find_start ~mode ~git_action xs + | x::xs when is_git mode && Lib.String.is_prefix ~prefix:"deleted file mode " x -> + let git_action = match mode with + | Some (Git (Some git_filename)) -> Some (Delete git_filename) + | Some (Git None) -> git_action + | None -> assert false + in + find_start ~mode ~git_action xs + | x::y::xs when Lib.String.is_prefix ~prefix:"--- " x && Lib.String.is_prefix ~prefix:"+++ " y -> Some (operation_of_strings ~p x y), xs - | _::xs -> find_start ?hdr xs + | x::y::_xs when Lib.String.is_prefix ~prefix:"*** " x && Lib.String.is_prefix ~prefix:"--- " y -> + failwith "Context diffs are not supported" + | _::xs -> find_start ~mode ~git_action xs in - match find_start data with + match find_start ~mode:None ~git_action:None data with | Some (Rename_only _ as operation), rest -> let hunks = [] and mine_no_nl = false and their_no_nl = false in Some ({ operation ; hunks ; mine_no_nl ; their_no_nl }, rest) @@ -280,7 +369,7 @@ let parse_one ~p data = | None, [] -> None | None, _ -> assert false -let to_lines = String.cuts '\n' +let to_lines = Lib.String.cuts '\n' let parse ~p data = let lines = to_lines data in @@ -292,7 +381,7 @@ let parse ~p data = in doit ~p [] lines -let patch filedata diff = +let patch ~cleanly filedata diff = match diff.operation with | Rename_only _ -> filedata | Delete _ -> None @@ -304,15 +393,14 @@ let patch filedata diff = Some (String.concat "\n" lines) | _ -> assert false end - | _ -> + | Edit _ -> let old = match filedata with None -> [] | Some x -> to_lines x in - let idx, lines = List.fold_left (apply_hunk old) (0, []) diff.hunks in - let lines = lines @ drop old idx in + let _, _, lines = List.fold_left (apply_hunk ~cleanly ~fuzz:0) (0, 0, old) diff.hunks in let lines = match diff.mine_no_nl, diff.their_no_nl with | false, true -> (match List.rev lines with ""::tl -> List.rev tl | _ -> lines) | true, false -> lines @ [ "" ] - | false, false when filedata = None -> lines @ [ "" ] + | false, false when filedata = None -> lines @ [ "" ] (* TODO: i'm not sure about this *) | false, false -> lines | true, true -> lines in @@ -369,8 +457,8 @@ let diff_op operation a b = l1 l2 in aux - ~mine_start:0 ~mine_len:0 ~mine:[] - ~their_start:0 ~their_len:0 ~their:[] + ~mine_start:(if a = "" then 0 else 1) ~mine_len:0 ~mine:[] + ~their_start:(if b = "" then 0 else 1) ~their_len:0 ~their:[] (to_lines a) (to_lines b) let diff operation a b = match a, b with diff --git a/src/patch.mli b/src/patch.mli index f67f957..50f3031 100644 --- a/src/patch.mli +++ b/src/patch.mli @@ -61,7 +61,7 @@ val parse : p:int -> string -> t list @raise Parse_error if a filename was unable to be parsed *) -val patch : string option -> t -> string option +val patch : cleanly:bool -> string option -> t -> string option (** [patch file_contents diff] applies [diff] on [file_contents], resulting in the new file contents (or None if deleted). *) diff --git a/src/patch_command.ml b/src/patch_command.ml index 868c99a..a020396 100644 --- a/src/patch_command.ml +++ b/src/patch_command.ml @@ -21,7 +21,7 @@ let run ~p ~input ~diff = which is not supported by this command."; exit exit_several_chunks | [diff] -> - begin match Patch.patch (Some input) diff with + begin match Patch.patch ~cleanly:true (Some input) diff with | None -> Printf.eprintf "Error during patching:\n%!"; exit exit_patch_failure diff --git a/test/crowbar_test.ml b/test/crowbar_test.ml index 6be2c9b..1b37f41 100644 --- a/test/crowbar_test.ml +++ b/test/crowbar_test.ml @@ -141,7 +141,7 @@ let check_Patch file1 file2 = | _::_::_ -> Crowbar.fail "not a single diff!" | [diff] -> let data = string_of_file file1 in - match Patch.patch (Some data) diff with + match Patch.patch ~cleanly:true (Some data) diff with | None -> let exp = string_of_file file2 in Crowbar.fail ("input file\n" ^ data ^ "\ndiff\n" ^ text_diff ^ "\nexpected\n" ^ exp) diff --git a/test/dune b/test/dune index 06d3fac..0a8ff8b 100644 --- a/test/dune +++ b/test/dune @@ -12,3 +12,5 @@ (name crowbar_test) (modules crowbar_test) (libraries patch crowbar)) + +(dirs :standard \ opam-repository) diff --git a/test/opam-repository/.gitignore b/test/opam-repository/.gitignore new file mode 100644 index 0000000..dd5e97e --- /dev/null +++ b/test/opam-repository/.gitignore @@ -0,0 +1,5 @@ +/opam-repository +/old +/new +/old.log +/new.log diff --git a/test/opam-repository/test.sh b/test/opam-repository/test.sh new file mode 100755 index 0000000..4ce3082 --- /dev/null +++ b/test/opam-repository/test.sh @@ -0,0 +1,78 @@ +#!/bin/sh + +set -e +set -u +set -o pipefail + +if test "$(dirname "$0")" != "." ; then + echo "usage: ./test.sh" + exit 1 +fi + +export OPAMROOT=./.opam-root + +printf "Enter the path to the opam binary without builtin patch: " +read -r OLD_OPAM +printf "Enter the path to the opam binary with builtin patch: " +read -r NEW_OPAM + +echo +echo '## Getting the list of packages with patches' +echo + +rm -rf ./opam-repository +git clone -b 2025-01-before-archiving-phase1 --depth=1 https://github.com/ocaml/opam-repository.git +pushd ./opam-repository/packages > /dev/null +grep -l '^patches:' */*/opam | cut -d/ -f2 > ../../pkgs-with-patches +popd > /dev/null + +NB_OF_PKGS=$(cat ./pkgs-with-patches | wc -l) +echo "Number of packages to process: ${NB_OF_PKGS}" + +rm_faulty_pkg_artefacts() { + rm -rf ./ocaml-variants.4.10.0+nnpcheck/.git + rm ./coq.8.7.1+1/test-suite/bugs/closed/4722/tata + rm ./coq.8.7.1+2/test-suite/bugs/closed/4722/tata + rm ./opa-base.1.1.0+4263/ocamllib/libbase/default.trx +} + +echo +echo "## Extract and apply patches using ${OLD_OPAM}" +echo + +rm -rf ./tmp ./old +mkdir ./tmp +pushd ./tmp > /dev/null +"${OLD_OPAM}" init --bare --no-setup --no-opamrc ../opam-repository +cat ../pkgs-with-patches | time -p xargs -n1 "${OLD_OPAM}" source > ../old.log 2>&1 || true +rm_faulty_pkg_artefacts +rm -rf "${OPAMROOT}" +popd > /dev/null +mv ./tmp ./old + +echo +echo "## Extract and apply patches using ${NEW_OPAM}" +echo + +rm -rf ./tmp ./new +mkdir ./tmp +pushd ./tmp > /dev/null +"${NEW_OPAM}" init --bare --no-setup --no-opamrc ../opam-repository +cat ../pkgs-with-patches | time -p xargs -n1 "${NEW_OPAM}" source > ../new.log 2>&1 || true +rm_faulty_pkg_artefacts +rm -rf "${OPAMROOT}" +popd > /dev/null +mv ./tmp ./new + +echo >> ./test.log +echo >> ./test.log +echo "Run on the $(date):" >> ./test.log +echo "Time of old run:" >> ./test.log +tail -n3 ./old.log >> ./test.log +echo "Time of new run:" >> ./test.log +tail -n3 ./new.log >> ./test.log +echo "Diff:" >> ./test.log +diff -qr ./old ./new >> ./test.log 2>&1 || true +echo "Done" >> ./test.log + +echo "Done." diff --git a/test/test.ml b/test/test.ml index e480c55..acdb9ec 100644 --- a/test/test.ml +++ b/test/test.ml @@ -171,41 +171,41 @@ let basic_diffs = let basic_hunks = let open Patch in - let hunk1 = [ { mine_start = 0 ; mine_len = 1 ; mine = ["foo"] ; - their_start = 0 ; their_len = 1 ; their = ["foobar"] } ] + let hunk1 = [ { mine_start = 1 ; mine_len = 1 ; mine = ["foo"] ; + their_start = 1 ; their_len = 1 ; their = ["foobar"] } ] in let diff = { operation = Edit ("a", "b") ; hunks = hunk1 ; mine_no_nl = false ; their_no_nl = false } in let hunk2 = - [ { mine_start = 1 ; mine_len = 7 ; mine = [ "bar" ; "baz" ; "boo" ; "foo" ; "bar" ; "baz" ; "boo" ] ; - their_start = 1 ; their_len = 7 ; their = [ "bar" ; "baz" ; "boo" ; "foo2" ; "bar" ; "baz" ; "boo" ] } ] + [ { mine_start = 2 ; mine_len = 7 ; mine = [ "bar" ; "baz" ; "boo" ; "foo" ; "bar" ; "baz" ; "boo" ] ; + their_start = 2 ; their_len = 7 ; their = [ "bar" ; "baz" ; "boo" ; "foo2" ; "bar" ; "baz" ; "boo" ] } ] in let hunk3 = [ - { mine_start = 0 ; mine_len = 5 ; mine = [ "foo" ; "bar" ; "baz" ; "boo" ; "foo" ] ; - their_start = 0 ; their_len = 5 ; their = [ "foo" ; "bar2" ; "baz" ; "boo" ; "foo" ] } ; - { mine_start = 7 ; mine_len = 4 ; mine = [ "boo" ; "foo" ; "bar" ; "baz" ] ; - their_start = 7 ; their_len = 4 ; their = [ "boo" ; "foo" ; "bar" ; "baz3" ] } + { mine_start = 1 ; mine_len = 5 ; mine = [ "foo" ; "bar" ; "baz" ; "boo" ; "foo" ] ; + their_start = 1 ; their_len = 5 ; their = [ "foo" ; "bar2" ; "baz" ; "boo" ; "foo" ] } ; + { mine_start = 8 ; mine_len = 4 ; mine = [ "boo" ; "foo" ; "bar" ; "baz" ] ; + their_start = 8 ; their_len = 4 ; their = [ "boo" ; "foo" ; "bar" ; "baz3" ] } ] in let hunk4 = [ - { mine_start = 0 ; mine_len = 6 ; mine = [ "foo" ; "foo" ; "foo" ; "foo" ; "foo" ; "foo" ] ; - their_start = 0 ; their_len = 7 ; their = [ "foo" ; "foo" ; "foo" ; "foo3" ; "foo" ; "foo" ; "foo" ] } ; - { mine_start = 8 ; mine_len = 6 ; mine = [ "foo" ; "foo" ; "foo" ; "foo" ; "foo" ; "foo" ] ; - their_start = 9 ; their_len = 7 ; their = [ "foo" ; "foo" ; "foo" ; "foo5" ; "foo" ; "foo" ; "foo" ] } ; - { mine_start = 30 ; mine_len = 6 ; mine = [ "foo" ; "foo" ; "foo" ; "foo" ; "foo" ; "foo" ] ; - their_start = 32 ; their_len = 11 ; their = [ "foo" ; "foo" ; "foo" ; "bar" ; "foo" ; "foo" ; "foo" ; "foo" ; "foo" ; "foo" ; "bar2" ] } + { mine_start = 1 ; mine_len = 6 ; mine = [ "foo" ; "foo" ; "foo" ; "foo" ; "foo" ; "foo" ] ; + their_start = 1 ; their_len = 7 ; their = [ "foo" ; "foo" ; "foo" ; "foo3" ; "foo" ; "foo" ; "foo" ] } ; + { mine_start = 9 ; mine_len = 6 ; mine = [ "foo" ; "foo" ; "foo" ; "foo" ; "foo" ; "foo" ] ; + their_start = 10 ; their_len = 7 ; their = [ "foo" ; "foo" ; "foo" ; "foo5" ; "foo" ; "foo" ; "foo" ] } ; + { mine_start = 31 ; mine_len = 6 ; mine = [ "foo" ; "foo" ; "foo" ; "foo" ; "foo" ; "foo" ] ; + their_start = 33 ; their_len = 11 ; their = [ "foo" ; "foo" ; "foo" ; "bar" ; "foo" ; "foo" ; "foo" ; "foo" ; "foo" ; "foo" ; "bar2" ] } ] in let hunk5= [ { mine_start = 0 ; mine_len = 0 ; mine = [] ; - their_start = 0 ; their_len = 1 ; their = [ "foo" ] } + their_start = 1 ; their_len = 1 ; their = [ "foo" ] } ] in let diff5 = { diff with operation = Create "b" ; hunks = hunk5 } in let hunk6 = [ - { mine_start = 0 ; mine_len = 1 ; mine = [ "foo" ] ; + { mine_start = 1 ; mine_len = 1 ; mine = [ "foo" ] ; their_start = 0 ; their_len = 0 ; their = [ ] } ] in let diff6 = { diff with operation = Delete "a" ; hunks = hunk6 } in let hunk7 = [ - { mine_start = 0 ; mine_len = 1 ; mine = [ "foo" ] ; - their_start = 0 ; their_len = 2 ; their = [ "foo" ; "foo" ] } + { mine_start = 1 ; mine_len = 1 ; mine = [ "foo" ] ; + their_start = 1 ; their_len = 2 ; their = [ "foo" ; "foo" ] } ] in let diff7 = { diff with operation = Edit ("a", "b") ; hunks = hunk7 } in List.map (fun d -> [ d ]) @@ -306,7 +306,7 @@ let parse_diffs = let basic_apply file diff exp () = match Patch.parse ~p:0 diff with | [ diff ] -> - let res = Patch.patch file diff in + let res = Patch.patch ~cleanly:true file diff in Alcotest.(check (option string) __LOC__ exp res) | _ -> Alcotest.fail "expected one" @@ -341,23 +341,23 @@ let multi_diff = {| let multi_hunks = let open Patch in - let hunk1 = [ { mine_start = 0 ; mine_len = 1 ; mine = ["bar"] ; - their_start = 0 ; their_len = 1 ; their = ["foobar"] } ] + let hunk1 = [ { mine_start = 1 ; mine_len = 1 ; mine = ["bar"] ; + their_start = 1 ; their_len = 1 ; their = ["foobar"] } ] in let diff1 = { operation = Edit ("foo", "bar") ; hunks = hunk1 ; mine_no_nl = false ; their_no_nl = false } in let hunk2 = - [ { mine_start = 0 ; mine_len = 1 ; mine = [ "baz" ] ; + [ { mine_start = 1 ; mine_len = 1 ; mine = [ "baz" ] ; their_start = 0 ; their_len = 0 ; their = [] } ] in let diff2 = { operation = Delete "foobar" ; hunks = hunk2 ; mine_no_nl = false ; their_no_nl = false } in let hunk3 = [ { mine_start = 0 ; mine_len = 0 ; mine = [ ] ; - their_start = 0 ; their_len = 1 ; their = [ "baz" ] } + their_start = 1 ; their_len = 1 ; their = [ "baz" ] } ] in let diff3 = { operation = Create "baz" ; hunks = hunk3 ; mine_no_nl = false ; their_no_nl = true } in let hunk4 = [ - { mine_start = 0 ; mine_len = 1 ; mine = [ "foobarbaz" ] ; - their_start = 0 ; their_len = 1 ; their = [ "foobar" ] } + { mine_start = 1 ; mine_len = 1 ; mine = [ "foobarbaz" ] ; + their_start = 1 ; their_len = 1 ; their = [ "foobar" ] } ] in let diff4 = { operation = Edit ("foobarbaz", "foobarbaz") ; hunks = hunk4 ; mine_no_nl = false ; their_no_nl = false } in [ diff1 ; diff2 ; diff3 ; diff4 ] @@ -371,7 +371,7 @@ let multi_apply () = Alcotest.(check int __LOC__ (List.length multi_files) (List.length diffs)); Alcotest.(check int __LOC__ (List.length multi_exp) (List.length diffs)); List.iter2 (fun diff (input, expected) -> - let res = Patch.patch input diff in + let res = Patch.patch ~cleanly:true input diff in Alcotest.(check (option string) __LOC__ expected res)) diffs (List.combine multi_files multi_exp) @@ -390,8 +390,8 @@ let regression_diff, regression_hunks = +aaa |}, [ { operation = Edit ("a", "b"); - hunks = [ { mine_start = 0; mine_len = 1; mine = ["-- /dev/null"]; - their_start = 0; their_len = 1; their = ["aaa"]} ]; + hunks = [ { mine_start = 1; mine_len = 1; mine = ["-- /dev/null"]; + their_start = 1; their_len = 1; their = ["aaa"]} ]; mine_no_nl = false; their_no_nl = false} ] let basic_regression_diffs = [ @@ -437,7 +437,7 @@ let regression_test name () = let exp = opt_read (name ^ ".new") in match Patch.parse ~p:0 diff with | [ diff ] -> - let res = Patch.patch old diff in + let res = Patch.patch ~cleanly:true old diff in Alcotest.(check (option string) __LOC__ exp res) | ds -> Alcotest.fail ("expected one, found " ^ string_of_int (List.length ds)) @@ -481,7 +481,7 @@ eee|}^(if their_no_nl then "" else "\n") let hunk = { Patch.operation = Create "b"; hunks = [ { mine_start = 0; mine_len = 0; mine = []; - their_start = 0; their_len = 5; their = ["aaa"; "bbb"; "ccc"; "ddd"; "eee"]} ]; + their_start = 1; their_len = 5; their = ["aaa"; "bbb"; "ccc"; "ddd"; "eee"]} ]; mine_no_nl = false; their_no_nl} in diff, Some hunk @@ -503,7 +503,7 @@ eee|}^(if mine_no_nl then "" else "\n") let diff = Patch.diff (Delete "a") (Some a) b in let hunk = { Patch.operation = Delete "a"; - hunks = [ { mine_start = 0; mine_len = 5; mine = ["aaa"; "bbb"; "ccc"; "ddd"; "eee"]; + hunks = [ { mine_start = 1; mine_len = 5; mine = ["aaa"; "bbb"; "ccc"; "ddd"; "eee"]; their_start = 0; their_len = 0; their = []} ]; mine_no_nl; their_no_nl = false} in @@ -525,8 +525,8 @@ let diff_tests_empty_gen ~mine_no_nl ~their_no_nl = let mine_len, mine = if mine_no_nl then 0, [] else 1, [""] in let their_len, their = if their_no_nl then 0, [] else 1, [""] in Some { Patch.operation = Edit ("a", "b"); - hunks = [ { mine_start = 0; mine_len; mine; - their_start = 0; their_len; their} ]; + hunks = [ { mine_start = if mine_no_nl then 0 else 1; mine_len; mine; + their_start = if their_no_nl then 0 else 1; their_len; their} ]; mine_no_nl = false; their_no_nl = false} in diff, hunk @@ -560,8 +560,8 @@ eee|}^(if their_no_nl then "" else "\n") None else Some { Patch.operation = Edit ("a", "b"); - hunks = [ { mine_start = 4; mine_len = 1; mine = ["eee"]; - their_start = 4; their_len = 1; their = ["eee"]} ]; + hunks = [ { mine_start = 5; mine_len = 1; mine = ["eee"]; + their_start = 5; their_len = 1; their = ["eee"]} ]; mine_no_nl; their_no_nl} in diff, hunk @@ -592,8 +592,8 @@ eee|}^(if their_no_nl then "" else "\n") let diff = Patch.diff (Edit ("a", "b")) (Some a) (Some b) in let hunk = { Patch.operation = Edit ("a", "b"); - hunks = [ { mine_start = 2; mine_len = 3; mine = ["ccc"; "ddd"; "eee"]; - their_start = 2; their_len = 3; their = ["test1"; "test2"; "eee"]} ]; + hunks = [ { mine_start = 3; mine_len = 3; mine = ["ccc"; "ddd"; "eee"]; + their_start = 3; their_len = 3; their = ["test1"; "test2"; "eee"]} ]; mine_no_nl; their_no_nl} in diff, Some hunk @@ -623,8 +623,8 @@ eee|}^(if their_no_nl then "" else "\n") let diff = Patch.diff (Edit ("a", "b")) (Some a) (Some b) in let hunk = { Patch.operation = Edit ("a", "b"); - hunks = [ { mine_start = 2; mine_len = 3; mine = ["ccc"; "ddd"; "eee"]; - their_start = 2; their_len = 2; their = ["test1"; "eee"]} ]; + hunks = [ { mine_start = 3; mine_len = 3; mine = ["ccc"; "ddd"; "eee"]; + their_start = 3; their_len = 2; their = ["test1"; "eee"]} ]; mine_no_nl; their_no_nl} in diff, Some hunk @@ -655,8 +655,8 @@ eee|}^(if their_no_nl then "" else "\n") let diff = Patch.diff (Edit ("a", "b")) (Some a) (Some b) in let hunk = { Patch.operation = Edit ("a", "b"); - hunks = [ { mine_start = 0; mine_len = 5; mine = ["aaa"; "bbb"; "ccc"; "ddd"; "eee"]; - their_start = 0; their_len = 5; their = ["test1"; "bbb"; "ccc"; "ddd"; "eee"]} ]; + hunks = [ { mine_start = 1; mine_len = 5; mine = ["aaa"; "bbb"; "ccc"; "ddd"; "eee"]; + their_start = 1; their_len = 5; their = ["test1"; "bbb"; "ccc"; "ddd"; "eee"]} ]; mine_no_nl; their_no_nl} in diff, Some hunk @@ -686,8 +686,8 @@ eee|}^(if their_no_nl then "" else "\n") let diff = Patch.diff (Edit ("a", "b")) (Some a) (Some b) in let hunk = { Patch.operation = Edit ("a", "b"); - hunks = [ { mine_start = 0; mine_len = 5; mine = ["aaa"; "bbb"; "ccc"; "ddd"; "eee"]; - their_start = 0; their_len = 4; their = ["test1"; "ccc"; "ddd"; "eee"]} ]; + hunks = [ { mine_start = 1; mine_len = 5; mine = ["aaa"; "bbb"; "ccc"; "ddd"; "eee"]; + their_start = 1; their_len = 4; their = ["test1"; "ccc"; "ddd"; "eee"]} ]; mine_no_nl; their_no_nl} in diff, Some hunk @@ -718,8 +718,8 @@ test1|}^(if their_no_nl then "" else "\n") let diff = Patch.diff (Edit ("a", "b")) (Some a) (Some b) in let hunk = { Patch.operation = Edit ("a", "b"); - hunks = [ { mine_start = 4; mine_len = 1; mine = ["eee"]; - their_start = 4; their_len = 1; their = ["test1"]} ]; + hunks = [ { mine_start = 5; mine_len = 1; mine = ["eee"]; + their_start = 5; their_len = 1; their = ["test1"]} ]; mine_no_nl; their_no_nl} in diff, Some hunk @@ -749,8 +749,8 @@ test1|}^(if their_no_nl then "" else "\n") let diff = Patch.diff (Edit ("a", "b")) (Some a) (Some b) in let hunk = { Patch.operation = Edit ("a", "b"); - hunks = [ { mine_start = 3; mine_len = 2; mine = ["ddd"; "eee"]; - their_start = 3; their_len = 1; their = ["test1"]} ]; + hunks = [ { mine_start = 4; mine_len = 2; mine = ["ddd"; "eee"]; + their_start = 4; their_len = 1; their = ["test1"]} ]; mine_no_nl; their_no_nl} in diff, Some hunk @@ -824,8 +824,8 @@ let unified_diff_spaces = let git_diff_spaces = {|\ diff --git a/foo bar b/foo bar index ef00db3..88adca3 100644 ---- a/foo bar -+++ b/foo bar +--- a/foo bar ++++ b/foo bar @@ -1 +1 @@ -This is wrong. +This is right. @@ -843,7 +843,7 @@ let busybox_diff_spaces = {|\ |} let busybox_diff_spaces = - operations [Patch.Edit ("a/foo bar", "b/foo bar")] busybox_diff_spaces + operations [Patch.Edit ("a/foo", "b/foo")] busybox_diff_spaces let unified_diff_quotes = {|\ --- "foo bar \"baz\"" 2024-09-27 11:09:48.325541553 +0200 @@ -878,7 +878,7 @@ let busybox_diff_quotes = {|\ |} let busybox_diff_quotes = - operations [Patch.Edit ({|foo bar "baz"|}, {|"foo" bar baz|})] busybox_diff_quotes + operations [Patch.Edit ({|foo|}, {|foo|})] busybox_diff_quotes let dev_null_like = {|\ --- /dev/null_but_actually_not