Skip to content

Commit 632f997

Browse files
committed
diff: add long-running diff process via diff.<driver>.process
Add support for external diff processes that communicate via the long-running process protocol (pkt-line over stdin/stdout). A diff process is configured per userdiff driver: [diff "cdiff"] process = /path/to/diff-tool The tool provides custom line-matching: it receives file pairs and returns hunks that reference line numbers in the content. When textconv is also configured, the tool receives the textconv-transformed content. The tool controls which lines are marked as changed while the display shows the file content. Patch output features (word diff, function context, color) work normally; summary formats like --stat use their own diff path and are not affected. The handshake negotiates version=1 and capability=hunks. Per-file requests send command=hunks, pathname, and both file contents as packetized data. The tool responds with hunk lines and a status packet (success, error, or abort). On error, git warns and falls back to the builtin diff algorithm for that file. On abort, git silently falls back for the current file and stops sending further requests to the tool for the remainder of the session. When the tool returns no hunks with status=success, git treats the file as having no changes and produces no diff output. This also means --exit-code reports no changes for that file. The subprocess is stored on the userdiff_driver struct and launched on first use, avoiding new global variables. If the process fails to start, the handshake fails, or a communication error occurs mid-stream, the failure is cached on the driver to avoid retrying and re-warning on every subsequent file. diff_process_fill_hunks() is the sole public entry point. It handles driver lookup, flag checks, subprocess management, and error reporting, returning an enum that lets callers distinguish "hunks populated" from "files equivalent" from "not applicable" from "tool failure." Signed-off-by: Michael Montalbo <mmontalbo@gmail.com>
1 parent 099526c commit 632f997

12 files changed

Lines changed: 1022 additions & 0 deletions

File tree

Documentation/config/diff.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,11 @@ endif::git-diff[]
218218
Set this option to `true` to make the diff driver cache the text
219219
conversion outputs. See linkgit:gitattributes[5] for details.
220220

221+
`diff.<driver>.process`::
222+
The command to run as a long-running diff process that
223+
provides hunks to Git's diff pipeline.
224+
See linkgit:gitattributes[5] for details.
225+
221226
`diff.indentHeuristic`::
222227
Set this option to `false` to disable the default heuristics
223228
that shift diff hunk boundaries to make patches easier to read.

Documentation/gitattributes.adoc

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,141 @@ NOTE: If `diff.<name>.command` is defined for path with the
821821
(see above), and adding `diff.<name>.algorithm` has no effect, as the
822822
algorithm is not passed to the external diff driver.
823823

824+
Using an external diff process
825+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
826+
827+
If `diff.<name>.process` is defined,
828+
Git sends the old and new file content to an external tool and
829+
receives back a list of changed regions (pairs of line ranges in
830+
the old and new file). Git uses these instead of its builtin diff
831+
algorithm, but still controls all output formatting, so features
832+
like word diff, function context, color, and blame work normally.
833+
This is achieved by using the long-running process protocol
834+
(described in Documentation/technical/long-running-process-protocol.adoc).
835+
Unlike `diff.<name>.command`, which replaces Git's output entirely,
836+
the diff process feeds results back into the standard pipeline.
837+
838+
First, in `.gitattributes`, assign the `diff` attribute for paths.
839+
840+
------------------------
841+
*.c diff=cdiff
842+
------------------------
843+
844+
Then, define a "diff.<name>.process" configuration to specify
845+
the diff process command.
846+
847+
----------------------------------------------------------------
848+
[diff "cdiff"]
849+
process = /path/to/diff-process-tool
850+
----------------------------------------------------------------
851+
852+
When Git encounters the first file that needs to be diffed, it starts
853+
the process and performs the handshake. In the handshake, the welcome
854+
message sent by Git is "git-diff-client", only version 1 is supported,
855+
and the supported capability is "hunks" (the changed regions
856+
described below).
857+
858+
For each file, Git sends a list of "key=value" pairs terminated with
859+
a flush packet, followed by the old and new file content as packetized
860+
data, each terminated with a flush packet. The pathname is relative
861+
to the repository root. When `diff.<name>.textconv` is also set,
862+
the tool receives the textconv-transformed content rather than the
863+
raw blob. Git does not send binary files to the diff process.
864+
865+
-----------------------
866+
packet: git> command=hunks
867+
packet: git> pathname=path/file.c
868+
packet: git> 0000
869+
packet: git> OLD_CONTENT
870+
packet: git> 0000
871+
packet: git> NEW_CONTENT
872+
packet: git> 0000
873+
-----------------------
874+
875+
The tool is expected to respond with zero or more hunk lines,
876+
a flush packet, and a status packet terminated with a flush packet.
877+
Each hunk line has the form:
878+
879+
`hunk <old_start> <old_count> <new_start> <new_count>`
880+
881+
where `<old_start>` and `<old_count>` identify a range of lines in
882+
the old file, and `<new_start>` and `<new_count>` identify the
883+
replacement range in the new file. Start values are 1-based and
884+
counts are non-negative. Ranges must not extend beyond the end of
885+
the file. For example, `hunk 3 2 3 4` means that 2 lines starting
886+
at line 3 in the old file were replaced by 4 lines starting at
887+
line 3 in the new file. `hunk 3 0 3 2` is a pure insertion (2
888+
new lines added after line 2, nothing removed from the old file).
889+
`hunk 3 2 3 0` is a pure deletion (2 old lines removed, nothing
890+
added in the new file).
891+
892+
Lines are delimited by newlines. A file `"foo\nbar\n"` and a
893+
file `"foo\nbar"` both have 2 lines.
894+
895+
Hunks must be listed in order and must not overlap. The number
896+
of unchanged lines (lines not covered by any hunk) must be the
897+
same in both files.
898+
899+
-----------------------
900+
packet: git< hunk 1 3 1 5
901+
packet: git< hunk 10 2 12 2
902+
packet: git< 0000
903+
packet: git< status=success
904+
packet: git< 0000
905+
-----------------------
906+
907+
If the tool responds with hunks and "success", Git marks those lines
908+
as changed and feeds them into the standard diff pipeline. Patch
909+
output features (word diff, function context, color) work normally.
910+
Note that `--stat` and other summary formats use their own diff path
911+
and are not affected by the diff process.
912+
913+
If no hunk lines precede the flush (zero hunks with "success"), Git
914+
treats the files as having no changes: `git diff` produces no output
915+
and `git blame` skips the commit, attributing lines to earlier commits.
916+
917+
-----------------------
918+
packet: git< 0000
919+
packet: git< status=success
920+
packet: git< 0000
921+
-----------------------
922+
923+
If the tool returns invalid hunks (out of bounds, overlapping), Git
924+
silently falls back to the builtin diff algorithm.
925+
926+
In case the tool cannot or does not want to process the content,
927+
it is expected to respond with an "error" status. Git warns and
928+
falls back to the builtin diff algorithm for this file. The tool
929+
remains available for subsequent files.
930+
931+
-----------------------
932+
packet: git< 0000
933+
packet: git< status=error
934+
packet: git< 0000
935+
-----------------------
936+
937+
In case the tool cannot or does not want to process the content as
938+
well as any future content for the lifetime of the Git process, it
939+
is expected to respond with an "abort" status. Git silently falls
940+
back to the builtin diff algorithm for this file and does not send
941+
further requests to the tool.
942+
943+
-----------------------
944+
packet: git< 0000
945+
packet: git< status=abort
946+
packet: git< 0000
947+
-----------------------
948+
949+
If the tool dies during the communication or does not adhere to the
950+
protocol then Git will stop the process and fall back to the builtin
951+
diff algorithm. Git warns once and does not restart the process for
952+
subsequent files.
953+
954+
Tools should ignore unknown keys in the per-file request to remain
955+
forward-compatible. Future versions of Git may send additional
956+
`command=` values; tools that receive an unrecognized command should
957+
respond with `status=error` rather than terminating.
958+
824959
Defining a custom hunk-header
825960
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
826961

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,7 @@ LIB_OBJS += diff-delta.o
11421142
LIB_OBJS += diff-merges.o
11431143
LIB_OBJS += diff-lib.o
11441144
LIB_OBJS += diff-no-index.o
1145+
LIB_OBJS += diff-process.o
11451146
LIB_OBJS += diff.o
11461147
LIB_OBJS += diffcore-break.o
11471148
LIB_OBJS += diffcore-delta.o

0 commit comments

Comments
 (0)