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 () =

Reply via email to