vcs_toolkit 0.1.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.
@@ -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.
@@ -0,0 +1,8 @@
1
+ VCSToolkit
2
+ ==========
3
+
4
+ This gem implements the most basic tasks and objects needed
5
+ to build a fully working Version Control System.
6
+
7
+ The sample VCS that is being implemented using the VCSToolkit
8
+ is [SCV](https://github.com/stormbreakerbg/scv).
@@ -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,10 @@
1
+ module VCSToolkit
2
+ class VCSToolkitError < StandardError
3
+ end
4
+
5
+ class InvalidObjectError < VCSToolkitError
6
+ end
7
+
8
+ class UnknownLabelError < VCSToolkitError
9
+ end
10
+ 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