riffdiff 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_BIN: bin
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
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rspec', '~> 3.0'
4
+ gem 'diff-lcs', '~> 1.2.5'
5
+ gem 'slop', '~> 4.1.0'
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
@@ -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.
@@ -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