Source file compare_core.ml
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
open! Core
open! Import
let lines_of_contents contents =
let lines = Array.of_list (String.split_lines contents) in
let has_trailing_newline =
let length = String.length contents in
if length = 0 || Char.equal contents.[length - 1] '\n'
then `With_trailing_newline
else `Missing_trailing_newline
in
lines, has_trailing_newline
;;
let%test_unit _ =
let test contents ~expect =
[%test_result: string array * [ `With_trailing_newline | `Missing_trailing_newline ]]
(lines_of_contents contents)
~expect
in
test "" ~expect:([||], `With_trailing_newline);
test "hello" ~expect:([| "hello" |], `Missing_trailing_newline);
test "hello\nworld" ~expect:([| "hello"; "world" |], `Missing_trailing_newline);
test "hello\nworld\n" ~expect:([| "hello"; "world" |], `With_trailing_newline)
;;
let convert_to_patch_compatible_hunks ?prev_diff ?next_diff lines_prev lines_next hunks =
let open Diff_input in
let open Patience_diff.Hunk in
let open Patience_diff.Range in
if Option.is_none prev_diff || Option.is_none next_diff then
()
else
let prev = Option.value_exn prev_diff in
let next = Option.value_exn next_diff in
let has_trailing_newline text =
try String.contains ~pos:(String.length text - 1) text '\n' with _ -> false
in
let prev_file_no_trailing_newline = not (has_trailing_newline prev.text) in
let next_file_no_trailing_newline = not (has_trailing_newline next.text) in
if prev_file_no_trailing_newline || next_file_no_trailing_newline then
List.iter hunks ~f:(fun hunk ->
if
Array.length lines_prev = (hunk.prev_start + hunk.prev_size - 1)
|| Array.length lines_next = (hunk.next_start + hunk.next_size - 1)
then
let correction = "\n\\ No newline at end of file" in
List.last hunk.ranges
|> function
| Some Same a ->
let prev, next = Array.get a (Array.length a - 1) in
Array.set a (Array.length a - 1) (prev^correction, next^correction)
| Some Prev a when prev_file_no_trailing_newline ->
let prev = Array.get a (Array.length a - 1) in
Array.set a (Array.length a - 1) (prev^correction)
| Some Next a when next_file_no_trailing_newline ->
let next = Array.get a (Array.length a - 1) in
Array.set a (Array.length a - 1) (next^correction)
| Some Unified a ->
let unified = Array.get a (Array.length a - 1) in
Array.set a (Array.length a - 1) (unified^correction)
| Some Replace (a1, a2) ->
if prev_file_no_trailing_newline then
(
let prev = Array.get a1 (Array.length a1 - 1) in
Array.set a1 (Array.length a1 - 1) (prev^correction)
);
if next_file_no_trailing_newline then
(
let next = Array.get a2 (Array.length a2 - 1) in
Array.set a2 (Array.length a2 - 1) (next^correction)
)
| _ -> ())
;;
let compare_lines (config : Configuration.t) ?prev_diff ?next_diff ~prev ~next () =
let context = config.context in
let keep_ws = config.keep_ws in
let split_long_lines = config.split_long_lines in
let line_big_enough = config.line_big_enough in
let hunks =
let transform = if keep_ws then Fn.id else Patdiff_core.remove_ws in
match config.ext_cmp with
| None ->
Patience_diff.String.get_hunks
~transform
~context
~big_enough:line_big_enough
~prev
~next
| Some prog ->
let compare x y =
let cmd = sprintf "%s %S %S" prog x y in
match Unix.system cmd with
| Ok () -> 0
| Error (`Exit_non_zero 1) -> 1
| Error _ -> failwithf "External compare %S failed!" prog ()
in
let module P =
Patience_diff.Make (struct
type t = string [@@deriving sexp]
let hash = String.hash
let compare = compare
end)
in
P.get_hunks ~transform ~context ~big_enough:line_big_enough ~prev ~next
in
let hunks =
match config.float_tolerance with
| None -> hunks
| Some tolerance -> Float_tolerance.apply hunks tolerance ~context
in
if config.unrefined
then
(
convert_to_patch_compatible_hunks ?prev_diff ?next_diff prev next hunks;
Patience_diff.Hunks.unified hunks
)
else (
let rules = config.rules in
let output = config.output in
let produce_unified_lines = config.produce_unified_lines in
let interleave = config.interleave in
let word_big_enough = config.word_big_enough in
Patdiff_core.refine
~rules
~output
~keep_ws
~produce_unified_lines
~split_long_lines
~interleave
hunks
~word_big_enough)
;;
let warn_if_no_trailing_newline
~warn_if_no_trailing_newline_in_both
(prev_file_newline, prev_file)
(next_file_newline, next_file)
=
let warn = eprintf "No newline at the end of %s\n%!" in
match prev_file_newline, next_file_newline with
| `With_trailing_newline, `With_trailing_newline -> ()
| `With_trailing_newline, `Missing_trailing_newline -> warn next_file
| `Missing_trailing_newline, `With_trailing_newline -> warn prev_file
| `Missing_trailing_newline, `Missing_trailing_newline ->
if warn_if_no_trailing_newline_in_both
then (
warn prev_file;
warn next_file)
;;
let compare_files (config : Configuration.t) ~prev_file ~next_file =
let prev = In_channel.read_all (File_name.real_name_exn prev_file) in
let next = In_channel.read_all (File_name.real_name_exn next_file) in
Comparison_result.create
config
~prev:{ name = File_name.display_name prev_file; text = prev }
~next:{ name = File_name.display_name next_file; text = next }
~compare_assuming_text:(fun config ~prev ~next ->
let prev_lines, prev_file_newline = lines_of_contents prev.text in
let next_lines, next_file_newline = lines_of_contents next.text in
warn_if_no_trailing_newline
(prev_file_newline, prev.name)
(next_file_newline, next.name)
~warn_if_no_trailing_newline_in_both:config.warn_if_no_trailing_newline_in_both;
compare_lines config ~prev:prev_lines ~next:next_lines ())
;;
let binary_different_message
~(config : Configuration.t)
~prev_file
~prev_is_binary
~next_file
~next_is_binary
=
match config.location_style with
| Diff | None ->
sprintf
!"Files %{File_name#hum}%s and %{File_name#hum}%s differ"
prev_file
(if prev_is_binary then " (binary)" else "")
next_file
(if next_is_binary then " (binary)" else "")
| Omake ->
sprintf
!"%s\n File \"%{File_name#hum}\"\n binary files differ\n"
(Format.Location_style.omake_style_error_message_start
~file:(File_name.display_name prev_file)
~line:1)
next_file
;;
let print hunks ~file_names ~(config : Configuration.t) =
let prev_file, next_file = file_names in
if Comparison_result.has_no_diff hunks
then (
if config.double_check
then (
match
Unix.system
(sprintf
"cmp -s %s %s"
(Sys.quote (File_name.real_name_exn prev_file))
(Sys.quote (File_name.real_name_exn next_file)))
with
| Ok () -> ()
| Error (`Exit_non_zero 1) ->
printf "There are no differences except those filtered by your settings\n%!"
| Error _ -> ()))
else if
not config.quiet
then (
let output = config.output in
let rules = config.rules in
match hunks with
| Binary_same -> assert false
| Binary_different { prev_is_binary; next_is_binary } ->
Printf.printf
"%s\n"
(binary_different_message
~config
~prev_file
~prev_is_binary
~next_file
~next_is_binary)
| Hunks hunks ->
Patdiff_core.print
hunks
~file_names
~output
~rules
~location_style:config.location_style)
;;
let diff_files_internal (config : Configuration.t) ~prev_file ~next_file =
let hunks = compare_files ~prev_file ~next_file config in
print hunks ~file_names:(prev_file, next_file) ~config;
if Comparison_result.has_no_diff hunks then `Same else `Different
;;
let with_alt (config : Configuration.t) ~prev ~next : File_name.t * File_name.t =
( Real { real_name = prev; alt_name = config.prev_alt }
, Real { real_name = next; alt_name = config.next_alt } )
;;
let diff_files (config : Configuration.t) ~prev_file ~next_file =
let prev_file, next_file = with_alt config ~prev:prev_file ~next:next_file in
diff_files_internal config ~prev_file ~next_file
;;
let split_lines_preserve_slash_r =
let back_up_at_newline ~t:_ ~pos ~eol =
pos := !pos - 1;
eol := !pos + 1;
in
fun t ->
let n = String.length t in
if n = 0
then []
else
let pos = ref (n - 1) in
let eol = ref n in
let ac = ref [] in
if Char.equal t.[!pos] '\n' then back_up_at_newline ~t ~pos ~eol;
while !pos >= 0 do
if Char.( <> ) t.[!pos] '\n'
then decr pos
else
let start = !pos + 1 in
ac := String.sub t ~pos:start ~len:(!eol - start) :: !ac;
back_up_at_newline ~t ~pos ~eol
done;
String.sub t ~pos:0 ~len:!eol :: !ac
;;
let diff_strings
?
(config : Configuration.t)
~(prev : Diff_input.t)
~(next : Diff_input.t)
=
let lines { Diff_input.name = _; text } = split_lines_preserve_slash_r text |> Array.of_list in
let hunks =
Comparison_result.create
config
~prev
~next
~compare_assuming_text:(fun config ~prev ~next ->
let lines_prev = lines prev in
let lines_next = lines next in
compare_lines config ~prev_diff:prev ~next_diff:next ~prev:lines_prev ~next:lines_next ())
in
if Comparison_result.has_no_diff hunks
then `Same
else
`Different
(match hunks with
| Binary_same -> assert false
| Binary_different { prev_is_binary; next_is_binary } ->
binary_different_message
~config
~prev_file:(Fake prev.name)
~prev_is_binary
~next_file:(Fake next.name)
~next_is_binary
| Hunks hunks ->
Patdiff_core.output_to_string
hunks
?print_global_header
~file_names:(Fake prev.name, Fake next.name)
~output:config.output
~rules:config.rules
~location_style:config.location_style)
;;
let is_reg file =
match Unix.stat (File_name.real_name_exn file) with
| { st_kind = S_REG; _ } -> true
| _ -> false
;;
let is_dir file =
match Unix.stat (File_name.real_name_exn file) with
| { st_kind = S_DIR; _ } -> true
| _ -> false
;;
let rec diff_dirs_internal (config : Configuration.t) ~prev_dir ~next_dir ~file_filter =
assert (is_dir prev_dir);
assert (is_dir next_dir);
let set_of_dir dir =
let file_filter =
match file_filter with
| None -> Fn.const true
| Some file_filter -> file_filter
in
Sys.ls_dir (File_name.real_name_exn dir)
|> List.filter ~f:(fun x ->
let x = File_name.real_name_exn dir ^/ x in
match Unix.stat x with
| exception Unix.Unix_error (ENOENT, _, _) ->
false
| stats -> file_filter (x, stats))
|> String.Set.of_list
in
let prev_set = set_of_dir prev_dir in
let next_set = set_of_dir next_dir in
let union = Set.union prev_set next_set in
let prev_uniques = Set.diff union next_set in
let next_uniques = Set.diff union prev_set in
let handle_unique which file ~dir =
printf !"Only in %{File_name#hum}: %s\n%!" dir file;
if not config.mask_uniques
then (
let file = File_name.append dir file in
if is_reg file
then (
let diff = diff_files_internal config in
let null = File_name.dev_null in
match which with
| `Prev -> ignore (diff ~prev_file:file ~next_file:null : [ `Different | `Same ])
| `Next -> ignore (diff ~prev_file:null ~next_file:file : [ `Different | `Same ])))
in
Set.iter prev_uniques ~f:(handle_unique `Prev ~dir:prev_dir);
Set.iter next_uniques ~f:(handle_unique `Next ~dir:next_dir);
let inter = Set.inter prev_set next_set in
let exit_code = ref `Same in
let diff file =
let prev_file = File_name.append prev_dir file in
let next_file = File_name.append next_dir file in
if is_reg prev_file && is_reg next_file
then (
let hunks = compare_files ~prev_file ~next_file config in
if not (Comparison_result.has_no_diff hunks)
then (
exit_code := `Different;
match config.quiet with
| false -> print hunks ~file_names:(prev_file, next_file) ~config
| true ->
printf
!"Files %{File_name#hum} and %{File_name#hum} differ\n%!"
prev_file
next_file))
else if is_dir prev_file && is_dir next_file
then
if not config.shallow
then (
match
diff_dirs_internal ~prev_dir:prev_file ~next_dir:next_file config ~file_filter
with
| `Same -> ()
| `Different -> exit_code := `Different)
else
printf
!"Common subdirectories: %{File_name#hum} and %{File_name#hum}\n%!"
prev_file
next_file
else (
exit_code := `Different;
printf
!"Files %{File_name#hum} and %{File_name#hum} are not the same type\n%!"
prev_file
next_file)
in
Set.iter inter ~f:diff;
if Set.is_empty prev_uniques && Set.is_empty next_uniques
then !exit_code
else `Different
;;
let diff_dirs (config : Configuration.t) ~prev_dir ~next_dir ~file_filter =
let prev_dir, next_dir = with_alt config ~prev:prev_dir ~next:next_dir in
if not (is_dir prev_dir)
then
invalid_argf !"diff_dirs: prev_dir '%{File_name#hum}' is not a directory" prev_dir ();
if not (is_dir next_dir)
then
invalid_argf !"diff_dirs: next_dir '%{File_name#hum}' is not a directory" next_dir ();
diff_dirs_internal config ~prev_dir ~next_dir ~file_filter
;;