scvcs 0.2.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 +7 -0
- data/LICENSE +20 -0
- data/README.md +152 -0
- data/bin/commands/branch.rb +86 -0
- data/bin/commands/commit.rb +88 -0
- data/bin/commands/config.rb +35 -0
- data/bin/commands/diff.rb +19 -0
- data/bin/commands/history.rb +19 -0
- data/bin/commands/init.rb +17 -0
- data/bin/commands/merge.rb +56 -0
- data/bin/commands/remote.rb +157 -0
- data/bin/commands/restore.rb +17 -0
- data/bin/commands/server.rb +10 -0
- data/bin/commands/status.rb +52 -0
- data/bin/formatters/changeset.rb +43 -0
- data/bin/formatters/hierarchy.rb +43 -0
- data/bin/formatters/merge_report.rb +25 -0
- data/bin/scv +107 -0
- data/bin/utilities/output.rb +38 -0
- data/bin/utilities/shell.rb +15 -0
- data/lib/scv.rb +13 -0
- data/lib/scv/config.rb +52 -0
- data/lib/scv/file_store.rb +81 -0
- data/lib/scv/http_file_store.rb +69 -0
- data/lib/scv/object_store.rb +125 -0
- data/lib/scv/objects.rb +4 -0
- data/lib/scv/objects/blob.rb +10 -0
- data/lib/scv/objects/commit.rb +34 -0
- data/lib/scv/objects/label.rb +10 -0
- data/lib/scv/objects/tree.rb +10 -0
- data/lib/scv/repository.rb +124 -0
- data/lib/scv/server.rb +66 -0
- data/lib/scv/version.rb +3 -0
- metadata +218 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
module Output
|
2
|
+
class Pager
|
3
|
+
attr_reader :pager
|
4
|
+
|
5
|
+
def initialize(raw: false)
|
6
|
+
if raw or not $stdout.isatty
|
7
|
+
@pager = nil
|
8
|
+
elsif Shell.command_exist? 'less'
|
9
|
+
@pager = 'less -R -F -X'
|
10
|
+
elsif Shell.command_exist? 'more'
|
11
|
+
@pager = 'more'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def print_block
|
16
|
+
return yield($stdout) if pager.nil?
|
17
|
+
|
18
|
+
IO.popen(pager, 'w') do |stream|
|
19
|
+
begin
|
20
|
+
$stdout = stream
|
21
|
+
yield stream
|
22
|
+
ensure
|
23
|
+
$stdout = STDOUT
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def output(raw: false, &block)
|
30
|
+
Pager.new(raw: raw).print_block &block
|
31
|
+
end
|
32
|
+
|
33
|
+
extend self
|
34
|
+
end
|
35
|
+
|
36
|
+
class Object
|
37
|
+
include Output
|
38
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Shell
|
2
|
+
##
|
3
|
+
# Detect if a command exists and can be executed using
|
4
|
+
# the OS shell.
|
5
|
+
#
|
6
|
+
# Based on http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby/5471032#5471032
|
7
|
+
#
|
8
|
+
def self.command_exist?(name)
|
9
|
+
extensions = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
10
|
+
|
11
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).find do |path|
|
12
|
+
extensions.find { |ext| File.executable? File.join(path, "#{name}#{ext}") }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/scv.rb
ADDED
data/lib/scv/config.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module SCV
|
4
|
+
class Config
|
5
|
+
attr_accessor :data
|
6
|
+
|
7
|
+
def initialize(file_store, file_name)
|
8
|
+
@file_store = file_store
|
9
|
+
@file_name = file_name
|
10
|
+
|
11
|
+
@data = {}
|
12
|
+
load
|
13
|
+
end
|
14
|
+
|
15
|
+
def load
|
16
|
+
@data = YAML.load @file_store.fetch(@file_name) if @file_store.file? @file_name
|
17
|
+
end
|
18
|
+
|
19
|
+
def save
|
20
|
+
@file_store.store @file_name, @data.to_yaml
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](key)
|
24
|
+
hash, key = resolve_key key
|
25
|
+
hash[key]
|
26
|
+
end
|
27
|
+
|
28
|
+
def []=(key, value)
|
29
|
+
hash, key = resolve_key key, create: true
|
30
|
+
hash[key] = value
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete(key)
|
34
|
+
hash, key = resolve_key key, create: false
|
35
|
+
hash.delete key
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def resolve_key(hash=@data, key, create: false)
|
41
|
+
key, rest = key.split '.', 2
|
42
|
+
|
43
|
+
if rest.nil?
|
44
|
+
[hash, key]
|
45
|
+
else
|
46
|
+
hash[key] = {} if create and not hash.key? key
|
47
|
+
|
48
|
+
resolve_key hash[key], rest, create: create
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'vcs_toolkit/file_store'
|
2
|
+
|
3
|
+
module SCV
|
4
|
+
|
5
|
+
class FileStore < VCSToolkit::FileStore
|
6
|
+
def initialize(path)
|
7
|
+
raise 'The path is not a directory' unless File.directory? path
|
8
|
+
|
9
|
+
@base_path = path
|
10
|
+
end
|
11
|
+
|
12
|
+
def store(path, content)
|
13
|
+
path = path_for path
|
14
|
+
|
15
|
+
FileUtils.mkdir_p File.dirname(path)
|
16
|
+
|
17
|
+
File.open(path, 'wb') do |file|
|
18
|
+
file.write(content)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def fetch(path)
|
23
|
+
raise KeyError unless file? path
|
24
|
+
|
25
|
+
File.open(path_for(path), 'rb') do |file|
|
26
|
+
file.read
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete_file(path)
|
31
|
+
File.unlink path_for(path)
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete_dir(path)
|
35
|
+
Dir.unlink path_for(path)
|
36
|
+
end
|
37
|
+
|
38
|
+
def file?(path)
|
39
|
+
File.file? path_for(path)
|
40
|
+
end
|
41
|
+
|
42
|
+
def directory?(path)
|
43
|
+
File.directory? path_for(path)
|
44
|
+
end
|
45
|
+
|
46
|
+
def changed?(path, blob)
|
47
|
+
fetch(path) != blob.content
|
48
|
+
end
|
49
|
+
|
50
|
+
def each_file(path='', &block)
|
51
|
+
path = '.' if path.empty?
|
52
|
+
|
53
|
+
Dir.entries(path_for(path)).select do |file_name|
|
54
|
+
file? path_for(file_name, path)
|
55
|
+
end.each(&block)
|
56
|
+
end
|
57
|
+
|
58
|
+
def each_directory(path='', &block)
|
59
|
+
path = '.' if path.empty?
|
60
|
+
|
61
|
+
Dir.entries(path_for(path)).select do |name|
|
62
|
+
not %w(. ..).include? name and directory? path_for(name, path)
|
63
|
+
end.each(&block)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def path_for(path, base=@base_path)
|
69
|
+
if base.empty? or base == '.'
|
70
|
+
if path.empty?
|
71
|
+
'.'
|
72
|
+
else
|
73
|
+
path
|
74
|
+
end
|
75
|
+
else
|
76
|
+
File.join(base, path)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'vcs_toolkit/file_store'
|
3
|
+
|
4
|
+
module SCV
|
5
|
+
|
6
|
+
class HTTPFileStore < VCSToolkit::FileStore
|
7
|
+
attr_reader :base_url
|
8
|
+
|
9
|
+
def initialize(url)
|
10
|
+
@base_url = url.sub(/\/$/, '')
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch(path)
|
14
|
+
http_response = HTTParty.get("#{base_url}/#{path}")
|
15
|
+
|
16
|
+
case http_response.code
|
17
|
+
when 200
|
18
|
+
http_response.body
|
19
|
+
when 404
|
20
|
+
raise KeyError, "The file #{base_url}/#{path} cannot be found"
|
21
|
+
else
|
22
|
+
raise "Invalid status code #{http_response.code} for #{base_url}/#{path}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def store(path, content)
|
27
|
+
http_response = HTTParty.put("#{base_url}/#{path}", {body: content})
|
28
|
+
|
29
|
+
unless http_response.code == 200
|
30
|
+
raise "Invalid status code #{http_response.code} for #{base_url}/#{path} (put)"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def file?(path)
|
35
|
+
http_response = HTTParty.head("#{base_url}/#{path}")
|
36
|
+
|
37
|
+
case http_response.code
|
38
|
+
when 200
|
39
|
+
true
|
40
|
+
when 404
|
41
|
+
false
|
42
|
+
else
|
43
|
+
raise "Invalid status code #{http_response.code} for #{base_url}/#{path}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# These methods should not be used currently as this class
|
48
|
+
# will only be used by the object_store.
|
49
|
+
|
50
|
+
# def delete_file(path)
|
51
|
+
# end
|
52
|
+
|
53
|
+
# def delete_dir(path)
|
54
|
+
# end
|
55
|
+
|
56
|
+
# def directory?(path)
|
57
|
+
# end
|
58
|
+
|
59
|
+
# def changed?(path, blob)
|
60
|
+
# end
|
61
|
+
|
62
|
+
# def each_file(path='', &block)
|
63
|
+
# end
|
64
|
+
|
65
|
+
# def each_directory(path='', &block)
|
66
|
+
# end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'vcs_toolkit/exceptions'
|
2
|
+
require 'scv/objects'
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module SCV
|
7
|
+
|
8
|
+
##
|
9
|
+
# Implements VCSToolkit::ObjectStore to store objects on the file system.
|
10
|
+
# The directory structure is as follows:
|
11
|
+
#
|
12
|
+
# .scv/
|
13
|
+
#
|
14
|
+
# objects/
|
15
|
+
# 59/
|
16
|
+
# 59873e99cef61a60b3826e1cbb9d4b089ae78c2b.json
|
17
|
+
# ...
|
18
|
+
# ...
|
19
|
+
#
|
20
|
+
# refs/
|
21
|
+
# HEAD.json
|
22
|
+
# master.json
|
23
|
+
# ...
|
24
|
+
#
|
25
|
+
# blobs/
|
26
|
+
# 59/
|
27
|
+
# 59873e99cef61a60b3826e1cbb9d4b089ae78c2b
|
28
|
+
# ...
|
29
|
+
# ...
|
30
|
+
#
|
31
|
+
# Each object in `.scv/objects/` is stored in a directory with a name
|
32
|
+
# of the first two symbols of the object id.
|
33
|
+
# The file extension determines the object format. Possible formats
|
34
|
+
# are `json` and `json.gz` (`json.gz` will be supported in the future).
|
35
|
+
#
|
36
|
+
# For each blob object in `.scv/blobs/` there may be a file in
|
37
|
+
# `.scv/objects/`. These blobs follow the same naming scheme as the
|
38
|
+
# objects, but they are just binary files (not in `json` format).
|
39
|
+
#
|
40
|
+
# The refs are named objects (object.named? == true) and can be
|
41
|
+
# enumerated.
|
42
|
+
#
|
43
|
+
class ObjectStore < VCSToolkit::ObjectStore
|
44
|
+
def initialize(file_store)
|
45
|
+
@store = file_store
|
46
|
+
end
|
47
|
+
|
48
|
+
def store(object_id, object)
|
49
|
+
if object.named?
|
50
|
+
object_path = get_object_path object_id, named: true
|
51
|
+
else
|
52
|
+
object_path = get_object_path object_id
|
53
|
+
end
|
54
|
+
|
55
|
+
if object.is_a? VCSToolkit::Objects::Blob
|
56
|
+
@store.store(get_blob_path(object_id), object.content)
|
57
|
+
else
|
58
|
+
@store.store(object_path, JSON.generate(object.to_hash))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def fetch(object_id)
|
63
|
+
object_location = find_object object_id
|
64
|
+
|
65
|
+
return fetch_blob(object_id) if object_location[:type] == :blob
|
66
|
+
|
67
|
+
hash = JSON.parse(@store.fetch(object_location[:path]))
|
68
|
+
object_type = hash['object_type'].capitalize
|
69
|
+
|
70
|
+
raise 'Unknown object type' unless Objects.const_defined? object_type
|
71
|
+
|
72
|
+
Objects.const_get(object_type).from_hash hash
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# A method not required by VCSToolkit used to remove
|
77
|
+
# labels or possibly garbage collection.
|
78
|
+
#
|
79
|
+
def remove(object_id)
|
80
|
+
raise KeyError, 'The object does not exit' unless key? object_id
|
81
|
+
|
82
|
+
location = find_object object_id
|
83
|
+
|
84
|
+
@store.delete_file location[:path]
|
85
|
+
end
|
86
|
+
|
87
|
+
def key?(object_id)
|
88
|
+
not find_object(object_id).nil?
|
89
|
+
end
|
90
|
+
|
91
|
+
def each(&block)
|
92
|
+
return [] unless @store.directory? 'refs'
|
93
|
+
|
94
|
+
@store.files('refs').map { |name| name.sub /\..*$/, '' }.each &block
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def fetch_blob(object_id)
|
100
|
+
content = @store.fetch get_blob_path(object_id)
|
101
|
+
|
102
|
+
SCV::Objects::Blob.new id: object_id, content: content
|
103
|
+
end
|
104
|
+
|
105
|
+
def get_object_path(object_id, named: false)
|
106
|
+
if named
|
107
|
+
File.join 'refs', "#{object_id}.json"
|
108
|
+
else
|
109
|
+
File.join 'objects', object_id[0...2], "#{object_id}.json"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def get_blob_path(object_id)
|
114
|
+
File.join 'blobs', object_id.to_s[0...2], object_id.to_s
|
115
|
+
end
|
116
|
+
|
117
|
+
def find_object(object_id)
|
118
|
+
[
|
119
|
+
{path: get_object_path(object_id), type: :object},
|
120
|
+
{path: get_object_path(object_id, named: true), type: :label },
|
121
|
+
{path: get_blob_path(object_id), type: :blob },
|
122
|
+
].find { |location| @store.file? location[:path] }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
data/lib/scv/objects.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'vcs_toolkit/objects/commit'
|
2
|
+
|
3
|
+
module SCV
|
4
|
+
module Objects
|
5
|
+
|
6
|
+
class Commit < VCSToolkit::Objects::Commit
|
7
|
+
|
8
|
+
def initialize(date:, **kwargs)
|
9
|
+
# Parse the date on deserialization
|
10
|
+
date = DateTime.parse date if date.is_a? String
|
11
|
+
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def hash_objects
|
16
|
+
[@message, @tree, parents_for_hash, @author, @date.to_s]
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def parents_for_hash
|
22
|
+
if parents.empty?
|
23
|
+
nil
|
24
|
+
elsif parents.size == 1
|
25
|
+
parents.first
|
26
|
+
else
|
27
|
+
parents
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|