vcs_toolkit 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +8 -0
- data/lib/vcs_toolkit.rb +16 -0
- data/lib/vcs_toolkit/conflict.rb +55 -0
- data/lib/vcs_toolkit/diff.rb +61 -0
- data/lib/vcs_toolkit/exceptions.rb +10 -0
- data/lib/vcs_toolkit/file_store.rb +139 -0
- data/lib/vcs_toolkit/merge.rb +96 -0
- data/lib/vcs_toolkit/object_store.rb +51 -0
- data/lib/vcs_toolkit/objects.rb +5 -0
- data/lib/vcs_toolkit/objects/blob.rb +37 -0
- data/lib/vcs_toolkit/objects/commit.rb +76 -0
- data/lib/vcs_toolkit/objects/label.rb +29 -0
- data/lib/vcs_toolkit/objects/object.rb +47 -0
- data/lib/vcs_toolkit/objects/tree.rb +81 -0
- data/lib/vcs_toolkit/repository.rb +302 -0
- data/lib/vcs_toolkit/serializable.rb +23 -0
- data/lib/vcs_toolkit/utils/hashable_object.rb +45 -0
- data/lib/vcs_toolkit/utils/memory_file_store.rb +101 -0
- data/lib/vcs_toolkit/utils/status.rb +60 -0
- data/lib/vcs_toolkit/utils/sync.rb +73 -0
- data/lib/vcs_toolkit/version.rb +3 -0
- metadata +124 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2f7c92235e805cd0715922732c68622c509ea05e
|
4
|
+
data.tar.gz: f72a425fcf33a19ff060fb20a16e4818730c82e1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2b8c306c6e8ce861ae375f2090abbc7f45cbcba7c3b09d4663a86e03a185a34a8a9c7ae2ebc5bd775719cd515b6005b070e01aa4b0cac51fc595116f9bfdfdc9
|
7
|
+
data.tar.gz: 7cee17342b67f593b5abedbdc4ccd20684745a5f224c4269f2169a9c025bfbb935f850416d7478508e28d4de0ec206fb375716d549107916413e6bda28b8c266
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Georgy Angelov
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
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, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
data/lib/vcs_toolkit.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'vcs_toolkit/version'
|
2
|
+
require 'vcs_toolkit/exceptions'
|
3
|
+
|
4
|
+
require 'vcs_toolkit/diff'
|
5
|
+
require 'vcs_toolkit/conflict'
|
6
|
+
require 'vcs_toolkit/merge'
|
7
|
+
require 'vcs_toolkit/objects'
|
8
|
+
|
9
|
+
require 'vcs_toolkit/object_store'
|
10
|
+
require 'vcs_toolkit/file_store'
|
11
|
+
|
12
|
+
require 'vcs_toolkit/repository'
|
13
|
+
|
14
|
+
require 'vcs_toolkit/utils/memory_file_store'
|
15
|
+
require 'vcs_toolkit/utils/status'
|
16
|
+
require 'vcs_toolkit/utils/sync'
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'diff-lcs'
|
2
|
+
require 'vcs_toolkit/diff'
|
3
|
+
|
4
|
+
# I know monkey patching is evil but
|
5
|
+
# this is used only for consistency in arrays
|
6
|
+
# that contain both Diff::LCS::Change and Conflict.
|
7
|
+
# So we can do this: `changes.any? { |change| change.conflict? }`
|
8
|
+
# instead of `changes.any? { |change| change.is_a? VCSToolkit::Conflict }`
|
9
|
+
class Diff::LCS::Change
|
10
|
+
def conflict?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module VCSToolkit
|
16
|
+
class Conflict
|
17
|
+
attr_reader :diff_one, :diff_two
|
18
|
+
|
19
|
+
def initialize(diff_one, diff_two)
|
20
|
+
@diff_one = diff_one
|
21
|
+
@diff_two = diff_two
|
22
|
+
end
|
23
|
+
|
24
|
+
def conflict?
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
# These methods are used for compatibility with ::Diff::LCS::Change
|
29
|
+
# so we can do this: `changes.any? { |change| change.adding? }`
|
30
|
+
# instead of `changes.any? { |change| (not change.is_a? VCSToolkit::Conflict) and change.adding? }`
|
31
|
+
def adding?
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
def deleting?
|
36
|
+
false
|
37
|
+
end
|
38
|
+
|
39
|
+
def unchanged?
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
def changed?
|
44
|
+
false
|
45
|
+
end
|
46
|
+
|
47
|
+
def finished_a?
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
def finished_b?
|
52
|
+
false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'diff-lcs'
|
2
|
+
|
3
|
+
module VCSToolkit
|
4
|
+
class Diff
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize(changes)
|
8
|
+
@changes = changes
|
9
|
+
end
|
10
|
+
|
11
|
+
def has_changes?
|
12
|
+
not @changes.all?(&:unchanged?)
|
13
|
+
end
|
14
|
+
|
15
|
+
def has_conflicts?
|
16
|
+
@changes.any?(&:conflict?)
|
17
|
+
end
|
18
|
+
|
19
|
+
def each(&block)
|
20
|
+
@changes.each &block
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
flat_map do |change|
|
25
|
+
if change.unchanged?
|
26
|
+
[change.new_element]
|
27
|
+
elsif change.deleting?
|
28
|
+
["-#{change.old_element}"]
|
29
|
+
elsif change.adding?
|
30
|
+
["+#{change.new_element}"]
|
31
|
+
elsif change.changed?
|
32
|
+
["-#{change.old_element}", "+#{change.new_element}"]
|
33
|
+
else
|
34
|
+
raise "Unknown change in the diff #{change.action}"
|
35
|
+
end
|
36
|
+
end.join ''
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Reconstruct the new sequence from the diff
|
41
|
+
#
|
42
|
+
def new_content(conflict_start='<<<', conflict_switch='>>>', conflict_end='===')
|
43
|
+
flat_map do |change|
|
44
|
+
if change.conflict?
|
45
|
+
version_one = change.diff_one.new_content(conflict_start, conflict_switch, conflict_end)
|
46
|
+
version_two = change.diff_two.new_content(conflict_start, conflict_switch, conflict_end)
|
47
|
+
|
48
|
+
[conflict_start] + version_one + [conflict_switch] + version_two + [conflict_end]
|
49
|
+
elsif change.deleting?
|
50
|
+
[]
|
51
|
+
else
|
52
|
+
[change.new_element]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.from_sequences(sequence_one, sequence_two)
|
58
|
+
new ::Diff::LCS.sdiff(sequence_one, sequence_two)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'vcs_toolkit/exceptions'
|
2
|
+
|
3
|
+
module VCSToolkit
|
4
|
+
##
|
5
|
+
# This class is used to implement a custom storage provider for
|
6
|
+
# files.
|
7
|
+
#
|
8
|
+
class FileStore
|
9
|
+
##
|
10
|
+
# Implement this to store a specific file in persistent storage.
|
11
|
+
#
|
12
|
+
def store(path, blob)
|
13
|
+
raise NotImplementedError, 'You must implement FileStore#store'
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Implement this to retrieve a file with the specified name.
|
18
|
+
#
|
19
|
+
def fetch(path)
|
20
|
+
raise NotImplementedError, 'You must implement FileStore#fetch'
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Implement this to delete the specified file
|
25
|
+
#
|
26
|
+
def delete_file(path)
|
27
|
+
raise NotImplementedError, 'You must implement FileStore#delete_file'
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Implement this to delete the specified directory.
|
32
|
+
# This method will be called only on empty directories.
|
33
|
+
# Use #delete if you want to recursively delete a directory.
|
34
|
+
#
|
35
|
+
def delete_dir(path)
|
36
|
+
raise NotImplementedError, 'You must implement FileStore#delete_dir'
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Implement this to detect wether a file with that name exists.
|
41
|
+
#
|
42
|
+
def file?(path)
|
43
|
+
raise NotImplementedError, 'You must implement FileStore#file?'
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Implement this to detect wether a directory with that name exists.
|
48
|
+
#
|
49
|
+
def directory?(path)
|
50
|
+
raise NotImplementedError, 'You must implement FileStore#directory?'
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Implement this to compare a file and a blob object.
|
55
|
+
# It should return boolean value to indicate wether the file is different.
|
56
|
+
#
|
57
|
+
# The hash algorithm should be the same one used in `Objects::*`.
|
58
|
+
#
|
59
|
+
def changed?(path, blob)
|
60
|
+
raise NotImplementedError, 'You must implement FileStore#changed?'
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Implement this to enumerate over all files in the given directory.
|
65
|
+
#
|
66
|
+
# The order of enumeration doesn't matter.
|
67
|
+
#
|
68
|
+
def each_file(path='')
|
69
|
+
raise NotImplementedError, 'You must implement FileStore#each_file'
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Implement this to enumerate over all files.
|
74
|
+
#
|
75
|
+
# The order of enumeration doesn't matter.
|
76
|
+
#
|
77
|
+
def each_directory(path='')
|
78
|
+
raise NotImplementedError, 'You must implement FileStore#each_directory'
|
79
|
+
end
|
80
|
+
|
81
|
+
def exist?(path)
|
82
|
+
file?(path) or directory?(path)
|
83
|
+
end
|
84
|
+
|
85
|
+
def files(path='', ignore: [])
|
86
|
+
enum_for(:each_file, path).reject { |file| ignored? file, ignore }
|
87
|
+
end
|
88
|
+
|
89
|
+
def directories(path='', ignore: [])
|
90
|
+
enum_for(:each_directory, path).reject { |file| ignored? file, ignore }
|
91
|
+
end
|
92
|
+
|
93
|
+
def all_files(path='', ignore: [])
|
94
|
+
enum_for :yield_all_files, path, ignore: ignore
|
95
|
+
end
|
96
|
+
|
97
|
+
def delete(path)
|
98
|
+
if directory? path
|
99
|
+
files(path).each do |file|
|
100
|
+
delete_file File.join(path, file)
|
101
|
+
end
|
102
|
+
|
103
|
+
directories(path).each do |directory|
|
104
|
+
delete File.join(path, directory)
|
105
|
+
end
|
106
|
+
|
107
|
+
delete_dir path
|
108
|
+
else
|
109
|
+
delete_file path
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def yield_all_files(path='', ignore: [], &block)
|
116
|
+
files(path).reject { |path| ignored? path, ignore }.each &block
|
117
|
+
|
118
|
+
directories(path).each do |dir_name|
|
119
|
+
dir_path = File.join(path, dir_name).sub(/^\/+/, '')
|
120
|
+
|
121
|
+
all_files(dir_path).each do |file|
|
122
|
+
file_path = File.join(dir_name, file)
|
123
|
+
|
124
|
+
yield file_path unless ignored?(file_path, ignore) or ignored?(file.split('/').last, ignore)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def ignored?(path, ignores)
|
130
|
+
ignores.any? do |ignore|
|
131
|
+
if ignore.is_a? Regexp
|
132
|
+
ignore =~ path
|
133
|
+
else
|
134
|
+
ignore == path
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'vcs_toolkit/diff'
|
2
|
+
require 'vcs_toolkit/conflict'
|
3
|
+
|
4
|
+
module VCSToolkit
|
5
|
+
module Merge
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def three_way(sequence_one, sequence_two, sequence_three)
|
9
|
+
diff_one = Diff.from_sequences(sequence_one, sequence_two)
|
10
|
+
diff_two = Diff.from_sequences(sequence_one, sequence_three)
|
11
|
+
|
12
|
+
combined_changes = combine_diffs diff_one, diff_two
|
13
|
+
merge_changes = combined_changes.flat_map do |line_number, (changeset_one, changeset_two)|
|
14
|
+
if changeset_one.all?(&:unchanged?)
|
15
|
+
changeset_two
|
16
|
+
elsif changeset_two.all?(&:unchanged?)
|
17
|
+
changeset_one
|
18
|
+
elsif same_changes(changeset_one, changeset_two)
|
19
|
+
# TODO: Check if they can be the same but one is split in two parts
|
20
|
+
changeset_one
|
21
|
+
else
|
22
|
+
extract_conflict(changeset_one, changeset_two)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Diff.new merge_changes
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
##
|
32
|
+
# Return common prefix and suffix of the two changesets
|
33
|
+
# in the following format:
|
34
|
+
#
|
35
|
+
# [<common_prefix_list>, Conflict(Diff, Diff), <common_suffix_list>]
|
36
|
+
#
|
37
|
+
def extract_conflict(changeset_one, changeset_two)
|
38
|
+
common_start = changeset_one.zip(changeset_two).take_while do |change_one, change_two|
|
39
|
+
same_change(change_one, change_two)
|
40
|
+
end
|
41
|
+
|
42
|
+
common_end = changeset_one.reverse.zip(changeset_two.reverse).take_while do |change_one, change_two|
|
43
|
+
same_change(change_one, change_two)
|
44
|
+
end
|
45
|
+
|
46
|
+
common_size = common_end.size + common_start.size
|
47
|
+
|
48
|
+
diff_one = Diff.new changeset_one.slice(common_start.size, changeset_one.size - common_size)
|
49
|
+
diff_two = Diff.new changeset_two.slice(common_start.size, changeset_two.size - common_size)
|
50
|
+
|
51
|
+
common_start.map(&:first) + [Conflict.new(diff_one, diff_two)] + common_end.map(&:first)
|
52
|
+
end
|
53
|
+
|
54
|
+
def same_changes(changeset_one, changeset_two)
|
55
|
+
changeset_one.size == changeset_two.size and
|
56
|
+
changeset_one.zip(changeset_two).all? do |change_one, change_two|
|
57
|
+
same_change(change_one, change_two)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def same_change(change_one, change_two)
|
62
|
+
# new_position is not compared deliberately
|
63
|
+
# because any additions on a file will increase new_position
|
64
|
+
# and because of that it will cause conflicts even
|
65
|
+
# if the changes are the same
|
66
|
+
change_one.action == change_two.action and
|
67
|
+
change_one.old_position == change_two.old_position and
|
68
|
+
change_one.old_element == change_two.old_element and
|
69
|
+
change_one.new_element == change_two.new_element
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Group changes by their old index.
|
74
|
+
#
|
75
|
+
# The structure is as follows:
|
76
|
+
#
|
77
|
+
# {
|
78
|
+
# <line_number_on_ancestor> => [
|
79
|
+
# [ <change>, ... ], # The changes in the first file
|
80
|
+
# [ <change>, ... ] # The changes in the second file
|
81
|
+
# ]
|
82
|
+
# }
|
83
|
+
def combine_diffs(diff_one, diff_two)
|
84
|
+
Hash.new { |hash, key| hash[key] = [[], []] }.tap do |combined_diff|
|
85
|
+
diff_one.each do |change|
|
86
|
+
combined_diff[change.old_position].first << change
|
87
|
+
end
|
88
|
+
|
89
|
+
diff_two.each do |change|
|
90
|
+
combined_diff[change.old_position].last << change
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'vcs_toolkit/exceptions'
|
2
|
+
|
3
|
+
module VCSToolkit
|
4
|
+
##
|
5
|
+
# This class is used to implement a custom storage provider for
|
6
|
+
# objects.
|
7
|
+
#
|
8
|
+
# The methods are compatible with the interface of Hash so that
|
9
|
+
# a simple Hash can be used instead of a full-featured object manager.
|
10
|
+
#
|
11
|
+
class ObjectStore
|
12
|
+
include Enumerable
|
13
|
+
|
14
|
+
##
|
15
|
+
# Implement this to store a specific object in persistent storage.
|
16
|
+
#
|
17
|
+
# object_id is here for compatibility with Hash
|
18
|
+
#
|
19
|
+
def store(object_id, object)
|
20
|
+
raise NotImplementedError, 'You must implement ObjectStore#store'
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Implement this to retrieve an object with the specified object_id.
|
25
|
+
# It should detect the object type and instantiate the specific
|
26
|
+
# object class (or a subclass of it).
|
27
|
+
#
|
28
|
+
def fetch(object_id)
|
29
|
+
raise NotImplementedError, 'You must implement ObjectStore#fetch'
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Implement this to detect wether a object with that object_id exists.
|
34
|
+
#
|
35
|
+
def key?(object_id)
|
36
|
+
raise NotImplementedError, 'You must implement ObjectStore#key?'
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Implement this to enumerate over all objects that
|
41
|
+
# have Object#named? set to true.
|
42
|
+
# Even enumeration of all objects is possible, but is not
|
43
|
+
# neccessary.
|
44
|
+
#
|
45
|
+
# The order of enumeration doesn't matter.
|
46
|
+
#
|
47
|
+
def each
|
48
|
+
raise NotImplementedError, 'You must implement ObjectStore#each'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|