right_develop 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +7 -1
- data/Gemfile.lock +28 -22
- data/README.rdoc +2 -0
- data/VERSION +1 -1
- data/bin/right_develop +41 -0
- data/lib/right_develop.rb +4 -2
- data/lib/right_develop/ci/java_spec_formatter.rb +1 -1
- data/lib/right_develop/commands.rb +6 -0
- data/lib/right_develop/commands/git.rb +190 -0
- data/lib/right_develop/git.rb +22 -0
- data/lib/right_develop/git/branch.rb +74 -0
- data/lib/right_develop/git/branch_collection.rb +72 -0
- data/lib/right_develop/git/commit.rb +30 -0
- data/lib/right_develop/git/repository.rb +57 -0
- data/right_develop.gemspec +171 -8
- metadata +920 -102
data/Gemfile
CHANGED
@@ -1,12 +1,18 @@
|
|
1
|
-
source "
|
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.
|
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:
|
14
|
+
remote: https://rubygems.org/
|
13
15
|
specs:
|
14
|
-
|
15
|
-
|
16
|
-
|
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.
|
21
|
+
builder (3.2.0)
|
19
22
|
columnize (0.3.6)
|
20
|
-
cucumber (1.2.
|
23
|
+
cucumber (1.2.3)
|
21
24
|
builder (>= 2.1.2)
|
22
25
|
diff-lcs (>= 1.1.3)
|
23
|
-
gherkin (~> 2.11.
|
24
|
-
|
25
|
-
diff-lcs (1.1
|
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.
|
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.
|
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 (
|
49
|
+
rdoc (4.0.0)
|
47
50
|
json (~> 1.4)
|
48
51
|
right_support (2.6.17)
|
49
|
-
rspec (2.
|
50
|
-
rspec-core (~> 2.
|
51
|
-
rspec-expectations (~> 2.
|
52
|
-
rspec-mocks (~> 2.
|
53
|
-
rspec-core (2.
|
54
|
-
rspec-expectations (2.
|
55
|
-
diff-lcs (
|
56
|
-
rspec-mocks (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
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
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,
|
28
|
-
autoload :
|
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,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
|