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).

Commit
673bdd117d12b0c7c924692f204d193752e93b8c
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/diff.ml
index 00000000..5613f587 000000..100644
@@ -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
index 6d4c3a0c..8baa0e9d 100644..100644
@@ -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