scvcs 0.2.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 +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
|