riffdiff 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +26 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +26 -0
- data/LICENSE +22 -0
- data/README.md +100 -0
- data/Rakefile +23 -0
- data/bin/benchmark +62 -0
- data/bin/bundler +16 -0
- data/bin/htmldiff +16 -0
- data/bin/ldiff +16 -0
- data/bin/riff +34 -0
- data/bin/rspec +16 -0
- data/lib/colors.rb +22 -0
- data/lib/diff_string.rb +52 -0
- data/lib/options.rb +46 -0
- data/lib/pager.rb +51 -0
- data/lib/refiner.rb +86 -0
- data/lib/riff.rb +171 -0
- data/lib/version.rb +39 -0
- data/riffdiff.gemspec +19 -0
- data/spec/added-newline-at-eof.diff +11 -0
- data/spec/colors_spec.rb +35 -0
- data/spec/diff_string_spec.rb +91 -0
- data/spec/no-newline-at-eof-context.diff +12 -0
- data/spec/refiner_spec.rb +117 -0
- data/spec/removed-newline-at-eof.diff +11 -0
- data/spec/riff_spec.rb +45 -0
- data/spec/version_spec.rb +43 -0
- metadata +82 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: d5898446e7539cc53b995c89422d06926fe837ed
|
|
4
|
+
data.tar.gz: 9d0f0b66dccb850b5eb801daa9cf10275081673a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 638ec5d15529d20ba6e65962a892fcb386b3c6cfd3c8e7c141678bc3743e7da317d071d1fff77422238990251d264922e3641e52597c400b23a024f389564c53
|
|
7
|
+
data.tar.gz: 2a078dbacd85fd198ac49ca8e122e626db3e8aac662b6a5bc6da6070dbbcdc36a2e0870737e82b517ed182fe781e943c6c9b12990ca8e09e9beef1b0a0d7ccec
|
data/.bundle/config
ADDED
data/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*.gem
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# This configuration was generated by `rubocop --auto-gen-config`
|
|
2
|
+
# on 2015-03-27 21:35:37 +0100 using RuboCop version 0.29.1.
|
|
3
|
+
# The point is for the user to remove these configuration records
|
|
4
|
+
# one by one as the offenses are removed from the code base.
|
|
5
|
+
# Note that changes in the inspected code, or installation of new
|
|
6
|
+
# versions of RuboCop, may require this file to be generated again.
|
|
7
|
+
|
|
8
|
+
# I *like* these parentheses, kill the warnings /johan.walles@gmail.com
|
|
9
|
+
Style/MethodCallParentheses:
|
|
10
|
+
Enabled: false
|
|
11
|
+
Style/DefWithParentheses:
|
|
12
|
+
Enabled: false
|
|
13
|
+
|
|
14
|
+
# Johan doesn't like the single-line form of if()
|
|
15
|
+
Style/IfUnlessModifier:
|
|
16
|
+
Enabled: false
|
|
17
|
+
Style/GuardClause:
|
|
18
|
+
Enabled: false
|
|
19
|
+
|
|
20
|
+
# Johan thinks return statements improve readability
|
|
21
|
+
Style/RedundantReturn:
|
|
22
|
+
Enabled: false
|
|
23
|
+
|
|
24
|
+
# Johan thinks these are OK as long as there aren't too many of them
|
|
25
|
+
Style/PerlBackrefs:
|
|
26
|
+
Enabled: false
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
GEM
|
|
2
|
+
remote: https://rubygems.org/
|
|
3
|
+
specs:
|
|
4
|
+
diff-lcs (1.2.5)
|
|
5
|
+
rspec (3.2.0)
|
|
6
|
+
rspec-core (~> 3.2.0)
|
|
7
|
+
rspec-expectations (~> 3.2.0)
|
|
8
|
+
rspec-mocks (~> 3.2.0)
|
|
9
|
+
rspec-core (3.2.2)
|
|
10
|
+
rspec-support (~> 3.2.0)
|
|
11
|
+
rspec-expectations (3.2.0)
|
|
12
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
13
|
+
rspec-support (~> 3.2.0)
|
|
14
|
+
rspec-mocks (3.2.1)
|
|
15
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
16
|
+
rspec-support (~> 3.2.0)
|
|
17
|
+
rspec-support (3.2.2)
|
|
18
|
+
slop (4.1.0)
|
|
19
|
+
|
|
20
|
+
PLATFORMS
|
|
21
|
+
ruby
|
|
22
|
+
|
|
23
|
+
DEPENDENCIES
|
|
24
|
+
diff-lcs (~> 1.2.5)
|
|
25
|
+
rspec (~> 3.0)
|
|
26
|
+
slop (~> 4.1.0)
|
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015 Johan Walles
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
data/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Riff, the Refining Diff
|
|
2
|
+
Riff is a wrapper around diff that highlights not only which lines
|
|
3
|
+
have changed, but also which parts of the lines that have changed.
|
|
4
|
+
|
|
5
|
+
# Usage
|
|
6
|
+
git diff | riff
|
|
7
|
+
|
|
8
|
+
Or if you do...
|
|
9
|
+
|
|
10
|
+
git config --global pager.diff riff
|
|
11
|
+
git config --global pager.show riff
|
|
12
|
+
|
|
13
|
+
... then all future 'git diff's and 'git show's will be refined.
|
|
14
|
+
|
|
15
|
+
# TODO before first release
|
|
16
|
+
* Release version 0.0.0
|
|
17
|
+
|
|
18
|
+
# TODO post first release
|
|
19
|
+
* Think about how to visualize one line changing to itself with a
|
|
20
|
+
comma at the end plus a bunch of entirely new lines. Think of a
|
|
21
|
+
constant array getting one or more extra members.
|
|
22
|
+
* Think about highlighting whitespace errors like Git does
|
|
23
|
+
* Think about how to visualize an added line break together with some
|
|
24
|
+
indentation on the following line.
|
|
25
|
+
* Do "git show 57f27da" and think about what rule we should use to get
|
|
26
|
+
the REVERSE vs reversed() lines highlighted.
|
|
27
|
+
* Do "git show 2ac5b06" and think about what rule we should use to
|
|
28
|
+
highlight all of both "some" and "one or".
|
|
29
|
+
* Do "git show -b 77c8f77" and think about what rule we should use to
|
|
30
|
+
highlight the leading spaces of the "+ refined" and "+ page" lines
|
|
31
|
+
at the end of the file.
|
|
32
|
+
* Make sure we highlight the output of "git log -p" properly. If we
|
|
33
|
+
get something unexpected, maybe just go back to :initial?
|
|
34
|
+
* Make sure we highlight the output of "git show --stat" properly
|
|
35
|
+
* Make sure we can handle a git conflict
|
|
36
|
+
resolution diff. File format is described at
|
|
37
|
+
http://git-scm.com/docs/git-diff#_combined_diff_format.
|
|
38
|
+
* Given two files on the command line, we should pass them and any
|
|
39
|
+
options on to "diff" and highlight the result.
|
|
40
|
+
* Given three files on the command line, we should pass them and any
|
|
41
|
+
options on to "diff3" and highlight the result
|
|
42
|
+
|
|
43
|
+
# TODO future
|
|
44
|
+
* Detect moved blocks and use a number as a prefix for both the add
|
|
45
|
+
and the remove part of the move. Hightlight any changes just like
|
|
46
|
+
for other changes.
|
|
47
|
+
|
|
48
|
+
# DONE
|
|
49
|
+
* Make a main program that can read input from stdin and print it to
|
|
50
|
+
stdout.
|
|
51
|
+
* Make the main program identify different kinds of lines by prefix
|
|
52
|
+
and color them accordingly. Use the same color scheme as `git`.
|
|
53
|
+
* Make the main program identify blocks of lines that have been
|
|
54
|
+
replaced by another block of lines.
|
|
55
|
+
* Use http://www.rubydoc.info/github/halostatue/diff-lcs rather
|
|
56
|
+
than our own refinement algorithm
|
|
57
|
+
* Make it possible to print rather than puts Refiner output
|
|
58
|
+
* "print" rather than "puts" the Refiner output
|
|
59
|
+
* Make the Refiner not highlight anything if either old or new is
|
|
60
|
+
empty
|
|
61
|
+
* Ask the Refiner even if either old or new is empty
|
|
62
|
+
* Use DiffString for context lines
|
|
63
|
+
* Preserve linefeeds when sending lines to the Refiner
|
|
64
|
+
* All context lines must be prefixed by ' ', currently they aren't
|
|
65
|
+
* Refine each pair of blocks, make sure both added characters and
|
|
66
|
+
removed characters are highlighted in a readable fashion, both in
|
|
67
|
+
added blocks and removed blocks.
|
|
68
|
+
* Diffing <x "hej"> vs <x 'hej'> shows the first space as a
|
|
69
|
+
difference.
|
|
70
|
+
* If stdout is a terminal, pipe the output to a pager using the
|
|
71
|
+
algorithm described under "core.pager" in "git help config".
|
|
72
|
+
* Do some effort to prevent fork loops if people set riff as $PAGER
|
|
73
|
+
* Make the Refiner not highlight anything if there are "too many"
|
|
74
|
+
differences between the sections. The point here is that we want to
|
|
75
|
+
highlight changes, but if it's a *replacement* rather than a change
|
|
76
|
+
then we don't want to highlight it.
|
|
77
|
+
* Refine added line endings properly
|
|
78
|
+
* Refine removed line endings properly
|
|
79
|
+
* Refine "ax"->"bx\nc" properly
|
|
80
|
+
* Strip all color from the input before handling it to enable users to
|
|
81
|
+
set Git's pager.diff and pager.show variables to 'riff' without also
|
|
82
|
+
needing to set color.diff=false.
|
|
83
|
+
* Visualize removed linefeed at end of file properly
|
|
84
|
+
* Visualize adding a missing linefeed at end of file properly
|
|
85
|
+
* Visualize missing linefeed at end of file as part of the context
|
|
86
|
+
properly
|
|
87
|
+
* On exceptions, print the riff.rb @state
|
|
88
|
+
* On exceptions, print the line riff.rb was processing
|
|
89
|
+
* On exceptions, print a link to the issue tracker
|
|
90
|
+
* Add support for --help
|
|
91
|
+
* Print help and bail if stdin is a terminal
|
|
92
|
+
* Add support for --version
|
|
93
|
+
* On exceptions, print the current version just like --version
|
|
94
|
+
* Put an upper bound on how large regions we should attempt to refine
|
|
95
|
+
* Find out how the LCS algorithm scales and improve the heuristic for
|
|
96
|
+
when not to call it.
|
|
97
|
+
* You can do `git diff | riff` and get reasonable output.
|
|
98
|
+
* Test that we work as expected when "gem install"ed system-wide
|
|
99
|
+
* Create a Rakefile that can install dependencies, build, run tests, package and
|
|
100
|
+
optionally deploy as well.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'rspec/core/rake_task'
|
|
2
|
+
|
|
3
|
+
task default: :spec
|
|
4
|
+
desc 'Run the unit tests (default)'
|
|
5
|
+
task spec: [:deps]
|
|
6
|
+
RSpec::Core::RakeTask.new do |t|
|
|
7
|
+
t.pattern = FileList['spec/**/*_spec.rb']
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
desc 'Create a .gem package'
|
|
11
|
+
task package: [:spec] do
|
|
12
|
+
system('rm -f *.gem ; gem build riffdiff.gemspec') || fail
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc 'Install dependencies'
|
|
16
|
+
task :deps do
|
|
17
|
+
system('bundle install') || fail
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc 'Publish to rubygems.org'
|
|
21
|
+
task publish: [:package] do
|
|
22
|
+
system('gem push *.gem') || fail
|
|
23
|
+
end
|
data/bin/benchmark
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# This program is for finding out how our refiner's performance scales
|
|
4
|
+
# with the size of the input.
|
|
5
|
+
|
|
6
|
+
# Inspired by http://timelessrepo.com/making-ruby-gems
|
|
7
|
+
begin
|
|
8
|
+
require 'riff'
|
|
9
|
+
rescue LoadError
|
|
10
|
+
$LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
|
|
11
|
+
require 'riff'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
$ks = {}
|
|
15
|
+
def time_string(dt, m, n, formula)
|
|
16
|
+
k = dt / eval(formula)
|
|
17
|
+
$ks[formula] = k unless $ks.key? formula
|
|
18
|
+
k /= $ks[formula]
|
|
19
|
+
return "#{k.round(1)}: dt/(#{formula})"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
$k0_a_plus_b = nil
|
|
23
|
+
$k0_a_times_b = nil
|
|
24
|
+
$k0_dist = nil
|
|
25
|
+
def print_timings(old_size, new_size)
|
|
26
|
+
old = 'o' * old_size
|
|
27
|
+
new = 'n' * new_size
|
|
28
|
+
t0 = Time.now
|
|
29
|
+
Refiner.new(old, new)
|
|
30
|
+
t1 = Time.now
|
|
31
|
+
dt = t1 - t0
|
|
32
|
+
|
|
33
|
+
puts "Timings: old_size=#{old_size} new_size=#{new_size} dt=#{dt.round(1)}s"
|
|
34
|
+
|
|
35
|
+
puts " #{time_string(dt, old_size, new_size, 'm+n')}"
|
|
36
|
+
puts " #{time_string(dt, old_size, new_size, 'm*n*Math.log(m)')}"
|
|
37
|
+
puts " #{time_string(dt, old_size, new_size, 'Math.sqrt(m*m + n*n)')}"
|
|
38
|
+
puts " #{time_string(dt, old_size, new_size, 'Math.log(m*m + n*n)')}"
|
|
39
|
+
puts " #{time_string(dt, old_size, new_size, 'Math.sqrt(m + n)')}"
|
|
40
|
+
|
|
41
|
+
puts
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
print_timings(40_000, 40_000)
|
|
45
|
+
print_timings(40_000, 40_000)
|
|
46
|
+
print_timings(40_000, 40_000)
|
|
47
|
+
print_timings(40_000, 40_000)
|
|
48
|
+
puts
|
|
49
|
+
|
|
50
|
+
# Exercise O(m+n)
|
|
51
|
+
print_timings(10, 79_990)
|
|
52
|
+
|
|
53
|
+
# Exercise O(m*n)
|
|
54
|
+
print_timings(20_000, 80_000)
|
|
55
|
+
|
|
56
|
+
# Excercise m+n=15000
|
|
57
|
+
print_timings(7_500, 7_500)
|
|
58
|
+
|
|
59
|
+
# Excercise m+n=30000
|
|
60
|
+
print_timings(15_000, 15_000)
|
|
61
|
+
|
|
62
|
+
puts
|
data/bin/bundler
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#
|
|
3
|
+
# This file was generated by Bundler.
|
|
4
|
+
#
|
|
5
|
+
# The application 'bundler' is installed as part of a gem, and
|
|
6
|
+
# this file is here to facilitate running it.
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
require 'pathname'
|
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
|
11
|
+
Pathname.new(__FILE__).realpath)
|
|
12
|
+
|
|
13
|
+
require 'rubygems'
|
|
14
|
+
require 'bundler/setup'
|
|
15
|
+
|
|
16
|
+
load Gem.bin_path('bundler', 'bundler')
|
data/bin/htmldiff
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#
|
|
3
|
+
# This file was generated by Bundler.
|
|
4
|
+
#
|
|
5
|
+
# The application 'htmldiff' is installed as part of a gem, and
|
|
6
|
+
# this file is here to facilitate running it.
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
require 'pathname'
|
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
|
11
|
+
Pathname.new(__FILE__).realpath)
|
|
12
|
+
|
|
13
|
+
require 'rubygems'
|
|
14
|
+
require 'bundler/setup'
|
|
15
|
+
|
|
16
|
+
load Gem.bin_path('diff-lcs', 'htmldiff')
|
data/bin/ldiff
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#
|
|
3
|
+
# This file was generated by Bundler.
|
|
4
|
+
#
|
|
5
|
+
# The application 'ldiff' is installed as part of a gem, and
|
|
6
|
+
# this file is here to facilitate running it.
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
require 'pathname'
|
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
|
11
|
+
Pathname.new(__FILE__).realpath)
|
|
12
|
+
|
|
13
|
+
require 'rubygems'
|
|
14
|
+
require 'bundler/setup'
|
|
15
|
+
|
|
16
|
+
load Gem.bin_path('diff-lcs', 'ldiff')
|
data/bin/riff
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
# Inspired by http://timelessrepo.com/making-ruby-gems
|
|
5
|
+
begin
|
|
6
|
+
require 'riff'
|
|
7
|
+
rescue LoadError
|
|
8
|
+
$LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
|
|
9
|
+
require 'riff'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require 'options'
|
|
13
|
+
include Options
|
|
14
|
+
handle_options
|
|
15
|
+
|
|
16
|
+
require 'pager'
|
|
17
|
+
include Pager
|
|
18
|
+
|
|
19
|
+
refined = Riff.new().do_stream(STDIN)
|
|
20
|
+
page(refined)
|
|
21
|
+
rescue => e
|
|
22
|
+
STDERR.puts e.to_s
|
|
23
|
+
STDERR.puts e.backtrace.join("\n\t")
|
|
24
|
+
STDERR.puts
|
|
25
|
+
begin
|
|
26
|
+
STDERR.puts "Riff version #{version}"
|
|
27
|
+
rescue => e
|
|
28
|
+
STDERR.puts 'Also, getting the Riff version failed:'
|
|
29
|
+
STDERR.puts e.to_s
|
|
30
|
+
STDERR.puts e.backtrace.join("\n\t")
|
|
31
|
+
end
|
|
32
|
+
STDERR.puts
|
|
33
|
+
STDERR.puts 'Please report this to https://github.com/walles/riff/issues'
|
|
34
|
+
end
|
data/bin/rspec
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
#
|
|
3
|
+
# This file was generated by Bundler.
|
|
4
|
+
#
|
|
5
|
+
# The application 'rspec' is installed as part of a gem, and
|
|
6
|
+
# this file is here to facilitate running it.
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
require 'pathname'
|
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
|
11
|
+
Pathname.new(__FILE__).realpath)
|
|
12
|
+
|
|
13
|
+
require 'rubygems'
|
|
14
|
+
require 'bundler/setup'
|
|
15
|
+
|
|
16
|
+
load Gem.bin_path('rspec-core', 'rspec')
|
data/lib/colors.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ANSI Color related escape code constants
|
|
2
|
+
module Colors
|
|
3
|
+
ESC = 27.chr
|
|
4
|
+
|
|
5
|
+
BOLD = "#{ESC}[1m"
|
|
6
|
+
CYAN = "#{ESC}[36m"
|
|
7
|
+
GREEN = "#{ESC}[32m"
|
|
8
|
+
RED = "#{ESC}[31m"
|
|
9
|
+
|
|
10
|
+
REVERSE = "#{ESC}[7m"
|
|
11
|
+
NOT_REVERSE = "#{ESC}[27m"
|
|
12
|
+
|
|
13
|
+
RESET = "#{ESC}[m"
|
|
14
|
+
|
|
15
|
+
def reversed(string)
|
|
16
|
+
return "#{REVERSE}#{string}#{NOT_REVERSE}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def uncolor(string)
|
|
20
|
+
return string.gsub(/#{ESC}[^m]*m/, '')
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/diff_string.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
require 'colors'
|
|
3
|
+
|
|
4
|
+
# An added or removed part of a diff, which can contain both
|
|
5
|
+
# highlighted and not highlighted characters. For multi line strings,
|
|
6
|
+
# each line will be prefixed with prefix and color.
|
|
7
|
+
class DiffString
|
|
8
|
+
include Colors
|
|
9
|
+
|
|
10
|
+
def initialize(prefix, color)
|
|
11
|
+
@reverse = false
|
|
12
|
+
@prefix = prefix
|
|
13
|
+
@color = color
|
|
14
|
+
@string = ''
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add(string, reverse)
|
|
18
|
+
if reverse && string == "\n"
|
|
19
|
+
add('↵', true)
|
|
20
|
+
add("\n", false)
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if @string.empty?() || @string.end_with?("\n")
|
|
25
|
+
@string += @color
|
|
26
|
+
@string += @prefix
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if reverse != @reverse
|
|
30
|
+
@string += reverse ? REVERSE : NOT_REVERSE
|
|
31
|
+
end
|
|
32
|
+
@reverse = reverse
|
|
33
|
+
|
|
34
|
+
@string += string
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_s()
|
|
38
|
+
return '' if @string.empty?
|
|
39
|
+
|
|
40
|
+
string = @string
|
|
41
|
+
string.chomp! if string.end_with? "\n"
|
|
42
|
+
|
|
43
|
+
suffix = @color.empty? ? '' : RESET
|
|
44
|
+
return string + suffix + "\n"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.decorate_string(prefix, color, string)
|
|
48
|
+
decorated = DiffString.new(prefix, color)
|
|
49
|
+
decorated.add(string, false)
|
|
50
|
+
return decorated.to_s
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/options.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require 'slop'
|
|
2
|
+
require 'version'
|
|
3
|
+
|
|
4
|
+
# Handle command line options
|
|
5
|
+
module Options
|
|
6
|
+
include Version
|
|
7
|
+
|
|
8
|
+
def handle_options
|
|
9
|
+
opts = Slop::Options.new do |o|
|
|
10
|
+
o.banner = <<-EOS
|
|
11
|
+
Usage: diff ... | riff
|
|
12
|
+
Colors diff and highlights what parts of changed lines have changed.
|
|
13
|
+
|
|
14
|
+
Git integration:
|
|
15
|
+
git config --global pager.diff riff
|
|
16
|
+
git config --global pager.show riff
|
|
17
|
+
EOS
|
|
18
|
+
o.separator 'Options:'
|
|
19
|
+
o.on '--version', 'Print version information and exit' do
|
|
20
|
+
puts "riff #{version}"
|
|
21
|
+
puts
|
|
22
|
+
puts 'Developed at http://github.com/walles/riff'
|
|
23
|
+
|
|
24
|
+
exit
|
|
25
|
+
end
|
|
26
|
+
o.on '-h', '--help', 'Print this help' do
|
|
27
|
+
puts o
|
|
28
|
+
exit
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
begin
|
|
33
|
+
opts.parse(ARGV)
|
|
34
|
+
rescue Slop::Error => e
|
|
35
|
+
STDERR.puts "ERROR: #{e}"
|
|
36
|
+
STDERR.puts
|
|
37
|
+
STDERR.puts opts
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if $stdin.isatty
|
|
42
|
+
puts opts
|
|
43
|
+
exit
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/pager.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Add a pager() method that can send text to a pager.
|
|
2
|
+
#
|
|
3
|
+
# With "pager" referring to something like less or moar.
|
|
4
|
+
#
|
|
5
|
+
# This file attempts to be as close as possible to what Git is
|
|
6
|
+
# doing. For reference, do "git help config", search for "core.pager"
|
|
7
|
+
# and compare that text to the page() method below.
|
|
8
|
+
module Pager
|
|
9
|
+
# Checking for this variable before launching $PAGER should prevent
|
|
10
|
+
# us from fork bombing if somebody sets the PAGER environment
|
|
11
|
+
# variable to point to us.
|
|
12
|
+
DONT_USE_PAGER_VAR = '_RIFF_PREVENT_PAGING_LOOP'
|
|
13
|
+
|
|
14
|
+
def pipe_text_into_command(text, command)
|
|
15
|
+
env = {
|
|
16
|
+
DONT_USE_PAGER_VAR => '1'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Set LESS=FRX unless $LESS already has a value
|
|
20
|
+
env['LESS'] = 'FRX' unless ENV['LESS']
|
|
21
|
+
|
|
22
|
+
# Set LV=-c unless $LV already has a value
|
|
23
|
+
env['LV'] = '-c' unless ENV['LV']
|
|
24
|
+
|
|
25
|
+
IO.popen(env, command, 'w') { |pager| pager.print text }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def less(text)
|
|
29
|
+
pipe_text_into_command(text, 'less')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# If $DONT_USE_PAGER_VAR is set, we shouldn't use $PAGER
|
|
33
|
+
def dont_use_pager?
|
|
34
|
+
return true if ENV[DONT_USE_PAGER_VAR]
|
|
35
|
+
return true if ENV['PAGER'].nil?
|
|
36
|
+
return true if ENV['PAGER'].empty?
|
|
37
|
+
return false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def page(text)
|
|
41
|
+
if !$stdout.isatty
|
|
42
|
+
print text
|
|
43
|
+
elsif dont_use_pager?
|
|
44
|
+
less(text)
|
|
45
|
+
elsif ENV['PAGER']
|
|
46
|
+
pipe_text_into_command(text, ENV['PAGER'])
|
|
47
|
+
else
|
|
48
|
+
less(text)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/refiner.rb
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
require 'diff/lcs'
|
|
3
|
+
require 'diff_string'
|
|
4
|
+
|
|
5
|
+
# Compute longest common substring based diff between two strings.
|
|
6
|
+
#
|
|
7
|
+
# The diff format is first the old string:
|
|
8
|
+
# * in red
|
|
9
|
+
# * with each line prefixed with minuses
|
|
10
|
+
# * removed characters highlighted in inverse video
|
|
11
|
+
#
|
|
12
|
+
# Then comes the new string:
|
|
13
|
+
# * in green
|
|
14
|
+
# * with each line prefixed with plusses
|
|
15
|
+
# * added characters highlighted in inverse video
|
|
16
|
+
class Refiner
|
|
17
|
+
include Colors
|
|
18
|
+
|
|
19
|
+
attr_reader :refined_old
|
|
20
|
+
attr_reader :refined_new
|
|
21
|
+
|
|
22
|
+
# If either old or new would get more than this percentage of chars
|
|
23
|
+
# highlighted, consider this to be a replacement rather than a
|
|
24
|
+
# change and just don't highlight anything.
|
|
25
|
+
REFINEMENT_THRESHOLD = 30
|
|
26
|
+
|
|
27
|
+
def collect_highlights(diff, old_highlights, new_highlights)
|
|
28
|
+
diff.each do |section|
|
|
29
|
+
section.each do |highlight|
|
|
30
|
+
case highlight.action
|
|
31
|
+
when '-'
|
|
32
|
+
old_highlights << highlight.position
|
|
33
|
+
when '+'
|
|
34
|
+
new_highlights << highlight.position
|
|
35
|
+
else
|
|
36
|
+
fail("Unsupported diff type: <#{type}>")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def censor_highlights(old, new, old_highlights, new_highlights)
|
|
43
|
+
old_highlights_percentage = 100 * old_highlights.size / old.length
|
|
44
|
+
new_highlights_percentage = 100 * new_highlights.size / new.length
|
|
45
|
+
|
|
46
|
+
if old_highlights_percentage > REFINEMENT_THRESHOLD \
|
|
47
|
+
|| new_highlights_percentage > REFINEMENT_THRESHOLD
|
|
48
|
+
# We'll consider this a replacement rather than a change, don't
|
|
49
|
+
# highlight it.
|
|
50
|
+
old_highlights.clear
|
|
51
|
+
new_highlights.clear
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def should_highlight?(old, new)
|
|
56
|
+
return false if old.empty? || new.empty?
|
|
57
|
+
|
|
58
|
+
# The 15_000 constant has been determined using the "benchmark"
|
|
59
|
+
# program in our bin/ directory.
|
|
60
|
+
return false if old.length + new.length > 15_000
|
|
61
|
+
|
|
62
|
+
return true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def initialize(old, new)
|
|
66
|
+
old_highlights = Set.new
|
|
67
|
+
new_highlights = Set.new
|
|
68
|
+
if should_highlight?(old, new)
|
|
69
|
+
collect_highlights(Diff::LCS.diff(old, new),
|
|
70
|
+
old_highlights,
|
|
71
|
+
new_highlights)
|
|
72
|
+
|
|
73
|
+
censor_highlights(old, new, old_highlights, new_highlights)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@refined_old = DiffString.new('-', RED)
|
|
77
|
+
old.each_char.with_index do |char, index|
|
|
78
|
+
@refined_old.add(char, old_highlights.include?(index))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
@refined_new = DiffString.new('+', GREEN)
|
|
82
|
+
new.each_char.with_index do |char, index|
|
|
83
|
+
@refined_new.add(char, new_highlights.include?(index))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/riff.rb
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
require 'colors'
|
|
2
|
+
require 'refiner'
|
|
3
|
+
require 'diff_string'
|
|
4
|
+
|
|
5
|
+
# Call do_stream() with the output of some diff-like tool (diff,
|
|
6
|
+
# diff3, git diff, ...) and it will highlight that output for you.
|
|
7
|
+
class Riff
|
|
8
|
+
DIFF_HEADER = /^diff /
|
|
9
|
+
DIFF_HUNK_HEADER = /^@@ /
|
|
10
|
+
|
|
11
|
+
DIFF_ADDED = /^\+(.*)/
|
|
12
|
+
DIFF_REMOVED = /^-(.*)/
|
|
13
|
+
DIFF_CONTEXT = /^ /
|
|
14
|
+
DIFF_NO_ENDING_NEWLINE = /^\\/
|
|
15
|
+
|
|
16
|
+
include Colors
|
|
17
|
+
|
|
18
|
+
LINE_PREFIX = {
|
|
19
|
+
initial: '',
|
|
20
|
+
diff_header: BOLD,
|
|
21
|
+
diff_hunk_header: CYAN,
|
|
22
|
+
diff_hunk: '',
|
|
23
|
+
diff_added: GREEN,
|
|
24
|
+
diff_removed: RED,
|
|
25
|
+
diff_context: '',
|
|
26
|
+
diff_no_ending_newline: ''
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def initialize()
|
|
30
|
+
@state = :initial
|
|
31
|
+
|
|
32
|
+
@replace_old = ''
|
|
33
|
+
@replace_new = ''
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def handle_initial_line(line)
|
|
37
|
+
if line =~ DIFF_HEADER
|
|
38
|
+
@state = :diff_header
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def handle_diff_header_line(line)
|
|
43
|
+
if line =~ DIFF_HUNK_HEADER
|
|
44
|
+
@state = :diff_hunk_header
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def handle_diff_hunk_header_line(line)
|
|
49
|
+
handle_diff_hunk_line(line)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def handle_no_ending_newline(line)
|
|
53
|
+
case @state
|
|
54
|
+
when :diff_added
|
|
55
|
+
@replace_new.sub!(/\n$/, '')
|
|
56
|
+
when :diff_removed
|
|
57
|
+
@replace_old.sub!(/\n$/, '')
|
|
58
|
+
when :diff_context
|
|
59
|
+
# Intentionally ignored
|
|
60
|
+
return
|
|
61
|
+
else
|
|
62
|
+
fail NotImplementedError,
|
|
63
|
+
"Can't handle no-ending-newline in <#{@state}> line: <#{line}>"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
@state = :diff_no_ending_newline
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def handle_diff_hunk_line(line)
|
|
70
|
+
case line
|
|
71
|
+
when DIFF_HUNK_HEADER
|
|
72
|
+
@state = :diff_hunk_header
|
|
73
|
+
when DIFF_HEADER
|
|
74
|
+
@state = :diff_header
|
|
75
|
+
when DIFF_ADDED
|
|
76
|
+
@state = :diff_added
|
|
77
|
+
when DIFF_REMOVED
|
|
78
|
+
@state = :diff_removed
|
|
79
|
+
when DIFF_CONTEXT
|
|
80
|
+
@state = :diff_context
|
|
81
|
+
when DIFF_NO_ENDING_NEWLINE
|
|
82
|
+
handle_no_ending_newline(line)
|
|
83
|
+
else
|
|
84
|
+
fail NotImplementedError, "Can't handle <#{@state}> line: <#{line}>"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def handle_diff_added_line(line)
|
|
89
|
+
handle_diff_hunk_line(line)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def handle_diff_removed_line(line)
|
|
93
|
+
handle_diff_hunk_line(line)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def handle_diff_context_line(line)
|
|
97
|
+
handle_diff_hunk_line(line)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def handle_diff_no_ending_newline_line(line)
|
|
101
|
+
handle_diff_hunk_line(line)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# If we have stored adds / removes, calling this method will flush
|
|
105
|
+
# those.
|
|
106
|
+
def consume_replacement()
|
|
107
|
+
return '' if @replace_old.empty? && @replace_new.empty?
|
|
108
|
+
|
|
109
|
+
refiner = Refiner.new(@replace_old, @replace_new)
|
|
110
|
+
return_me = refiner.refined_old.to_s
|
|
111
|
+
return_me += refiner.refined_new.to_s
|
|
112
|
+
|
|
113
|
+
@replace_old = ''
|
|
114
|
+
@replace_new = ''
|
|
115
|
+
|
|
116
|
+
return return_me
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Call handle_<state>_line() for the given state and line
|
|
120
|
+
def handle_line_for_state(state, line)
|
|
121
|
+
method_name = "handle_#{state}_line"
|
|
122
|
+
fail "Unknown state: <:#{state}>" unless
|
|
123
|
+
self.respond_to? method_name
|
|
124
|
+
|
|
125
|
+
send(method_name, line)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def handle_diff_line(line)
|
|
129
|
+
line.chomp!
|
|
130
|
+
line = uncolor(line)
|
|
131
|
+
|
|
132
|
+
handle_line_for_state(@state, line)
|
|
133
|
+
|
|
134
|
+
case @state
|
|
135
|
+
when :diff_added
|
|
136
|
+
@replace_new += DIFF_ADDED.match(line)[1] + "\n"
|
|
137
|
+
return ''
|
|
138
|
+
when :diff_removed
|
|
139
|
+
@replace_old += DIFF_REMOVED.match(line)[1] + "\n"
|
|
140
|
+
return ''
|
|
141
|
+
when :diff_no_ending_newline
|
|
142
|
+
return ''
|
|
143
|
+
else
|
|
144
|
+
refined = consume_replacement()
|
|
145
|
+
|
|
146
|
+
color = LINE_PREFIX.fetch(@state)
|
|
147
|
+
|
|
148
|
+
return refined + DiffString.decorate_string('', color, line + "\n")
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Read diff from a stream and output a highlighted version to stdout
|
|
153
|
+
def do_stream(diff_stream)
|
|
154
|
+
output = ''
|
|
155
|
+
current_line = nil
|
|
156
|
+
|
|
157
|
+
begin
|
|
158
|
+
diff_stream.each do |line|
|
|
159
|
+
current_line = line
|
|
160
|
+
output += handle_diff_line(line)
|
|
161
|
+
end
|
|
162
|
+
output += consume_replacement()
|
|
163
|
+
rescue
|
|
164
|
+
STDERR.puts "State: <#{@state}>"
|
|
165
|
+
STDERR.puts "Current line: <#{current_line}>"
|
|
166
|
+
raise
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
return output
|
|
170
|
+
end
|
|
171
|
+
end
|
data/lib/version.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require 'English'
|
|
2
|
+
|
|
3
|
+
# Methods for finding out the Riff version
|
|
4
|
+
module Version
|
|
5
|
+
def git_version
|
|
6
|
+
version = `cd #{__dir__} ; git describe --dirty 2> /dev/null`.chomp
|
|
7
|
+
if $CHILD_STATUS.success?
|
|
8
|
+
return version
|
|
9
|
+
else
|
|
10
|
+
return nil
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def rubygems_version
|
|
15
|
+
return Gem::Specification.find_by_name('riffdiff').version.to_s
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Turn git describe output into a semantic version, inspired by riff.gemspec
|
|
19
|
+
def semantify_git_version(raw)
|
|
20
|
+
return nil if raw.nil?
|
|
21
|
+
|
|
22
|
+
return raw if raw.end_with? '-dirty'
|
|
23
|
+
|
|
24
|
+
return (raw + '.0') if /^[0-9]+\.[0-9]+$/.match(raw)
|
|
25
|
+
|
|
26
|
+
if /^([0-9]+\.[0-9]+)-([0-9]+)-/.match(raw)
|
|
27
|
+
return "#{$1}.#{$2}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
return raw
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def version
|
|
34
|
+
semantic_git_version = semantify_git_version(git_version)
|
|
35
|
+
return semantic_git_version unless semantic_git_version.nil?
|
|
36
|
+
|
|
37
|
+
return rubygems_version
|
|
38
|
+
end
|
|
39
|
+
end
|
data/riffdiff.gemspec
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.join(File.absolute_path(__dir__), 'lib')
|
|
2
|
+
require 'version'
|
|
3
|
+
|
|
4
|
+
Gem::Specification.new do |s|
|
|
5
|
+
extend Version
|
|
6
|
+
|
|
7
|
+
s.name = 'riffdiff'
|
|
8
|
+
s.version = semantify_git_version(git_version)
|
|
9
|
+
s.summary = 'A diff highlighter showing what parts of lines have changed'
|
|
10
|
+
s.authors = ['Johan Walles']
|
|
11
|
+
s.email = 'johan.walles@gmail.com'
|
|
12
|
+
s.homepage = 'http://github.com/walles/riff'
|
|
13
|
+
s.license = 'MIT'
|
|
14
|
+
|
|
15
|
+
s.files = `git ls-files`.split("\n")
|
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
17
|
+
s.executables = ['riff']
|
|
18
|
+
s.require_paths = ['lib']
|
|
19
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
diff --git a/README.md b/README.md
|
|
2
|
+
index bb8be87..950ec73 100644
|
|
3
|
+
--- a/README.md
|
|
4
|
+
+++ b/README.md
|
|
5
|
+
@@ -79,4 +79,4 @@ then we don't want to highlight it.
|
|
6
|
+
* Refine "ax"->"bx\nc" properly
|
|
7
|
+
* Strip all color from the input before handling it to enable users to
|
|
8
|
+
set Git's pager.diff and pager.show variables to 'riff' without also
|
|
9
|
+
- needing to set color.diff=false.
|
|
10
|
+
|
|
11
|
+
+ needing to set color.diff=false.
|
data/spec/colors_spec.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
require 'colors'
|
|
3
|
+
|
|
4
|
+
include Colors
|
|
5
|
+
|
|
6
|
+
RSpec.describe Colors, '#uncolor' do
|
|
7
|
+
context 'string with no colors' do
|
|
8
|
+
not_colored = 'not colored'
|
|
9
|
+
|
|
10
|
+
it 'does not change the contents' do
|
|
11
|
+
expect(uncolor(not_colored)).to eq(not_colored)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
context 'all our codes' do
|
|
16
|
+
[BOLD, CYAN, GREEN, RED, REVERSE, NOT_REVERSE, RESET].each do |code|
|
|
17
|
+
printable_code = code.gsub(ESC, '\e')
|
|
18
|
+
it "can remove #{printable_code} by itself" do
|
|
19
|
+
expect(uncolor(code)).to eq('')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "can remove #{printable_code} from the start of the string" do
|
|
23
|
+
expect(uncolor("#{code}foo")).to eq('foo')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "can remove #{printable_code} from the end of the string" do
|
|
27
|
+
expect(uncolor("bar#{code}")).to eq('bar')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "can remove #{printable_code} from the middle of the string" do
|
|
31
|
+
expect(uncolor("hej#{code}san")).to eq('hejsan')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
require 'colors'
|
|
3
|
+
require 'diff_string'
|
|
4
|
+
|
|
5
|
+
include Colors
|
|
6
|
+
|
|
7
|
+
RSpec.describe DiffString, '#add' do
|
|
8
|
+
context 'first and last highlighted' do
|
|
9
|
+
it 'renders correctly with one char in between' do
|
|
10
|
+
diff_string = DiffString.new('+', GREEN)
|
|
11
|
+
diff_string.add('1', true)
|
|
12
|
+
diff_string.add('2', false)
|
|
13
|
+
diff_string.add('3', true)
|
|
14
|
+
diff_string.add("\n", false)
|
|
15
|
+
expect(diff_string.to_s).to eq(
|
|
16
|
+
"#{GREEN}+#{reversed('1')}2#{reversed('3')}#{RESET}\n")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'renders correctly with a few chars in between' do
|
|
20
|
+
diff_string = DiffString.new('+', GREEN)
|
|
21
|
+
diff_string.add('1', true)
|
|
22
|
+
diff_string.add('2', false)
|
|
23
|
+
diff_string.add('3', false)
|
|
24
|
+
diff_string.add('4', false)
|
|
25
|
+
diff_string.add('5', true)
|
|
26
|
+
diff_string.add("\n", false)
|
|
27
|
+
expect(diff_string.to_s).to eq(
|
|
28
|
+
"#{GREEN}+#{reversed('1')}234#{reversed('5')}#{RESET}\n")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
context 'multiline' do
|
|
33
|
+
it 'colors both lines' do
|
|
34
|
+
diff_string = DiffString.new('+', GREEN)
|
|
35
|
+
diff_string.add('a', false)
|
|
36
|
+
diff_string.add("\n", false)
|
|
37
|
+
diff_string.add('b', false)
|
|
38
|
+
diff_string.add("\n", false)
|
|
39
|
+
|
|
40
|
+
expect(diff_string.to_s).to eq(
|
|
41
|
+
"#{GREEN}+a\n" \
|
|
42
|
+
"#{GREEN}+b#{RESET}\n")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
context %(with added newline) do
|
|
47
|
+
diff_string = DiffString.new('+', GREEN)
|
|
48
|
+
diff_string.add('a', false)
|
|
49
|
+
diff_string.add('b', false)
|
|
50
|
+
diff_string.add("\n", true)
|
|
51
|
+
diff_string.add('c', false)
|
|
52
|
+
diff_string.add('d', false)
|
|
53
|
+
diff_string.add("\n", false)
|
|
54
|
+
|
|
55
|
+
it %(properly highlights the newline) do
|
|
56
|
+
expect(diff_string.to_s).to eq(
|
|
57
|
+
%(#{GREEN}+ab#{reversed('↵')}\n) +
|
|
58
|
+
%(#{GREEN}+cd#{RESET}\n))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
context %(with highlighted ending newline) do
|
|
63
|
+
diff_string = DiffString.new('+', GREEN)
|
|
64
|
+
diff_string.add('x', false)
|
|
65
|
+
diff_string.add('y', false)
|
|
66
|
+
diff_string.add("\n", true)
|
|
67
|
+
|
|
68
|
+
it %(properly highlights the newline) do
|
|
69
|
+
expect(diff_string.to_s).to eq(
|
|
70
|
+
%(#{GREEN}+xy#{reversed('↵')}#{RESET}\n))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
context %(empty) do
|
|
75
|
+
diff_string = DiffString.new('+', GREEN)
|
|
76
|
+
|
|
77
|
+
it %(is empty) do
|
|
78
|
+
expect(diff_string.to_s).to eq('')
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
context %(doesn't end in a newline) do
|
|
83
|
+
diff_string = DiffString.new('+', GREEN)
|
|
84
|
+
diff_string.add('x', false)
|
|
85
|
+
|
|
86
|
+
it %(gets a newline added) do
|
|
87
|
+
expect(diff_string.to_s).to eq(
|
|
88
|
+
%(#{GREEN}+x#{RESET}\n))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
diff --git a/README.md b/README.md
|
|
2
|
+
index bb8be87..0769058 100644
|
|
3
|
+
--- a/README.md
|
|
4
|
+
+++ b/README.md
|
|
5
|
+
@@ -78,5 +78,5 @@ then we don't want to highlight it.
|
|
6
|
+
* Refine removed line endings properly
|
|
7
|
+
* Refine "ax"->"bx\nc" properly
|
|
8
|
+
* Strip all color from the input before handling it to enable users to
|
|
9
|
+
- set Git's pager.diff and pager.show variables to 'riff' without also
|
|
10
|
+
+ set Git's pager.diff and pager.xshow variables to 'riff' without also
|
|
11
|
+
needing to set color.diff=false.
|
|
12
|
+
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
require 'colors'
|
|
3
|
+
require 'refiner'
|
|
4
|
+
|
|
5
|
+
include Colors
|
|
6
|
+
|
|
7
|
+
RSpec.describe Refiner, '#new' do
|
|
8
|
+
context 'with single quotes to double quotes' do
|
|
9
|
+
refiner = Refiner.new(%('quoted'\n), %("quoted"\n))
|
|
10
|
+
|
|
11
|
+
it 'highlights the quotes and nothing else' do
|
|
12
|
+
expect(refiner.refined_old.to_s).to eq(
|
|
13
|
+
%(#{RED}-#{reversed("'")}quoted#{reversed("'")}#{RESET}\n))
|
|
14
|
+
expect(refiner.refined_new.to_s).to eq(
|
|
15
|
+
%(#{GREEN}+#{reversed('"')}quoted#{reversed('"')}#{RESET}\n))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
context 'with empty old' do
|
|
20
|
+
refiner = Refiner.new('', "something\n")
|
|
21
|
+
|
|
22
|
+
it 'refines old to the empty string' do
|
|
23
|
+
expect(refiner.refined_old.to_s).to eq('')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'does not highlight anything in new' do
|
|
27
|
+
expect(refiner.refined_new.to_s).to eq(
|
|
28
|
+
%(#{GREEN}+something#{RESET}\n))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
context 'with empty new' do
|
|
33
|
+
refiner = Refiner.new("something\n", '')
|
|
34
|
+
|
|
35
|
+
it 'does not highlight anything in old' do
|
|
36
|
+
expect(refiner.refined_old.to_s).to eq(
|
|
37
|
+
%(#{RED}-something#{RESET}\n))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'refines new to the empty string' do
|
|
41
|
+
expect(refiner.refined_new.to_s).to eq('')
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
context %(with <x "hej"> to <x 'hej'>) do
|
|
46
|
+
refiner = Refiner.new(%(x "hej"\n), %(x 'hej'\n))
|
|
47
|
+
|
|
48
|
+
it 'highlights the quotes and nothing else' do
|
|
49
|
+
expect(refiner.refined_old.to_s).to eq(
|
|
50
|
+
%(#{RED}-x #{reversed('"')}hej#{reversed('"')}#{RESET}\n))
|
|
51
|
+
expect(refiner.refined_new.to_s).to eq(
|
|
52
|
+
%(#{GREEN}+x #{reversed("'")}hej#{reversed("'")}#{RESET}\n))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
context %(with too many differences) do
|
|
57
|
+
refiner = Refiner.new("0123456789\n",
|
|
58
|
+
"abcdefghij\n")
|
|
59
|
+
|
|
60
|
+
it %(doesn't highlight anything) do
|
|
61
|
+
expect(refiner.refined_old.to_s).to eq(
|
|
62
|
+
%(#{RED}-0123456789#{RESET}\n))
|
|
63
|
+
expect(refiner.refined_new.to_s).to eq(
|
|
64
|
+
%(#{GREEN}+abcdefghij#{RESET}\n))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
context %(with added ending newline) do
|
|
69
|
+
refiner = Refiner.new('abcde',
|
|
70
|
+
"abcde\n")
|
|
71
|
+
|
|
72
|
+
it %(highlights the newline) do
|
|
73
|
+
expect(refiner.refined_old.to_s).to eq(
|
|
74
|
+
%(#{RED}-abcde#{RESET}\n))
|
|
75
|
+
expect(refiner.refined_new.to_s).to eq(
|
|
76
|
+
%(#{GREEN}+abcde#{reversed('↵')}#{RESET}\n))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it %(ends in a newline) do
|
|
80
|
+
expect(refiner.refined_old.to_s).to end_with("\n")
|
|
81
|
+
expect(refiner.refined_new.to_s).to end_with("\n")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
context %(with removed ending newline) do
|
|
86
|
+
refiner = Refiner.new("abcde\n",
|
|
87
|
+
'abcde')
|
|
88
|
+
|
|
89
|
+
it %(highlights the newline) do
|
|
90
|
+
expect(refiner.refined_old.to_s).to eq(
|
|
91
|
+
%(#{RED}-abcde#{reversed('↵')}#{RESET}\n))
|
|
92
|
+
expect(refiner.refined_new.to_s).to eq(
|
|
93
|
+
%(#{GREEN}+abcde#{RESET}\n))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it %(ends in a newline) do
|
|
97
|
+
expect(refiner.refined_old.to_s).to end_with("\n")
|
|
98
|
+
expect(refiner.refined_new.to_s).to end_with("\n")
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
context %(with large input) do
|
|
103
|
+
# A Refiner that fails if trying to collect highlights
|
|
104
|
+
class NonHighlightingRefiner < Refiner
|
|
105
|
+
def collect_highlights(_diff, _old_highlights, _new_highlights)
|
|
106
|
+
fail "Shouldn't collect highlights"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
old = "0123456789\n"
|
|
111
|
+
new = '0123456789' * 1500 + "\n"
|
|
112
|
+
|
|
113
|
+
it %(doesn't even attempt highlighting) do
|
|
114
|
+
NonHighlightingRefiner.new(old, new)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
diff --git a/README.md b/README.md
|
|
2
|
+
index d0aba72..e1fd9c9 100644
|
|
3
|
+
--- a/README.md
|
|
4
|
+
+++ b/README.md
|
|
5
|
+
@@ -78,4 +78,4 @@ then we don't want to highlight it.
|
|
6
|
+
* Refine "ax"->"bx\nc" properly
|
|
7
|
+
* Strip all color from the input before handling it to enable users to
|
|
8
|
+
set Git's pager.diff and pager.show variables to 'riff' without also
|
|
9
|
+
- needing to set color.diff=false.
|
|
10
|
+
+ needing to set color.diff=false.
|
|
11
|
+
|
data/spec/riff_spec.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
require 'colors'
|
|
3
|
+
require 'riff'
|
|
4
|
+
|
|
5
|
+
include Colors
|
|
6
|
+
|
|
7
|
+
RSpec.describe Riff, '#handle_diff_line' do
|
|
8
|
+
context 'Removed newline at end of file' do
|
|
9
|
+
highlighted =
|
|
10
|
+
Riff.new.do_stream(
|
|
11
|
+
File.open(File.join(__dir__, 'removed-newline-at-eof.diff')))
|
|
12
|
+
|
|
13
|
+
it 'ends the right way' do
|
|
14
|
+
expect(highlighted.split("\n", -1)[-3..-1]).to eq(
|
|
15
|
+
"#{RED}- needing to set color.diff=false.#{reversed('↵')}#{RESET}\n" \
|
|
16
|
+
"#{GREEN}+ needing to set color.diff=false.#{RESET}\n".split("\n", -1))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
context 'Added newline at end of file' do
|
|
21
|
+
highlighted =
|
|
22
|
+
Riff.new.do_stream(
|
|
23
|
+
File.open(File.join(__dir__, 'added-newline-at-eof.diff')))
|
|
24
|
+
|
|
25
|
+
it 'ends the right way' do
|
|
26
|
+
expect(highlighted.split("\n", -1)[-3..-1]).to eq(
|
|
27
|
+
"#{RED}- needing to set color.diff=false.#{RESET}\n" \
|
|
28
|
+
"#{GREEN}+ needing to set color.diff=false.#{reversed('↵')}#{RESET}\n"
|
|
29
|
+
.split("\n", -1))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context 'No newline at end of file as part of context' do
|
|
34
|
+
highlighted =
|
|
35
|
+
Riff.new.do_stream(
|
|
36
|
+
File.open(File.join(__dir__, 'no-newline-at-eof-context.diff')))
|
|
37
|
+
|
|
38
|
+
it 'ends the right way' do
|
|
39
|
+
expect(highlighted.split("\n", -1)[-3..-1]).to eq(
|
|
40
|
+
" needing to set color.diff=false.\n" \
|
|
41
|
+
"\\n"
|
|
42
|
+
.split("\n", -1))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
require 'version'
|
|
3
|
+
|
|
4
|
+
include Version
|
|
5
|
+
|
|
6
|
+
RSpec.describe Version, '#semantify_git_version' do
|
|
7
|
+
context 'nil version' do
|
|
8
|
+
it 'returns nil' do
|
|
9
|
+
expect(semantify_git_version(nil)).to eq(nil)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
context 'tagged version' do
|
|
14
|
+
it 'returns tag.0' do
|
|
15
|
+
expect(semantify_git_version('1.2')).to eq('1.2.0')
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
context 'tagged dirty version' do
|
|
20
|
+
it 'returns the raw version string' do
|
|
21
|
+
expect(semantify_git_version('1.2-dirty')).to eq('1.2-dirty')
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
context 'non tagged version' do
|
|
26
|
+
it 'returns tag.tagged-commits-ago' do
|
|
27
|
+
expect(semantify_git_version('1.2-5-g695a668')).to eq('1.2.5')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context 'non tagged dirty version' do
|
|
32
|
+
it 'returns the raw version string' do
|
|
33
|
+
expect(semantify_git_version('1.2-5-g695a668-dirty')).to(
|
|
34
|
+
eq('1.2-5-g695a668-dirty'))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
RSpec.describe Version, '#git_version' do
|
|
40
|
+
it "doesn't end in a newline" do
|
|
41
|
+
expect(git_version).to(eq(git_version.chomp))
|
|
42
|
+
end
|
|
43
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: riffdiff
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.9.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Johan Walles
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2015-05-30 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description:
|
|
14
|
+
email: johan.walles@gmail.com
|
|
15
|
+
executables:
|
|
16
|
+
- riff
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- ".bundle/config"
|
|
21
|
+
- ".gitignore"
|
|
22
|
+
- ".rubocop.yml"
|
|
23
|
+
- Gemfile
|
|
24
|
+
- Gemfile.lock
|
|
25
|
+
- LICENSE
|
|
26
|
+
- README.md
|
|
27
|
+
- Rakefile
|
|
28
|
+
- bin/benchmark
|
|
29
|
+
- bin/bundler
|
|
30
|
+
- bin/htmldiff
|
|
31
|
+
- bin/ldiff
|
|
32
|
+
- bin/riff
|
|
33
|
+
- bin/rspec
|
|
34
|
+
- lib/colors.rb
|
|
35
|
+
- lib/diff_string.rb
|
|
36
|
+
- lib/options.rb
|
|
37
|
+
- lib/pager.rb
|
|
38
|
+
- lib/refiner.rb
|
|
39
|
+
- lib/riff.rb
|
|
40
|
+
- lib/version.rb
|
|
41
|
+
- riffdiff.gemspec
|
|
42
|
+
- spec/added-newline-at-eof.diff
|
|
43
|
+
- spec/colors_spec.rb
|
|
44
|
+
- spec/diff_string_spec.rb
|
|
45
|
+
- spec/no-newline-at-eof-context.diff
|
|
46
|
+
- spec/refiner_spec.rb
|
|
47
|
+
- spec/removed-newline-at-eof.diff
|
|
48
|
+
- spec/riff_spec.rb
|
|
49
|
+
- spec/version_spec.rb
|
|
50
|
+
homepage: http://github.com/walles/riff
|
|
51
|
+
licenses:
|
|
52
|
+
- MIT
|
|
53
|
+
metadata: {}
|
|
54
|
+
post_install_message:
|
|
55
|
+
rdoc_options: []
|
|
56
|
+
require_paths:
|
|
57
|
+
- lib
|
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '0'
|
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
requirements: []
|
|
69
|
+
rubyforge_project:
|
|
70
|
+
rubygems_version: 2.0.14
|
|
71
|
+
signing_key:
|
|
72
|
+
specification_version: 4
|
|
73
|
+
summary: A diff highlighter showing what parts of lines have changed
|
|
74
|
+
test_files:
|
|
75
|
+
- spec/added-newline-at-eof.diff
|
|
76
|
+
- spec/colors_spec.rb
|
|
77
|
+
- spec/diff_string_spec.rb
|
|
78
|
+
- spec/no-newline-at-eof-context.diff
|
|
79
|
+
- spec/refiner_spec.rb
|
|
80
|
+
- spec/removed-newline-at-eof.diff
|
|
81
|
+
- spec/riff_spec.rb
|
|
82
|
+
- spec/version_spec.rb
|