Skip to content

Commit bbf3fb8

Browse files
authored
Add word based textual diff output (#131)
* Update diff representation * Update diff types * Update inline tests * Fix variant diff type * Update textual diff output to have highlighting of exact changes in type declarations * Add a command flag for word diff textual diff output * Add pretty printer for word-based textual diff output * Minor fix * Add tests for word-based textual diffs * Add changelog entry * Fix spacing * Minor fixes * Restore old text diff representation * Add tests for other word-based diff CLI options * Add an optional argument to --word-diff * Update word-based printers for the new optionalargument to --word-diff * Minor fix
1 parent 9c53d20 commit bbf3fb8

File tree

5 files changed

+159
-29
lines changed

5 files changed

+159
-29
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Add detection of addition and removal of class type declarations (#103, @azzsal)
99
- Add initial support for unwrapped libraries (#107, @Siddhi-agg, @azzsal)
1010
- Add detection of modified class declarations and class types (#106, @azzsal)
11+
- Add word-level display of textual diffs (#131, @azzsal)
1112

1213
### Changed
1314

bin/api_diff.ml

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
let tool_name = "api-diff"
22

33
type mode = Unwrapped | Wrapped of string | Cmi
4+
type word = Plain | Color
5+
type display = Line | Word of word
6+
7+
let display_mode word =
8+
match word with
9+
| Some "plain" -> Ok (Word Plain)
10+
| Some "color" -> Ok (Word Color)
11+
| None -> Ok Line
12+
| _ ->
13+
Error
14+
"Invalid argument. --word-diff takes an optional mode arguemnt.\n\
15+
The mode can be either color or plain. If no mode is provided, it \
16+
defaults to plain."
417

518
let both_directories reference current =
619
match (Sys.is_directory reference, Sys.is_directory current) with
@@ -40,8 +53,16 @@ let mode ~reference ~current ~main_module ~unwrapped =
4053
"Either --main-module or --unwrapped must be provided when diffing \
4154
entire libraries."
4255

43-
let run (`Main_module main_module) (`Unwrapped_library unwrapped)
44-
(`Ref_cmi reference) (`Current_cmi current) =
56+
let print_diff text_diff display_mode =
57+
match display_mode with
58+
| Line -> Api_watch.Text_diff.With_colors.pp Format.std_formatter text_diff
59+
| Word Plain ->
60+
Api_watch.Text_diff.Word.pp ~mode:`Plain Format.std_formatter text_diff
61+
| Word Color ->
62+
Api_watch.Text_diff.Word.pp ~mode:`Color Format.std_formatter text_diff
63+
64+
let run (`Word_diff word_diff) (`Main_module main_module)
65+
(`Unwrapped_library unwrapped) (`Ref_cmi reference) (`Current_cmi current) =
4566
let open CCResult.Infix in
4667
let* reference_map, current_map =
4768
let* curr_mode = mode ~reference ~current ~main_module ~unwrapped in
@@ -72,15 +93,29 @@ let run (`Main_module main_module) (`Unwrapped_library unwrapped)
7293
|> List.filter_map (fun (_, v) -> v)
7394
in
7495
let has_changes = not (List.is_empty diff_map) in
96+
let* display_mode = display_mode word_diff in
7597
List.iter
7698
(fun diff ->
7799
let text_diff = Api_watch.Text_diff.from_diff diff in
78-
Api_watch.Text_diff.With_colors.pp Format.std_formatter text_diff)
100+
print_diff text_diff display_mode)
79101
diff_map;
80102
if has_changes then Ok 1 else Ok 0
81103

82104
let named f = Cmdliner.Term.(app (const f))
83105

106+
let word_diff =
107+
let docv = "MODE" in
108+
let doc =
109+
"Display the API diff in an inline word diff format rather than the usual \
110+
line diff. Follows the same conventions as $(b,git diff --word-diff)."
111+
in
112+
named
113+
(fun x -> `Word_diff x)
114+
Cmdliner.Arg.(
115+
value
116+
& opt ~vopt:(Some "plain") (some string) None
117+
& info ~doc ~docv [ "word-diff" ])
118+
84119
let main_module =
85120
let docv = "MAIN_MODULE_NAME" in
86121
let doc =
@@ -128,7 +163,8 @@ let info =
128163

129164
let term =
130165
Cmdliner.Term.(
131-
const run $ main_module $ unwrapped_library $ ref_cmi $ current_cmi)
166+
const run $ word_diff $ main_module $ unwrapped_library $ ref_cmi
167+
$ current_cmi)
132168

133169
let () =
134170
Fmt_tty.setup_std_outputs ();

lib/text_diff.ml

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -235,36 +235,31 @@ and process_privacy_diff privacy_diff =
235235
| Changed Added_p -> [ Iconflict { iorig = None; inew = Some " private" } ]
236236
| Changed Removed_p -> [ Iconflict { iorig = Some " private"; inew = None } ]
237237

238-
and punctuate p s f = Printf.sprintf "%s%s" (if f then p else "") s
239-
240238
and process_type_params_diff params_diff =
239+
let open Stddiff in
241240
let open Diff in
242241
let params_hunks =
243242
match params_diff with
244243
| Same params ->
245244
List.mapi
246-
(fun i p -> Icommon (punctuate ", " (type_expr_to_string p) (i > 0)))
245+
(fun i p ->
246+
let comma = if i > 0 then ", " else "" in
247+
Icommon (Printf.sprintf "%s%s" comma (type_expr_to_string p)))
247248
params
248249
| Changed changed_params ->
249250
List.mapi
250251
(fun i p ->
252+
let comma = if i > 0 then ", " else "" in
251253
match p with
252-
| Stddiff.Same same_param ->
254+
| Same same_param ->
253255
Icommon
254-
(punctuate ", " (type_expr_to_string same_param) (i > 0))
255-
| Stddiff.Changed (Added_tp p) ->
256+
(Printf.sprintf "%s%s" comma (type_expr_to_string same_param))
257+
| Changed (Added_tp p) ->
256258
Iconflict
257-
{
258-
iorig = None;
259-
inew = Some (punctuate ", " (type_expr_to_string p) (i > 0));
260-
}
261-
| Stddiff.Changed (Removed_tp p) ->
259+
{ iorig = None; inew = Some (comma ^ type_expr_to_string p) }
260+
| Changed (Removed_tp p) ->
262261
Iconflict
263-
{
264-
iorig =
265-
Some (punctuate ", " (type_expr_to_string p) (i > 0));
266-
inew = None;
267-
})
262+
{ iorig = Some (comma ^ type_expr_to_string p); inew = None })
268263
changed_params
269264
in
270265
let open_paren = Icommon " (" in
@@ -470,24 +465,19 @@ and process_tuple_type_diff tuple_diff =
470465
let open Stddiff in
471466
List.mapi
472467
(fun i te_diff ->
468+
let star = if i > 0 then " * " else "" in
473469
match te_diff with
474470
| Same same_te ->
475-
[ Icommon (punctuate " * " (type_expr_to_string same_te) (i > 0)) ]
471+
[ Icommon (Printf.sprintf "%s%s" star (type_expr_to_string same_te)) ]
476472
| Changed (Added te) ->
477473
[
478474
Iconflict
479-
{
480-
iorig = None;
481-
inew = Some (punctuate " * " (type_expr_to_string te) (i > 0));
482-
};
475+
{ iorig = None; inew = Some (star ^ type_expr_to_string te) };
483476
]
484477
| Changed (Removed te) ->
485478
[
486479
Iconflict
487-
{
488-
iorig = Some (punctuate " * " (type_expr_to_string te) (i > 0));
489-
inew = None;
490-
};
480+
{ iorig = Some (star ^ type_expr_to_string te); inew = None };
491481
]
492482
| Changed (Modified { reference; current }) ->
493483
let te_hunk =
@@ -657,3 +647,33 @@ module With_colors = struct
657647
let pp_diff fmt diff = pp_ printer fmt diff
658648
let pp fmt t = gen_pp pp_diff fmt t
659649
end
650+
651+
module Word = struct
652+
let pp_inline_conflict_string ~src ~mode ppf s =
653+
match (src, mode) with
654+
| _, `Color -> Fmt.pf ppf "%s" s
655+
| `Orig, `Plain -> Fmt.pf ppf "[-%s-]" s
656+
| `New, `Plain -> Fmt.pf ppf "{+%s+}" s
657+
658+
let pp_inline_conflict ~src ~mode ppf conflict =
659+
let color = match src with `Orig -> `Red | `New -> `Green in
660+
Fmt.styled color
661+
(Fmt.option ~none:Fmt.nop (pp_inline_conflict_string ~src ~mode))
662+
ppf conflict
663+
664+
let pp_inline_hunk ~mode ppf inline_hunk =
665+
match inline_hunk with
666+
| Icommon s -> Fmt.string ppf s
667+
| Iconflict { iorig; inew } ->
668+
pp_inline_conflict ~src:`Orig ~mode ppf iorig;
669+
pp_inline_conflict ~src:`New ~mode ppf inew
670+
671+
let pp_inline_hunks ~mode ppf ihunks =
672+
Fmt.pf ppf " %a\n" (Fmt.list ~sep:Fmt.nop (pp_inline_hunk ~mode)) ihunks
673+
674+
let printer ~mode =
675+
{ With_colors.printer with inline_hunks = pp_inline_hunks ~mode }
676+
677+
let pp_diff ~mode fmt diff = pp_ (printer ~mode) fmt diff
678+
let pp ~(mode : [ `Plain | `Color ]) fmt t = gen_pp (pp_diff ~mode) fmt t
679+
end

lib/text_diff.mli

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,9 @@ module With_colors : sig
2323
(** Same as regular [pp] but prints added lines in green and removed lines in
2424
red. *)
2525
end
26+
27+
module Word : sig
28+
val pp : mode:[ `Plain | `Color ] -> Format.formatter -> t -> unit
29+
(** Pretty-print the text diff in an inlined word diff format, similar to
30+
[git diff --word-diff=[<mode>]] *)
31+
end
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
Here we generate a `.mli` file with some types:
2+
3+
$ cat > ref.mli << EOF
4+
> type t = int
5+
> type ('a, 'b, 'c) u = { mutable a : 'a; b : 'b; c : 'c }
6+
> type v = A of int * int | B of { a : int; b : float }
7+
> type 'a p = 'a * 'a
8+
> EOF
9+
10+
We generate the .cmi file
11+
12+
$ ocamlc ref.mli
13+
14+
Changing the types in the ref.mli
15+
16+
$ cat > cur.mli << EOF
17+
> type t
18+
> type ('a) u = { a : 'a; b : int; }
19+
> type v = A of { a : int; b : int } | B of { a : int; b : string }
20+
> type p = int * int
21+
> EOF
22+
23+
We generate the .cmi file
24+
25+
$ ocamlc cur.mli
26+
27+
Run api-watcher on the two cmi file with default word-level diffing option enabled
28+
29+
$ api-diff ref.cmi cur.cmi --word-diff
30+
diff module Cur:
31+
type [-'a-] p = [-'a * 'a-]{+int * int+}
32+
type t[- =-][- int-]
33+
type ('a[-, 'b-][-, 'c-]) u =
34+
{[- mutable-] a : 'a; b : [-'b-]{+int+};[- c : 'c;-] }
35+
type v =
36+
| A of [-int * int-]{+{ a : int; b : int; }+}
37+
| B of { a : int; b : [-float-]{+string+}; }
38+
39+
[1]
40+
41+
Run api-watcher on the two cmi file with plain word-level diffing option enabled
42+
43+
$ api-diff --word-diff=plain ref.cmi cur.cmi
44+
diff module Cur:
45+
type [-'a-] p = [-'a * 'a-]{+int * int+}
46+
type t[- =-][- int-]
47+
type ('a[-, 'b-][-, 'c-]) u =
48+
{[- mutable-] a : 'a; b : [-'b-]{+int+};[- c : 'c;-] }
49+
type v =
50+
| A of [-int * int-]{+{ a : int; b : int; }+}
51+
| B of { a : int; b : [-float-]{+string+}; }
52+
53+
[1]
54+
55+
Run api-watcher on the two cmi file with color word-level diffing option enabled
56+
57+
$ api-diff --word-diff=color ref.cmi cur.cmi
58+
diff module Cur:
59+
type 'a p = 'a * 'aint * int
60+
type t = int
61+
type ('a, 'b, 'c) u =
62+
{ mutable a : 'a; b : 'bint; c : 'c; }
63+
type v =
64+
| A of int * int{ a : int; b : int; }
65+
| B of { a : int; b : floatstring; }
66+
67+
[1]

0 commit comments

Comments
 (0)