[OCaml] Mobile-friendly clone of cgit.
Extract Diff algorithm into its own module
The pure diff logic (LCS, line_diff, hunks, types) is now in lib/diff.ml with no git dependencies. Resolvers.Diff includes it and adds the git integration layer (flatten_tree, read_file, of_commit).
Changed files
lib/diff.ml
@@ -0,0 +1,165 @@
1
Added:
(* -*- mode: tuareg; -*- *)
2
Added:
3
Added:
type line_kind = Context | Addition | Deletion
4
Added:
5
Added:
type line = {
6
Added:
kind : line_kind;
7
Added:
old_number : int option;
8
Added:
new_number : int option;
9
Added:
text : string;
10
Added:
}
11
Added:
12
Added:
type hunk = {
13
Added:
old_start : int;
14
Added:
old_count : int;
15
Added:
new_start : int;
16
Added:
new_count : int;
17
Added:
lines : line list;
18
Added:
}
19
Added:
20
Added:
type file = {
21
Added:
path : string;
22
Added:
old_hash : string option;
23
Added:
new_hash : string option;
24
Added:
old_mode : int option;
25
Added:
new_mode : int option;
26
Added:
binary : bool;
27
Added:
hunks : hunk list;
28
Added:
}
29
Added:
30
Added:
let split_lines content =
31
Added:
match String.split_on_char '\n' content with
32
Added:
| [] -> []
33
Added:
| lines ->
34
Added:
if content = "" then []
35
Added:
else if String.ends_with ~suffix:"\n" content then
36
Added:
List.rev lines |> List.tl |> List.rev
37
Added:
else lines
38
Added:
39
Added:
let line_diff old_content new_content =
40
Added:
let old_lines = Array.of_list (split_lines old_content) in
41
Added:
let new_lines = Array.of_list (split_lines new_content) in
42
Added:
let old_length = Array.length old_lines in
43
Added:
let new_length = Array.length new_lines in
44
Added:
let matrix_size = old_length * new_length in
45
Added:
let rec all_deletions index acc =
46
Added:
if index = old_length then List.rev acc
47
Added:
else
48
Added:
all_deletions (index + 1)
49
Added:
({
50
Added:
kind = Deletion;
51
Added:
old_number = Some (index + 1);
52
Added:
new_number = None;
53
Added:
text = old_lines.(index);
54
Added:
}
55
Added:
:: acc)
56
Added:
in
57
Added:
let rec all_additions index acc =
58
Added:
if index = new_length then List.rev acc
59
Added:
else
60
Added:
all_additions (index + 1)
61
Added:
({
62
Added:
kind = Addition;
63
Added:
old_number = None;
64
Added:
new_number = Some (index + 1);
65
Added:
text = new_lines.(index);
66
Added:
}
67
Added:
:: acc)
68
Added:
in
69
Added:
if matrix_size > 4_000_000 then all_deletions 0 [] @ all_additions 0 []
70
Added:
else
71
Added:
let lengths = Array.make_matrix (old_length + 1) (new_length + 1) 0 in
72
Added:
for old_index = old_length - 1 downto 0 do
73
Added:
for new_index = new_length - 1 downto 0 do
74
Added:
lengths.(old_index).(new_index) <-
75
Added:
(if old_lines.(old_index) = new_lines.(new_index) then
76
Added:
lengths.(old_index + 1).(new_index + 1) + 1
77
Added:
else
78
Added:
max
79
Added:
lengths.(old_index + 1).(new_index)
80
Added:
lengths.(old_index).(new_index + 1))
81
Added:
done
82
Added:
done;
83
Added:
let rec build old_index new_index acc =
84
Added:
if old_index = old_length then List.rev acc @ all_additions new_index []
85
Added:
else if new_index = new_length then
86
Added:
List.rev acc @ all_deletions old_index []
87
Added:
else if old_lines.(old_index) = new_lines.(new_index) then
88
Added:
build (old_index + 1) (new_index + 1)
89
Added:
({
90
Added:
kind = Context;
91
Added:
old_number = Some (old_index + 1);
92
Added:
new_number = Some (new_index + 1);
93
Added:
text = old_lines.(old_index);
94
Added:
}
95
Added:
:: acc)
96
Added:
else if
97
Added:
lengths.(old_index + 1).(new_index)
98
Added:
>= lengths.(old_index).(new_index + 1)
99
Added:
then
100
Added:
build (old_index + 1) new_index
101
Added:
({
102
Added:
kind = Deletion;
103
Added:
old_number = Some (old_index + 1);
104
Added:
new_number = None;
105
Added:
text = old_lines.(old_index);
106
Added:
}
107
Added:
:: acc)
108
Added:
else
109
Added:
build old_index (new_index + 1)
110
Added:
({
111
Added:
kind = Addition;
112
Added:
old_number = None;
113
Added:
new_number = Some (new_index + 1);
114
Added:
text = new_lines.(new_index);
115
Added:
}
116
Added:
:: acc)
117
Added:
in
118
Added:
build 0 0 []
119
Added:
120
Added:
let hunks ?(context = 3) lines =
121
Added:
let lines = Array.of_list lines in
122
Added:
let length = Array.length lines in
123
Added:
let changed =
124
Added:
Array.to_list (Array.mapi (fun index line -> (index, line.kind)) lines)
125
Added:
|> List.filter_map (function
126
Added:
| index, (Addition | Deletion) -> Some index
127
Added:
| _, Context -> None)
128
Added:
in
129
Added:
let ranges =
130
Added:
let add_range ranges index =
131
Added:
let first = max 0 (index - context) in
132
Added:
let last = min (length - 1) (index + context) in
133
Added:
match ranges with
134
Added:
| (range_first, range_last) :: rest when first <= range_last + 1 ->
135
Added:
(range_first, max range_last last) :: rest
136
Added:
| _ -> (first, last) :: ranges
137
Added:
in
138
Added:
List.fold_left add_range [] changed |> List.rev
139
Added:
in
140
Added:
let number_or_zero get_number slice =
141
Added:
List.find_map get_number slice |> Option.value ~default:0
142
Added:
in
143
Added:
let make_hunk (first, last) =
144
Added:
let rec slice index acc =
145
Added:
if index > last then List.rev acc
146
Added:
else slice (index + 1) (lines.(index) :: acc)
147
Added:
in
148
Added:
let lines = slice first [] in
149
Added:
{
150
Added:
old_start = number_or_zero (fun line -> line.old_number) lines;
151
Added:
old_count =
152
Added:
List.fold_left
153
Added:
(fun count line ->
154
Added:
if Option.is_some line.old_number then count + 1 else count)
155
Added:
0 lines;
156
Added:
new_start = number_or_zero (fun line -> line.new_number) lines;
157
Added:
new_count =
158
Added:
List.fold_left
159
Added:
(fun count line ->
160
Added:
if Option.is_some line.new_number then count + 1 else count)
161
Added:
0 lines;
162
Added:
lines;
163
Added:
}
164
Added:
in
165
Added:
List.map make_hunk ranges
lib/resolvers.ml
@@ -339,173 +339,10 @@
339
339
340
340
module Diff = struct
341
341
module Path_map = Map.Make (String)
342
Added:
include Diff
342
343
343
Removed:
type line_kind = Context | Addition | Deletion
344
Removed:
345
Removed:
type line = {
346
Removed:
kind : line_kind;
347
Removed:
old_number : int option;
348
Removed:
new_number : int option;
349
Removed:
text : string;
350
Removed:
}
351
Removed:
352
Removed:
type hunk = {
353
Removed:
old_start : int;
354
Removed:
old_count : int;
355
Removed:
new_start : int;
356
Removed:
new_count : int;
357
Removed:
lines : line list;
358
Removed:
}
359
Removed:
360
Removed:
type file = {
361
Removed:
path : string;
362
Removed:
old_hash : string option;
363
Removed:
new_hash : string option;
364
Removed:
old_mode : int option;
365
Removed:
new_mode : int option;
366
Removed:
binary : bool;
367
Removed:
hunks : hunk list;
368
Removed:
}
369
Removed:
370
344
type tree_file = { hash : string; perm : Git.Tree.perm }
371
345
372
Removed:
let split_lines content =
373
Removed:
match String.split_on_char '\n' content with
374
Removed:
| [] -> []
375
Removed:
| lines ->
376
Removed:
if content = "" then []
377
Removed:
else if String.ends_with ~suffix:"\n" content then
378
Removed:
List.rev lines |> List.tl |> List.rev
379
Removed:
else lines
380
Removed:
381
Removed:
let line_diff old_content new_content =
382
Removed:
let old_lines = Array.of_list (split_lines old_content) in
383
Removed:
let new_lines = Array.of_list (split_lines new_content) in
384
Removed:
let old_length = Array.length old_lines in
385
Removed:
let new_length = Array.length new_lines in
386
Removed:
let matrix_size = old_length * new_length in
387
Removed:
let rec all_deletions index acc =
388
Removed:
if index = old_length then List.rev acc
389
Removed:
else
390
Removed:
all_deletions (index + 1)
391
Removed:
({
392
Removed:
kind = Deletion;
393
Removed:
old_number = Some (index + 1);
394
Removed:
new_number = None;
395
Removed:
text = old_lines.(index);
396
Removed:
}
397
Removed:
:: acc)
398
Removed:
in
399
Removed:
let rec all_additions index acc =
400
Removed:
if index = new_length then List.rev acc
401
Removed:
else
402
Removed:
all_additions (index + 1)
403
Removed:
({
404
Removed:
kind = Addition;
405
Removed:
old_number = None;
406
Removed:
new_number = Some (index + 1);
407
Removed:
text = new_lines.(index);
408
Removed:
}
409
Removed:
:: acc)
410
Removed:
in
411
Removed:
if matrix_size > 4_000_000 then all_deletions 0 [] @ all_additions 0 []
412
Removed:
else
413
Removed:
let lengths = Array.make_matrix (old_length + 1) (new_length + 1) 0 in
414
Removed:
for old_index = old_length - 1 downto 0 do
415
Removed:
for new_index = new_length - 1 downto 0 do
416
Removed:
lengths.(old_index).(new_index) <-
417
Removed:
(if old_lines.(old_index) = new_lines.(new_index) then
418
Removed:
lengths.(old_index + 1).(new_index + 1) + 1
419
Removed:
else
420
Removed:
max
421
Removed:
lengths.(old_index + 1).(new_index)
422
Removed:
lengths.(old_index).(new_index + 1))
423
Removed:
done
424
Removed:
done;
425
Removed:
let rec build old_index new_index acc =
426
Removed:
if old_index = old_length then List.rev acc @ all_additions new_index []
427
Removed:
else if new_index = new_length then
428
Removed:
List.rev acc @ all_deletions old_index []
429
Removed:
else if old_lines.(old_index) = new_lines.(new_index) then
430
Removed:
build (old_index + 1) (new_index + 1)
431
Removed:
({
432
Removed:
kind = Context;
433
Removed:
old_number = Some (old_index + 1);
434
Removed:
new_number = Some (new_index + 1);
435
Removed:
text = old_lines.(old_index);
436
Removed:
}
437
Removed:
:: acc)
438
Removed:
else if
439
Removed:
lengths.(old_index + 1).(new_index)
440
Removed:
>= lengths.(old_index).(new_index + 1)
441
Removed:
then
442
Removed:
build (old_index + 1) new_index
443
Removed:
({
444
Removed:
kind = Deletion;
445
Removed:
old_number = Some (old_index + 1);
446
Removed:
new_number = None;
447
Removed:
text = old_lines.(old_index);
448
Removed:
}
449
Removed:
:: acc)
450
Removed:
else
451
Removed:
build old_index (new_index + 1)
452
Removed:
({
453
Removed:
kind = Addition;
454
Removed:
old_number = None;
455
Removed:
new_number = Some (new_index + 1);
456
Removed:
text = new_lines.(new_index);
457
Removed:
}
458
Removed:
:: acc)
459
Removed:
in
460
Removed:
build 0 0 []
461
Removed:
462
Removed:
let hunks ?(context = 3) lines =
463
Removed:
let lines = Array.of_list lines in
464
Removed:
let length = Array.length lines in
465
Removed:
let changed =
466
Removed:
Array.to_list (Array.mapi (fun index line -> (index, line.kind)) lines)
467
Removed:
|> List.filter_map (function
468
Removed:
| index, (Addition | Deletion) -> Some index
469
Removed:
| _, Context -> None)
470
Removed:
in
471
Removed:
let ranges =
472
Removed:
let add_range ranges index =
473
Removed:
let first = max 0 (index - context) in
474
Removed:
let last = min (length - 1) (index + context) in
475
Removed:
match ranges with
476
Removed:
| (range_first, range_last) :: rest when first <= range_last + 1 ->
477
Removed:
(range_first, max range_last last) :: rest
478
Removed:
| _ -> (first, last) :: ranges
479
Removed:
in
480
Removed:
List.fold_left add_range [] changed |> List.rev
481
Removed:
in
482
Removed:
let number_or_zero get_number slice =
483
Removed:
List.find_map get_number slice |> Option.value ~default:0
484
Removed:
in
485
Removed:
let make_hunk (first, last) =
486
Removed:
let rec slice index acc =
487
Removed:
if index > last then List.rev acc
488
Removed:
else slice (index + 1) (lines.(index) :: acc)
489
Removed:
in
490
Removed:
let lines = slice first [] in
491
Removed:
{
492
Removed:
old_start = number_or_zero (fun line -> line.old_number) lines;
493
Removed:
old_count =
494
Removed:
List.fold_left
495
Removed:
(fun count line ->
496
Removed:
if Option.is_some line.old_number then count + 1 else count)
497
Removed:
0 lines;
498
Removed:
new_start = number_or_zero (fun line -> line.new_number) lines;
499
Removed:
new_count =
500
Removed:
List.fold_left
501
Removed:
(fun count line ->
502
Removed:
if Option.is_some line.new_number then count + 1 else count)
503
Removed:
0 lines;
504
Removed:
lines;
505
Removed:
}
506
Removed:
in
507
Removed:
List.map make_hunk ranges
508
Removed:
509
346
let rec flatten_tree store prefix tree_hash files =
510
347
let* hash = hash_of_hex tree_hash in
511
348
Lwt_result.bind (Store.read store hash) @@ function
@@ -590,7 +427,7 @@
590
427
binary;
591
428
hunks =
592
429
(if binary then []
593
Removed:
else line_diff old_content new_content |> hunks);
430
Added:
else Diff.line_diff old_content new_content |> Diff.hunks);
594
431
}
595
432
in
596
433
build (file :: acc) files