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,56 @@
|
|
1
|
+
desc 'Merges two branches to the working directory'
|
2
|
+
arg_name '<branch name>'
|
3
|
+
command :merge do |c|
|
4
|
+
c.desc 'Abort the current merge. Resets the working dir and the next commit parents'
|
5
|
+
c.arg_name 'abort'
|
6
|
+
c.switch :abort
|
7
|
+
|
8
|
+
c.desc 'Squashes the changes to a single commit. The next commit will have one parent'
|
9
|
+
c.arg_name 'squash'
|
10
|
+
c.switch :squash
|
11
|
+
|
12
|
+
c.action do |global_options, options, args|
|
13
|
+
repository = global_options[:repository]
|
14
|
+
commit = repository[:head, :commit]
|
15
|
+
|
16
|
+
if options[:abort]
|
17
|
+
repository.config['merge'] = {}
|
18
|
+
repository.config.save
|
19
|
+
|
20
|
+
repository.restore '.', commit
|
21
|
+
|
22
|
+
next
|
23
|
+
end
|
24
|
+
|
25
|
+
raise 'No branch specified to merge from' if args.empty?
|
26
|
+
|
27
|
+
commit_two = repository[args.first, :commit]
|
28
|
+
|
29
|
+
raise 'There is no branch with that name' if commit_two.nil?
|
30
|
+
|
31
|
+
merge_status = repository.merge commit, commit_two
|
32
|
+
|
33
|
+
unless options[:squash]
|
34
|
+
# Save the merged commit ids so that the next
|
35
|
+
# commit has both as parents.
|
36
|
+
repository.config['merge'] = {
|
37
|
+
'parents' => [
|
38
|
+
commit.id,
|
39
|
+
commit_two.id,
|
40
|
+
]
|
41
|
+
}
|
42
|
+
repository.config.save
|
43
|
+
end
|
44
|
+
|
45
|
+
puts "# Merged #{args.first} into #{repository.head}"
|
46
|
+
puts
|
47
|
+
|
48
|
+
SCV::Formatters::MergeReport.print merge_status
|
49
|
+
|
50
|
+
unless merge_status[:conflicted].any?
|
51
|
+
puts "# You can now use `scv commit` to create the merge commit"
|
52
|
+
puts "# The next commit will have two parents" unless options[:squash]
|
53
|
+
puts "# To abort the merge (not create a merge commit) use `scv merge --abort`"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
desc 'A set of commands to manage remotes and push/pull to them.'
|
2
|
+
arg_name ''
|
3
|
+
command :remote do |c|
|
4
|
+
|
5
|
+
c.desc 'Add a remote'
|
6
|
+
c.arg_name '<remote name> <remote HTTP url>'
|
7
|
+
c.command [:add, :new] do |add|
|
8
|
+
add.action do |global_options, options, args|
|
9
|
+
raise 'Please specify remote name and address' unless args.size == 2
|
10
|
+
|
11
|
+
name = args[0]
|
12
|
+
url = args[1]
|
13
|
+
|
14
|
+
repository = global_options[:repository]
|
15
|
+
repository.config['remotes'] = {} unless repository.config['remotes']
|
16
|
+
|
17
|
+
raise 'There is already a remote with that name' if repository.config['remotes'].key? name
|
18
|
+
|
19
|
+
repository.config['remotes'][name] = {
|
20
|
+
'url' => url
|
21
|
+
}
|
22
|
+
repository.config.save
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
c.desc 'Delete a remote'
|
27
|
+
c.arg_name '<remote name>'
|
28
|
+
c.command [:delete, :remove] do |remove|
|
29
|
+
remove.action do |global_options, options, args|
|
30
|
+
raise 'Please specify remote name' unless args.size == 1
|
31
|
+
|
32
|
+
name = args[0]
|
33
|
+
|
34
|
+
repository = global_options[:repository]
|
35
|
+
|
36
|
+
unless repository.config['remotes'] and repository.config['remotes'].key? name
|
37
|
+
raise 'There is no remote with that name'
|
38
|
+
end
|
39
|
+
|
40
|
+
repository.config['remotes'].delete name
|
41
|
+
repository.config.save
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
c.desc 'Push local history to remote'
|
46
|
+
c.arg_name '<remote> <local branch> [<remote branch>]'
|
47
|
+
c.command :push do |push|
|
48
|
+
push.action do |global_options, options, args|
|
49
|
+
raise 'Please specify remote name and local branch' unless args.size >= 2
|
50
|
+
|
51
|
+
repository = global_options[:repository]
|
52
|
+
|
53
|
+
remote_name = args[0]
|
54
|
+
local_branch = args[1]
|
55
|
+
|
56
|
+
raise 'There is no remote with this name' unless repository.config['remotes'][remote_name]
|
57
|
+
raise 'There is no branch with this name' unless repository[local_branch]
|
58
|
+
|
59
|
+
if args.size > 2
|
60
|
+
remote_branch = args[2]
|
61
|
+
else
|
62
|
+
remote_branch = local_branch
|
63
|
+
end
|
64
|
+
|
65
|
+
remote_file_store = SCV::HTTPFileStore.new repository.config['remotes'][remote_name]['url']
|
66
|
+
remote_object_store = SCV::ObjectStore.new remote_file_store
|
67
|
+
|
68
|
+
repository.push remote_object_store, local_branch, remote_branch
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
c.desc 'Fetch remote history objects to local branch. Does not do merging, only Fast-Forward'
|
73
|
+
c.arg_name '<remote> <remote branch> [<local branch>]'
|
74
|
+
c.command :fetch do |fetch|
|
75
|
+
fetch.action do |global_options, options, args|
|
76
|
+
raise 'Please specify remote name and remote branch' unless args.size >= 2
|
77
|
+
|
78
|
+
repository = global_options[:repository]
|
79
|
+
|
80
|
+
remote_name = args[0]
|
81
|
+
remote_branch = args[1]
|
82
|
+
|
83
|
+
if args.size > 2
|
84
|
+
local_branch = args[2]
|
85
|
+
else
|
86
|
+
local_branch = remote_branch
|
87
|
+
end
|
88
|
+
|
89
|
+
unless repository[local_branch]
|
90
|
+
# There is no local branch with this name, so create it
|
91
|
+
repository.set_label local_branch, nil
|
92
|
+
end
|
93
|
+
|
94
|
+
raise 'There is no remote with this name' unless repository.config['remotes'][remote_name]
|
95
|
+
|
96
|
+
remote_file_store = SCV::HTTPFileStore.new repository.config['remotes'][remote_name]['url']
|
97
|
+
remote_object_store = SCV::ObjectStore.new remote_file_store
|
98
|
+
|
99
|
+
repository.fetch remote_object_store, remote_branch, local_branch
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
c.desc 'Pull remote history objects to local branch. Merges automatically'
|
104
|
+
c.arg_name '<remote> <remote branch> [<local branch>]'
|
105
|
+
c.command :pull do |pull|
|
106
|
+
pull.action do |global_options, options, args|
|
107
|
+
raise 'Please specify remote name and remote branch' unless args.size >= 2
|
108
|
+
|
109
|
+
repository = global_options[:repository]
|
110
|
+
|
111
|
+
remote_name = args[0]
|
112
|
+
remote_branch = args[1]
|
113
|
+
|
114
|
+
if args.size > 2
|
115
|
+
local_branch = args[2]
|
116
|
+
else
|
117
|
+
local_branch = remote_branch
|
118
|
+
end
|
119
|
+
|
120
|
+
unless repository[local_branch]
|
121
|
+
# There is no local branch with this name, so create it
|
122
|
+
repository.set_label local_branch, nil
|
123
|
+
end
|
124
|
+
|
125
|
+
raise 'There is no remote with this name' unless repository.config['remotes'][remote_name]
|
126
|
+
|
127
|
+
remote_file_store = SCV::HTTPFileStore.new repository.config['remotes'][remote_name]['url']
|
128
|
+
remote_object_store = SCV::ObjectStore.new remote_file_store
|
129
|
+
|
130
|
+
# Step 1: Fetch to another branch
|
131
|
+
temp_branch = "#{remote_name}~#{remote_branch}"
|
132
|
+
|
133
|
+
# Create the temp branch if not existing
|
134
|
+
repository.set_label temp_branch, nil unless repository[temp_branch]
|
135
|
+
|
136
|
+
repository.fetch remote_object_store, remote_branch, temp_branch
|
137
|
+
|
138
|
+
# Step 2: Merge temp_branch into local_branch
|
139
|
+
dest_commit = repository[temp_branch, :commit]
|
140
|
+
source_commit = repository[local_branch, :commit]
|
141
|
+
merge_status = repository.merge source_commit, dest_commit
|
142
|
+
|
143
|
+
# Save the merged commit ids so that the next
|
144
|
+
# commit has both as parents.
|
145
|
+
repository.config['merge'] = {
|
146
|
+
'parents' => [
|
147
|
+
source_commit.id,
|
148
|
+
dest_commit.id,
|
149
|
+
]
|
150
|
+
}
|
151
|
+
repository.config.save
|
152
|
+
|
153
|
+
SCV::Formatters::MergeReport.print merge_status
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
desc 'Resets files to the state they were in the specified commit'
|
2
|
+
arg_name '<file> ...'
|
3
|
+
command [:restore] do |c|
|
4
|
+
c.desc 'Use this specify which commit should be the used as reference'
|
5
|
+
c.arg_name 'source'
|
6
|
+
c.default_value 'head'
|
7
|
+
c.flag [:s, :source]
|
8
|
+
|
9
|
+
c.action do |global_options, options, args|
|
10
|
+
repository = global_options[:repository]
|
11
|
+
commit = repository[options[:source], :commit]
|
12
|
+
|
13
|
+
args.each do |path|
|
14
|
+
repository.restore path, commit
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
desc 'Starts a barebones HTTP server to act as a remote repo'
|
2
|
+
arg_name ''
|
3
|
+
command [:server] do |c|
|
4
|
+
c.action do |global_options, options, args|
|
5
|
+
repository = global_options[:repository]
|
6
|
+
|
7
|
+
server = SCV::Server.new File.join(global_options[:dir], '.scv')
|
8
|
+
server.start
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
desc 'Shows the created, modified and deleted files'
|
2
|
+
arg_name ''
|
3
|
+
command :status do |c|
|
4
|
+
c.action do |global_options, options, args|
|
5
|
+
repository = global_options[:repository]
|
6
|
+
commit = repository[:head, :commit]
|
7
|
+
status = repository.status commit,
|
8
|
+
ignore: [/^\.|\/\./]
|
9
|
+
|
10
|
+
if repository.head.nil?
|
11
|
+
puts "# No commits"
|
12
|
+
else
|
13
|
+
puts "# On branch #{repository.head}"
|
14
|
+
puts "# On commit #{commit.id.yellow}"
|
15
|
+
end
|
16
|
+
|
17
|
+
if repository.config['merge'] and repository.config['merge']['parents']
|
18
|
+
parents = repository.config['merge']['parents']
|
19
|
+
|
20
|
+
puts
|
21
|
+
puts "# Next commit parents:"
|
22
|
+
|
23
|
+
parents.each do |commit_id|
|
24
|
+
puts "# - #{commit_id.yellow}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
puts
|
29
|
+
|
30
|
+
if status.none? { |_, files| files.any? }
|
31
|
+
puts "# No changes"
|
32
|
+
end
|
33
|
+
|
34
|
+
if status[:changed].any?
|
35
|
+
puts "# Changed:"
|
36
|
+
puts status[:changed].map { |file| "+- #{file}".yellow }
|
37
|
+
puts
|
38
|
+
end
|
39
|
+
|
40
|
+
if status[:created].any?
|
41
|
+
puts "# New:"
|
42
|
+
puts status[:created].map { |file| "+ #{file}".green }
|
43
|
+
puts
|
44
|
+
end
|
45
|
+
|
46
|
+
if status[:deleted].any?
|
47
|
+
puts "# Deleted:"
|
48
|
+
puts status[:deleted].map { |file| "- #{file}".red }
|
49
|
+
puts
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module SCV::Formatters
|
2
|
+
module Changeset
|
3
|
+
def self.print(file_path, changeset)
|
4
|
+
added_count = changeset.count { |change| change.adding? }
|
5
|
+
changed_count = changeset.count { |change| change.changed? }
|
6
|
+
deleted_count = changeset.count { |change| change.deleting? }
|
7
|
+
|
8
|
+
diff_lines = changeset.flat_map do |change|
|
9
|
+
if change.unchanged?
|
10
|
+
[" #{change.new_element}"]
|
11
|
+
elsif change.deleting?
|
12
|
+
["-#{change.old_element}".red]
|
13
|
+
elsif change.adding?
|
14
|
+
["+#{change.new_element}".green]
|
15
|
+
elsif change.changed?
|
16
|
+
["-#{change.old_element}".red, "+#{change.new_element}".green]
|
17
|
+
else
|
18
|
+
raise "Unknown change in the diff #{change.action}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
puts
|
23
|
+
puts "##".bold
|
24
|
+
puts "# #{file_path}".bold
|
25
|
+
puts '#'.bold
|
26
|
+
|
27
|
+
if changeset.all? { |change| change.adding? }
|
28
|
+
puts "# New file".bold
|
29
|
+
puts "# #{added_count} #{added_count == 1 ? 'line' : 'lines'}".bold
|
30
|
+
elsif changeset.all? { |change| change.deleting? }
|
31
|
+
puts "# Deleted file".bold
|
32
|
+
puts "# #{deleted_count} #{deleted_count == 1 ? 'line' : 'lines'}".bold
|
33
|
+
else
|
34
|
+
puts "# #{added_count} #{added_count == 1 ? 'line' : 'lines'} added".bold unless added_count.zero?
|
35
|
+
puts "# #{changed_count} #{changed_count == 1 ? 'line' : 'lines'} changed".bold unless changed_count.zero?
|
36
|
+
puts "# #{deleted_count} #{deleted_count == 1 ? 'line' : 'lines'} deleted".bold unless deleted_count.zero?
|
37
|
+
end
|
38
|
+
|
39
|
+
puts "##".bold
|
40
|
+
puts diff_lines.join('')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module SCV::Formatters
|
2
|
+
module Hierarchy
|
3
|
+
def self.print(hash, indent_size: 3, prefix: '')
|
4
|
+
hash.each_with_index do |(name, value), index|
|
5
|
+
is_last = index == hash.size - 1
|
6
|
+
|
7
|
+
Kernel.print prefix
|
8
|
+
|
9
|
+
unless prefix.empty?
|
10
|
+
if is_last
|
11
|
+
Kernel.print ' └'
|
12
|
+
else
|
13
|
+
Kernel.print ' ├'
|
14
|
+
end
|
15
|
+
|
16
|
+
Kernel.print '─' * indent_size.pred.pred
|
17
|
+
Kernel.print ' '
|
18
|
+
end
|
19
|
+
Kernel.print name
|
20
|
+
|
21
|
+
if value.is_a? Hash
|
22
|
+
if value.size.zero?
|
23
|
+
puts ": {}"
|
24
|
+
else
|
25
|
+
puts
|
26
|
+
|
27
|
+
if prefix.empty?
|
28
|
+
inner_prefix = ' ' * indent_size.pred.pred
|
29
|
+
else
|
30
|
+
inner_prefix = prefix + ' ' + (is_last ? ' ' : '│') + (' ' * indent_size.pred.pred)
|
31
|
+
end
|
32
|
+
|
33
|
+
print value, indent_size: indent_size, prefix: inner_prefix
|
34
|
+
end
|
35
|
+
elsif value.is_a? Array
|
36
|
+
puts ": [#{value.join(', ')}]"
|
37
|
+
else
|
38
|
+
puts ": #{value}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SCV::Formatters
|
2
|
+
module MergeReport
|
3
|
+
def self.print(merge_status)
|
4
|
+
if merge_status[:merged].any?
|
5
|
+
puts "# Automatic merges:"
|
6
|
+
|
7
|
+
merge_status[:merged].each do |file|
|
8
|
+
puts " #{file}".yellow
|
9
|
+
end
|
10
|
+
|
11
|
+
puts
|
12
|
+
end
|
13
|
+
|
14
|
+
if merge_status[:conflicted].any?
|
15
|
+
puts "# Conflicted files:"
|
16
|
+
|
17
|
+
merge_status[:conflicted].each do |file|
|
18
|
+
puts " #{file}".red
|
19
|
+
end
|
20
|
+
|
21
|
+
puts
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/bin/scv
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'scv'
|
4
|
+
require 'gli'
|
5
|
+
require 'colorize'
|
6
|
+
|
7
|
+
include GLI::App
|
8
|
+
|
9
|
+
program_desc 'A simple VCS implemented in Ruby on top of the VCSToolkit library'
|
10
|
+
|
11
|
+
version SCV::VERSION
|
12
|
+
subcommand_option_handling :normal
|
13
|
+
|
14
|
+
desc 'Print more stuff'
|
15
|
+
switch [:v, :verbose]
|
16
|
+
|
17
|
+
desc 'Colorize output if running in an interactive shell'
|
18
|
+
default_value true
|
19
|
+
switch [:colors]
|
20
|
+
|
21
|
+
desc 'The path to the working directory'
|
22
|
+
default_value '.'
|
23
|
+
arg_name 'directory path'
|
24
|
+
flag [:dir]
|
25
|
+
|
26
|
+
# Require utilities
|
27
|
+
require_relative 'utilities/shell'
|
28
|
+
require_relative 'utilities/output'
|
29
|
+
|
30
|
+
# Require formatters
|
31
|
+
require_relative 'formatters/changeset'
|
32
|
+
require_relative 'formatters/hierarchy'
|
33
|
+
require_relative 'formatters/merge_report'
|
34
|
+
|
35
|
+
# Require all available commands
|
36
|
+
require_relative 'commands/init'
|
37
|
+
require_relative 'commands/commit'
|
38
|
+
require_relative 'commands/status'
|
39
|
+
require_relative 'commands/history'
|
40
|
+
require_relative 'commands/diff'
|
41
|
+
require_relative 'commands/restore'
|
42
|
+
require_relative 'commands/branch'
|
43
|
+
require_relative 'commands/merge'
|
44
|
+
require_relative 'commands/config'
|
45
|
+
require_relative 'commands/server'
|
46
|
+
require_relative 'commands/remote'
|
47
|
+
|
48
|
+
pre do |global, command, options, args|
|
49
|
+
# Pre logic here
|
50
|
+
# Return true to proceed; false to abort and not call the
|
51
|
+
# chosen command
|
52
|
+
# Use skips_pre before a command to skip this block
|
53
|
+
# on that command only
|
54
|
+
|
55
|
+
# Check if the current directory is an SCV repository
|
56
|
+
# or is a subdirectory of one.
|
57
|
+
unless command.name == :init
|
58
|
+
working_dir = Pathname.new(global[:dir]).realpath
|
59
|
+
raise 'The working directory cannot be found' unless working_dir.directory?
|
60
|
+
|
61
|
+
working_dir = working_dir.enum_for(:ascend).find { |dir| dir.join('.scv').directory? }
|
62
|
+
raise "This directory is not (in) an SCV repository" if working_dir.nil?
|
63
|
+
|
64
|
+
global[:dir] = working_dir.to_path
|
65
|
+
end
|
66
|
+
|
67
|
+
# Initialize the repository object.
|
68
|
+
unless command.name == :init
|
69
|
+
repository = SCV::Repository.new global[:dir]
|
70
|
+
global[:repository] = repository
|
71
|
+
end
|
72
|
+
|
73
|
+
# Check if there is a commit in the repository
|
74
|
+
# as most commands expect one to exist.
|
75
|
+
unless [:init, :status, :commit, :server].include? command.name
|
76
|
+
raise "There are no commits in the repository" if repository.branch_head.nil?
|
77
|
+
end
|
78
|
+
|
79
|
+
# Switch colors on or off
|
80
|
+
unless global[:colors] and $stdout.isatty
|
81
|
+
# Disable colorize
|
82
|
+
String.class_eval do
|
83
|
+
alias_method :original_colorize, :colorize
|
84
|
+
|
85
|
+
def colorize(*args)
|
86
|
+
# Ignore color requests
|
87
|
+
self
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
true
|
93
|
+
end
|
94
|
+
|
95
|
+
post do |global, command, options, args|
|
96
|
+
# Post logic here
|
97
|
+
# Use skips_post before a command to skip this
|
98
|
+
# block on that command only
|
99
|
+
end
|
100
|
+
|
101
|
+
on_error do |exception|
|
102
|
+
# Error logic here
|
103
|
+
# return false to skip default error handling
|
104
|
+
true
|
105
|
+
end
|
106
|
+
|
107
|
+
exit run(ARGV)
|