right_develop 2.0.2 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/lib/right_develop/commands/git.rb +171 -30
- data/lib/right_develop/net.rb +1 -1
- data/lib/right_develop/utility/git.rb +0 -1
- data/lib/right_develop/utility/shell.rb +2 -2
- data/right_develop.gemspec +5 -5
- metadata +8 -6
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.0
|
1
|
+
2.1.0
|
@@ -20,17 +20,24 @@
|
|
20
20
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
21
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
22
|
|
23
|
+
require 'set'
|
24
|
+
require 'uri'
|
25
|
+
|
23
26
|
require 'right_git'
|
27
|
+
require 'right_support'
|
28
|
+
|
24
29
|
require 'right_develop'
|
25
|
-
require "action_view"
|
26
30
|
|
27
31
|
module RightDevelop::Commands
|
28
32
|
class Git
|
33
|
+
include RightSupport::Log::Mixin
|
34
|
+
|
29
35
|
NAME_SPLIT_CHARS = /-|_|\//
|
30
36
|
YES = /(ye?s?)/i
|
31
37
|
NO = /(no?)/i
|
32
|
-
|
33
|
-
|
38
|
+
TASKS = %w(prune tickets)
|
39
|
+
MERGE_COMMENT = /^Merge (?:remote[- ])?(?:tracking )?(?:branch|pull request #[0-9]+ from) ['"]?(.*)['"]?$/i
|
40
|
+
WORD_BOUNDARY = %r{[_ /-]+}
|
34
41
|
|
35
42
|
# Parse command-line options and create a Command object
|
36
43
|
def self.create
|
@@ -48,62 +55,85 @@ Where <task> is one of:
|
|
48
55
|
#{task_list}
|
49
56
|
|
50
57
|
And [options] are selected from:
|
51
|
-
EOS
|
58
|
+
EOS
|
52
59
|
opt :age, "Minimum age to consider",
|
53
|
-
|
54
|
-
opt :only, "Limit to branches matching this prefix",
|
55
|
-
|
60
|
+
:default => "3.months"
|
61
|
+
opt :only, "Limit to branches matching this prefix",
|
62
|
+
:type => :string
|
56
63
|
opt :except, "Ignore branches matching this prefix",
|
57
|
-
|
58
|
-
|
64
|
+
:type => :string,
|
65
|
+
:default => "(release|ve?r?)?[0-9.]+"
|
59
66
|
opt :local, "Limit to local branches"
|
60
67
|
opt :remote, "Limit to remote branches"
|
61
|
-
opt :merged, "Limit to branches that are
|
62
|
-
|
63
|
-
|
64
|
-
|
68
|
+
opt :merged, "Limit to branches that are merged into this branch",
|
69
|
+
:type => :string,
|
70
|
+
:default => "master"
|
71
|
+
opt :since, "Base branch or tag to compare against for determining 'new' commits",
|
72
|
+
:default => "origin/master"
|
73
|
+
opt :link, "Word prefix indicating a link to an external ticketing system",
|
74
|
+
:default => "(?:[#A-Za-z]+)([0-9]+)$"
|
75
|
+
opt :link_to, "URL pattern to generate ticket links, don't forget trailing slash!",
|
76
|
+
:type => :string
|
77
|
+
opt :debug, "Enable verbose debug output",
|
78
|
+
:default => false
|
65
79
|
end
|
66
80
|
|
67
81
|
task = ARGV.shift
|
68
82
|
|
83
|
+
repo = ::RightGit::Git::Repository.new(
|
84
|
+
::Dir.pwd,
|
85
|
+
::RightDevelop::Utility::Git::DEFAULT_REPO_OPTIONS)
|
86
|
+
|
69
87
|
case task
|
70
88
|
when "prune"
|
71
|
-
repo = ::RightGit::Git::Repository.new(
|
72
|
-
::Dir.pwd,
|
73
|
-
::RightDevelop::Utility::Git::DEFAULT_REPO_OPTIONS)
|
74
89
|
self.new(repo, :prune, options)
|
90
|
+
when "tickets"
|
91
|
+
self.new(repo, :tickets, options)
|
75
92
|
else
|
76
93
|
Trollop.die "unknown task #{task}"
|
77
94
|
end
|
78
95
|
end
|
79
96
|
|
97
|
+
# @param [RightGit::Git::Repository] repo the Git repository to operate on
|
98
|
+
# @param [Symbol] task one of :prune or :tickets
|
80
99
|
# @option options [String] :age Ignore branches newer than this time-ago-in-words e.g. "3 months"; default unit is months
|
81
100
|
# @option options [String] :except Ignore branches matching this regular expression
|
82
101
|
# @option options [String] :only Consider only branches matching this regular expression
|
83
|
-
# @option options [
|
84
|
-
# @option options [
|
102
|
+
# @option options [Boolean] :local Consider only local branches
|
103
|
+
# @option options [Boolean] :remote Consider only remote branches
|
85
104
|
# @option options [String] :merged Consider only branches that are fully merged into this branch (e.g. master)
|
105
|
+
# @option options [String] :since the name of a "base branch" representing the previous release
|
106
|
+
# @option options [String] :link word prefix connoting a link to an external ticketing system
|
107
|
+
# @option options [String] :link_to URL prefix to use when generating ticket links
|
86
108
|
def initialize(repo, task, options)
|
109
|
+
logger = Logger.new(STDERR)
|
110
|
+
logger.level = options[:debug] ? Logger::DEBUG : Logger::WARN
|
111
|
+
RightSupport::Log::Mixin.default_logger = logger
|
112
|
+
|
87
113
|
# Post-process "age" option; transform from natural-language expression into a timestamp.
|
88
114
|
if (age = options.delete(:age))
|
89
|
-
|
90
|
-
debugger
|
91
|
-
age = parse_age(age)
|
115
|
+
age = parse_age(age)
|
92
116
|
options[:age] = age
|
93
117
|
end
|
94
118
|
|
95
119
|
# Post-process "except" option; transform into a Regexp.
|
96
120
|
if (except = options.delete(:except))
|
97
|
-
except
|
121
|
+
except = Regexp.new("^(origin/)?(#{except})")
|
98
122
|
options[:except] = except
|
99
123
|
end
|
100
124
|
|
101
125
|
# Post-process "only" option; transform into a Regexp.
|
102
126
|
if (only = options.delete(:only))
|
103
|
-
only
|
127
|
+
only = Regexp.new("^(origin/)?(#{only})")
|
104
128
|
options[:only] = only
|
105
129
|
end
|
106
130
|
|
131
|
+
# Post-process "since" option; transform into a Regexp.
|
132
|
+
if (link = options.delete(:link))
|
133
|
+
link = Regexp.new("^#{link}")
|
134
|
+
options[:link] = link
|
135
|
+
end
|
136
|
+
|
107
137
|
@git = repo
|
108
138
|
@task = task
|
109
139
|
@options = options
|
@@ -115,6 +145,8 @@ EOS
|
|
115
145
|
case @task
|
116
146
|
when :prune
|
117
147
|
prune(@options)
|
148
|
+
when :tickets
|
149
|
+
tickets(@options)
|
118
150
|
else
|
119
151
|
raise StateError, "Invalid @task; check Git.create!"
|
120
152
|
end
|
@@ -127,11 +159,16 @@ EOS
|
|
127
159
|
# @option options [Time] :age Ignore branches whose HEAD commit is newer than this timestamp
|
128
160
|
# @option options [Regexp] :except Ignore branches matching this pattern
|
129
161
|
# @option options [Regexp] :only Consider only branches matching this pattern
|
130
|
-
# @option options [
|
131
|
-
# @option options [
|
162
|
+
# @option options [Boolean] :local Consider only local branches
|
163
|
+
# @option options [Boolean] :remote Consider only remote branches
|
132
164
|
# @option options [String] :merged Consider only branches that are fully merged into this branch (e.g. master)
|
133
165
|
def prune(options={})
|
134
|
-
|
166
|
+
puts describe_prune(options)
|
167
|
+
|
168
|
+
puts "Fetching latest branches and tags from remotes"
|
169
|
+
@git.fetch_all(:prune => true)
|
170
|
+
|
171
|
+
branches = @git.branches(:all => true)
|
135
172
|
|
136
173
|
#Filter by name prefix
|
137
174
|
branches = branches.select { |x| x =~ options[:only] } if options[:only]
|
@@ -148,12 +185,13 @@ EOS
|
|
148
185
|
|
149
186
|
#Filter by merge status
|
150
187
|
if options[:merged]
|
188
|
+
puts "Checking merge status of #{branches.size} branches; please be patient"
|
151
189
|
branches = branches.merged(options[:merged])
|
152
190
|
end
|
153
191
|
|
154
192
|
old = {}
|
155
193
|
branches.each do |branch|
|
156
|
-
latest
|
194
|
+
latest = @git.log(branch, :tail => 1).first
|
157
195
|
timestamp = latest.timestamp
|
158
196
|
if timestamp < options[:age] &&
|
159
197
|
old[branch] = timestamp
|
@@ -161,10 +199,12 @@ EOS
|
|
161
199
|
end
|
162
200
|
|
163
201
|
if old.empty?
|
164
|
-
STDERR.puts "No branches
|
202
|
+
STDERR.puts "No branches found; try different options"
|
165
203
|
exit -2
|
166
204
|
end
|
167
205
|
|
206
|
+
puts
|
207
|
+
|
168
208
|
all_by_prefix = branches.group_by { |b| b.name.split(NAME_SPLIT_CHARS).first }
|
169
209
|
|
170
210
|
all_by_prefix.each_pair do |prefix, branches|
|
@@ -185,11 +225,97 @@ EOS
|
|
185
225
|
|
186
226
|
old.each do |branch, timestamp|
|
187
227
|
branch.delete
|
228
|
+
puts " deleted #{branch}"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Produce a report of all the tickets that have been merged into the named branch. This works
|
233
|
+
# by scanning merge commit comments, recognizing words that look like a ticket reference, and
|
234
|
+
# extracting a matched segment as the ticket ID. The user must specify a matching Regexp using
|
235
|
+
# the :link option.
|
236
|
+
#
|
237
|
+
# @example Match Acunote stories
|
238
|
+
# git.tickets(:link=>/acu([0-9]+)/)
|
239
|
+
#
|
240
|
+
# @option options [String] :since the name of a "base branch" representing the previous release
|
241
|
+
# @option options [Regexp] :merged the name of a branch (e.g. master) representing the next release
|
242
|
+
# @option options [Regexp] :link a word prefix that connotes links to an external ticketing system
|
243
|
+
# @option options [Boolean] :local Consider only local branches
|
244
|
+
# @option options [Boolean] :remote Consider only remote branches
|
245
|
+
def tickets(options={})
|
246
|
+
since = options[:since]
|
247
|
+
merged = options[:merged]
|
248
|
+
|
249
|
+
tickets = Set.new
|
250
|
+
link = options[:link]
|
251
|
+
|
252
|
+
@git.log("#{since}..#{merged}", :merges => true).each do |commit|
|
253
|
+
if (match = MERGE_COMMENT.match(commit.comment))
|
254
|
+
words = match[1].split(WORD_BOUNDARY)
|
255
|
+
else
|
256
|
+
words = commit.comment.split(WORD_BOUNDARY)
|
257
|
+
end
|
258
|
+
|
259
|
+
got = words.detect do |w|
|
260
|
+
if match = link.match(w)
|
261
|
+
if match[1]
|
262
|
+
tickets << match[1]
|
263
|
+
else
|
264
|
+
raise ArgumentError, "Regexp '#{link}' lacks capture groups; please use a () somewhere"
|
265
|
+
end
|
266
|
+
else
|
267
|
+
nil
|
268
|
+
end
|
269
|
+
end
|
270
|
+
unless got
|
271
|
+
logger.warn "Couldn't infer a ticket link from '#{commit.comment}'"
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
if (link_to = options[:link_to])
|
276
|
+
link_to = link_to + '/' unless link_to =~ %r{/$}
|
277
|
+
tickets.each { |t| puts link_to + t }
|
278
|
+
else
|
279
|
+
tickets.each { |t| puts t }
|
188
280
|
end
|
189
281
|
end
|
190
282
|
|
191
283
|
private
|
192
284
|
|
285
|
+
# Build a plain-English description of a prune command based on the
|
286
|
+
# options given.
|
287
|
+
# @param [Hash] options
|
288
|
+
def describe_prune(options)
|
289
|
+
statement = ['Pruning']
|
290
|
+
|
291
|
+
if options[:remote]
|
292
|
+
statement << 'remote'
|
293
|
+
elsif options[:local]
|
294
|
+
statement << 'local'
|
295
|
+
end
|
296
|
+
|
297
|
+
statement << 'branches'
|
298
|
+
|
299
|
+
if options[:age]
|
300
|
+
statement << "older than #{time_ago_in_words(options[:age])}"
|
301
|
+
end
|
302
|
+
|
303
|
+
if options[:merged]
|
304
|
+
statement << "that are fully merged into #{options[:merged]}"
|
305
|
+
end
|
306
|
+
|
307
|
+
if options[:only]
|
308
|
+
naming = "with a name containing '#{options[:only]}'"
|
309
|
+
if options[:except]
|
310
|
+
naming << " (but not '#{options[:except]}')"
|
311
|
+
end
|
312
|
+
statement << naming
|
313
|
+
end
|
314
|
+
|
315
|
+
statement.join(' ')
|
316
|
+
end
|
317
|
+
|
318
|
+
# Ask the user a yes-or-no question
|
193
319
|
def prompt(p, yes_no=false)
|
194
320
|
puts #newline for newline's sake!
|
195
321
|
|
@@ -217,6 +343,16 @@ EOS
|
|
217
343
|
[1, 'second'],
|
218
344
|
]
|
219
345
|
|
346
|
+
# Workalike for ActiveSupport date-helper method. Given a Time in the past, return
|
347
|
+
# a natural-language English string that describes the duration separating that time from
|
348
|
+
# the present. The duration is very approximate, and will be rounded down to the nearest
|
349
|
+
# appropriate interval (e.g. 2.5 hours becomes 2 hours).
|
350
|
+
#
|
351
|
+
# @example about three days ago
|
352
|
+
# time_ago_in_words(Time.now - 86400*3.1) # => "3 days"
|
353
|
+
#
|
354
|
+
# @param [Time] once_upon_a the long-ago time to compare to Time.now
|
355
|
+
# @return [String] an English time duration
|
220
356
|
def time_ago_in_words(once_upon_a)
|
221
357
|
dt = Time.now.to_f - once_upon_a.to_f
|
222
358
|
|
@@ -225,7 +361,7 @@ EOS
|
|
225
361
|
TIME_INTERVALS.each do |pair|
|
226
362
|
mag, term = pair.first, pair.last
|
227
363
|
if dt >= mag
|
228
|
-
units = dt / mag
|
364
|
+
units = Integer(dt / mag)
|
229
365
|
words = "%d %s%s" % [units, term, units > 1 ? 's' : '']
|
230
366
|
break
|
231
367
|
end
|
@@ -238,9 +374,14 @@ EOS
|
|
238
374
|
end
|
239
375
|
end
|
240
376
|
|
377
|
+
# Given a natural-language English description of a time duration, return a Time in the past,
|
378
|
+
# that is the same duration from Time.now that is expressed in the string.
|
379
|
+
#
|
380
|
+
# @param [String] str an English time duration
|
381
|
+
# @return [Time] a Time object in the past, as described relative to now by str
|
241
382
|
def parse_age(str)
|
242
383
|
ord, word = str.split(/[. ]+/, 2)
|
243
|
-
ord
|
384
|
+
ord = Integer(ord)
|
244
385
|
word.gsub!(/s$/, '')
|
245
386
|
|
246
387
|
ago = nil
|
data/lib/right_develop/net.rb
CHANGED
@@ -75,9 +75,9 @@ module RightDevelop
|
|
75
75
|
NullLoggerSingleton.instance
|
76
76
|
end
|
77
77
|
|
78
|
-
# @return [Logger]
|
78
|
+
# @return [Logger] RightSupport::Log::Mixin.default_logger
|
79
79
|
def default_logger
|
80
|
-
|
80
|
+
RightSupport::Log::Mixin.default_logger
|
81
81
|
end
|
82
82
|
|
83
83
|
# Overrides ::RightGit::Shell::Default#execute
|
data/right_develop.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{right_develop}
|
8
|
-
s.version = "2.0
|
8
|
+
s.version = "2.1.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Tony Spataro"]
|
12
|
-
s.date = %q{2014-01-
|
12
|
+
s.date = %q{2014-01-16}
|
13
13
|
s.default_executable = %q{right_develop}
|
14
14
|
s.description = %q{A toolkit of development tools created by RightScale.}
|
15
15
|
s.email = %q{support@rightscale.com}
|
@@ -66,7 +66,7 @@ Gem::Specification.new do |s|
|
|
66
66
|
s.add_runtime_dependency(%q<rspec>, ["< 3.0", ">= 1.3"])
|
67
67
|
s.add_runtime_dependency(%q<cucumber>, ["< 1.3.3", "~> 1.0"])
|
68
68
|
s.add_runtime_dependency(%q<trollop>, ["< 3.0", ">= 1.0"])
|
69
|
-
s.add_runtime_dependency(%q<right_git>, ["
|
69
|
+
s.add_runtime_dependency(%q<right_git>, ["~> 0.1.0"])
|
70
70
|
s.add_runtime_dependency(%q<right_aws>, [">= 2.1.0"])
|
71
71
|
s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
|
72
72
|
s.add_development_dependency(%q<rdoc>, [">= 2.4.2"])
|
@@ -77,7 +77,7 @@ Gem::Specification.new do |s|
|
|
77
77
|
s.add_dependency(%q<rspec>, ["< 3.0", ">= 1.3"])
|
78
78
|
s.add_dependency(%q<cucumber>, ["< 1.3.3", "~> 1.0"])
|
79
79
|
s.add_dependency(%q<trollop>, ["< 3.0", ">= 1.0"])
|
80
|
-
s.add_dependency(%q<right_git>, ["
|
80
|
+
s.add_dependency(%q<right_git>, ["~> 0.1.0"])
|
81
81
|
s.add_dependency(%q<right_aws>, [">= 2.1.0"])
|
82
82
|
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
83
83
|
s.add_dependency(%q<rdoc>, [">= 2.4.2"])
|
@@ -89,7 +89,7 @@ Gem::Specification.new do |s|
|
|
89
89
|
s.add_dependency(%q<rspec>, ["< 3.0", ">= 1.3"])
|
90
90
|
s.add_dependency(%q<cucumber>, ["< 1.3.3", "~> 1.0"])
|
91
91
|
s.add_dependency(%q<trollop>, ["< 3.0", ">= 1.0"])
|
92
|
-
s.add_dependency(%q<right_git>, ["
|
92
|
+
s.add_dependency(%q<right_git>, ["~> 0.1.0"])
|
93
93
|
s.add_dependency(%q<right_aws>, [">= 2.1.0"])
|
94
94
|
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
95
95
|
s.add_dependency(%q<rdoc>, [">= 2.4.2"])
|
metadata
CHANGED
@@ -5,9 +5,9 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 2
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 2.0.2
|
10
|
+
version: 2.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Tony Spataro
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2014-01-
|
18
|
+
date: 2014-01-16 00:00:00 -08:00
|
19
19
|
default_executable: right_develop
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -143,12 +143,14 @@ dependencies:
|
|
143
143
|
version_requirements: &id007 !ruby/object:Gem::Requirement
|
144
144
|
none: false
|
145
145
|
requirements:
|
146
|
-
- -
|
146
|
+
- - ~>
|
147
147
|
- !ruby/object:Gem::Version
|
148
|
-
hash:
|
148
|
+
hash: 27
|
149
149
|
segments:
|
150
150
|
- 0
|
151
|
-
|
151
|
+
- 1
|
152
|
+
- 0
|
153
|
+
version: 0.1.0
|
152
154
|
prerelease: false
|
153
155
|
requirement: *id007
|
154
156
|
name: right_git
|