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,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)
|