right_develop 2.0.2 → 2.1.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.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.2
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
- TASKS = %w(prune)
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
- :default => "3.months"
54
- opt :only, "Limit to branches matching this prefix",
55
- :type=>:string
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
- :type=>:string,
58
- :default => "^(release|v?[0-9.]+|)"
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 fully merged into the named branch",
62
- :type=>:string,
63
- :default => "master"
64
- stop_on TASKS
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 [true|false] :local Consider local branches
84
- # @option options [true|false] :remote Consider remote branches
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
- require 'ruby-debug'
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 = Regexp.new("^(origin/)?(#{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 = Regexp.new("^(origin/)?(#{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 [true|false] :local Consider local branches
131
- # @option options [true|false] :remote Consider remote branches
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
- branches = @git.branches
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 = @git.log(branch, :tail=>1).first
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 older than #{time_ago_in_words(options[:age])} found; do you need to specify --remote?"
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 = Integer(ord)
384
+ ord = Integer(ord)
244
385
  word.gsub!(/s$/, '')
245
386
 
246
387
  ago = nil
@@ -21,7 +21,7 @@
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
 
23
23
  # Try to load RSpec 2.x - 1.x
24
- ['rspec', 'spec'].each do |f|
24
+ ['rspec', 'rspec/mocks', 'spec'].each do |f|
25
25
  begin
26
26
  require f
27
27
  rescue LoadError
@@ -27,7 +27,6 @@ require 'right_git'
27
27
  module RightDevelop::Utility::Git
28
28
 
29
29
  DEFAULT_REPO_OPTIONS = {
30
- :logger => ::RightDevelop::Utility::Shell.default_logger,
31
30
  :shell => ::RightDevelop::Utility::Shell
32
31
  }.freeze
33
32
 
@@ -75,9 +75,9 @@ module RightDevelop
75
75
  NullLoggerSingleton.instance
76
76
  end
77
77
 
78
- # @return [Logger] default logger for STDOUT
78
+ # @return [Logger] RightSupport::Log::Mixin.default_logger
79
79
  def default_logger
80
- @default_logger ||= ::Logger.new(STDOUT)
80
+ RightSupport::Log::Mixin.default_logger
81
81
  end
82
82
 
83
83
  # Overrides ::RightGit::Shell::Default#execute
@@ -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.2"
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-13}
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>, [">= 0"])
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>, [">= 0"])
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>, [">= 0"])
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
- - 2
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-13 00:00:00 -08:00
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: 3
148
+ hash: 27
149
149
  segments:
150
150
  - 0
151
- version: "0"
151
+ - 1
152
+ - 0
153
+ version: 0.1.0
152
154
  prerelease: false
153
155
  requirement: *id007
154
156
  name: right_git