-
Notifications
You must be signed in to change notification settings - Fork 10
/
gitcycle.rb
403 lines (338 loc) · 9.85 KB
/
gitcycle.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
require 'rubygems'
require 'fileutils'
require 'uri'
require 'yaml'
require 'httpclient'
require 'httpi'
gem 'launchy', '= 2.0.5'
require 'launchy'
gem 'yajl-ruby', '= 1.1.0'
require 'yajl'
$:.unshift File.dirname(__FILE__)
require "ext/string"
require "gitcycle/assist"
require "gitcycle/branch"
require "gitcycle/checkout"
require "gitcycle/commit"
require "gitcycle/discuss"
require "gitcycle/incident"
require "gitcycle/open"
require "gitcycle/pull"
require "gitcycle/push"
require "gitcycle/qa"
require "gitcycle/ready"
require "gitcycle/review"
require "gitcycle/setup"
class Gitcycle
API =
if ENV['ENV'] == 'test'
"http://127.0.0.1:3000/api"
else
"http://gitcycle.bleacherreport.com/api"
end
ERROR = {
:unrecognized_url => 1,
:could_not_find_branch => 2,
:told_not_to_merge => 3,
:cannot_qa => 4,
:conflict_when_merging => 5,
:something_went_wrong => 6,
:git_origin_not_found => 7,
:last_command_errored => 8,
}
include Assist
include Branch
include Checkout
include Commit
include Discuss
include Incident
include Open
include Pull
include Push
include QA
include Ready
include Review
include Setup
def initialize(args=nil)
$remotes = {}
if ENV['CONFIG']
@config_path = File.expand_path(ENV['CONFIG'])
else
@config_path = File.expand_path("~/.gitcycle.yml")
end
load_config
load_git
start(args) if args
end
def start(args=[])
command = args.shift
`git --help`.scan(/\s{3}(\w+)\s{3}/).flatten.each do |cmd|
if command == cmd && !self.respond_to?(command)
exec_git(cmd, args)
end
end
if command.nil?
puts "\nNo command specified\n".red
elsif command =~ /^-/
command_not_recognized
elsif self.respond_to?(command)
send(command, *args)
else
command_not_recognized
end
end
private
def add_remote_and_fetch(options={})
owner = options[:owner]
repo = options[:repo]
unless $remotes[owner]
$remotes[owner] = true
unless remotes(:match => owner)
puts "Adding remote repo '#{owner}/#{repo}'.\n".green
run("git remote add #{owner} git@github.com:#{owner}/#{repo}.git")
end
puts "Fetching remote '#{owner}'.\n".green
run("git fetch -q #{owner}", :catch => options[:catch])
end
end
def branches(options={})
b = `git branch#{" -a" if options[:all]}#{" -r" if options[:remote]}`
if options[:current]
b.match(/\*\s+(.+)/)[1]
elsif options[:match]
b.match(/([\s]+|origin\/)(#{options[:match]})$/)[2] rescue nil
elsif options[:array]
b.split(/\n/).map{|b| b[2..-1]}
else
b
end
end
def checkout_or_track(options={})
name = options[:name]
remote = options[:remote]
if branches(:match => name)
puts "Checking out branch '#{name}'.\n".green
run("git checkout #{name} -q")
else
puts "Tracking branch '#{remote}/#{name}'.\n".green
run("git fetch -q #{remote}")
run("git checkout -q -b #{name} #{remote}/#{name}")
end
run("git pull #{remote} #{name} -q")
end
def checkout_remote_branch(options={})
owner = options[:owner]
repo = options[:repo]
branch = options[:branch]
target = options[:target] || branch
if branches(:match => target)
if yes?("You already have a branch called '#{target}'. Overwrite?")
run("git push origin :#{target} -q")
run("git checkout master -q")
run("git branch -D #{target}")
else
run("git checkout #{target} -q")
run("git pull origin #{target} -q")
return
end
end
add_remote_and_fetch(options)
puts "Checking out remote branch '#{target}' from '#{owner}/#{repo}/#{branch}'.\n".green
run("git checkout -q -b #{target} #{owner}/#{branch}")
puts "Fetching remote 'origin'.\n".green
run("git fetch -q origin")
if branches(:remote => true, :match => "origin/#{target}")
puts "Pulling 'origin/#{target}'.\n".green
run("git pull origin #{target} -q")
end
puts "Pushing 'origin/#{target}'.\n".green
run("git push origin #{target} -q")
end
def command_not_recognized
readme = "https://github.com/winton/gitcycle/blob/master/README.md"
puts "\nCommand not recognized.".red
puts "\nOpening #{readme}\n".green
Launchy.open(readme)
end
def create_pull_request(branch=nil, force=false)
unless branch
puts "\nRetrieving branch information from gitcycle.\n".green
branch = get('branch',
'branch[name]' => branches(:current => true),
'create' => 0
)
end
if branch && (force || !branch['issue_url'])
puts "Creating GitHub pull request.\n".green
branch = get('branch',
'branch[create_pull_request]' => true,
'branch[name]' => branch['name'],
'create' => 0
)
end
branch
end
def errored?(output)
output.include?("fatal: ") || output.include?("ERROR: ") || $?.exitstatus != 0
end
def exec_git(command, args)
args.unshift("git", command)
Kernel.exec(*args.collect(&:to_s))
end
def fix_conflict(options)
owner = options[:owner]
repo = options[:repo]
branch = options[:branch]
issue = options[:issue]
issues = options[:issues]
type = options[:type]
if $? != 0
puts "Conflict occurred when merging '#{branch}'#{" (issue ##{issue})" if issue}.\n".red
if type == :to_qa
puts "Please resolve this conflict with '#{owner}'.\n".yellow
puts "\nSending conflict information to gitcycle.\n".green
get('qa_branch', 'issues' => issues, "conflict_#{type}" => issue)
puts "Type 'gitc qa resolved' when finished resolving.\n".yellow
exit ERROR[:conflict_when_merging]
end
elsif type # from_qa or to_qa
branch = branches(:current => true)
puts "Pushing branch '#{branch}'.\n".green
run("git push origin #{branch} -q")
end
end
def get(path, hash={})
hash.merge!(
:login => @login,
:token => @token,
:uid => (0...20).map{ ('a'..'z').to_a[rand(26)] }.join
)
hash[:test] = 1 if ENV['ENV'] == 'test'
puts "Transaction ID: #{hash[:uid]}".green
params = ''
hash[:session] = 0
hash.each do |k, v|
if v && v.is_a?(::Array)
params << "#{URI.escape(k.to_s)}=#{URI.escape(v.inspect)}&"
elsif v
params << "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}&"
end
end
params.chop! # trailing &
begin
HTTPI.log = false
req = HTTPI::Request.new "#{API}/#{path}.json?#{params}"
json = HTTPI.get(req).body
rescue Exception => error
puts error.to_s
puts "\nCould not connect to Gitcycle.".red
puts "\nPlease verify your Internet connection and try again later.\n".yellow
exit
end
match = json.match(/Gitcycle error reference code (\d+)/)
error = match && match[1]
if error
puts "\nSomething went wrong :(".red
puts "\nEmail error code #{error} to wwelsh@bleacherreport.com.".yellow
puts "\nInclude a gist of your terminal output if possible.\n".yellow
exit ERROR[:something_went_wrong]
else
Yajl::Parser.parse(json)
end
end
def git_config_path(path)
config = "#{path}/.git/config"
if File.exists?(config)
return config
elsif path == '/'
return nil
else
path = File.expand_path(path + '/..')
git_config_path(path)
end
end
def load_config
if File.exists?(@config_path)
@config = YAML.load(File.read(@config_path))
else
@config = {}
end
end
def load_git
path = git_config_path(Dir.pwd)
if path
@git_url = File.read(path).match(/\[remote "origin"\][^\[]*url = ([^\n]+)/m)[1]
@git_repo = @git_url.match(/([^\/]+)\.git/)[1]
@git_login = @git_url.match(/([^\/:]+)\/[^\/]+\.git/)[1]
@login, @token = @config["#{@git_login}/#{@git_repo}"] rescue [ nil, nil ]
end
end
def merge_remote_branch(options={})
owner = options[:owner]
repo = options[:repo]
branch = options[:branch]
add_remote_and_fetch(options)
if branches(:remote => true, :match => "#{owner}/#{branch}")
puts "\nMerging remote branch '#{branch}' from '#{owner}/#{repo}'.\n".green
run("git merge #{owner}/#{branch}")
fix_conflict(options)
end
end
def options?(args)
args.any? { |arg| arg =~ /^-/ }
end
def q(question, extra='')
puts "#{question.yellow}#{extra}"
$input ? $input.shift : $stdin.gets.strip
end
def remotes(options={})
b = `git remote`
if options[:match]
b.match(/^(#{options[:match]})$/)[1] rescue nil
else
b
end
end
def require_config
unless @login && @token
puts "\nGitcycle configuration not found.".red
puts "Are you in the right repository?".yellow
puts "Have you set up this repository at http://gitcycle.com?\n".yellow
exit
end
true
end
def require_git
unless @git_url && @git_repo && @git_login
puts "\norigin entry within '.git/config' not found!".red
puts "Are you sure you are in a git repository?\n".yellow
exit ERROR[:git_origin_not_found]
end
true
end
def run(cmd, options={})
if ENV['RUN'] == '0'
puts cmd
else
output = `#{cmd} 2>&1`
end
if options[:catch] != false && errored?(output)
puts "#{output}\n\n"
puts "Gitcycle encountered an error when running the last command:".red
puts " #{cmd}\n"
puts "Please copy this session's output and send it to gitcycle@bleacherreport.com.\n".yellow
exit ERROR[:last_command_errored]
else
output
end
end
def save_config
FileUtils.mkdir_p(File.dirname(@config_path))
File.open(@config_path, 'w') do |f|
f.write(YAML.dump(@config))
end
end
def yes?(question)
q(question, " (#{"y".green}/#{"n".red})").downcase[0..0] == 'y'
end
end