Skip to content

Commit 570cd9f

Browse files
Merge pull request #3 from software-for-life/improve_git_diff
Improve git diff by adding perl script diff-highlight
2 parents e5f7ccf + 941fcec commit 570cd9f

File tree

4 files changed

+260
-8
lines changed

4 files changed

+260
-8
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.DS_Store

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ install:
2424
mkdir -p $(path_bin)
2525
install -m 0655 src/git-sync-fork.sh "$(path_bin)"
2626
ln -s /usr/local/bin/git-sync-fork.sh "$(path_bin)"/git-sync-fork
27+
install -m 0655 src/diff-highlight.pl "$(path_bin)"
2728

2829
.PHONY: remove
2930
remove:
3031
rm "$(path_bin)"/git-sync-fork
31-
rm "$(path_bin)"/git-sync-fork.sh
32+
rm "$(path_bin)"/git-sync-fork.sh
33+
rm "$(path_bin)"/diff-highlight.pl

README.md

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,56 @@
11
# git-helper
22

3-
A set of commands that execute common git tasks. ([git-sync-fork](#git-sync-fork))
3+
A set of commands that execute common git tasks. ([diff-highlight](#diff-highlight), [git-sync-fork](#git-sync-fork))
44

5-
## git-sync-fork
5+
----
66

7-
### Synopsis:
7+
### diff-highlight
8+
9+
##### Description:
10+
11+
This perl script should be used in your git configuration.
12+
13+
##### Example:
14+
15+
Extract of _~/.gitconfig_
16+
17+
```
18+
[diff]
19+
compactionHeuristic = true
20+
[interactive]
21+
diffFilter = diff-highlight.pl
22+
[pager]
23+
diff = diff-highlight.pl | less
24+
log = diff-highlight.pl | less
25+
show = diff-highlight.pl | less
26+
```
27+
28+
### git-sync-fork
29+
30+
##### Synopsis:
831

932
```
1033
git-sync-fork [BRANCH]...
1134
```
1235

13-
### Example:
36+
##### Example:
1437
```
1538
user@pc:git-project/$ git-sync-fork master gh-pages
1639
```
1740

18-
### Description:
41+
##### Description:
1942

2043
It updates the branches in fork with the branches in the upstream repository.
2144

22-
### Assumptions:
45+
##### Assumptions:
2346
- The current directory is the git project.
2447
- The name of the remote for the upstream repository is 'origin'.
2548
- The name of the remote for the forked repository is 'fork'.
2649

50+
----
2751

2852
# OBS
2953

3054
This software is packaged and distributed using [OBS](https://build.opensuse.org)
3155

32-
- Visit its OBS project site here [obs#git-helper](https://build.opensuse.org/package/show/home:binary_sequence:software-for-life/git-helper)
56+
- Visit its OBS project site here [obs#git-helper](https://build.opensuse.org/package/show/home:binary_sequence:software-for-life/git-helper)

src/diff-highlight.pl

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
#!/usr/bin/perl
2+
3+
use 5.008;
4+
use warnings FATAL => 'all';
5+
use strict;
6+
7+
# Highlight by reversing foreground and background. You could do
8+
# other things like bold or underline if you prefer.
9+
my @OLD_HIGHLIGHT = (
10+
color_config('color.diff-highlight.oldnormal'),
11+
color_config('color.diff-highlight.oldhighlight', "\x1b[7m"),
12+
color_config('color.diff-highlight.oldreset', "\x1b[27m")
13+
);
14+
my @NEW_HIGHLIGHT = (
15+
color_config('color.diff-highlight.newnormal', $OLD_HIGHLIGHT[0]),
16+
color_config('color.diff-highlight.newhighlight', $OLD_HIGHLIGHT[1]),
17+
color_config('color.diff-highlight.newreset', $OLD_HIGHLIGHT[2])
18+
);
19+
20+
my $RESET = "\x1b[m";
21+
my $COLOR = qr/\x1b\[[0-9;]*m/;
22+
my $BORING = qr/$COLOR|\s/;
23+
24+
# The patch portion of git log -p --graph should only ever have preceding | and
25+
# not / or \ as merge history only shows up on the commit line.
26+
my $GRAPH = qr/$COLOR?\|$COLOR?\s+/;
27+
28+
my @removed;
29+
my @added;
30+
my $in_hunk;
31+
32+
# Some scripts may not realize that SIGPIPE is being ignored when launching the
33+
# pager--for instance scripts written in Python.
34+
$SIG{PIPE} = 'DEFAULT';
35+
36+
while (<>) {
37+
if (!$in_hunk) {
38+
print;
39+
$in_hunk = /^$GRAPH*$COLOR*\@\@ /;
40+
}
41+
elsif (/^$GRAPH*$COLOR*-/) {
42+
push @removed, $_;
43+
}
44+
elsif (/^$GRAPH*$COLOR*\+/) {
45+
push @added, $_;
46+
}
47+
else {
48+
show_hunk(\@removed, \@added);
49+
@removed = ();
50+
@added = ();
51+
52+
print;
53+
$in_hunk = /^$GRAPH*$COLOR*[\@ ]/;
54+
}
55+
56+
# Most of the time there is enough output to keep things streaming,
57+
# but for something like "git log -Sfoo", you can get one early
58+
# commit and then many seconds of nothing. We want to show
59+
# that one commit as soon as possible.
60+
#
61+
# Since we can receive arbitrary input, there's no optimal
62+
# place to flush. Flushing on a blank line is a heuristic that
63+
# happens to match git-log output.
64+
if (!length) {
65+
local $| = 1;
66+
}
67+
}
68+
69+
# Flush any queued hunk (this can happen when there is no trailing context in
70+
# the final diff of the input).
71+
show_hunk(\@removed, \@added);
72+
73+
exit 0;
74+
75+
# Ideally we would feed the default as a human-readable color to
76+
# git-config as the fallback value. But diff-highlight does
77+
# not otherwise depend on git at all, and there are reports
78+
# of it being used in other settings. Let's handle our own
79+
# fallback, which means we will work even if git can't be run.
80+
sub color_config {
81+
my ($key, $default) = @_;
82+
my $s = `git config --get-color $key 2>/dev/null`;
83+
return length($s) ? $s : $default;
84+
}
85+
86+
sub show_hunk {
87+
my ($a, $b) = @_;
88+
89+
# If one side is empty, then there is nothing to compare or highlight.
90+
if (!@$a || !@$b) {
91+
print @$a, @$b;
92+
return;
93+
}
94+
95+
# If we have mismatched numbers of lines on each side, we could try to
96+
# be clever and match up similar lines. But for now we are simple and
97+
# stupid, and only handle multi-line hunks that remove and add the same
98+
# number of lines.
99+
if (@$a != @$b) {
100+
print @$a, @$b;
101+
return;
102+
}
103+
104+
my @queue;
105+
for (my $i = 0; $i < @$a; $i++) {
106+
my ($rm, $add) = highlight_pair($a->[$i], $b->[$i]);
107+
print $rm;
108+
push @queue, $add;
109+
}
110+
print @queue;
111+
}
112+
113+
sub highlight_pair {
114+
my @a = split_line(shift);
115+
my @b = split_line(shift);
116+
117+
# Find common prefix, taking care to skip any ansi
118+
# color codes.
119+
my $seen_plusminus;
120+
my ($pa, $pb) = (0, 0);
121+
while ($pa < @a && $pb < @b) {
122+
if ($a[$pa] =~ /$COLOR/) {
123+
$pa++;
124+
}
125+
elsif ($b[$pb] =~ /$COLOR/) {
126+
$pb++;
127+
}
128+
elsif ($a[$pa] eq $b[$pb]) {
129+
$pa++;
130+
$pb++;
131+
}
132+
elsif (!$seen_plusminus && $a[$pa] eq '-' && $b[$pb] eq '+') {
133+
$seen_plusminus = 1;
134+
$pa++;
135+
$pb++;
136+
}
137+
else {
138+
last;
139+
}
140+
}
141+
142+
# Find common suffix, ignoring colors.
143+
my ($sa, $sb) = ($#a, $#b);
144+
while ($sa >= $pa && $sb >= $pb) {
145+
if ($a[$sa] =~ /$COLOR/) {
146+
$sa--;
147+
}
148+
elsif ($b[$sb] =~ /$COLOR/) {
149+
$sb--;
150+
}
151+
elsif ($a[$sa] eq $b[$sb]) {
152+
$sa--;
153+
$sb--;
154+
}
155+
else {
156+
last;
157+
}
158+
}
159+
160+
if (is_pair_interesting(\@a, $pa, $sa, \@b, $pb, $sb)) {
161+
return highlight_line(\@a, $pa, $sa, \@OLD_HIGHLIGHT),
162+
highlight_line(\@b, $pb, $sb, \@NEW_HIGHLIGHT);
163+
}
164+
else {
165+
return join('', @a),
166+
join('', @b);
167+
}
168+
}
169+
170+
# we split either by $COLOR or by character. This has the side effect of
171+
# leaving in graph cruft. It works because the graph cruft does not contain "-"
172+
# or "+"
173+
sub split_line {
174+
local $_ = shift;
175+
return utf8::decode($_) ?
176+
map { utf8::encode($_); $_ }
177+
map { /$COLOR/ ? $_ : (split //) }
178+
split /($COLOR+)/ :
179+
map { /$COLOR/ ? $_ : (split //) }
180+
split /($COLOR+)/;
181+
}
182+
183+
sub highlight_line {
184+
my ($line, $prefix, $suffix, $theme) = @_;
185+
186+
my $start = join('', @{$line}[0..($prefix-1)]);
187+
my $mid = join('', @{$line}[$prefix..$suffix]);
188+
my $end = join('', @{$line}[($suffix+1)..$#$line]);
189+
190+
# If we have a "normal" color specified, then take over the whole line.
191+
# Otherwise, we try to just manipulate the highlighted bits.
192+
if (defined $theme->[0]) {
193+
s/$COLOR//g for ($start, $mid, $end);
194+
chomp $end;
195+
return join('',
196+
$theme->[0], $start, $RESET,
197+
$theme->[1], $mid, $RESET,
198+
$theme->[0], $end, $RESET,
199+
"\n"
200+
);
201+
} else {
202+
return join('',
203+
$start,
204+
$theme->[1], $mid, $theme->[2],
205+
$end
206+
);
207+
}
208+
}
209+
210+
# Pairs are interesting to highlight only if we are going to end up
211+
# highlighting a subset (i.e., not the whole line). Otherwise, the highlighting
212+
# is just useless noise. We can detect this by finding either a matching prefix
213+
# or suffix (disregarding boring bits like whitespace and colorization).
214+
sub is_pair_interesting {
215+
my ($a, $pa, $sa, $b, $pb, $sb) = @_;
216+
my $prefix_a = join('', @$a[0..($pa-1)]);
217+
my $prefix_b = join('', @$b[0..($pb-1)]);
218+
my $suffix_a = join('', @$a[($sa+1)..$#$a]);
219+
my $suffix_b = join('', @$b[($sb+1)..$#$b]);
220+
221+
return $prefix_a !~ /^$GRAPH*$COLOR*-$BORING*$/ ||
222+
$prefix_b !~ /^$GRAPH*$COLOR*\+$BORING*$/ ||
223+
$suffix_a !~ /^$BORING*$/ ||
224+
$suffix_b !~ /^$BORING*$/;
225+
}

0 commit comments

Comments
 (0)