File: opatch.ml

package info (click to toggle)
ocaml-patch 3.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 384 kB
  • sloc: ml: 2,379; sh: 71; makefile: 3
file content (200 lines) | stat: -rw-r--r-- 7,239 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
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