right_develop 1.1.0 → 1.2.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/Gemfile CHANGED
@@ -1,12 +1,18 @@
1
- source "http://rubygems.org"
1
+ source "https://rubygems.org"
2
2
 
3
3
  # Runtime dependencies of RightDevelop
4
+
5
+ # Gems used by the CI harness
4
6
  gem "rake", [">= 0.8.7", "< 0.10"]
5
7
  gem "right_support", "~> 2.0"
6
8
  gem "builder", "~> 3.0"
7
9
  gem "rspec", [">= 1.3", "< 3.0"]
8
10
  gem "cucumber", "~> 1.0"
9
11
 
12
+ # Gems used by the command-line Git tools
13
+ gem "trollop", [">= 1.0", "< 3.0"]
14
+ gem "actionpack", [">= 2.3.0", "< 4.0"]
15
+
10
16
  # Gems used during RightDevelop development that should be called out in the gemspec
11
17
  group :development do
12
18
  gem "jeweler", "~> 1.8.3"
data/Gemfile.lock CHANGED
@@ -1,59 +1,62 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- right_develop (1.1.0)
4
+ right_develop (1.2.0)
5
+ actionpack (>= 2.3.0, < 4.0)
5
6
  builder (~> 3.0)
6
7
  cucumber (~> 1.0)
7
8
  rake (>= 0.8.7, < 0.10)
8
9
  right_support (~> 2.0)
9
10
  rspec (>= 1.3, < 3.0)
11
+ trollop (>= 1.0, < 3.0)
10
12
 
11
13
  GEM
12
- remote: http://rubygems.org/
14
+ remote: https://rubygems.org/
13
15
  specs:
14
- activesupport (3.2.11)
15
- i18n (~> 0.6)
16
- multi_json (~> 1.0)
16
+ actionpack (2.3.17)
17
+ activesupport (= 2.3.17)
18
+ rack (~> 1.1.0)
19
+ activesupport (2.3.17)
17
20
  archive-tar-minitar (0.5.2)
18
- builder (3.1.4)
21
+ builder (3.2.0)
19
22
  columnize (0.3.6)
20
- cucumber (1.2.1)
23
+ cucumber (1.2.3)
21
24
  builder (>= 2.1.2)
22
25
  diff-lcs (>= 1.1.3)
23
- gherkin (~> 2.11.0)
24
- json (>= 1.4.6)
25
- diff-lcs (1.1.3)
26
+ gherkin (~> 2.11.6)
27
+ multi_json (~> 1.3)
28
+ diff-lcs (1.2.1)
26
29
  flexmock (0.8.11)
27
30
  gherkin (2.11.6)
28
31
  json (>= 1.7.6)
29
32
  git (1.2.5)
30
- i18n (0.6.1)
31
33
  jeweler (1.8.4)
32
34
  bundler (~> 1.0)
33
35
  git (>= 1.2.5)
34
36
  rake
35
37
  rdoc
36
- json (1.7.6)
38
+ json (1.7.7)
37
39
  libxml-ruby (2.4.0)
38
40
  linecache (0.46)
39
41
  rbx-require-relative (> 0.0.4)
40
42
  linecache19 (0.5.12)
41
43
  ruby_core_source (>= 0.1.4)
42
- multi_json (1.5.0)
44
+ multi_json (1.6.1)
43
45
  nokogiri (1.5.6)
46
+ rack (1.1.6)
44
47
  rake (0.9.6)
45
48
  rbx-require-relative (0.0.9)
46
- rdoc (3.12.1)
49
+ rdoc (4.0.0)
47
50
  json (~> 1.4)
48
51
  right_support (2.6.17)
49
- rspec (2.12.0)
50
- rspec-core (~> 2.12.0)
51
- rspec-expectations (~> 2.12.0)
52
- rspec-mocks (~> 2.12.0)
53
- rspec-core (2.12.2)
54
- rspec-expectations (2.12.1)
55
- diff-lcs (~> 1.1.3)
56
- rspec-mocks (2.12.2)
52
+ rspec (2.13.0)
53
+ rspec-core (~> 2.13.0)
54
+ rspec-expectations (~> 2.13.0)
55
+ rspec-mocks (~> 2.13.0)
56
+ rspec-core (2.13.1)
57
+ rspec-expectations (2.13.0)
58
+ diff-lcs (>= 1.1.3, < 2.0)
59
+ rspec-mocks (2.13.0)
57
60
  ruby-debug (0.10.4)
58
61
  columnize (>= 0.1)
59
62
  ruby-debug-base (~> 0.10.4.0)
@@ -70,11 +73,13 @@ GEM
70
73
  ruby_core_source (0.1.5)
71
74
  archive-tar-minitar (>= 0.5.2)
72
75
  syntax (1.0.0)
76
+ trollop (2.0)
73
77
 
74
78
  PLATFORMS
75
79
  ruby
76
80
 
77
81
  DEPENDENCIES
82
+ actionpack (>= 2.3.0, < 4.0)
78
83
  activesupport
79
84
  builder (~> 3.0)
80
85
  cucumber (~> 1.0)
@@ -90,3 +95,4 @@ DEPENDENCIES
90
95
  ruby-debug (>= 0.10)
91
96
  ruby-debug19 (>= 0.11.6)
92
97
  syntax (~> 1.0.0)
98
+ trollop (>= 1.0, < 3.0)
data/README.rdoc CHANGED
@@ -1,5 +1,7 @@
1
1
  RightDevelop is a library of reusable, unit- and functional-tested Ruby code that RightScale has found broadly useful.
2
2
 
3
+ Maintained by the RightScale Teal Team
4
+
3
5
  == What Does It Do?
4
6
 
5
7
  === Continuous Integration
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.0
1
+ 1.2.0
data/bin/right_develop ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "trollop"
4
+
5
+ require "right_develop"
6
+
7
+ gemspec = Gem.latest_spec_for("right_develop")
8
+
9
+ commands = {}
10
+ RightDevelop::Commands.constants.each do |konst|
11
+ name = konst.to_s.downcase
12
+ commands[name] = RightDevelop::Commands.const_get(konst.to_sym)
13
+ end
14
+
15
+ # Use a Trollop parser for help/banner display, but do not actually parse anything
16
+ # just yet.
17
+ command_list = commands.keys.map { |c| " * #{c}" }.join("\n")
18
+ p = Trollop::Parser.new do
19
+ version "right_develop #{gemspec.version} (c) 2013- RightScale, Inc."
20
+ banner <<-EOS
21
+ A command-line interface to various tools offered by the right_develop gem.
22
+
23
+ Usage:
24
+ right_develop <command> [options]
25
+
26
+ Where <command> is one of:
27
+ #{command_list}
28
+
29
+ To get help on a command:
30
+ right_develop <command> --help
31
+ EOS
32
+
33
+ stop_on commands.keys
34
+ end
35
+
36
+ opts = Trollop::with_standard_exception_handling p do
37
+ raise Trollop::HelpNeeded if ARGV.empty?
38
+ p.parse ARGV
39
+ cmd = ARGV.shift
40
+ commands[cmd].create.run
41
+ end
data/lib/right_develop.rb CHANGED
@@ -24,8 +24,10 @@ require 'right_support'
24
24
 
25
25
  # Autoload everything possible
26
26
  module RightDevelop
27
- autoload :CI, 'right_develop/ci'
28
- autoload :Parsers, 'right_develop/parsers'
27
+ autoload :CI, 'right_develop/ci'
28
+ autoload :Commands, 'right_develop/commands'
29
+ autoload :Git, 'right_develop/git'
30
+ autoload :Parsers, 'right_develop/parsers'
29
31
  end
30
32
 
31
33
  # Automatically include RightSupport networking extensions
@@ -25,7 +25,7 @@ require 'time'
25
25
  require 'builder'
26
26
 
27
27
  # Try to load RSpec 2.x - 1.x formatters
28
- ['rspec/core/formatters', 'spec/runner/formatter/base_text_formatter'].each do |f|
28
+ ['rspec/core', 'spec', 'rspec/core/formatters/base_text_formatter', 'spec/runner/formatter/base_text_formatter'].each do |f|
29
29
  begin
30
30
  require f
31
31
  rescue LoadError
@@ -0,0 +1,6 @@
1
+ module RightDevelop
2
+ module Commands
3
+ end
4
+ end
5
+
6
+ require "right_develop/commands/git"
@@ -0,0 +1,190 @@
1
+ require "action_view"
2
+
3
+ module RightDevelop::Commands
4
+ class Git
5
+ include ActionView::Helpers::DateHelper
6
+
7
+ NAME_SPLIT_CHARS = /-|_|\//
8
+ YES = /(ye?s?)/i
9
+ NO = /(no?)/i
10
+
11
+ TASKS = %w(prune)
12
+
13
+ # Parse command-line options and create a Command object
14
+ def self.create
15
+ task_list = TASKS.map { |c| " * #{c}" }.join("\n")
16
+
17
+ options = Trollop.options do
18
+ banner <<-EOS
19
+ The 'git' command automates various repository management tasks. All tasks
20
+ accept the same options, although not every option applies to every command.
21
+
22
+ Usage:
23
+ right_develop git <task> [options]
24
+
25
+ Where <task> is one of:
26
+ #{task_list}
27
+
28
+ And [options] are selected from:
29
+ EOS
30
+ opt :age, "Minimum age to consider",
31
+ :default => "3.months"
32
+ opt :only, "Limit to branches matching this prefix",
33
+ :type=>:string
34
+ opt :except, "Ignore branches matching this prefix",
35
+ :type=>:string,
36
+ :default => "^(release|v?[0-9.]+|)"
37
+ opt :local, "Limit to local branches"
38
+ opt :remote, "Limit to remote branches"
39
+ opt :merged, "Limit to branches that are fully merged into the named branch",
40
+ :type=>:string,
41
+ :default => "master"
42
+ stop_on TASKS
43
+ end
44
+
45
+ task = ARGV.shift
46
+
47
+ case task
48
+ when "prune"
49
+ git = RightDevelop::Git::Repository.new(Dir.pwd)
50
+ self.new(git, :prune, options)
51
+ else
52
+ Trollop.die "unknown task #{task}"
53
+ end
54
+ end
55
+
56
+ # @option options [String] :age Ignore branches newer than this time-ago-in-words e.g. "3 months"; default unit is months
57
+ # @option options [String] :except Ignore branches matching this regular expression
58
+ # @option options [String] :only Consider only branches matching this regular expression
59
+ # @option options [true|false] :local Consider local branches
60
+ # @option options [true|false] :remote Consider remote branches
61
+ # @option options [String] :merged Consider only branches that are fully merged into this branch (e.g. master)
62
+ def initialize(repo, task, options)
63
+ # Post-process "age" option; transform from natural-language expression into a timestamp.
64
+ if (age = options.delete(:age))
65
+ age = age.gsub(/\s+/, ".")
66
+
67
+ if age =~ /^[0-9]+\.?(hours|days|weeks|months|years)$/
68
+ age = eval(age).ago
69
+ elsif age =~ /^[0-9]+$/
70
+ age = age.to_i.months.ago
71
+ else
72
+ raise ArgumentError, "Can't parse age of '#{age}'"
73
+ end
74
+ options[:age] = age
75
+ end
76
+
77
+ # Post-process "except" option; transform into a Regexp.
78
+ if (except = options.delete(:except))
79
+ except = Regexp.new("^(origin/)?(#{except})")
80
+ options[:except] = except
81
+ end
82
+
83
+ # Post-process "only" option; transform into a Regexp.
84
+ if (only = options.delete(:only))
85
+ only = Regexp.new("^(origin/)?(#{only})")
86
+ options[:only] = only
87
+ end
88
+
89
+ @git = repo
90
+ @task = task
91
+ @options = options
92
+ end
93
+
94
+ # Run the task that was specified when this object was instantiated. This
95
+ # method does no work; it just delegates to a task method.
96
+ def run
97
+ case @task
98
+ when :prune
99
+ prune(@options)
100
+ else
101
+ raise StateError, "Invalid @task; check Git.create!"
102
+ end
103
+ end
104
+
105
+ protected
106
+
107
+ # Prune dead branches from the repository.
108
+ #
109
+ # @option options [Time] :age Ignore branches whose HEAD commit is newer than this timestamp
110
+ # @option options [Regexp] :except Ignore branches matching this pattern
111
+ # @option options [Regexp] :only Consider only branches matching this pattern
112
+ # @option options [true|false] :local Consider local branches
113
+ # @option options [true|false] :remote Consider remote branches
114
+ # @option options [String] :merged Consider only branches that are fully merged into this branch (e.g. master)
115
+ def prune(options={})
116
+ branches = @git.branches
117
+
118
+ #Filter by name prefix
119
+ branches = branches.select { |x| x =~ options[:only] } if options[:only]
120
+ branches = branches.reject { |x| x =~ options[:except] } if options[:except]
121
+
122
+ #Filter by location (local/remote)
123
+ if options[:local] && !options[:remote]
124
+ branches = branches.local
125
+ elsif options[:remote] && !options[:local]
126
+ branches = branches.remote
127
+ elsif options[:remote] && options[:local]
128
+ raise ArgumentError, "Cannot specify both --local and --remote!"
129
+ end
130
+
131
+ #Filter by merge status
132
+ if options[:merged]
133
+ branches = branches.merged(options[:merged])
134
+ end
135
+
136
+ old = {}
137
+ branches.each do |branch|
138
+ latest = @git.log(branch, :tail=>1).first
139
+ timestamp = latest.timestamp
140
+ if timestamp < options[:age] &&
141
+ old[branch] = timestamp
142
+ end
143
+ end
144
+
145
+ if old.empty?
146
+ STDERR.puts "No branches older than #{time_ago_in_words(options[:age])} found; do you need to specify --remote?"
147
+ exit -2
148
+ end
149
+
150
+ all_by_prefix = branches.group_by { |b| b.name.split(NAME_SPLIT_CHARS).first }
151
+
152
+ all_by_prefix.each_pair do |prefix, branches|
153
+ old_in_group = branches.select { |b| old.key?(b) }
154
+ next if old_in_group.empty?
155
+ old_in_group = old_in_group.sort { |a, b| old[a] <=> old[b] }
156
+ puts prefix
157
+ puts '-' * prefix.length
158
+ old_in_group.each do |b|
159
+ puts "\t" + b.display(40) + "\t" + time_ago_in_words(old[b])
160
+ end
161
+ puts
162
+ end
163
+
164
+ unless options[:force]
165
+ return unless prompt("Delete all #{old.size} branches above?", true)
166
+ end
167
+
168
+ old.each do |branch, timestamp|
169
+ branch.delete
170
+ end
171
+ end
172
+
173
+ private
174
+
175
+ def prompt(p, yes_no=false)
176
+ puts #newline for newline's sake!
177
+
178
+ loop do
179
+ print p, ' '
180
+ line = STDIN.readline.strip
181
+ if yes_no
182
+ return true if line =~ YES
183
+ return false if line =~ NO
184
+ else
185
+ return line
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,22 @@
1
+ module RightDevelop
2
+ module Git
3
+ # A Git command failed unexpectedly.
4
+ class CommandError < StandardError
5
+ attr_reader :output
6
+
7
+ def initialize(message)
8
+ @output = message
9
+ lines = message.split("\n").map { |l| l.strip }.reject { |l| l.empty? }
10
+ super(lines.last || @output)
11
+ end
12
+ end
13
+
14
+ # A Git command's output did not match with expected output.
15
+ class FormatError < StandardError; end
16
+ end
17
+
18
+ require "right_develop/git/branch"
19
+ require "right_develop/git/branch_collection"
20
+ require "right_develop/git/commit"
21
+ require "right_develop/git/repository"
22
+ end
@@ -0,0 +1,74 @@
1
+ module RightDevelop::Git
2
+ # A branch in a Git repository. Has some proxy methods that make it act a bit like
3
+ # a string, whose value is the name of the branch. This allows branches to be sorted,
4
+ # matched against Regexp, and certain other string-y operations.
5
+ class Branch
6
+ BRANCH_NAME = '[#A-Za-z0-9._\/-]+'
7
+ BRANCH_INFO = /(\* | )?(#{BRANCH_NAME})( -> )?(#{BRANCH_NAME})?/
8
+ BRANCH_FULLNAME = /(remotes\/)?(#{BRANCH_NAME})/
9
+
10
+ def initialize(repo, line)
11
+ match = BRANCH_INFO.match(line)
12
+ if match && (fullname = match[2])
13
+ match = BRANCH_FULLNAME.match(fullname)
14
+ if match
15
+ @fullname = match[2]
16
+ @remote = !!match[1]
17
+ @repo = repo
18
+ else
19
+ raise FormatError, "Unrecognized branch name '#{line}'"
20
+ end
21
+ else
22
+ raise FormatError, "Unrecognized branch info '#{line}'"
23
+ end
24
+ end
25
+
26
+ def to_s
27
+ @fullname
28
+ end
29
+ alias inspect to_s
30
+
31
+ def =~(other)
32
+ @fullname =~ other
33
+ end
34
+
35
+ def ==(other)
36
+ self.to_s == other.to_s
37
+ end
38
+
39
+ def <=>(other)
40
+ self.to_s <=> other.to_s
41
+ end
42
+
43
+ def remote?
44
+ @remote
45
+ end
46
+
47
+ def name
48
+ if remote?
49
+ #remove the initial remote-name in the branch (origin/master --> master)
50
+ bits = @fullname.split('/')
51
+ bits.shift
52
+ bits.join('/')
53
+ else
54
+ @fullname
55
+ end
56
+ end
57
+
58
+ def display(width=40)
59
+ if @fullname.length >= width
60
+ (@fullname[0..(width-5)] + "...").ljust(width)
61
+ else
62
+ @fullname.ljust(width)
63
+ end
64
+ end
65
+
66
+ def delete
67
+ if self.remote?
68
+ @repo.shell("git push origin :#{self.name}")
69
+ else
70
+ @repo.shell("git branch -D #{@fullname}")
71
+ end
72
+ end
73
+ end
74
+ end