git-maintain 0.7.0 → 0.8.0

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.
@@ -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