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 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