git-maintain 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,88 @@
1
+ module GitMaintain
2
+ class CI
3
+
4
+ def self.load(repo)
5
+ repo_name = File.basename(repo.path)
6
+ return GitMaintain::loadClass(CI, repo_name, repo)
7
+ end
8
+
9
+ def initialize(repo)
10
+ GitMaintain::checkDirectConstructor(self.class)
11
+
12
+ @repo = repo
13
+ @cachedJson={}
14
+ end
15
+
16
+ private
17
+ def log(lvl, str)
18
+ GitMaintain::log(lvl, str)
19
+ end
20
+
21
+ def fetch(uri_str, limit = 10)
22
+ # You should choose a better exception.
23
+ raise ArgumentError, 'too many HTTP redirects' if limit == 0
24
+
25
+ response = Net::HTTP.get_response(URI(uri_str))
26
+
27
+ case response
28
+ when Net::HTTPSuccess then
29
+ response
30
+ when Net::HTTPRedirection then
31
+ location = response['location']
32
+ fetch(location, limit - 1)
33
+ else
34
+ response.value
35
+ end
36
+ end
37
+ def getJson(base_url, query_label, query, json=true)
38
+ return @cachedJson[query_label] if @cachedJson[query_label] != nil
39
+ url = base_url + query
40
+ uri = URI(url)
41
+ log(:INFO, "Querying CI...")
42
+ log(:DEBUG_CI, url)
43
+ response = fetch(uri)
44
+ raise("CI request failed '#{url}'") if response.code.to_s() != '200'
45
+
46
+ if json == true
47
+ @cachedJson[query_label] = JSON.parse(response.body)
48
+ else
49
+ @cachedJson[query_label] = response.body
50
+ end
51
+ return @cachedJson[query_label]
52
+ end
53
+
54
+ public
55
+ def getValidState(br, sha1)
56
+ raise("Unimplemented")
57
+ end
58
+ def checkValidState(br, sha1)
59
+ raise("Unimplemented")
60
+ end
61
+ def getValidLog(br, sha1)
62
+ raise("Unimplemented")
63
+ end
64
+ def getValidTS(br, sha1)
65
+ raise("Unimplemented")
66
+ end
67
+
68
+ def getStableState(br, sha1)
69
+ raise("Unimplemented")
70
+ end
71
+ def checkStableState(br, sha1)
72
+ raise("Unimplemented")
73
+ end
74
+ def getStableLog(br, sha1)
75
+ raise("Unimplemented")
76
+ end
77
+ def getStableTS(br, sha1)
78
+ raise("Unimplemented")
79
+ end
80
+ def emptyCache()
81
+ @cachedJson={}
82
+ end
83
+
84
+ def isErrored(br, status)
85
+ raise("Unimplemented")
86
+ end
87
+ end
88
+ end
@@ -1,6 +1,8 @@
1
1
  $LOAD_PATH.push(BACKPORT_LIB_DIR)
2
2
 
3
+ require 'ci'
3
4
  require 'travis'
5
+ require 'azure'
4
6
  require 'repo'
5
7
  require 'branch'
6
8
 
@@ -50,7 +52,7 @@ module GitMaintain
50
52
 
51
53
  ACTION_CLASS = [ Common, Branch, Repo ]
52
54
  @@custom_classes = {}
53
-
55
+ @@load_class = []
54
56
  @@verbose_log = false
55
57
 
56
58
  def registerCustom(repo_name, classes)
@@ -72,20 +74,22 @@ module GitMaintain
72
74
  module_function :getClass
73
75
 
74
76
  def loadClass(default_class, repo_name, *more)
75
- return GitMaintain::getClass(default_class, repo_name).new(*more)
77
+ @@load_class.push(default_class)
78
+ obj = GitMaintain::getClass(default_class, repo_name).new(*more)
79
+ @@load_class.pop()
80
+ return obj
76
81
  end
77
82
  module_function :loadClass
78
83
 
79
84
  # Check that the constructor was called through loadClass
80
85
  def checkDirectConstructor(theClass)
81
- # Look for the "new" in the calling tree
82
- depth = 1
83
- while caller_locations(depth, 1)[0].label != "new"
84
- depth +=1
86
+ curLoad= @@load_class.last()
87
+ cl = theClass
88
+ while cl != Object
89
+ return if cl == curLoad
90
+ cl = cl.superclass
85
91
  end
86
- # The function that called the constructer is just one step below
87
- raise("Use GitMaintain::loadClass to construct a #{theClass} class") if
88
- caller_locations(depth + 1, 1)[0].label != "loadClass"
92
+ raise("Use GitMaintain::loadClass to construct a #{theClass} class")
89
93
  end
90
94
  module_function :checkDirectConstructor
91
95
 
@@ -112,6 +116,7 @@ module GitMaintain
112
116
 
113
117
  def checkOpts(opts)
114
118
  ACTION_CLASS.each(){|x|
119
+ next if x::ACTION_LIST.index(opts[:action]) == nil
115
120
  next if x.singleton_methods().index(:check_opts) == nil
116
121
  x.check_opts(opts)
117
122
 
@@ -173,8 +178,8 @@ module GitMaintain
173
178
  case lvl
174
179
  when :DEBUG
175
180
  _log("DEBUG".magenta(), str) if ENV["DEBUG"].to_s() != ""
176
- when :DEBUG_TRAVIS
177
- _log("DEBUG_TRAVIS".magenta(), str) if ENV["DEBUG_TRAVIS"].to_s() != ""
181
+ when :DEBUG_CI
182
+ _log("DEBUG_CI".magenta(), str) if ENV["DEBUG_CI"].to_s() != ""
178
183
  when :VERBOSE
179
184
  _log("INFO".blue(), str) if @@verbose_log == true
180
185
  when :INFO
@@ -1,3 +1,6 @@
1
+ require 'octokit'
2
+ require 'io/console'
3
+
1
4
  module GitMaintain
2
5
  class Repo
3
6
  @@VALID_REPO = "github"
@@ -45,6 +48,7 @@ module GitMaintain
45
48
  @branch_list=nil
46
49
  @stable_branches=nil
47
50
  @suffix_list=nil
51
+ @config_cache={}
48
52
 
49
53
  if path == nil
50
54
  @path = Dir.pwd()
@@ -134,7 +138,7 @@ module GitMaintain
134
138
  fi; git --work-tree=#{@path} imap-send #{cmd}`
135
139
  end
136
140
  def getGitConfig(entry)
137
- return runGit("config #{entry} 2> /dev/null").chomp()
141
+ return @config_cache[entry] ||= runGit("config #{entry} 2> /dev/null").chomp()
138
142
  end
139
143
 
140
144
  def runBash()
@@ -194,7 +198,7 @@ module GitMaintain
194
198
  return @suffix_list
195
199
  end
196
200
 
197
- def submitReleases(opts)
201
+ def getUnreleasedTags(opts)
198
202
  remote_tags=runGit("ls-remote --tags #{@stable_repo} |
199
203
  egrep 'refs/tags/v[0-9.]*$'").split("\n").map(){
200
204
  |x| x.gsub(/.*refs\/tags\//, '')
@@ -202,75 +206,80 @@ module GitMaintain
202
206
  local_tags =runGit("tag -l | egrep '^v[0-9.]*$'").split("\n")
203
207
 
204
208
  new_tags = local_tags - remote_tags
205
- if new_tags.empty? then
206
- log(:INFO, "All tags are already submitted.")
207
- return
208
- end
209
-
210
- log(:WARNING, "This will officially release these tags: #{new_tags.join(", ")}")
211
- rep = GitMaintain::confirm(opts, "release them")
212
- if rep != 'y' then
213
- raise "Aborting.."
214
- end
215
-
216
- if @NOTIFY_RELEASE != false
217
- mail_path=`mktemp`.chomp()
218
- mail = File.open(mail_path, "w+")
219
- mail.puts "From " + runGit("rev-parse HEAD") + " " + `date`.chomp()
220
- mail.puts "From: " + getGitConfig("user.name") +
221
- " <" + getGitConfig("user.email") +">"
222
- mail.puts "To: " + getGitConfig("patch.target")
223
- mail.puts "Date: " + `date -R`.chomp()
224
-
225
- if new_tags.length > 4 then
226
- mail.puts "Subject: [ANNOUNCE] " + File.basename(@path) + ": new stable releases"
227
- mail.puts ""
228
- mail.puts "These version were tagged/released:\n * " +
229
- new_tags.join("\n * ")
230
- mail.puts ""
231
- else
232
- mail.puts "Subject: [ANNOUNCE] " + File.basename(@path) + " " +
233
- (new_tags.length > 1 ?
234
- (new_tags[0 .. -2].join(", ") + " and " + new_tags[-1] + " have") :
235
- (new_tags.join(" ") + " has")) +
236
- " been tagged/released"
237
- mail.puts ""
238
- end
239
- mail.puts "It's available at the normal places:"
209
+ return new_tags
210
+ end
211
+ def genReleaseNotif(opts, new_tags)
212
+ return if @NOTIFY_RELEASE == false
213
+
214
+ mail_path=`mktemp`.chomp()
215
+ mail = File.open(mail_path, "w+")
216
+ mail.puts "From " + runGit("rev-parse HEAD") + " " + `date`.chomp()
217
+ mail.puts "From: " + getGitConfig("user.name") +
218
+ " <" + getGitConfig("user.email") +">"
219
+ mail.puts "To: " + getGitConfig("patch.target")
220
+ mail.puts "Date: " + `date -R`.chomp()
221
+
222
+ if new_tags.length > 4 then
223
+ mail.puts "Subject: [ANNOUNCE] " + File.basename(@path) + ": new stable releases"
240
224
  mail.puts ""
241
- mail.puts "git://github.com/#{@remote_stable}"
242
- mail.puts "https://github.com/#{@remote_stable}/releases"
225
+ mail.puts "These version were tagged/released:\n * " +
226
+ new_tags.join("\n * ")
243
227
  mail.puts ""
244
- mail.puts "---"
228
+ else
229
+ mail.puts "Subject: [ANNOUNCE] " + File.basename(@path) + " " +
230
+ (new_tags.length > 1 ?
231
+ (new_tags[0 .. -2].join(", ") + " and " + new_tags[-1] + " have") :
232
+ (new_tags.join(" ") + " has")) +
233
+ " been tagged/released"
245
234
  mail.puts ""
246
- mail.puts "Here's the information from the tags:"
247
- new_tags.sort().each(){|tag|
248
- mail.puts `git show #{tag} --no-decorate -q | awk '!p;/^-----END PGP SIGNATURE-----/{p=1}'`
249
- mail.puts ""
250
- }
251
- mail.puts "It's available at the normal places:"
235
+ end
236
+ mail.puts "It's available at the normal places:"
237
+ mail.puts ""
238
+ mail.puts "git://github.com/#{@remote_stable}"
239
+ mail.puts "https://github.com/#{@remote_stable}/releases"
240
+ mail.puts ""
241
+ mail.puts "---"
242
+ mail.puts ""
243
+ mail.puts "Here's the information from the tags:"
244
+ new_tags.sort().each(){|tag|
245
+ mail.puts `git show #{tag} --no-decorate -q | awk '!p;/^-----END PGP SIGNATURE-----/{p=1}'`
252
246
  mail.puts ""
253
- mail.puts "git://github.com/#{@remote_stable}"
254
- mail.puts "https://github.com/#{@remote_stable}/releases"
255
- mail.close()
256
-
257
- case @mail_format
258
- when :imap_send
259
- puts runGitImap("< #{mail_path}")
260
- when :send_email
261
- run("cp #{mail_path} announce-release.eml")
262
- log(:INFO, "Generated annoucement email in #{@path}/announce-release.eml")
263
- end
264
- run("rm -f #{mail_path}")
247
+ }
248
+ mail.puts "It's available at the normal places:"
249
+ mail.puts ""
250
+ mail.puts "git://github.com/#{@remote_stable}"
251
+ mail.puts "https://github.com/#{@remote_stable}/releases"
252
+ mail.close()
253
+
254
+ case @mail_format
255
+ when :imap_send
256
+ puts runGitImap("< #{mail_path}")
257
+ when :send_email
258
+ run("cp #{mail_path} announce-release.eml")
259
+ log(:INFO, "Generated annoucement email in #{@path}/announce-release.eml")
265
260
  end
261
+ run("rm -f #{mail_path}")
262
+ end
263
+ def submitReleases(opts, new_tags)
264
+ new_tags.each(){|tag|
265
+ createRelease(opts, tag)
266
+ }
267
+ end
266
268
 
267
- log(:WARNING, "Last chance to cancel before submitting")
268
- rep= GitMaintain::confirm(opts, "submit these releases")
269
- if rep != 'y' then
270
- raise "Aborting.."
269
+ def createRelease(opts, tag, github_rel=true)
270
+ log(:INFO, "Creating a release for #{tag}")
271
+ runGit("push #{@stable_repo} refs/tags/#{tag}")
272
+
273
+ if github_rel == true then
274
+ msg = runGit("tag -l -n1000 '#{tag}'") + "\n"
275
+
276
+ # Ye ghods is is a horrific format to parse
277
+ name, body = msg.split("\n", 2)
278
+ name = name.gsub(/^#{tag}/, '').strip
279
+ body = body.split("\n").map { |l| l.sub(/^ /, '') }.join("\n")
280
+ api.create_release(@remote_stable, tag, :name => name, :body => body)
271
281
  end
272
- puts `#{@@SUBMIT_BINARY}`
273
- end
282
+ end
274
283
 
275
284
  def versionToLocalBranch(version, suff)
276
285
  return @branch_format_raw.gsub(/\\\//, '/').
@@ -304,12 +313,33 @@ module GitMaintain
304
313
  puts getSuffixList()
305
314
  end
306
315
  def submit_release(opts)
307
- submitReleases(opts)
316
+ new_tags = getUnreleasedTags(opts)
317
+ if new_tags.empty? then
318
+ log(:INFO, "All tags are already submitted.")
319
+ return
320
+ end
321
+
322
+ log(:WARNING, "This will officially release these tags: #{new_tags.join(", ")}")
323
+ rep = GitMaintain::confirm(opts, "release them")
324
+ if rep != 'y' then
325
+ raise "Aborting.."
326
+ end
327
+
328
+ if @NOTIFY_RELEASE != false
329
+ genReleaseNotif(opts, new_tags)
330
+ end
331
+
332
+ log(:WARNING, "Last chance to cancel before submitting")
333
+ rep= GitMaintain::confirm(opts, "submit these releases")
334
+ if rep != 'y' then
335
+ raise "Aborting.."
336
+ end
337
+ submitReleases(opts, new_tags)
308
338
  end
309
339
  def summary(opts)
310
340
  log(:INFO, "Configuration summary:")
311
- log(:INFO, "Stable remote: #{@@STABLE_REPO}")
312
- log(:INFO, "Validation remote: #{@@VALID_REPO}")
341
+ log(:INFO, "Stable remote: #{@stable_repo}")
342
+ log(:INFO, "Validation remote: #{@valid_repo}")
313
343
  log(:INFO, "")
314
344
  log(:INFO, "Branch config:")
315
345
  log(:INFO, "Local branch format: /#{@branch_format_raw}/")
@@ -369,5 +399,50 @@ module GitMaintain
369
399
 
370
400
  return alts
371
401
  end
372
- end
402
+
403
+ #
404
+ # Github API stuff
405
+ #
406
+ def api
407
+ @api ||= Octokit::Client.new(:access_token => token, :auto_paginate => true)
408
+ end
409
+
410
+ def token
411
+ @token ||= begin
412
+ # We cannot use the 'defaults' functionality of git_config here,
413
+ # because get_new_token would be evaluated before git_config ran
414
+ tok = getGitConfig("maintain.api-token")
415
+ tok.to_s() == "" ? get_new_token : tok
416
+ end
417
+ end
418
+ def get_new_token
419
+ puts "Requesting a new OAuth token from Github..."
420
+ print "Github username: "
421
+ user = $stdin.gets.chomp
422
+ print "Github password: "
423
+ pass = $stdin.noecho(&:gets).chomp
424
+ puts
425
+
426
+ api = Octokit::Client.new(:login => user, :password => pass)
427
+
428
+ begin
429
+ res = api.create_authorization(:scopes => [:repo], :note => "git-maintain")
430
+ rescue Octokit::Unauthorized
431
+ puts "Username or password incorrect. Please try again."
432
+ return get_new_token
433
+ rescue Octokit::OneTimePasswordRequired
434
+ print "Github OTP: "
435
+ otp = $stdin.noecho(&:gets).chomp
436
+ res = api.create_authorization(:scopes => [:repo], :note => "git-maintain",
437
+ :headers => {"X-GitHub-OTP" => otp})
438
+ end
439
+
440
+ token = res[:token]
441
+ runGit("config --global maintain.api-token '#{token}'")
442
+
443
+ # Now reopen with the token so OTP does not bother us
444
+ @api=nil
445
+ token
446
+ end
447
+ end
373
448
  end
@@ -1,56 +1,13 @@
1
1
  module GitMaintain
2
- class TravisChecker
2
+ class TravisCI < CI
3
3
  TRAVIS_URL='https://api.travis-ci.org/'
4
4
 
5
- def self.load(repo)
6
- repo_name = File.basename(repo.path)
7
- return GitMaintain::loadClass(TravisChecker, repo_name, repo)
8
- end
9
-
10
5
  def initialize(repo)
11
- GitMaintain::checkDirectConstructor(self.class)
12
-
13
- @repo = repo
14
- @cachedJson={}
6
+ super(repo)
7
+ @url = TRAVIS_URL
15
8
  end
16
9
 
17
10
  private
18
- def log(lvl, str)
19
- GitMaintain::log(lvl, str)
20
- end
21
-
22
- def fetch(uri_str, limit = 10)
23
- # You should choose a better exception.
24
- raise ArgumentError, 'too many HTTP redirects' if limit == 0
25
-
26
- response = Net::HTTP.get_response(URI(uri_str))
27
-
28
- case response
29
- when Net::HTTPSuccess then
30
- response
31
- when Net::HTTPRedirection then
32
- location = response['location']
33
- fetch(location, limit - 1)
34
- else
35
- response.value
36
- end
37
- end
38
- def getJson(query_label, query, json=true)
39
- return @cachedJson[query_label] if @cachedJson[query_label] != nil
40
- url = TRAVIS_URL + query
41
- uri = URI(url)
42
- log(:INFO, "Querying travis...")
43
- log(:DEBUG_TRAVIS, url)
44
- response = fetch(uri)
45
- raise("Travis request failed '#{url}'") if response.code.to_s() != '200'
46
-
47
- if json == true
48
- @cachedJson[query_label] = JSON.parse(response.body)
49
- else
50
- @cachedJson[query_label] = response.body
51
- end
52
- return @cachedJson[query_label]
53
- end
54
11
  def getState(sha1, resp)
55
12
  br = findBranch(sha1, resp)
56
13
  return "not found" if br == nil
@@ -61,7 +18,7 @@ module GitMaintain
61
18
  br = findBranch(sha1, resp)
62
19
  raise("Travis build not found") if br == nil
63
20
  job_id = br["job_ids"].last().to_s()
64
- return getJson("log_" + job_id, 'jobs/' + job_id + '/log', false)
21
+ return getJson(@url, "travis_log_" + job_id, 'jobs/' + job_id + '/log', false)
65
22
  end
66
23
  def getTS(sha1, resp)
67
24
  br = findBranch(sha1, resp)
@@ -73,17 +30,17 @@ module GitMaintain
73
30
  end
74
31
 
75
32
  def getBrValidJson()
76
- return getJson(:br_valid, 'repos/' + @repo.remote_valid + '/branches')
33
+ return getJson(@url, :travis_br_valid, 'repos/' + @repo.remote_valid + '/branches')
77
34
  end
78
35
  def getBrStableJson()
79
- return getJson(:br_stable, 'repos/' + @repo.remote_stable + '/branches')
36
+ return getJson(@url, :travis_br_stable, 'repos/' + @repo.remote_stable + '/branches')
80
37
  end
81
38
  def findBranch(sha1, resp)
82
- log(:DEBUG_TRAVIS, "Looking for build for #{sha1}")
39
+ log(:DEBUG_CI, "Looking for build for #{sha1}")
83
40
  resp["branches"].each(){|br|
84
41
  commit=resp["commits"].select(){|e| e["id"] == br["commit_id"]}.first()
85
42
  raise("Incomplete JSON received from Travis") if commit == nil
86
- log(:DEBUG_TRAVIS, "Found entry for sha #{commit["sha"]}")
43
+ log(:DEBUG_CI, "Found entry for sha #{commit["sha"]}")
87
44
  next if commit["sha"] != sha1
88
45
  return br
89
46
  }
@@ -91,33 +48,33 @@ module GitMaintain
91
48
  end
92
49
 
93
50
  public
94
- def getValidState(sha1)
51
+ def getValidState(br, sha1)
95
52
  return getState(sha1, getBrValidJson())
96
53
  end
97
- def checkValidState(sha1)
54
+ def checkValidState(br, sha1)
98
55
  return checkState(sha1, getBrValidJson())
99
56
  end
100
- def getValidLog(sha1)
57
+ def getValidLog(br, sha1)
101
58
  return getLog(sha1, getBrValidJson())
102
59
  end
103
- def getValidTS(sha1)
60
+ def getValidTS(br, sha1)
104
61
  return getTS(sha1, getBrValidJson())
105
62
  end
106
63
 
107
- def getStableState(sha1)
64
+ def getStableState(br, sha1)
108
65
  return getState(sha1, getBrStableJson())
109
66
  end
110
- def checkStableState(sha1)
67
+ def checkStableState(br, sha1)
111
68
  return checkState(sha1, getBrStableJson())
112
69
  end
113
- def getStableLog(sha1)
70
+ def getStableLog(br, sha1)
114
71
  return getLog(sha1, getBrStableJson())
115
72
  end
116
- def getStableTS(sha1)
73
+ def getStableTS(br, sha1)
117
74
  return getTS(sha1, getBrStableJson())
118
75
  end
119
- def emptyCache()
120
- @cachedJson={}
76
+ def isErrored(br, status)
77
+ return status == "failed" || status == "errored"
121
78
  end
122
79
  end
123
80
  end