version50 0.0.1
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.
- data/bin/version50 +20 -0
- data/lib/version50.rb +237 -0
- data/lib/version50/git.rb +144 -0
- data/lib/version50/scm.rb +72 -0
- metadata +95 -0
data/bin/version50
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'version50'
|
5
|
+
|
6
|
+
# display help if no options are given
|
7
|
+
if ARGV.length == 0
|
8
|
+
Version50.new :action => 'help'
|
9
|
+
exit
|
10
|
+
end
|
11
|
+
|
12
|
+
# hide ruby-related ctrl-c messages
|
13
|
+
trap('INT') {
|
14
|
+
puts ''
|
15
|
+
exit
|
16
|
+
}
|
17
|
+
|
18
|
+
# pass action to version50 handler
|
19
|
+
action = ARGV[0]
|
20
|
+
version50 = Version50.new :action => action
|
data/lib/version50.rb
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'version50/git'
|
3
|
+
require 'json'
|
4
|
+
require 'optparse'
|
5
|
+
require 'yaml'
|
6
|
+
require 'net/http'
|
7
|
+
require 'net/https'
|
8
|
+
require 'highline/import'
|
9
|
+
HighLine.track_eof = false
|
10
|
+
|
11
|
+
class Version50
|
12
|
+
def initialize(args)
|
13
|
+
# parse configuration
|
14
|
+
config = self.parse_config
|
15
|
+
@scm = self.scm config
|
16
|
+
|
17
|
+
# no configuration file, so prompt to create a new project
|
18
|
+
if !config
|
19
|
+
config = self.create
|
20
|
+
@scm = self.scm config
|
21
|
+
@scm.init
|
22
|
+
end
|
23
|
+
|
24
|
+
# set user info
|
25
|
+
@scm.config config
|
26
|
+
|
27
|
+
# commit a new version without pushing
|
28
|
+
if args[:action] == 'commit'
|
29
|
+
@scm.commit
|
30
|
+
end
|
31
|
+
|
32
|
+
# view the commit history
|
33
|
+
if args[:action] == 'history' || args[:action] == 'log'
|
34
|
+
commits = @scm.log
|
35
|
+
self.output_history commits
|
36
|
+
end
|
37
|
+
|
38
|
+
# push the current project
|
39
|
+
if args[:action] == 'push'
|
40
|
+
@scm.push
|
41
|
+
end
|
42
|
+
|
43
|
+
# save a new version, which means commit and push
|
44
|
+
if args[:action] == 'save'
|
45
|
+
@scm.save
|
46
|
+
puts "\n\033[032mSaved a new version!\033[0m"
|
47
|
+
end
|
48
|
+
|
49
|
+
# get the current status of files
|
50
|
+
if args[:action] == 'status'
|
51
|
+
files = @scm.status
|
52
|
+
self.output_status files
|
53
|
+
end
|
54
|
+
|
55
|
+
# warp to a past version
|
56
|
+
if args[:action] == 'warp'
|
57
|
+
@scm.warp
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def create
|
62
|
+
# prompt for user info
|
63
|
+
puts "\nLooks like you're creating a new project!\n\n"
|
64
|
+
name = ask("What's your name? ")
|
65
|
+
email = ask("And your email? ")
|
66
|
+
puts "If you're hosting your project using a service like GitHub or BitBucket, paste the URL here."
|
67
|
+
puts "If not, you can just leave this blank!"
|
68
|
+
remote = $stdin.gets.chomp
|
69
|
+
|
70
|
+
# create configuration hash
|
71
|
+
config = {
|
72
|
+
'name' => name.to_s,
|
73
|
+
'email' => email.to_s,
|
74
|
+
'remote' => remote.to_s,
|
75
|
+
'scm' => 'git'
|
76
|
+
}
|
77
|
+
|
78
|
+
# prompt to create ssh key if one doesn't exist
|
79
|
+
if !File.exists?(File.expand_path '~/.ssh/id_rsa') && !File.exists?(File.expand_path '~/.ssh/id_dsa')
|
80
|
+
puts "It looks like you don't have an SSH key!"
|
81
|
+
answer = ask("Would you like to create one now? [y/n] ")
|
82
|
+
|
83
|
+
# user responded with yes, so create key
|
84
|
+
if answer == 'y' || answer == 'yes'
|
85
|
+
# prompt for password of at length 5
|
86
|
+
path = File.expand_path '~/.ssh/id_rsa'
|
87
|
+
password = ''
|
88
|
+
while password.length < 5
|
89
|
+
password = ask("Type a password for your key (at least 5 characters): ") { |q| q.echo = '*' }
|
90
|
+
end
|
91
|
+
|
92
|
+
# use ssh keygen to create key
|
93
|
+
`ssh-keygen -q -C "#{email}" -t rsa -N "#{password}" -f #{path}`
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# prompt to add key to remote account
|
98
|
+
if remote =~ /github/
|
99
|
+
puts "Would you like to add your key to your GitHub account?"
|
100
|
+
answer = ask("If you've already done this, you won't need to do so again! [y/n] ")
|
101
|
+
|
102
|
+
# prompt for github info
|
103
|
+
if answer == 'y' || answer == 'yes'
|
104
|
+
# repeat until authentication is successful
|
105
|
+
response = nil
|
106
|
+
while !response || response.code != '201'
|
107
|
+
username = ask("What's your GitHub username? ")
|
108
|
+
password = ask("And your GitHub password? ") { |q| q.echo = '*' }
|
109
|
+
|
110
|
+
# post key to github
|
111
|
+
http = Net::HTTP.new('api.github.com', 443)
|
112
|
+
http.use_ssl = true
|
113
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
114
|
+
request = Net::HTTP::Post.new('/user/keys')
|
115
|
+
request['Content-Type'] = 'application/json'
|
116
|
+
request.basic_auth username, password
|
117
|
+
request.body = {
|
118
|
+
'title' => 'version50',
|
119
|
+
'key' => File.open(File.expand_path('~/.ssh/id_rsa.pub')).gets
|
120
|
+
}.to_json
|
121
|
+
response = http.request(request)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# save config
|
127
|
+
File.open(Dir.pwd + '/.version50', 'w') do |f|
|
128
|
+
f.write config.to_yaml
|
129
|
+
end
|
130
|
+
|
131
|
+
puts "\n\033[032mYour project was created successfully, now have fun!"
|
132
|
+
puts "<3 version50\033[0m"
|
133
|
+
|
134
|
+
return config
|
135
|
+
end
|
136
|
+
|
137
|
+
# given a parsed SCM history output, show log
|
138
|
+
def output_history commits
|
139
|
+
# output each commit
|
140
|
+
commits.each_with_index do |commit, i|
|
141
|
+
puts "\033[031m#%03d \033[0m#{commit[:message]} \033[34m(#{commit[:timestamp]} by #{commit[:author]})" % (commits.length - i)
|
142
|
+
end
|
143
|
+
|
144
|
+
# ansi reset
|
145
|
+
print "\033[0m"
|
146
|
+
end
|
147
|
+
|
148
|
+
# given a parsed SCM status output, show file status
|
149
|
+
def output_status files
|
150
|
+
# new files (ansi green)
|
151
|
+
if files[:added].length > 0
|
152
|
+
print "\033[32m"
|
153
|
+
puts "\nNew Files"
|
154
|
+
puts "=========\n\n"
|
155
|
+
|
156
|
+
files[:added].each do |file|
|
157
|
+
puts "* #{file}"
|
158
|
+
end
|
159
|
+
puts ""
|
160
|
+
end
|
161
|
+
|
162
|
+
# modified files (ansi yellow)
|
163
|
+
if files[:modified].length > 0
|
164
|
+
print "\033[33m"
|
165
|
+
puts "\nModified Files"
|
166
|
+
puts "==============\n\n"
|
167
|
+
|
168
|
+
files[:modified].each do |file|
|
169
|
+
puts "* #{file}"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# deleted files (ansi red)
|
174
|
+
if files[:deleted].length > 0
|
175
|
+
print "\033[31m"
|
176
|
+
puts "\nDeleted Files"
|
177
|
+
puts "=============\n\n"
|
178
|
+
|
179
|
+
files[:deleted].each do |file|
|
180
|
+
puts "* #{file}"
|
181
|
+
end
|
182
|
+
|
183
|
+
puts ""
|
184
|
+
end
|
185
|
+
|
186
|
+
# nothing changed
|
187
|
+
if files[:added].length == 0 && files[:modified].length == 0 && files[:deleted].length == 0
|
188
|
+
print "Nothing has changed since your last save!"
|
189
|
+
end
|
190
|
+
|
191
|
+
# ansi reset
|
192
|
+
print "\033[0m\n"
|
193
|
+
end
|
194
|
+
|
195
|
+
# parse the version50 configuration file
|
196
|
+
def parse_config
|
197
|
+
# search upward to find project root
|
198
|
+
path = self.root
|
199
|
+
if path
|
200
|
+
return YAML.load_file(path + '/.version50')
|
201
|
+
end
|
202
|
+
|
203
|
+
# project root not found
|
204
|
+
return false
|
205
|
+
end
|
206
|
+
|
207
|
+
# get the path of the project root, as determined by the location of the .version50 file
|
208
|
+
def root
|
209
|
+
# search upward for a file called ".version50"
|
210
|
+
path = Pathname.new(Dir.pwd)
|
211
|
+
while path.to_s != '/'
|
212
|
+
# check if file exists in this directory
|
213
|
+
if path.children(false).select { |e| e.to_s == '.version50' }.length > 0
|
214
|
+
return path.to_s
|
215
|
+
end
|
216
|
+
|
217
|
+
# continue to traverse upwards
|
218
|
+
path = path.parent
|
219
|
+
end
|
220
|
+
|
221
|
+
# .version50 file not found
|
222
|
+
return false
|
223
|
+
end
|
224
|
+
|
225
|
+
# determine the scm engine based on the config file
|
226
|
+
def scm config
|
227
|
+
# no engine specified
|
228
|
+
if !config
|
229
|
+
return nil
|
230
|
+
end
|
231
|
+
|
232
|
+
# git backend
|
233
|
+
if config['scm'] == 'git'
|
234
|
+
return Git.new(self)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'tmpdir'
|
3
|
+
|
4
|
+
require 'version50/scm'
|
5
|
+
|
6
|
+
class Git < SCM
|
7
|
+
# commit a new version without pushing
|
8
|
+
def commit
|
9
|
+
# prompt for commit message
|
10
|
+
message = super
|
11
|
+
|
12
|
+
# add all files and commit
|
13
|
+
`git add --all`
|
14
|
+
`git commit -m "#{message}"`
|
15
|
+
end
|
16
|
+
|
17
|
+
# configure the repo with user's info
|
18
|
+
def config info
|
19
|
+
# configure git user
|
20
|
+
`git config user.name "#{info['name']}"`
|
21
|
+
`git config user.email "#{info['email']}"`
|
22
|
+
|
23
|
+
# configure remote if not already
|
24
|
+
origin = `git remote`
|
25
|
+
if origin == '' && info['remote'] != ''
|
26
|
+
`git remote add origin #{info['remote']}`
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# create a new repo
|
31
|
+
def init
|
32
|
+
`git init`
|
33
|
+
`echo ".version50" > .gitignore`
|
34
|
+
end
|
35
|
+
|
36
|
+
# view the project history
|
37
|
+
def log
|
38
|
+
# great idea or greatest idea?
|
39
|
+
delimiter = '!@#%^&*'
|
40
|
+
history = `git log --graph --pretty=format:'#{delimiter} %h #{delimiter} %s #{delimiter} %cr #{delimiter} %an' --abbrev-commit`
|
41
|
+
|
42
|
+
# iterate over history lines
|
43
|
+
commits = []
|
44
|
+
lines = history.split "\n"
|
45
|
+
lines.each_with_index do |line, i|
|
46
|
+
# get information from individual commits
|
47
|
+
commit = line.split(delimiter).map { |s| s.strip }
|
48
|
+
commits.push({
|
49
|
+
:id => commit[1],
|
50
|
+
:message => commit[2],
|
51
|
+
:timestamp => commit[3],
|
52
|
+
:author => commit[4]
|
53
|
+
})
|
54
|
+
end
|
55
|
+
|
56
|
+
return commits
|
57
|
+
end
|
58
|
+
|
59
|
+
def pull
|
60
|
+
end
|
61
|
+
|
62
|
+
# push existing commits
|
63
|
+
def push
|
64
|
+
`git push -u origin master > /dev/null 2>&1`
|
65
|
+
end
|
66
|
+
|
67
|
+
# view changed files
|
68
|
+
def status
|
69
|
+
# get status from SCM
|
70
|
+
status = `git status`
|
71
|
+
|
72
|
+
# iterate over each line in status
|
73
|
+
tracked = 0
|
74
|
+
added, modified, deleted = [], [], []
|
75
|
+
status.split("\n").each do |line|
|
76
|
+
# ignore git system lines
|
77
|
+
if tracked > 0 && line && line !=~ /\(use "git add <file>\.\.\." to include in what will be committed\)/ &&
|
78
|
+
line !=~ /\(use "git add <file>\.\.\." to update what will be committed\)/ &&
|
79
|
+
line !=~ /\(use "git checkout -- <file>\.\.\." to discard changes in working directory\)/
|
80
|
+
|
81
|
+
# untracked files, so mark as added
|
82
|
+
if tracked == 1
|
83
|
+
# determine filename
|
84
|
+
line =~ /^#\s*([\w\/\.\-]+)/
|
85
|
+
if $1
|
86
|
+
added.push $1
|
87
|
+
end
|
88
|
+
|
89
|
+
# currently-tracked files
|
90
|
+
elsif tracked == 2
|
91
|
+
# determine filename and modified status
|
92
|
+
line =~ /^#\s*([\w]+):\s*([\w\/\.\-]+)/
|
93
|
+
|
94
|
+
# tracked and modified
|
95
|
+
if $1 == 'modified'
|
96
|
+
modified.push $2
|
97
|
+
|
98
|
+
# tracked and deleted
|
99
|
+
elsif $1 == 'deleted'
|
100
|
+
deleted.push $2
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# make sure untracked files are marked as added
|
106
|
+
if line =~ /Untracked files:/
|
107
|
+
tracked = 1
|
108
|
+
elsif line =~ /Changes not staged for commit:/ || line =~ /Changes to be committed:/
|
109
|
+
tracked = 2
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
return {
|
114
|
+
:added => added,
|
115
|
+
:deleted => deleted,
|
116
|
+
:modified => modified
|
117
|
+
}
|
118
|
+
end
|
119
|
+
|
120
|
+
# warp to a specific version
|
121
|
+
def warp
|
122
|
+
# save current state before doing anything
|
123
|
+
revision = super
|
124
|
+
|
125
|
+
# determine project root and warp destination
|
126
|
+
path = @version50.root
|
127
|
+
dest = "version50-#{revision[:revision]}"
|
128
|
+
|
129
|
+
# create temporary directory to clone project into
|
130
|
+
Dir.mktmpdir do |d|
|
131
|
+
# clone project into temporary directory and revert to given revision
|
132
|
+
Dir.chdir(File.expand_path d)
|
133
|
+
`git clone #{path} . > /dev/null 2> /dev/null`
|
134
|
+
`git checkout #{revision[:id]} -f > /dev/null 2> /dev/null`
|
135
|
+
|
136
|
+
# switch back to project root and create folder for warp
|
137
|
+
Dir.chdir(File.expand_path path)
|
138
|
+
FileUtils.mkdir dest
|
139
|
+
|
140
|
+
# move all files in temporary directory into warp directory
|
141
|
+
FileUtils.mv(Dir.glob(File.expand_path(d) + '/*'), dest)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
class SCM
|
4
|
+
def initialize(version50)
|
5
|
+
@version50 = version50
|
6
|
+
end
|
7
|
+
|
8
|
+
# commit changes without pushing
|
9
|
+
def commit
|
10
|
+
# check if we have anything to commit
|
11
|
+
files = self.status
|
12
|
+
if files[:added].length == 0 && files[:modified].length == 0 && files[:deleted].length == 0
|
13
|
+
puts "Nothing has changed since your last save!"
|
14
|
+
|
15
|
+
# prompt for commit message
|
16
|
+
else
|
17
|
+
puts "\033[34mWhat changes have you made since your last save?\033[0m "
|
18
|
+
message = $stdin.gets.chomp
|
19
|
+
end
|
20
|
+
|
21
|
+
return message
|
22
|
+
end
|
23
|
+
|
24
|
+
# configure the repo with the user's info
|
25
|
+
def config info
|
26
|
+
end
|
27
|
+
|
28
|
+
# initialize a new repo
|
29
|
+
def init
|
30
|
+
end
|
31
|
+
|
32
|
+
# view the project history
|
33
|
+
def log
|
34
|
+
end
|
35
|
+
|
36
|
+
def pull
|
37
|
+
end
|
38
|
+
|
39
|
+
# push existing commits
|
40
|
+
def push
|
41
|
+
end
|
42
|
+
|
43
|
+
# shortcut for commit and push
|
44
|
+
def save
|
45
|
+
self.commit
|
46
|
+
self.push
|
47
|
+
end
|
48
|
+
|
49
|
+
# view changed files
|
50
|
+
def status
|
51
|
+
end
|
52
|
+
|
53
|
+
# warp to a specific revision
|
54
|
+
def warp
|
55
|
+
# save before doing anything
|
56
|
+
self.save
|
57
|
+
|
58
|
+
# prompt for revision
|
59
|
+
print "\033[34mWhat version would you like to warp to?\033[0m "
|
60
|
+
revision = $stdin.gets.chomp.to_i(10)
|
61
|
+
|
62
|
+
puts "\033[32mPutting files into version50-#{revision}...\033[0m "
|
63
|
+
|
64
|
+
# get revision from numerical index
|
65
|
+
revisions = self.log
|
66
|
+
r = revisions[revisions.length - revision]
|
67
|
+
|
68
|
+
# add numerical index to return value
|
69
|
+
r[:revision] = revision
|
70
|
+
return r
|
71
|
+
end
|
72
|
+
end
|
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: version50
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Tommy MacWilliam
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-12-16 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: json
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: highline
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 3
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
48
|
+
description: A student-friendly SCM abstraction layer.
|
49
|
+
email: tmacwilliam@cs.harvard.edu
|
50
|
+
executables:
|
51
|
+
- version50
|
52
|
+
extensions: []
|
53
|
+
|
54
|
+
extra_rdoc_files: []
|
55
|
+
|
56
|
+
files:
|
57
|
+
- lib/version50.rb
|
58
|
+
- lib/version50/git.rb
|
59
|
+
- lib/version50/scm.rb
|
60
|
+
- bin/version50
|
61
|
+
homepage: https://github.com/tmacwill/version50
|
62
|
+
licenses: []
|
63
|
+
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
hash: 3
|
75
|
+
segments:
|
76
|
+
- 0
|
77
|
+
version: "0"
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
hash: 3
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
version: "0"
|
87
|
+
requirements: []
|
88
|
+
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 1.8.24
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: A student-friendly SCM abstraction layer.
|
94
|
+
test_files: []
|
95
|
+
|