version50 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+