kdiff3 0.9.1
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/.gitignore +37 -0
- data/.rspec +2 -0
- data/.travis.yml +16 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +71 -0
- data/LICENSE +340 -0
- data/README.md +52 -0
- data/Rakefile +1 -0
- data/bin/kdiff3 +0 -0
- data/kdiff3.gemspec +34 -0
- data/lib/kdiff3.rb +173 -0
- data/lib/kdiff3/version.rb +3 -0
- data/spec/kdiff3_spec.rb +121 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/support/a +7 -0
- data/spec/support/b +9 -0
- data/spec/support/base +10 -0
- data/spec/support/expected +6 -0
- metadata +608 -0
data/README.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
KDiff3
|
2
|
+
========
|
3
|
+
|
4
|
+
[](http://badge.fury.io/rb/KDiff3)
|
5
|
+
[](https://travis-ci.org/NullVoxPopuli/kdiff3-rb)
|
6
|
+
[](https://codeclimate.com/github/NullVoxPopuli/kdiff3-rb)
|
7
|
+
[](https://codeclimate.com/github/NullVoxPopuli/kdiff3-rb)
|
8
|
+
[](https://gemnasium.com/NullVoxPopuli/kdiff3-rb)
|
9
|
+
[](https://hakiri.io/github/NullVoxPopuli/kdiff3-rb/master)
|
10
|
+
|
11
|
+
Provides a simple way to utilize the power of [kdiff3 by Joachim Eibl](http://kdiff3.sourceforge.net/) in ruby.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
gem install KDiff3
|
16
|
+
|
17
|
+
or
|
18
|
+
|
19
|
+
gem 'KDiff3' # in your Gemfile
|
20
|
+
|
21
|
+
## Examples
|
22
|
+
|
23
|
+
Merging inline HTML
|
24
|
+
|
25
|
+
result = KDiff3.merge(
|
26
|
+
base: "<p>1,2,<p>1,<p>2,</p></p>3</p>",
|
27
|
+
yours: "<p>1,2,<p>1,</p>3</p>",
|
28
|
+
theirs: "<p>1,2,<p><p>2,</p></p>3</p>",
|
29
|
+
html: true
|
30
|
+
)
|
31
|
+
|
32
|
+
# => "<p>1,2,<p></p>3</p>"
|
33
|
+
|
34
|
+
Merging files
|
35
|
+
|
36
|
+
result = KDiff3.merge(
|
37
|
+
base: "path/to/base",
|
38
|
+
yours: "path/to/yours",
|
39
|
+
theirs: "path/to/theirs"
|
40
|
+
)
|
41
|
+
|
42
|
+
# => (whatever merged output is from the files)
|
43
|
+
|
44
|
+
|
45
|
+
## Development
|
46
|
+
|
47
|
+
KDiff3 exists in ext/kdiff3 using a git subtree
|
48
|
+
- KDiff3 repo here: https://github.com/NullVoxPopuli/kdiff3
|
49
|
+
- More info on subtrees here: https://medium.com/@v/git-subtrees-a-tutorial-6ff568381844
|
50
|
+
|
51
|
+
## Contributing
|
52
|
+
- Fork, Test, Pull Request :-)
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/bin/kdiff3
ADDED
Binary file
|
data/kdiff3.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "kdiff3/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "kdiff3"
|
9
|
+
s.version = KDiff3::VERSION
|
10
|
+
s.platform = Gem::Platform::RUBY
|
11
|
+
s.license = "GNU GPL v2"
|
12
|
+
s.authors = ["L. Preston Sego III"]
|
13
|
+
s.email = "LPSego3+dev@gmail.com"
|
14
|
+
s.homepage = "https://github.com/NullVoxPopuli/kdiff3-rb"
|
15
|
+
s.summary = "kdiff3-#{KDiff3::VERSION}"
|
16
|
+
s.description = "Ruby wrapper for the kdiff3 mergetool"
|
17
|
+
|
18
|
+
|
19
|
+
s.files = `git ls-files`.split($/)
|
20
|
+
s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
21
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
22
|
+
s.require_paths = ["lib"]
|
23
|
+
|
24
|
+
s.extensions = ['ext/kdiff3/extconf.rb']
|
25
|
+
|
26
|
+
s.add_dependency 'activesupport', '>= 3.2'
|
27
|
+
|
28
|
+
s.add_development_dependency "bundler", '>= 1.6.0'
|
29
|
+
s.add_development_dependency "awesome_print", '>= 1.2'
|
30
|
+
s.add_development_dependency "rspec", '>= 3.1.0'
|
31
|
+
s.add_development_dependency "pry-byebug", '>= 2.0.0'
|
32
|
+
s.add_development_dependency "codeclimate-test-reporter", '>= 0.4.3'
|
33
|
+
|
34
|
+
end
|
data/lib/kdiff3.rb
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
# string.present?
|
2
|
+
require 'active_support/core_ext/string'
|
3
|
+
|
4
|
+
module KDiff3
|
5
|
+
|
6
|
+
# list of created Tempfiles
|
7
|
+
TEMPFILES = []
|
8
|
+
# so we don't accidentally mess up formatting when we remove these later,
|
9
|
+
# we need to have a weird, non-stand sequence of characters to search
|
10
|
+
# and replace
|
11
|
+
NEWLINE = "\n\-"
|
12
|
+
|
13
|
+
|
14
|
+
# performs a 3 way merge between base, a, and b
|
15
|
+
# a 2 way merge will be performed if either yours
|
16
|
+
# or theirs are left out
|
17
|
+
#
|
18
|
+
# @param [String] base string or file path for common ancestor
|
19
|
+
# @param [String] yours string or file path for your changes
|
20
|
+
# @param [String] theirs string or file path for their changes
|
21
|
+
# @param [Boolean] html whether or not use an HTML diffing technique
|
22
|
+
# @return [String] result of merge
|
23
|
+
def self.merge(base: nil, yours: nil, theirs: nil, html: false)
|
24
|
+
raise ArgumentError.new('base is required') unless base.present?
|
25
|
+
raise ArgumentError.new('yours and/or theirs required') unless yours || theirs
|
26
|
+
|
27
|
+
# since HTML is often compressed to conserve transfer space, and therefore
|
28
|
+
# has few lines, we need to split up the HTML in to a multi-lined document
|
29
|
+
if html
|
30
|
+
base = add_new_lines(base)
|
31
|
+
yours = add_new_lines(yours)
|
32
|
+
theirs = add_new_lines(theirs)
|
33
|
+
end
|
34
|
+
|
35
|
+
base_path = tempfile(text: base, name: 'base')
|
36
|
+
your_path = tempfile(text: yours, name: 'yours')
|
37
|
+
their_path = tempfile(text: theirs, name: 'theirs')
|
38
|
+
output_path = tempfile(name: 'output')
|
39
|
+
|
40
|
+
# we don't need these open for anything
|
41
|
+
close_tempfiles
|
42
|
+
|
43
|
+
# the heavy lifting, courtesy of kdiff3
|
44
|
+
exit_code = run("#{base_path} #{your_path} #{their_path} -m --auto --fail -o #{output_path}")
|
45
|
+
conflicts_exist = exit_code == 1
|
46
|
+
|
47
|
+
result = IO.read(output_path) unless conflicts_exist
|
48
|
+
|
49
|
+
# clean up
|
50
|
+
delete_tempfiles
|
51
|
+
|
52
|
+
raise RuntimeError.new("Conflicts exist and could not be resolved") if conflicts_exist
|
53
|
+
|
54
|
+
if html
|
55
|
+
# remove the NEWLINES
|
56
|
+
result.gsub!(NEWLINE, "")
|
57
|
+
end
|
58
|
+
|
59
|
+
result
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# @param [String] name of the file
|
65
|
+
# @param [String] text content of file or path
|
66
|
+
# @return [String] path of the Tempfile or Pre-existing file
|
67
|
+
def self.tempfile(text: nil, name: nil)
|
68
|
+
result = ""
|
69
|
+
|
70
|
+
# if the file already exists,
|
71
|
+
# don't add it to the tempfile list,
|
72
|
+
# as we don't want it to be deleted
|
73
|
+
if text && is_file_path?(text)
|
74
|
+
result = text
|
75
|
+
else
|
76
|
+
t = Tempfile.new(name)
|
77
|
+
t << text if text
|
78
|
+
TEMPFILES << t
|
79
|
+
result = t.path
|
80
|
+
end
|
81
|
+
|
82
|
+
result
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.close_tempfiles
|
86
|
+
TEMPFILES.map(&:close)
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.delete_tempfiles
|
90
|
+
TEMPFILES.map(&:delete)
|
91
|
+
end
|
92
|
+
|
93
|
+
# @param [String] path can be a file path or arbitrary string
|
94
|
+
# @return [Boolean] if the given string is a path to a file
|
95
|
+
def self.is_file_path?(path)
|
96
|
+
File.exist?(path)
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.run(args)
|
100
|
+
%x(
|
101
|
+
#{kdiff3_path} #{args}
|
102
|
+
)
|
103
|
+
$?.exitstatus
|
104
|
+
end
|
105
|
+
|
106
|
+
# ensures the local copy of kdiff3 is present, if not, download and compile it
|
107
|
+
def self.kdiff3_path
|
108
|
+
current_folder = File.dirname(__FILE__)
|
109
|
+
path = "#{current_folder}/../ext/kdiff3/releaseQt/kdiff3"
|
110
|
+
|
111
|
+
unless File.exist?(path)
|
112
|
+
build_kdiff3
|
113
|
+
end
|
114
|
+
|
115
|
+
path
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.build_kdiff3
|
119
|
+
current_folder = File.dirname(__FILE__)
|
120
|
+
kdiff3_repo_path = "#{current_folder}/../ext/kdiff3"
|
121
|
+
|
122
|
+
if File.exist?(kdiff3_repo_path)
|
123
|
+
%x(
|
124
|
+
git pull # origin optionally-fail-on-conflict
|
125
|
+
)
|
126
|
+
else
|
127
|
+
%x(
|
128
|
+
git clone git@github.com:NullVoxPopuli/kdiff3.git #{kdiff3_repo_path}
|
129
|
+
cd #{kdiff3_repo_path} && git checkout optionally-fail-on-conflict
|
130
|
+
)
|
131
|
+
end
|
132
|
+
|
133
|
+
# build
|
134
|
+
%x(
|
135
|
+
cd #{kdiff3_repo_path} && ./configure qt4
|
136
|
+
)
|
137
|
+
end
|
138
|
+
|
139
|
+
# add newlines after every tag, and every character
|
140
|
+
def self.add_new_lines(text)
|
141
|
+
text = self.add_new_lines_to_non_tags(text)
|
142
|
+
text = self.add_new_lines_after_tags(text)
|
143
|
+
|
144
|
+
# trim accidental blank lines
|
145
|
+
text.gsub!("#{NEWLINE}#{NEWLINE}", NEWLINE)
|
146
|
+
|
147
|
+
text
|
148
|
+
end
|
149
|
+
|
150
|
+
# http://www.rubular.com/r/N2AHZgpPum
|
151
|
+
# http://stackoverflow.com/questions/7540489/javascript-regex-match-text-not-part-of-a-html-tag
|
152
|
+
def self.add_new_lines_after_tags(text)
|
153
|
+
tag_selection_regex = /<[^>]*>/
|
154
|
+
text.gsub(tag_selection_regex) do |match|
|
155
|
+
match << NEWLINE
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# http://www.rubular.com/r/mpX6Ee2r0k
|
160
|
+
# http://stackoverflow.com/questions/18621568/regex-replace-text-outside-html-tags
|
161
|
+
def self.add_new_lines_to_non_tags(text)
|
162
|
+
non_tags = /(?<=^|>)[^><]+?(?=<|$)/
|
163
|
+
# non_tags = /([^<>]+)(?![^<]*>|[^<>]*<\/)/
|
164
|
+
text.gsub(non_tags) do |match|
|
165
|
+
# consecutive words
|
166
|
+
match.gsub!(" ", " #{NEWLINE}")
|
167
|
+
# end with NEWLINE
|
168
|
+
match << NEWLINE
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
end
|
data/spec/kdiff3_spec.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe KDiff3 do
|
4
|
+
before(:each) do
|
5
|
+
if ENV['USE_BIN_KDIFF3']
|
6
|
+
current_folder = File.dirname(__FILE__)
|
7
|
+
# may only work on Ubuntu 14.04?
|
8
|
+
kdiff3_bin_path = "#{current_folder}/../bin/kdiff3"
|
9
|
+
allow(KDiff3).to receive(:kdiff3_path).and_return(kdiff3_bin_path)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
describe 'merge' do
|
15
|
+
it { expect{KDiff3.merge}.to raise_error(ArgumentError) }
|
16
|
+
it { expect{KDiff3.merge(base: "base")}.to raise_error(ArgumentError) }
|
17
|
+
it { expect{KDiff3.merge(base: "base", yours: "base")}.to_not raise_error }
|
18
|
+
it { expect{KDiff3.merge(base: "base", yours: "base", theirs: "base")}.to_not raise_error }
|
19
|
+
|
20
|
+
describe 'by line' do
|
21
|
+
before(:all) do
|
22
|
+
@base = "#{File.dirname(__FILE__)}/support/base"
|
23
|
+
@a = "#{File.dirname(__FILE__)}/support/a"
|
24
|
+
@b = "#{File.dirname(__FILE__)}/support/b"
|
25
|
+
@expected = "#{File.dirname(__FILE__)}/support/expected"
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'completes a 3-way merge' do
|
29
|
+
result = KDiff3.merge(
|
30
|
+
base: @base,
|
31
|
+
yours: @a,
|
32
|
+
theirs: @b
|
33
|
+
)
|
34
|
+
expected = IO.read(@expected)
|
35
|
+
|
36
|
+
expect(result).to eq expected
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'by word (the html option)' do
|
41
|
+
before(:all) do
|
42
|
+
@base = "<p>1,2,<p>1,<p>2,</p></p>3</p>"
|
43
|
+
@a = "<p>1,2,<p>1,</p>3</p>"
|
44
|
+
@b = "<p>1,2,<p><p>2,</p></p>3</p>"
|
45
|
+
@expected = "<p>1,2,<p></p>3</p>"
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'completes a 3-way merge' do
|
49
|
+
result = KDiff3.merge(
|
50
|
+
base: @base,
|
51
|
+
yours: @a,
|
52
|
+
theirs: @b,
|
53
|
+
html: true
|
54
|
+
)
|
55
|
+
expect(result).to eq @expected
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "raises an error if there are conflicts" do
|
60
|
+
it { expect{ KDiff3.merge(base: 'a b', yours: '1 b', theirs: '2 b', html: true) }.to raise_error }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe 'private methods' do
|
65
|
+
|
66
|
+
describe 'is_file?' do
|
67
|
+
it { expect(KDiff3.send(:is_file_path?, __FILE__)).to eq true }
|
68
|
+
it { expect(KDiff3.send(:is_file_path?, "#{File.dirname(__FILE__)}/spec_helper.rb")).to eq true }
|
69
|
+
it { expect(KDiff3.send(:is_file_path?, "not a file")).to eq false }
|
70
|
+
it { expect(KDiff3.send(:is_file_path?, "not/a/file")).to eq false }
|
71
|
+
end
|
72
|
+
|
73
|
+
describe 'tempfile' do
|
74
|
+
it 'returns path when path is a file' do
|
75
|
+
expect(KDiff3.send(:tempfile, text: __FILE__, name: 'rspec')).to eq __FILE__
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'creates a tempfile' do
|
79
|
+
expect{
|
80
|
+
KDiff3.send(:tempfile, name: 'base')
|
81
|
+
}.to change(KDiff3::TEMPFILES, :size).by 1
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'creates a tempfile with content' do
|
85
|
+
path = KDiff3.send(:tempfile, name: 'base', text: 'content')
|
86
|
+
# file must be closed before reading
|
87
|
+
KDiff3.send(:close_tempfiles)
|
88
|
+
expect(IO.read(path)).to eq 'content'
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe 'kdiff3_path' do
|
93
|
+
it "hasn't compiled kdiff3 yet"
|
94
|
+
it "has compiled kdiff3"
|
95
|
+
it "updates the kdiff3 repo"
|
96
|
+
end
|
97
|
+
|
98
|
+
describe 'add_new_lines' do
|
99
|
+
after(:each) do
|
100
|
+
result = KDiff3.send(:add_new_lines, @text)
|
101
|
+
# remove special character
|
102
|
+
# - this determines our line breaks from pre-existing ones
|
103
|
+
result.gsub!(KDiff3::NEWLINE, "\n")
|
104
|
+
expect(result).to eq @expected
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'adds new lines to text' do
|
108
|
+
@text = "something like this"
|
109
|
+
@expected = "something \nlike \nthis\n"
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'adds new lines to html' do
|
113
|
+
@text = "<p>something maybe<p><span>like <a>this</a></span></p></p>"
|
114
|
+
@expected = "<p>\nsomething \nmaybe\n<p>\n<span>\nlike \n<a>\nthis\n</a>\n</span>\n</p>\n</p>\n"
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
require "rubygems"
|
3
|
+
require "bundler/setup"
|
4
|
+
|
5
|
+
require "pry-byebug" # binding.pry # to debug!
|
6
|
+
require 'awesome_print' # ap object # to pretty print
|
7
|
+
|
8
|
+
# Coverage
|
9
|
+
require "codeclimate-test-reporter"
|
10
|
+
# ENV['CODECLIMATE_REPO_TOKEN'] = ""
|
11
|
+
CodeClimate::TestReporter.start
|
12
|
+
|
13
|
+
# This Gem
|
14
|
+
require "kdiff3"
|
15
|
+
|
16
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
17
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
18
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
19
|
+
# loaded once.
|
20
|
+
#
|
21
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
22
|
+
RSpec.configure do |config|
|
23
|
+
config.run_all_when_everything_filtered = true
|
24
|
+
config.filter_run :focus
|
25
|
+
|
26
|
+
# Run specs in random order to surface order dependencies. If you find an
|
27
|
+
# order dependency and want to debug it, you can fix the order by providing
|
28
|
+
# the seed, which is printed after each run.
|
29
|
+
# --seed 1234
|
30
|
+
config.order = 'random'
|
31
|
+
end
|
data/spec/support/b
ADDED