Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package ocaml-patch for openSUSE:Factory checked in at 2025-11-12 21:15:49 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/ocaml-patch (Old) and /work/SRC/openSUSE:Factory/.ocaml-patch.new.1980 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "ocaml-patch" Wed Nov 12 21:15:49 2025 rev:2 rq:1317345 version:3.1.0 Changes: -------- --- /work/SRC/openSUSE:Factory/ocaml-patch/ocaml-patch.changes 2025-07-18 16:00:59.928655811 +0200 +++ /work/SRC/openSUSE:Factory/.ocaml-patch.new.1980/ocaml-patch.changes 2025-11-12 21:16:38.719812582 +0100 @@ -1,0 +2,6 @@ +Tue Nov 11 11:11:11 UTC 2025 - [email protected] + +- Update to version 3.1.0 + see included CHANGES.md file for details + +------------------------------------------------------------------- Old: ---- ocaml-patch-3.0.0.tar.xz New: ---- ocaml-patch-3.1.0.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ ocaml-patch.spec ++++++ --- /var/tmp/diff_new_pack.st2ady/_old 2025-11-12 21:16:39.451843267 +0100 +++ /var/tmp/diff_new_pack.st2ady/_new 2025-11-12 21:16:39.455843435 +0100 @@ -1,7 +1,7 @@ # # spec file for package ocaml-patch # -# Copyright (c) 2025 SUSE LLC +# Copyright (c) 2025 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -32,7 +32,7 @@ %define pkg ocaml-patch Name: %pkg%nsuffix -Version: 3.0.0 +Version: 3.1.0 Release: 0 %{?ocaml_preserve_bytecode} Summary: Patch library purely in OCaml @@ -67,7 +67,7 @@ %autosetup -p1 -n %pkg-%version %build -dune_release_pkgs='patch' +dune_release_pkgs='patch,opatch' %ocaml_dune_setup %if "%build_flavor" == "" %ocaml_dune_build @@ -86,6 +86,7 @@ %if "%build_flavor" == "" %files -f %name.files +%_bindir/* %files devel -f %name.files.devel %endif ++++++ _service ++++++ --- /var/tmp/diff_new_pack.st2ady/_old 2025-11-12 21:16:39.507845614 +0100 +++ /var/tmp/diff_new_pack.st2ady/_new 2025-11-12 21:16:39.511845783 +0100 @@ -1,7 +1,7 @@ <services> <service name="tar_scm" mode="manual"> <param name="filename">ocaml-patch</param> - <param name="revision">76fb487c5f2f194c65a79b310c89fc91ee416f21</param> + <param name="revision">59a318563d745e63fc69f3d58a8a8ff302e91c4b</param> <param name="scm">git</param> <param name="submodules">disable</param> <param name="url">https://github.com/hannesm/patch.git</param> ++++++ ocaml-patch-3.0.0.tar.xz -> ocaml-patch-3.1.0.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ocaml-patch-3.0.0/.github/workflows/main.yml new/ocaml-patch-3.1.0/.github/workflows/main.yml --- old/ocaml-patch-3.0.0/.github/workflows/main.yml 2025-07-03 16:22:36.000000000 +0200 +++ new/ocaml-patch-3.1.0/.github/workflows/main.yml 2025-11-06 09:12:52.000000000 +0100 @@ -13,25 +13,25 @@ fail-fast: false matrix: os: - - macos-latest - ubuntu-latest - - windows-latest ocaml-compiler: + - 4.08.x - 4.14.x + - 5.4.x runs-on: ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v5 - name: Use OCaml ${{ matrix.ocaml-compiler }} - uses: ocaml/setup-ocaml@v2 + uses: ocaml/setup-ocaml@v3 with: ocaml-compiler: ${{ matrix.ocaml-compiler }} - - run: opam install . --deps-only --with-test + - run: opam install ./patch.opam --deps-only --with-test - - run: opam exec -- dune build + - run: opam exec -- dune build -p patch - - run: opam exec -- dune runtest + - run: opam exec -- dune runtest -p patch diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ocaml-patch-3.0.0/CHANGES.md new/ocaml-patch-3.1.0/CHANGES.md --- old/ocaml-patch-3.0.0/CHANGES.md 2025-07-03 16:22:36.000000000 +0200 +++ new/ocaml-patch-3.1.0/CHANGES.md 2025-11-06 09:12:52.000000000 +0100 @@ -1,3 +1,12 @@ +## v3.1.0 (2025-11-06) + +* Use a rope data structure instead of lists. Improves performance especially + with many hunks (#37 @hannesm, review by @shym @kit-ty-kate) + +## v3.0.1 (2025-10-15) + +* Only a release of opatch (#35 @shym @kit-ty-kate) + ## v3.0.0 (2025-07-03) * No change since 3.0.0~beta1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ocaml-patch-3.0.0/README.md new/ocaml-patch-3.1.0/README.md --- old/ocaml-patch-3.0.0/README.md 2025-07-03 16:22:36.000000000 +0200 +++ new/ocaml-patch-3.1.0/README.md 2025-11-06 09:12:52.000000000 +0100 @@ -3,7 +3,7 @@ The loosely specified `diff` file format is widely used for transmitting differences of line-based information. The motivating example is [`opam`](https://opam.ocaml.org), which is able to validate updates being -cryptographically signed (e.g. [conex](https://github.com/hannesm/conex)) by +cryptographically signed (e.g. [conex](https://github.com/robur-coop/conex)) by providing a unified diff. The [test-based infered specification](https://www.artima.com/weblogs/viewpost.jsp?thread=164293) @@ -50,11 +50,16 @@ to 1. NB from practical experiments, only "+1" and "-1" are supported. ```OCaml +type git_ext = + | Rename_only of string * string + | Delete_only + | Create_only + type operation = | Edit of string * string | Delete of string | Create of string - | Rename_only of string * string + | Git_ext of (string * string * git_ext) type hunk (* positions and contents *) @@ -73,8 +78,7 @@ The function `patch` assumes that the patch applies cleanly, and does not check this assumption. Exceptions may be raised if this assumption is violated. -The git diff format allows further features, such as file permissions, and also -a "copy from / to" header, which I was unable to spot in the wild. +The git diff format allows further features, such as file permissions. ## Installation diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ocaml-patch-3.0.0/bin/dune new/ocaml-patch-3.1.0/bin/dune --- old/ocaml-patch-3.0.0/bin/dune 1970-01-01 01:00:00.000000000 +0100 +++ new/ocaml-patch-3.1.0/bin/dune 2025-11-06 09:12:52.000000000 +0100 @@ -0,0 +1,9 @@ +(executable + (name opatch) + (public_name opatch) + (package opatch) + (modules opatch version) + (libraries unix patch)) + +(rule + (write-file version.ml "let v = \"%{version:opatch}\"")) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ocaml-patch-3.0.0/bin/opatch.ml new/ocaml-patch-3.1.0/bin/opatch.ml --- old/ocaml-patch-3.0.0/bin/opatch.ml 1970-01-01 01:00:00.000000000 +0100 +++ new/ocaml-patch-3.1.0/bin/opatch.ml 2025-11-06 09:12:52.000000000 +0100 @@ -0,0 +1,200 @@ +(* Copyright 2024-2025 Kate Deplaix *) +(* Copyright 2025 Samuel Hym, Tarides *) +(* SPDX-License-Identifier: ISC *) + +let errf fmt = Printf.ksprintf invalid_arg fmt +let read path = In_channel.(with_open_bin path input_all) + +let write ~create path content = + let flags = + Open_wronly :: Open_binary + :: (if create then [ Open_creat; Open_excl ] else [ Open_trunc ]) + in + Out_channel.( + with_open_gen flags 0o666 path (fun oc -> output_string oc content)) + +let remove ~bound path = + let bound = Filename.concat bound "" in + let rec rec_rmdir p = + try + let p = Filename.dirname p in + if String.starts_with ~prefix:bound p then ( + Sys.rmdir p; + rec_rmdir p) + with _ -> () + in + Unix.unlink path; + (* FIXME? Opam's original code makes extra efforts to unlink on Windows if the + first attempt fails (namely it retries after enabling read&write access); + is that necessary here? *) + rec_rmdir path + +let patch ~force ~patchname content diff = + (* NOTE: The None case returned by [Patch.patch] is only returned + if [diff = Patch.Delete _]. This sub-function is not called in + this case so we [assert false] instead. *) + match Patch.patch ~cleanly:true content diff with + | Some x -> x + | None -> assert false (* See NOTE above *) + | exception _ when not force -> + errf "Patch %S does not apply cleanly" patchname + | exception _ -> ( + match Patch.patch ~cleanly:false content diff with + | Some x -> x + | None -> assert false (* See NOTE above *) + | exception _ -> errf "Patch %S does not apply" patchname) + +let apply ~force ~dir ~patchname diffs = + let ( / ) = Filename.concat in + (* NOTE: It is important to keep this `concat dir ""` to ensure the + is_prefix_of below doesn't match another similarly named directory *) + let basedir = Unix.realpath dir in + let dir = basedir / "" in + let in_scope orig_path path = + if (not (String.starts_with ~prefix:dir path)) && basedir <> path then + errf "Patch tried to escape its scope to reach %S, out of %S (%S)" + orig_path dir path + in + let fullpath path = + if not (Filename.is_relative path) then errf "Path %S is not relative" path; + dir / path + and check_and_mkdir path = + let rec aux d = + if Sys.file_exists d then in_scope path (Unix.realpath d) + else ( + aux (Filename.dirname d); + if Sys.file_exists d then + (* Note d could already exists if the path is abc/.. and the recursive + call just created abc *) + in_scope path (Unix.realpath d) + else Sys.mkdir d 0o777) + in + aux path + in + let get_src path = + let fpath = fullpath path in + if not (Sys.file_exists fpath) then errf "File %S doesn't exist" path; + let fpath = Unix.realpath fpath in + in_scope path fpath; + fpath + and get_dst ~create path = + let fpath = fullpath path in + if create then ( + if Sys.file_exists fpath then errf "File %S exists" path + else + let d = Filename.dirname fpath in + check_and_mkdir d; + Unix.realpath d / Filename.basename fpath) + else Unix.realpath fpath + in + let apply diff = + match diff.Patch.operation with + | Patch.Edit (src, dst) -> + let create = src <> dst in + let src = get_src src in + (* see note about [Edit] operations below *) + let content = read src in + let content = patch ~force ~patchname (Some content) diff in + let dst = get_dst ~create dst in + write ~create dst content; + if create then remove ~bound:dir src + | Patch.Delete file | Patch.Git_ext (file, _, Patch.Delete_only) -> + let file = get_src file in + remove ~bound:dir file + | Patch.Create file | Patch.Git_ext (_, file, Patch.Create_only) -> + let content = patch ~force ~patchname None diff in + let file = get_dst ~create:true file in + write ~create:true file content + | Patch.Git_ext (_, _, Patch.Rename_only (src, dst)) -> + assert (src <> dst); + let src = get_src src in + let dst = get_dst ~create:true dst in + (* see note about [Rename_only] operations below *) + Unix.rename src dst; + remove ~bound:dir src + in + List.iter apply diffs + +(* NOTE: About [Edit] operations + Opam's original code to apply patches can deal with [Edit] operations where + the source doesn't exist but the destination does: in that case it patches + the destination directly, mimicking GNU patch. That behaviour is not accepted + by [get_src], which errors out on non-existing files. My intuition here is + that such a patch is erroneous in the first place. opatch is a different + position compared to opam: where opam replaced GNU patch with internal + patching code, opatch could depart from GNU patch on such weird cases. *) + +(* NOTE: About [Rename_only] operations + The way [Rename_only] operations are handled will fail on a patch that moves + [x] into [x/y], aka when the original file name is becoming a directory on + the fly ([get_dst] will try to create the directory while the original file + is still in place). While [git apply] can handle such patches, GNU patch + rejects them, so we probably can fail too. *) + +let apply ~dir ~strip patch = + let patchname, content = + let open In_channel in + match patch with + | None -> ("-", input_all stdin) + | Some path -> (path, with_open_bin path input_all) + in + let diffs = Patch.parse ~p:strip content in + apply ~force:false ~dir ~patchname diffs + +let parse_argv version argv = + let open Arg in + let strip = ref 1 + and dir = ref "." + and patches = ref [] + and verbose = ref false in + let add_patch special = function + | "-" when special -> patches := None :: !patches + | x -> patches := Some x :: !patches + and show_version () = + Printf.printf "opatch %s\n" version; + exit 0 + in + let specs = + [ + ( "-p", + Arg.Set_int strip, + "<NUM> Strip <NUM> directories from the diff paths (default: 1)" ); + ( "-C", + Arg.Set_string dir, + "<DIR> Locate files to patch as if launched in <DIR> instead of ." ); + ( "-v", + Arg.Set verbose, + " Set verbose mode, where applied patches are logged" ); + ("-version", Arg.Unit show_version, " Print version and exit"); + ("--version", Arg.Unit show_version, " Print version and exit"); + ( "--", + Arg.Rest (add_patch false), + " Process all remaining arguments as patches" ); + ] + and usage = "opatch [-C <DIR>] [-p <NUM>] [PATCH...]: apply a diff file" in + try + parse_argv ~current:(ref 0) argv specs (add_patch true) usage; + ( !strip, + !dir, + (match !patches with [] -> [ None ] | p -> List.rev p), + !verbose ) + with + | Help msg -> + Printf.printf "%s" msg; + exit 0 + | Bad msg -> + Printf.eprintf "%s" msg; + exit 1 + +let main version = + let strip, dir, patches, verbose = parse_argv version Sys.argv in + try + List.iter + (fun patch -> + apply ~dir ~strip patch; + if verbose then + Printf.printf "%S applied.\n%!" (Option.value ~default:"-" patch)) + patches + with Invalid_argument msg -> Printf.eprintf "Fatal error: %s\n" msg + +let () = main Version.v diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ocaml-patch-3.0.0/opatch.opam new/ocaml-patch-3.1.0/opatch.opam --- old/ocaml-patch-3.0.0/opatch.opam 1970-01-01 01:00:00.000000000 +0100 +++ new/ocaml-patch-3.1.0/opatch.opam 2025-11-06 09:12:52.000000000 +0100 @@ -0,0 +1,32 @@ +opam-version: "2.0" +synopsis: "Pure OCaml command-line tool to apply a patch" +description: + "Command-line tool to apply a unified diff or git-diff to a directory" +maintainer: ["Samuel Hym <[email protected]>"] +authors: [ + "Kate Deplaix <[email protected]>" "Samuel Hym <[email protected]>" +] +license: "ISC" +homepage: "https://github.com/hannesm/patch" +bug-reports: "https://github.com/hannesm/patch/issues" +depends: [ + "dune" {>= "3.0"} + "ocaml" {>= "4.14"} + "patch" {= version} + "odoc" {with-doc} +] +build: [ + ["dune" "subst"] {dev} + [ + "dune" + "build" + "-p" + name + "-j" + jobs + "@install" + "@doc" {with-doc} + ] +] +dev-repo: "git+https://github.com/hannesm/patch.git" +x-maintenance-intent: [ "(latest)" ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ocaml-patch-3.0.0/src/dune new/ocaml-patch-3.1.0/src/dune --- old/ocaml-patch-3.0.0/src/dune 2025-07-03 16:22:36.000000000 +0200 +++ new/ocaml-patch-3.1.0/src/dune 2025-11-06 09:12:52.000000000 +0100 @@ -2,7 +2,7 @@ (name patch) (synopsis "Patch purely in OCaml") (public_name patch) - (modules patch lib fname)) + (modules patch lib fname rope)) (executable (name patch_command) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ocaml-patch-3.0.0/src/lib.ml new/ocaml-patch-3.1.0/src/lib.ml --- old/ocaml-patch-3.0.0/src/lib.ml 2025-07-03 16:22:36.000000000 +0200 +++ new/ocaml-patch-3.1.0/src/lib.ml 2025-11-06 09:12:52.000000000 +0100 @@ -57,12 +57,4 @@ | [] -> invalid_arg "List.last" | [x] -> x | _::xs -> last xs - - let rev_cut idx l = - let rec aux acc idx = function - | l when idx = 0 -> (acc, l) - | [] -> invalid_arg "List.cut" - | x::xs -> aux (x :: acc) (idx - 1) xs - in - aux [] idx l end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ocaml-patch-3.0.0/src/lib.mli new/ocaml-patch-3.1.0/src/lib.mli --- old/ocaml-patch-3.0.0/src/lib.mli 2025-07-03 16:22:36.000000000 +0200 +++ new/ocaml-patch-3.1.0/src/lib.mli 2025-11-06 09:12:52.000000000 +0100 @@ -9,5 +9,4 @@ module List : sig val last : 'a list -> 'a - val rev_cut : int -> 'a list -> 'a list * 'a list end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ocaml-patch-3.0.0/src/patch.ml new/ocaml-patch-3.1.0/src/patch.ml --- old/ocaml-patch-3.0.0/src/patch.ml 2025-07-03 16:22:36.000000000 +0200 +++ new/ocaml-patch-3.1.0/src/patch.ml 2025-11-06 09:12:52.000000000 +0100 @@ -36,27 +36,30 @@ hunk.mine_start hunk.mine_len hunk.their_start hunk.their_len (unified_diff ~mine_no_nl ~their_no_nl hunk) -let rec apply_hunk ~cleanly ~fuzz (last_matched_line, offset, lines) ({mine_start; mine_len; mine; their_start = _; their_len; their} as hunk) = +let rec apply_hunk ~cleanly ~fuzz (last_matched_line, offset, rope) ({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 rev_prefix, rest = Lib.List.rev_cut (Stdlib.max 0 (mine_start - 1)) lines in - let rev_actual_mine, suffix = Lib.List.rev_cut mine_len rest in - let actual_mine = List.rev rev_actual_mine in - if actual_mine <> (mine : string list) then - invalid_arg "unequal mine"; - (* TODO: should we check their_len against List.length their? *) + let off_mine = Stdlib.max 0 (mine_start - 1) in + let prefix = Rope.chop rope off_mine in + let actual_mine = Rope.chop rope ~off:off_mine mine_len in + let off = off_mine + mine_len in + let suffix = Rope.shift rope off in + if not (Rope.equal_to_string_list actual_mine mine) then + invalid_arg "unequal mine"; + let theirs = + let nl = Rope.last_is_nl actual_mine in + Rope.of_strings their nl + in (mine_start + mine_len, offset + (their_len - mine_len), - (* TODO: Replace rev_append (rev ...) by the tail-rec when patch - requires OCaml >= 4.14 *) - List.rev_append rev_prefix (List.rev_append (List.rev their) suffix)) + Rope.concat prefix (Rope.concat theirs suffix)) in 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_pos_offset = Stdlib.max 0 (Rope.length rope - 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 = @@ -100,7 +103,7 @@ 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 + apply_hunk ~cleanly ~fuzz:(fuzz + 1) (last_matched_line, offset, rope) hunk else invalid_arg "apply_hunk" else @@ -476,9 +479,9 @@ | _ -> assert false end | Edit _ -> - let old = match filedata with None -> [] | Some x -> to_lines x in - let _, _, lines = List.fold_left (apply_hunk ~cleanly ~fuzz:0) (0, 0, old) diff.hunks in - let lines = String.concat "\n" lines in + let old = match filedata with None -> Rope.empty | Some x -> Rope.of_string x in + let _, _, rope = List.fold_left (apply_hunk ~cleanly ~fuzz:0) (0, 0, old) diff.hunks in + let lines = Rope.to_string rope in let lines = match diff.mine_no_nl, diff.their_no_nl with | false, true -> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ocaml-patch-3.0.0/src/rope.ml new/ocaml-patch-3.1.0/src/rope.ml --- old/ocaml-patch-3.0.0/src/rope.ml 1970-01-01 01:00:00.000000000 +0100 +++ new/ocaml-patch-3.1.0/src/rope.ml 2025-11-06 09:12:52.000000000 +0100 @@ -0,0 +1,117 @@ +(* Originally from https://github.com/robur-coop/utcp, written by + Calascibetta Romain <[email protected]> *) + +(* A rope data structure where each node is a line *) + +type t = + | Str of string array * bool * int * int + | App of t * t * int + +let length = function + | Str (_, _, len, _) -> len + | App (_, _, len) -> len + +(* keep compatibility with 4.08 *) +let min_int (a : int) (b : int) = min a b +external unsafe_blit_string : string -> int -> bytes -> int -> int -> unit + = "caml_blit_string" [@@noalloc] + +let append t1 t2 = + App (t1, t2, length t1 + length t2) + +let empty = Str (Array.make 0 "", true, 0, 0) + +let rec unsafe_sub t start stop = + if start = 0 && Int.equal stop (length t) then + t + else if Int.equal start stop then + empty + else match t with + | Str (data, nl, len, off) -> + assert (stop <= (len : int)); + Str (data, nl, stop - start, off + start) + | App (l, r, _) -> + let len = length l in + if stop <= (len : int) then unsafe_sub l start stop + else if start >= (len : int) then unsafe_sub r (start - len) (stop - len) + else append (unsafe_sub l start len) (unsafe_sub r 0 (stop - len)) + +let chop t ?(off = 0) len = + if len < 0 || len > (length t - off : int) then + invalid_arg "Rope.chop"; + if len = 0 then empty else unsafe_sub t off (off + len) + +let shift t len = + if len < 0 then + invalid_arg "Rope.shift"; + if len = 0 then + t + else + let max = length t in + let len = min_int max len in + let l = len + (max - len) in + unsafe_sub t len l + +let rec last_is_nl = function + | Str (a, nl, len, off) -> if Int.equal (Array.length a - off) len then nl else true + | App (_, r, _) -> last_is_nl r + +let rec byte_length = function + | Str (s, _, len, off) as a -> + let sum = ref 0 in + for idx = off to len + off - 1 do + let data = Array.unsafe_get s idx in + sum := !sum + String.length data + 1 + done; + !sum - if last_is_nl a then 0 else 1 + | App (l, r, _) -> byte_length l + byte_length r + +let rec into_bytes buf dst_off = function + | Str (s, _, len, off) as a -> + let off' = ref dst_off in + for idx = off to len + off - 1 do + let data = Array.unsafe_get s idx in + unsafe_blit_string data 0 buf !off' (String.length data); + off' := !off' + String.length data + 1; + if idx - off < (len - 1 : int) || (Int.equal (idx - off) (len - 1) && last_is_nl a) then + Bytes.unsafe_set buf (!off' - 1) '\n' + done + | App (l, r, _) -> + into_bytes buf dst_off l; + into_bytes buf (dst_off + byte_length l) r + +let to_string t = + let len = byte_length t in + let buf = Bytes.create len in + into_bytes buf 0 t; + Bytes.unsafe_to_string buf + +let concat a b = append a b + +let of_strings xs last_is_nl = + let d = Array.of_list xs in + Str (d, last_is_nl, Array.length d, 0) + +let of_string str = + let splitted = String.split_on_char '\n' str in + let last_is_nl = String.unsafe_get str (String.length str - 1) = '\n' in + let d = Array.of_list splitted in + Str (d, last_is_nl, Array.length d - (if last_is_nl then 1 else 0), 0) + +let rec equal_to_string_list t = function + | [] -> length t = 0 + | hd :: tl -> + let rec find_data = function + | Str (data, _, len, off) -> + if len > 0 then Some (Array.get data off) else None + | App (l, r, _) -> + if length l > 0 then + find_data l + else + find_data r + in + match find_data t with + | None -> false + | Some data -> + String.equal hd data && + equal_to_string_list (shift t 1) tl diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ocaml-patch-3.0.0/src/rope.mli new/ocaml-patch-3.1.0/src/rope.mli --- old/ocaml-patch-3.0.0/src/rope.mli 1970-01-01 01:00:00.000000000 +0100 +++ new/ocaml-patch-3.1.0/src/rope.mli 2025-11-06 09:12:52.000000000 +0100 @@ -0,0 +1,36 @@ +type t (** The type for a rope data structure *) + +val length : t -> int +(** [length t] returns the amount of strings in [t]. *) + +val empty : t +(** [empty] is the empty rope. *) + +val of_strings : string list -> bool -> t +(** [of_strings xs nl] is a rope [t] which contains the strings of [xs]. If + [nl] is true, the last string will have a newline, otherwise not. *) + +val of_string : string -> t +(** [of_string str] will split the string [str] on newline, and return a rope. *) + +val to_string : t -> string +(** [to_string t] is the string where the contents of [t] is present. *) + +val chop : t -> ?off:int -> int -> t +(** [chop t ~off len] returns a new rope that contains [len] strings starting + at [off] of the provided rope [t]. Raises Invalid_argument if [len] and + [off] are not inside the bounds. *) + +val shift : t -> int -> t +(** [shift t len] returns a new rope that does not contain the first [len] + strings, but only the remaining strings of [t]. *) + +val concat : t -> t -> t +(** [concat t t'] returns a new rope which contains [t] followed by [t']. *) + +val last_is_nl : t -> bool +(** [last_is_nl t] returns [true] if the last string should have a newline. *) + +val equal_to_string_list : t -> string list -> bool +(** [equal_to_string_list t xs] returns [true] if the content of [t] is equal to + the content of [xs]. *) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ocaml-patch-3.0.0/test/opam-repository/test.sh new/ocaml-patch-3.1.0/test/opam-repository/test.sh --- old/ocaml-patch-3.0.0/test/opam-repository/test.sh 2025-07-03 16:22:36.000000000 +0200 +++ new/ocaml-patch-3.1.0/test/opam-repository/test.sh 2025-11-06 09:12:52.000000000 +0100 @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash set -e set -u @@ -24,13 +24,13 @@ rm -rf ./opam-repository git clone --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 +grep -rl '^patches:' . | grep '/opam$' | cut -d/ -f3 >> ../../pkgs-with-patches popd > /dev/null rm -rf ./opam-repository-archive git clone --depth=1 https://github.com/ocaml/opam-repository-archive.git pushd ./opam-repository-archive/packages > /dev/null -grep -l '^patches:' */*/opam | cut -d/ -f2 >> ../../pkgs-with-patches +grep -rl '^patches:' . | grep '/opam$' | cut -d/ -f3 >> ../../pkgs-with-patches popd > /dev/null NB_OF_PKGS=$(cat ./pkgs-with-patches | wc -l) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ocaml-patch-3.0.0/test/test.ml new/ocaml-patch-3.1.0/test/test.ml --- old/ocaml-patch-3.0.0/test/test.ml 2025-07-03 16:22:36.000000000 +0200 +++ new/ocaml-patch-3.1.0/test/test.ml 2025-11-06 09:12:52.000000000 +0100 @@ -1101,12 +1101,24 @@ Alcotest.(check string) __LOC__ expected (Option.get actual) | None, _, _ | _, None, _ | _, _, None -> Alcotest.skip () +let many_hunks_old = lazy (opt_read "./external/many-hunks.old") +let many_hunks_new = lazy (opt_read "./external/many-hunks.new") +let many_hunks_diff = lazy (opt_read "./external/many-hunks.diff") +let many_hunks_apply () = + match Lazy.force many_hunks_old, Lazy.force many_hunks_new, Lazy.force many_hunks_diff with + | Some many_hunks_old, Some expected, Some diff -> + let patch = Patch.parse ~p:0 diff in + let actual = Patch.patch ~cleanly:true (Some many_hunks_old) (List.hd patch) in + Alcotest.(check string) __LOC__ expected (Option.get actual) + | None, _, _ | _, None, _ | _, _, None -> Alcotest.skip () + let big_diff = [ "parse", `Quick, parse_big; "print", `Quick, print_big; "parse own", `Quick, parse_own; "1_000_000 print", `Quick, one_mil_print; "1_000_000 apply", `Quick, one_mil_apply; + "many-hunks apply", `Quick, many_hunks_apply; ] let print_diff_mine_empty_their_no_nl () =
