git-maintain 0.11.0 → 0.13.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.
- checksums.yaml +4 -4
- data/CHANGELOG +21 -0
- data/README.md +14 -14
- data/bin/git-maintain +7 -55
- data/git-maintain-completion.sh +3 -3
- data/lib/{addons → git-maintain/addons}/RDMACore.rb +91 -33
- data/lib/{addons → git-maintain/addons}/git-maintain.rb +48 -15
- data/lib/git-maintain/addons/healthd.rb +73 -0
- data/lib/git-maintain/addons/hpc-testing.rb +102 -0
- data/lib/git-maintain/azure.rb +200 -0
- data/lib/git-maintain/branch.rb +944 -0
- data/lib/git-maintain/branch_iterator.rb +148 -0
- data/lib/git-maintain/ci.rb +163 -0
- data/lib/git-maintain/common.rb +117 -0
- data/lib/git-maintain/error.rb +81 -0
- data/lib/git-maintain/repo.rb +573 -0
- data/lib/git-maintain/travis.rb +179 -0
- data/lib/git-maintain.rb +2 -0
- metadata +30 -14
- data/lib/azure.rb +0 -98
- data/lib/branch.rb +0 -759
- data/lib/ci.rb +0 -88
- data/lib/common.rb +0 -253
- data/lib/repo.rb +0 -447
- data/lib/travis.rb +0 -80
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
require 'octokit'
|
|
2
|
+
require 'io/console'
|
|
3
|
+
|
|
4
|
+
# Main module for git-maintain repository maintenance tool.
|
|
5
|
+
module GitMaintain
|
|
6
|
+
# Class representing a git repository being maintained by git-maintain.
|
|
7
|
+
# Handles parsing and caching configuration, querying branches/tags, and executing git commands.
|
|
8
|
+
class Repo < Common
|
|
9
|
+
# Default name of the validation/upstream repository.
|
|
10
|
+
@@VALID_REPO = "github"
|
|
11
|
+
# Default name of the stable release repository.
|
|
12
|
+
@@STABLE_REPO = "stable"
|
|
13
|
+
# Default path to the release package submission helper command.
|
|
14
|
+
@@SUBMIT_BINARY="git-release"
|
|
15
|
+
|
|
16
|
+
# List of available actions for Repo class.
|
|
17
|
+
ACTION_LIST = [
|
|
18
|
+
:list_branches,
|
|
19
|
+
:summary,
|
|
20
|
+
# Internal commands for completion
|
|
21
|
+
:list_suffixes, :submit_release
|
|
22
|
+
]
|
|
23
|
+
# Description map of actions for CLI help output.
|
|
24
|
+
ACTION_HELP = {
|
|
25
|
+
:submit_release => "Push the tags to 'stable' remote and create the release packages",
|
|
26
|
+
:summary => "Displays a summary of the configuration and the branches git-maintain sees"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Factory method to load an instance of the Repo class or its repository-specific subclass.
|
|
30
|
+
#
|
|
31
|
+
# @param path [String] Repository directory path (defaults to current working directory)
|
|
32
|
+
# @return [Repo] The loaded Repo instance (or subclass)
|
|
33
|
+
# @raise [GitMaintainError] If class loading fails
|
|
34
|
+
def self.load()
|
|
35
|
+
(repo_path, repo_name) = GitMaintain::getRepoInfos()
|
|
36
|
+
return GitMaintain::loadClass(Repo, repo_name, repo_path)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Validate parsed options for repository-level actions.
|
|
40
|
+
#
|
|
41
|
+
# @param opts [Hash] Options hash to validate
|
|
42
|
+
# @raise [GitMaintainError] If options are invalid or conflicting
|
|
43
|
+
def self.check_opts(opts)
|
|
44
|
+
if opts[:action] == :submit_release then
|
|
45
|
+
if opts[:br_suff] != "master" then
|
|
46
|
+
raise GitMaintainError.new("Action #{opts[:action]} can only be done on 'master' suffixed branches")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Initialize the Repo instance, parsing git configurations and detecting remotes/formats.
|
|
52
|
+
#
|
|
53
|
+
# @param path [String, nil] Repository directory path (defaults to current working directory)
|
|
54
|
+
# @raise [GitMaintainError] If configuration format or values are invalid
|
|
55
|
+
def initialize(path)
|
|
56
|
+
GitMaintain::checkDirectConstructor(self.class)
|
|
57
|
+
|
|
58
|
+
@path = path
|
|
59
|
+
@branch_list=nil
|
|
60
|
+
@stable_branches=nil
|
|
61
|
+
@suffix_list=nil
|
|
62
|
+
@config_cache={}
|
|
63
|
+
|
|
64
|
+
if path == nil
|
|
65
|
+
@path = Dir.pwd()
|
|
66
|
+
end
|
|
67
|
+
@name = File.basename(@path)
|
|
68
|
+
|
|
69
|
+
@valid_repo = getGitConfig("maintain.valid-repo")
|
|
70
|
+
@valid_repo = @@VALID_REPO if @valid_repo == ""
|
|
71
|
+
@stable_repo = getGitConfig("maintain.stable-repo")
|
|
72
|
+
@stable_repo = @@STABLE_REPO if @stable_repo == ""
|
|
73
|
+
|
|
74
|
+
@remote_valid=runGit("remote -v | grep -E '^#{@valid_repo}' | grep fetch |
|
|
75
|
+
awk '{ print $2}' | sed -e 's/.*://' -e 's/\\.git//'")
|
|
76
|
+
@remote_stable=runGit("remote -v | grep -E '^#{@stable_repo}' | grep fetch |
|
|
77
|
+
awk '{ print $2}' | sed -e 's/.*://' -e 's/\\.git//'")
|
|
78
|
+
|
|
79
|
+
@auto_fetch = getGitConfig("maintain.autofetch")
|
|
80
|
+
case @auto_fetch
|
|
81
|
+
when ""
|
|
82
|
+
@auto_fetch = nil
|
|
83
|
+
when "true", "yes", "on"
|
|
84
|
+
@auto_fetch = true
|
|
85
|
+
when "false", "no", "off"
|
|
86
|
+
@auto_fetch = false
|
|
87
|
+
else
|
|
88
|
+
raise GitMaintainError.new("Invalid value '#{@auto_fetch}' in git config for maintain.autofetch")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
@branch_format_raw = getGitConfig("maintain.branch-format")
|
|
92
|
+
@branch_format = Regexp.new(@branch_format_raw)
|
|
93
|
+
@stable_branch_format = getGitConfig("maintain.stable-branch-format")
|
|
94
|
+
@stable_base_format = getGitConfig("maintain.stable-base-format")
|
|
95
|
+
|
|
96
|
+
@stable_base_patterns=
|
|
97
|
+
runGit("config --get-regexp stable-base | grep -E '^stable-base\.' | "+
|
|
98
|
+
"sed -e 's/stable-base\.//' -e 's/---/\\//g'").split("\n").inject({}){ |m, x|
|
|
99
|
+
y=x.split(" ");
|
|
100
|
+
m[y[0]] = y[1]
|
|
101
|
+
m
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@mail_format = getGitConfig("maintain.mail-format")
|
|
105
|
+
if @mail_format == "" then
|
|
106
|
+
@mail_format = :imap_send
|
|
107
|
+
else
|
|
108
|
+
# Check that the format is valid
|
|
109
|
+
case @mail_format
|
|
110
|
+
when "imap_send", "send_email"
|
|
111
|
+
else
|
|
112
|
+
raise GitMaintainError.new("Invalid mail-format #{@mail_format}")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
@mail_format = @mail_format.to_sym()
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
attr_reader :path, :name, :remote_valid, :remote_stable, :valid_repo, :stable_repo
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Check if the specified git reference exists.
|
|
123
|
+
#
|
|
124
|
+
# @param ref [String] Git reference to verify (e.g. 'HEAD' or 'origin/master')
|
|
125
|
+
# @return [String] Resolved SHA-1 string of the reference
|
|
126
|
+
# @raise [NoRefError] If the reference does not exist
|
|
127
|
+
def ref_exist?(ref)
|
|
128
|
+
begin
|
|
129
|
+
return runGit("rev-parse --verify --quiet '#{ref}'")
|
|
130
|
+
rescue RunError
|
|
131
|
+
raise(NoRefError.new(ref))
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Run a git imap-send command setting GIT_ASKPASS environment variables.
|
|
136
|
+
#
|
|
137
|
+
# @param cmd [String] The command arguments
|
|
138
|
+
# @return [String] Output of the command execution
|
|
139
|
+
def runGitImap(cmd)
|
|
140
|
+
return `export GIT_ASKPASS=$(dirname $(dirname $(which git)))/lib/git-core/git-gui--askpass;
|
|
141
|
+
if [ ! -f $GIT_ASKPASS ]; then
|
|
142
|
+
export GIT_ASKPASS=$(dirname $(which git))/git-gui--askpass;
|
|
143
|
+
fi;
|
|
144
|
+
if [ ! -f $GIT_ASKPASS ]; then
|
|
145
|
+
export GIT_ASKPASS=/usr/lib/ssh/ssh-askpass;
|
|
146
|
+
fi; git --work-tree=#{@path} imap-send #{cmd}`
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Retrieve a git configuration value, with caching.
|
|
150
|
+
#
|
|
151
|
+
# @param entry [String] Config key name (e.g., 'user.name')
|
|
152
|
+
# @return [String] Cached or fetched config value string
|
|
153
|
+
# @raise [RunError] If running git config fails unexpectedly
|
|
154
|
+
def getGitConfig(entry)
|
|
155
|
+
return @config_cache[entry] ||= runGit("config #{entry} 2> /dev/null", {}, false).chomp()
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Spawn an interactive subshell (bash), wrapping error conditions into a GitMaintainError.
|
|
159
|
+
#
|
|
160
|
+
# @param env [String] Optional environment variables string to prepend
|
|
161
|
+
# @raise [GitMaintainError] If the shell exits with a non-zero code and is cancelled by the user
|
|
162
|
+
def runBash(env="")
|
|
163
|
+
begin
|
|
164
|
+
runSystem(env + " bash")
|
|
165
|
+
rescue RunError
|
|
166
|
+
log(:ERROR, "Shell exited with code #{$?}. Exiting")
|
|
167
|
+
raise GitMaintainError.new("Cancelled by user")
|
|
168
|
+
end
|
|
169
|
+
log(:INFO, "Continuing...")
|
|
170
|
+
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Get the single-line headline/oneline description of a commit SHA.
|
|
174
|
+
#
|
|
175
|
+
# @param sha [String] Commit SHA
|
|
176
|
+
# @return [String] Single-line description (oneline)
|
|
177
|
+
# @raise [RunError] If git command execution fails
|
|
178
|
+
def getCommitHeadline(sha)
|
|
179
|
+
return runGit("show --format=oneline --no-patch --no-decorate #{sha}")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Get the commit subject text.
|
|
183
|
+
#
|
|
184
|
+
# @param sha [String] Commit SHA
|
|
185
|
+
# @return [String] The commit subject string
|
|
186
|
+
# @raise [RunError] If git command execution fails
|
|
187
|
+
def getCommitSubj(sha)
|
|
188
|
+
return runGit("log -1 --pretty=\"%s\" #{sha}")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Fetch stable updates if auto-fetching is enabled.
|
|
192
|
+
#
|
|
193
|
+
# @param fetch [Boolean, nil] Override to bypass or force fetch configuration
|
|
194
|
+
# @raise [RunError] If git fetch execution fails
|
|
195
|
+
def stableUpdate(fetch=nil)
|
|
196
|
+
fetch = @auto_fetch if fetch == nil
|
|
197
|
+
return if fetch == false
|
|
198
|
+
log(:VERBOSE, "Fetching stable updates...")
|
|
199
|
+
runGit("fetch #{@stable_repo}")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# List all local branches matching the configured stable suffix name.
|
|
203
|
+
#
|
|
204
|
+
# @param br_suff [String] Branch suffix (e.g. 'master')
|
|
205
|
+
# @return [Array<String>] List of matching branch version strings (e.g., ['1.0'])
|
|
206
|
+
# @raise [RunError] If git branch execution fails
|
|
207
|
+
def getBranchList(br_suff)
|
|
208
|
+
return @branch_list if @branch_list != nil
|
|
209
|
+
|
|
210
|
+
@branch_list=runGit("branch").split("\n").map(){|x|
|
|
211
|
+
x=~ Regexp.new("#{@branch_format_raw}/#{br_suff}$") ?
|
|
212
|
+
$1 : nil
|
|
213
|
+
}.compact().uniq()
|
|
214
|
+
|
|
215
|
+
return @branch_list
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# List all remote stable branches from the stable remote.
|
|
219
|
+
#
|
|
220
|
+
# @return [Array<String>] List of remote stable branch version strings
|
|
221
|
+
# @raise [RunError] If git branch execution fails
|
|
222
|
+
def getStableBranchList()
|
|
223
|
+
return @stable_branches if @stable_branches != nil
|
|
224
|
+
|
|
225
|
+
@stable_branches=runGit("branch -a").split("\n").map(){|x|
|
|
226
|
+
x=~ Regexp.new("remotes/#{@@STABLE_REPO}/#{@stable_branch_format.gsub(/\\1/, '([0-9]+)')}$") ?
|
|
227
|
+
$1 : nil
|
|
228
|
+
}.compact().uniq()
|
|
229
|
+
|
|
230
|
+
return @stable_branches
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Get all local branch suffix values.
|
|
234
|
+
#
|
|
235
|
+
# @return [Array<String>] List of local branch suffixes
|
|
236
|
+
# @raise [RunError] If git branch execution fails
|
|
237
|
+
def getSuffixList()
|
|
238
|
+
return @suffix_list if @suffix_list != nil
|
|
239
|
+
|
|
240
|
+
@suffix_list = runGit("branch").split("\n").map(){|x|
|
|
241
|
+
x=~ @branch_format ?
|
|
242
|
+
Regexp.new("^\\*?\\s*#{@branch_format_raw}/([a-zA-Z0-9_-]+)\\s*$").match(x)[-1] :
|
|
243
|
+
nil
|
|
244
|
+
}.compact().uniq()
|
|
245
|
+
|
|
246
|
+
return @suffix_list
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Find local release tags that have not yet been pushed to the remote stable repository.
|
|
250
|
+
#
|
|
251
|
+
# @param opts [Hash] Options hash
|
|
252
|
+
# @return [Array<String>] List of unreleased version tags (e.g. ['v1.0.1'])
|
|
253
|
+
# @raise [RunError] If running git ls-remote or git tag fails
|
|
254
|
+
def getUnreleasedTags(opts)
|
|
255
|
+
remote_tags=runGit("ls-remote --tags #{@stable_repo} |
|
|
256
|
+
grep -E 'refs/tags/v[0-9.]*$'").split("\n").map(){
|
|
257
|
+
|x| x.gsub(/.*refs\/tags\//, '')
|
|
258
|
+
}
|
|
259
|
+
local_tags =runGit("tag -l | grep -E '^v[0-9.]*$'").split("\n")
|
|
260
|
+
|
|
261
|
+
new_tags = local_tags - remote_tags
|
|
262
|
+
return new_tags
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Generate and transmit or save a release announcement email.
|
|
266
|
+
#
|
|
267
|
+
# @param opts [Hash] Options hash
|
|
268
|
+
# @param new_tags [Array<String>] List of newly released tags
|
|
269
|
+
# @raise [RunError] If running git show or other execution commands fails
|
|
270
|
+
def genReleaseNotif(opts, new_tags)
|
|
271
|
+
return if @NOTIFY_RELEASE == false
|
|
272
|
+
|
|
273
|
+
mail_path=`mktemp`.chomp()
|
|
274
|
+
mail = File.open(mail_path, "w+")
|
|
275
|
+
mail.puts "From " + runGit("rev-parse HEAD") + " " + `date`.chomp()
|
|
276
|
+
mail.puts "From: " + getGitConfig("user.name") +
|
|
277
|
+
" <" + getGitConfig("user.email") +">"
|
|
278
|
+
mail.puts "To: " + getGitConfig("patch.target")
|
|
279
|
+
mail.puts "Date: " + `date -R`.chomp()
|
|
280
|
+
|
|
281
|
+
if new_tags.length > 4 then
|
|
282
|
+
mail.puts "Subject: [ANNOUNCE] " + File.basename(@path) + ": new stable releases"
|
|
283
|
+
mail.puts ""
|
|
284
|
+
mail.puts "These version were tagged/released:\n * " +
|
|
285
|
+
new_tags.join("\n * ")
|
|
286
|
+
mail.puts ""
|
|
287
|
+
else
|
|
288
|
+
mail.puts "Subject: [ANNOUNCE] " + File.basename(@path) + " " +
|
|
289
|
+
(new_tags.length > 1 ?
|
|
290
|
+
(new_tags[0 .. -2].join(", ") + " and " + new_tags[-1] + " have") :
|
|
291
|
+
(new_tags.join(" ") + " has")) +
|
|
292
|
+
" been tagged/released"
|
|
293
|
+
mail.puts ""
|
|
294
|
+
end
|
|
295
|
+
mail.puts "It's available at the normal places:"
|
|
296
|
+
mail.puts ""
|
|
297
|
+
mail.puts "git://github.com/#{@remote_stable}"
|
|
298
|
+
mail.puts "https://github.com/#{@remote_stable}/releases"
|
|
299
|
+
mail.puts ""
|
|
300
|
+
mail.puts "---"
|
|
301
|
+
mail.puts ""
|
|
302
|
+
mail.puts "Here's the information from the tags:"
|
|
303
|
+
new_tags.sort().each(){|tag|
|
|
304
|
+
mail.puts `git show #{tag} --no-decorate -q | awk '!p;/^-----END PGP SIGNATURE-----/{p=1}'`
|
|
305
|
+
mail.puts ""
|
|
306
|
+
}
|
|
307
|
+
mail.close()
|
|
308
|
+
|
|
309
|
+
case @mail_format
|
|
310
|
+
when :imap_send
|
|
311
|
+
puts runGitImap("< #{mail_path}")
|
|
312
|
+
when :send_email
|
|
313
|
+
run("cp #{mail_path} announce-release.eml")
|
|
314
|
+
log(:INFO, "Generated annoucement email in #{@path}/announce-release.eml")
|
|
315
|
+
end
|
|
316
|
+
run("rm -f #{mail_path}")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Submit release tags by delegating to `createRelease`.
|
|
320
|
+
#
|
|
321
|
+
# @param opts [Hash] Options hash
|
|
322
|
+
# @param new_tags [Array<String>] List of release tags to submit
|
|
323
|
+
# @raise [GitMaintainError] If submitting release fails
|
|
324
|
+
def submitReleases(opts, new_tags)
|
|
325
|
+
new_tags.each(){|tag|
|
|
326
|
+
createRelease(opts, tag)
|
|
327
|
+
}
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Create a release for the specified tag on GitHub.
|
|
331
|
+
# Pushes the tag to the remote stable repository and optionally calls GitHub release API.
|
|
332
|
+
#
|
|
333
|
+
# @param opts [Hash] Options hash
|
|
334
|
+
# @param tag [String] Release tag name (e.g. 'v1.0.1')
|
|
335
|
+
# @param github_rel [Boolean] True to create a GitHub release via API
|
|
336
|
+
# @raise [GitMaintainError] If git pushing or API release creation fails
|
|
337
|
+
def createRelease(opts, tag, github_rel=true)
|
|
338
|
+
log(:INFO, "Creating a release for #{tag}")
|
|
339
|
+
runGit("push #{@stable_repo} refs/tags/#{tag}")
|
|
340
|
+
|
|
341
|
+
if github_rel == true then
|
|
342
|
+
msg = runGit("tag -l -n1000 '#{tag}'") + "\n"
|
|
343
|
+
|
|
344
|
+
# Ye ghods is is a horrific format to parse
|
|
345
|
+
name, body = msg.split("\n", 2)
|
|
346
|
+
name = name.gsub(/^#{tag}/, '').strip
|
|
347
|
+
body = body.split("\n").map { |l| l.sub(/^ /, '') }.join("\n")
|
|
348
|
+
api.create_release(@remote_stable, tag, :name => name, :body => body)
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Map a version number and suffix to a local branch name.
|
|
353
|
+
#
|
|
354
|
+
# @param version [String] Version string (e.g., '1.0')
|
|
355
|
+
# @param suff [String] Suffix string
|
|
356
|
+
# @return [String] Local branch name (e.g., 'stable/1.0/master')
|
|
357
|
+
def versionToLocalBranch(version, suff)
|
|
358
|
+
return @branch_format_raw.gsub(/\\\//, '/').
|
|
359
|
+
gsub(/\(.*\)/, version) + "/#{suff}"
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Map a version number to a stable branch name.
|
|
363
|
+
#
|
|
364
|
+
# @param version [String] Version string
|
|
365
|
+
# @return [String] Stable branch name
|
|
366
|
+
def versionToStableBranch(version)
|
|
367
|
+
return version.gsub(/^(.*)$/, @stable_branch_format)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Resolve the stable base commit/reference for a given branch.
|
|
371
|
+
#
|
|
372
|
+
# @param branch [String] Local branch name
|
|
373
|
+
# @return [String] Resolvable stable base reference
|
|
374
|
+
# @raise [GitMaintainError] If no stable base can be resolved for the branch
|
|
375
|
+
def findStableBase(branch)
|
|
376
|
+
base=nil
|
|
377
|
+
if branch =~ @branch_format then
|
|
378
|
+
base = branch.gsub(Regexp.new("^\\*?\\s*#{@branch_format_raw}/.*$"), @stable_base_format)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
@stable_base_patterns.each(){|pattern, b|
|
|
382
|
+
if branch =~ /#{pattern}\// || branch =~ /#{pattern}$/
|
|
383
|
+
base = b
|
|
384
|
+
break
|
|
385
|
+
end
|
|
386
|
+
}
|
|
387
|
+
raise GitMaintainError.new("Could not a find a stable base for branch #{branch}") if base == nil
|
|
388
|
+
return base
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Print the local stable branches list to standard output.
|
|
392
|
+
#
|
|
393
|
+
# @param opts [Hash] Options hash
|
|
394
|
+
# @raise [RunError] If running git branch fails
|
|
395
|
+
def list_branches(opts)
|
|
396
|
+
puts getBranchList(opts[:br_suff])
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Print all detected branch suffixes to standard output.
|
|
400
|
+
#
|
|
401
|
+
# @param opts [Hash] Options hash
|
|
402
|
+
# @raise [RunError] If running git branch fails
|
|
403
|
+
def list_suffixes(opts)
|
|
404
|
+
puts getSuffixList()
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Find and submit any unreleased tags, generating announcements and creating GitHub releases.
|
|
408
|
+
#
|
|
409
|
+
# @param opts [Hash] Options hash
|
|
410
|
+
# @raise [GitMaintainError] If the release submission is aborted or fails
|
|
411
|
+
def submit_release(opts)
|
|
412
|
+
new_tags = getUnreleasedTags(opts)
|
|
413
|
+
if new_tags.empty? then
|
|
414
|
+
log(:INFO, "All tags are already submitted.")
|
|
415
|
+
return
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
log(:WARNING, "This will officially release these tags: #{new_tags.join(", ")}")
|
|
419
|
+
rep = confirm(opts, "release them", true)
|
|
420
|
+
if rep != 'y' then
|
|
421
|
+
raise GitMaintainError.new("Aborting..")
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
if @NOTIFY_RELEASE != false
|
|
425
|
+
genReleaseNotif(opts, new_tags)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
log(:WARNING, "Last chance to cancel before submitting")
|
|
429
|
+
rep= confirm(opts, "submit these releases", true)
|
|
430
|
+
if rep != 'y' then
|
|
431
|
+
raise GitMaintainError.new("Aborting..")
|
|
432
|
+
end
|
|
433
|
+
submitReleases(opts, new_tags)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Print a diagnostic summary of the repository configuration and detected local/upstream branches.
|
|
437
|
+
#
|
|
438
|
+
# @param opts [Hash] Options hash
|
|
439
|
+
# @raise [RunError] If running git commands fails
|
|
440
|
+
def summary(opts)
|
|
441
|
+
log(:INFO, "Configuration summary:")
|
|
442
|
+
if self.class != GitMaintain::Repo then
|
|
443
|
+
log(:INFO, "Using custom repo class: #{self.class.to_s()}")
|
|
444
|
+
end
|
|
445
|
+
log(:INFO, "Stable remote: #{@stable_repo}")
|
|
446
|
+
log(:INFO, "Validation remote: #{@valid_repo}")
|
|
447
|
+
log(:INFO, "")
|
|
448
|
+
log(:INFO, "Branch config:")
|
|
449
|
+
log(:INFO, "Local branch format: /#{@branch_format_raw}/")
|
|
450
|
+
log(:INFO, "Remote stable branch format: #{@stable_branch_format}")
|
|
451
|
+
log(:INFO, "Remote stable base format: #{@stable_base_format}")
|
|
452
|
+
|
|
453
|
+
if @stable_base_patterns.length > 0 then
|
|
454
|
+
log(:INFO, "")
|
|
455
|
+
log(:INFO, "Stable base rules:")
|
|
456
|
+
@stable_base_patterns.each(){|name, base|
|
|
457
|
+
log(:INFO, "\t#{name} -> #{base}")
|
|
458
|
+
}
|
|
459
|
+
end
|
|
460
|
+
brList = getBranchList(opts[:br_suff])
|
|
461
|
+
brStList = getStableBranchList()
|
|
462
|
+
|
|
463
|
+
if brList.length > 0 then
|
|
464
|
+
log(:INFO, "")
|
|
465
|
+
log(:INFO, "Local branches:")
|
|
466
|
+
brList.each(){|br|
|
|
467
|
+
branch = Branch.load(self, br, nil, opts[:br_suff])
|
|
468
|
+
localBr = branch.local_branch
|
|
469
|
+
stableBr = @@STABLE_REPO + "/" + branch.remote_branch
|
|
470
|
+
stableBase = branch.stable_base
|
|
471
|
+
begin
|
|
472
|
+
ref_exist?(stableBr)
|
|
473
|
+
rescue NoRefError
|
|
474
|
+
stableBr = "<MISSING>"
|
|
475
|
+
end
|
|
476
|
+
log(:INFO, "\t#{localBr} -> #{stableBr} (#{stableBase})")
|
|
477
|
+
brStList.delete(br)
|
|
478
|
+
}
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
if brStList.length > 0 then
|
|
482
|
+
log(:INFO, "")
|
|
483
|
+
log(:INFO, "Upstream branches:")
|
|
484
|
+
brStList.each(){|br|
|
|
485
|
+
branch = Branch.load(self, br, nil, opts[:branch_suff])
|
|
486
|
+
stableBr = @@STABLE_REPO + "/" + branch.remote_branch
|
|
487
|
+
stableBase = branch.stable_base
|
|
488
|
+
log(:INFO, "\t<MISSING> -> #{stableBr} (#{stableBase})")
|
|
489
|
+
}
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Search remote stable branches for alternative commit SHAs with the same commit subject.
|
|
494
|
+
#
|
|
495
|
+
# @param commit [String] Original commit SHA
|
|
496
|
+
# @return [Array<String>] List of alternative commit SHAs with identical subject
|
|
497
|
+
# @raise [RunError] If running git commands fails
|
|
498
|
+
def find_alts(commit)
|
|
499
|
+
alts=[]
|
|
500
|
+
|
|
501
|
+
begin
|
|
502
|
+
subj=runGit("log -1 --pretty='%s' #{commit}")
|
|
503
|
+
rescue RunError
|
|
504
|
+
return []
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
branches = getStableBranchList().map(){|v| @@STABLE_REPO + "/" + versionToStableBranch(v)}
|
|
508
|
+
|
|
509
|
+
runGit("log -F --grep \"$#{subj}\" --format=\"%H\" #{branches.join(" ")}").
|
|
510
|
+
split("\n").each(){|c|
|
|
511
|
+
next if c == commit
|
|
512
|
+
cursubj=runGit("log -1 --pretty='%s' #{c}")
|
|
513
|
+
alts << c if subj == cursubj
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return alts
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# Get or initialize the Octokit GitHub API client.
|
|
520
|
+
#
|
|
521
|
+
# @return [Octokit::Client] The initialized API client instance
|
|
522
|
+
def api
|
|
523
|
+
@api ||= Octokit::Client.new(:access_token => token, :auto_paginate => true)
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# Get or fetch the cached GitHub OAuth API token.
|
|
527
|
+
#
|
|
528
|
+
# @return [String] The API token string
|
|
529
|
+
def token
|
|
530
|
+
@token ||= begin
|
|
531
|
+
# We cannot use the 'defaults' functionality of git_config here,
|
|
532
|
+
# because get_new_token would be evaluated before git_config ran
|
|
533
|
+
tok = getGitConfig("maintain.api-token")
|
|
534
|
+
tok.to_s() == "" ? get_new_token : tok
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# Interactively prompt the user for GitHub credentials to create and save a new OAuth token.
|
|
539
|
+
# Supports two-factor authentication (OneTimePasswordRequired).
|
|
540
|
+
#
|
|
541
|
+
# @return [String] The newly generated API token
|
|
542
|
+
# @raise [Octokit::Unauthorized] If username/password is incorrect
|
|
543
|
+
def get_new_token
|
|
544
|
+
puts "Requesting a new OAuth token from Github..."
|
|
545
|
+
print "Github username: "
|
|
546
|
+
user = $stdin.gets.chomp
|
|
547
|
+
print "Github password: "
|
|
548
|
+
pass = $stdin.noecho(&:gets).chomp
|
|
549
|
+
puts
|
|
550
|
+
|
|
551
|
+
api = Octokit::Client.new(:login => user, :password => pass)
|
|
552
|
+
|
|
553
|
+
begin
|
|
554
|
+
res = api.create_authorization(:scopes => [:repo], :note => "git-maintain")
|
|
555
|
+
rescue Octokit::Unauthorized
|
|
556
|
+
puts "Username or password incorrect. Please try again."
|
|
557
|
+
return get_new_token
|
|
558
|
+
rescue Octokit::OneTimePasswordRequired
|
|
559
|
+
print "Github OTP: "
|
|
560
|
+
otp = $stdin.noecho(&:gets).chomp
|
|
561
|
+
res = api.create_authorization(:scopes => [:repo], :note => "git-maintain",
|
|
562
|
+
:headers => {"X-GitHub-OTP" => otp})
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
token = res[:token]
|
|
566
|
+
runGit("config --global maintain.api-token '#{token}'")
|
|
567
|
+
|
|
568
|
+
# Now reopen with the token so OTP does not bother us
|
|
569
|
+
@api=nil
|
|
570
|
+
token
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
end
|