vcs_toolkit 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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