buildmeister 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2009-02-24
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,10 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.rdoc
4
+ Rakefile
5
+ bin/buildmeister
6
+ bin/git_cleanup
7
+ config/buildmeister_config.sample.yml
8
+ lib/buildmeister.rb
9
+ lib/git_cleanup.rb
10
+ test/test_buildmeister.rb
data/README.rdoc ADDED
@@ -0,0 +1,70 @@
1
+ = Buildmeister
2
+
3
+ Managing your build process is fun and easy!
4
+
5
+ == Description
6
+
7
+ Buildmeister provides some simple utilities for managing a small team's build process
8
+ using Lighthouse and Github.
9
+
10
+ == Features
11
+
12
+ - Growl notification of updated build status
13
+ - Utility scripts to move batch move tickets
14
+ - A very handy utility for pruning local and remote git branches
15
+
16
+ == Examples
17
+ # To start periodically updated growl notifications...
18
+ $ buildmeister notify
19
+
20
+ # To move all tickets in a bin to a particular state...
21
+ $ buildmeister move_all --from-bin "Staged" --to-state "verified"
22
+
23
+ # Clean up local git branches
24
+ $ git_cleanup local
25
+
26
+ # Clean up git branches on origin
27
+ $ git_cleanup remote
28
+
29
+ == Requirements
30
+
31
+ Depends on texel-lighthouse-api, activesupport, and growlnotify.
32
+
33
+ == Install
34
+
35
+ - Download and install Growl from http://growl.info/
36
+ - sudo gem install texel-buildmeister
37
+ - Use the included config/buildmeister_config.sample.yml file as a guide to creating your own configuration file.
38
+ The file should be called ".buildmeister_config.yml", and be placed in your home directory.
39
+
40
+ == TODO
41
+
42
+ - Generalize remote git cleanup so that it doesn't explicitly refer to origin
43
+ - Break Lighthouse Utilities out into their own gem
44
+
45
+ == License
46
+
47
+ (The MIT License)
48
+
49
+ Copyright (c) 2009 Onehub
50
+
51
+ http://onehub.com
52
+
53
+ Permission is hereby granted, free of charge, to any person obtaining
54
+ a copy of this software and associated documentation files (the
55
+ 'Software'), to deal in the Software without restriction, including
56
+ without limitation the rights to use, copy, modify, merge, publish,
57
+ distribute, sublicense, and/or sell copies of the Software, and to
58
+ permit persons to whom the Software is furnished to do so, subject to
59
+ the following conditions:
60
+
61
+ The above copyright notice and this permission notice shall be
62
+ included in all copies or substantial portions of the Software.
63
+
64
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
65
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
66
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
67
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
68
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
69
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
70
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/buildmeister.rb'
6
+
7
+ Hoe.new('buildmeister', Buildmeister::VERSION) do |p|
8
+ p.rubyforge_name = 'buildmeister' # if different than lowercase project name
9
+ p.developer('Leigh Caplan', 'lcaplan@onehub.com')
10
+ end
11
+
12
+ task :cultivate do
13
+ system "touch Manifest.txt; rake check_manifest | grep -v \"(in \" | patch"
14
+ system "rake debug_gem | grep -v \"(in \" > `basename \\`pwd\\``.gemspec"
15
+ end
16
+
17
+ # vim: syntax=Ruby
data/bin/buildmeister ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + "/../lib/buildmeister")
4
+
5
+ begin
6
+ Buildmeister.new.send ARGV.shift
7
+ rescue Interrupt => i
8
+ Buildmeister.post_notification("Buildmeister Shut Down", "Goodbye!")
9
+ puts "\rThank you for using Buildmeister!"
10
+ rescue Exception => e
11
+ Buildmeister.post_notification("Buildmeister Error: #{e.class}", e.message)
12
+ puts "Quitting Buildmeister due to error: #{e.message}"
13
+ puts e.backtrace
14
+ end
data/bin/git_cleanup ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + "/../lib/git_cleanup")
4
+
5
+ begin
6
+ GitCleanup.new.cleanup
7
+ rescue Interrupt => i
8
+ puts "\rThank you for using Git Cleanup!"
9
+ rescue Exception => e
10
+ puts "Quitting Git Cleanup due to error: #{e.message}"
11
+ puts e.backtrace
12
+ end
@@ -0,0 +1,16 @@
1
+ token: Your Lighthouse token
2
+ account: Your Lighthouse account name
3
+ project_name: Your Lighthouse project name
4
+
5
+ bin_groups:
6
+ - staging:
7
+ - Ready
8
+ - Staged
9
+ - Verified
10
+
11
+ - master:
12
+ - Ready (Experimental)
13
+ - Staged (Experimental)
14
+ - Verified (Experimental)
15
+
16
+ notification_interval: 5
@@ -0,0 +1,276 @@
1
+ require 'rubygems'
2
+ require 'lighthouse'
3
+ require 'activesupport'
4
+ require 'optparse'
5
+
6
+ class Buildmeister
7
+ attr_accessor :project, :project_name, :bin_groups, :notification_interval
8
+
9
+ def initialize
10
+ @options = {}
11
+ OptionParser.new do |opts|
12
+ opts.banner = "Usage: buildmeister notify"
13
+
14
+ opts.on('-f', '--from-bin BIN_NAME', 'Move From Bin') do |f|
15
+ @options[:move_from] = f
16
+ end
17
+
18
+ opts.on('-t', '--to-state STATE', 'Move to State') do |t|
19
+ @options[:to_state] = t
20
+ end
21
+
22
+ opts.on('-v', '--verbose', 'Verbose') do |t|
23
+ @options[:verbose] = true
24
+ end
25
+ end.parse!
26
+
27
+ @config = Buildmeister.load_config
28
+ Lighthouse.account = @config['account']
29
+ Lighthouse.token = @config['token']
30
+
31
+ self.project_name = @config['project_name']
32
+ self.get_project
33
+ self.bin_groups = []
34
+
35
+ self.notification_interval = @config['notification_interval']
36
+
37
+ @config['bin_groups'].each do |bin_group|
38
+ self.bin_groups << {
39
+ :name => bin_group.keys.first,
40
+ :bin_names => bin_group.values.first.map
41
+ }
42
+ end
43
+
44
+ self.bin_groups.each do |bin_group|
45
+ bin_group[:bin_names].each do |bin_name|
46
+ class << bin_name
47
+ def normalize
48
+ Buildmeister.normalize_bin_name(self)
49
+ end
50
+ end
51
+
52
+ attr_accessor_init = <<-eos
53
+ class << self
54
+ attr_accessor :"#{bin_name.normalize}", :"last_#{bin_name.normalize}"
55
+ end
56
+ eos
57
+
58
+ eval attr_accessor_init
59
+ end
60
+ end
61
+
62
+ load_project
63
+ end
64
+
65
+ def normalize(bin_name)
66
+ Buildmeister.normalize_bin_name(bin_name)
67
+ end
68
+
69
+ def new_hotfix
70
+ generate_timed_branch('hotfix')
71
+ end
72
+
73
+ def new_experimental
74
+ generate_timed_branch('experimental')
75
+ end
76
+
77
+ def generate_timed_branch(prefix)
78
+ branches = local_branches
79
+ now = Time.now
80
+ count = 1
81
+
82
+ loop do
83
+ new_branch_name = "#{prefix}-#{now.year}-#{now.month.to_s.rjust 2, '0'}-#{now.day.to_s.rjust 2, '0'}-#{count.to_s.rjust 3, '0'}"
84
+ unless branches.include? new_branch_name
85
+ `git checkout -b #{new_branch_name}`
86
+ puts "Created #{new_branch_name}"
87
+ return true
88
+ end
89
+
90
+ count += 1
91
+ end
92
+ end
93
+
94
+ def pull_bin(bin_name = ARGV.shift)
95
+ bin_name = normalize(bin_name)
96
+ existing_bin_names = bin_names.map { |b| b.normalize }
97
+
98
+ raise ArgumentError, "#{bin_name} is not a valid bin! Must be in #{bin_names.join(', ')}" unless existing_bin_names.include?(bin_name)
99
+
100
+ `git fetch origin`
101
+
102
+ branches = remote_branches
103
+ ticket_numbers = send(normalize(bin_name)).tickets.map { |tkt| tkt.id.to_s }
104
+
105
+ branches_to_pull = branches.select do |branch_name|
106
+ ticket_numbers.map { |tkt_number| branch_name =~ /#{tkt_number}/ }.any?
107
+ end
108
+
109
+ branches_to_pull.each do |branch|
110
+ result = `git pull origin #{branch.gsub("origin/", "")}`
111
+ puts result
112
+ end
113
+ end
114
+
115
+ def local_branches
116
+ `git branch`.split.reject { |name| name == "*" }
117
+ end
118
+
119
+ def remote_branches
120
+ `git branch -r`.split.reject { |name| name == "*" }
121
+ end
122
+
123
+ def current_branch
124
+ branches = `git branch`.split
125
+ i = branches.index "*"
126
+ branches[i + 1]
127
+ end
128
+
129
+ def move_all
130
+ bin_name = normalize @options[:move_from]
131
+ self.send(bin_name).tickets.each do |ticket|
132
+ ticket.state = @options[:to_state]
133
+ ticket.save
134
+ end
135
+
136
+ puts "All tickets from bin #{@options[:move_from]} have been moved to #{@options[:to_state]}"
137
+ end
138
+
139
+ def bin_group_report(bin_group_name = normalize(ARGV.shift))
140
+ bin_group = bin_groups.find { |group| group[:name] == bin_group_name }
141
+ bin_names = bin_group[:bin_names].map &:normalize
142
+
143
+ ticket_numbers = bin_names.map do |bin_name|
144
+ send(normalize(bin_name)).tickets.map &:id
145
+ end.flatten
146
+
147
+ # Pluck the relevant branch names using git...
148
+ relevant_branches =
149
+ remote_branches.select do |branch_name|
150
+ ticket_numbers.map { |tkt_number| branch_name =~ /#{tkt_number}/ }.any?
151
+ end.map { |b| b.gsub('origin/', '') }
152
+
153
+ output = ""
154
+ output << "#{current_branch}\n"
155
+ relevant_branches.each do |branch|
156
+ output << "\n#{branch}"
157
+ end
158
+
159
+ puts output
160
+ end
161
+
162
+ def resolve_verified
163
+ self.verified.tickets.each do |ticket|
164
+ ticket.state = 'resolved'
165
+ ticket.save
166
+ end
167
+ end
168
+
169
+ def stage_all
170
+ self.ready.tickets.each do |ticket|
171
+ ticket.state = 'staged'
172
+ ticket.save
173
+ end
174
+ end
175
+
176
+ def load_project
177
+ bins = self.get_project.bins
178
+
179
+ self.bin_groups.each do |bin_group|
180
+ bin_group[:bin_names].each do |bin_name|
181
+ self.send("#{bin_name.normalize}=", bins.find { |bin| bin.name == bin_name })
182
+
183
+ class << self.send("#{bin_name.normalize}")
184
+ attr_accessor :display_value
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ def reload_info
191
+ bin_names.each do |bin_name|
192
+ send("last_#{bin_name.normalize}=", display_value(bin_name))
193
+ end
194
+
195
+ self.load_project
196
+ end
197
+
198
+ def bin_names
199
+ bin_groups.map do |bin_group|
200
+ bin_group[:bin_names].map do |bin_name|
201
+ bin_name
202
+ end
203
+ end.flatten
204
+ end
205
+
206
+ def changed?
207
+ bin_names.map { |bin_name| display_value(bin_name) } != bin_names.map { |bin_name| send("last_#{bin_name.normalize}") }
208
+ end
209
+
210
+ def get_project
211
+ self.project ||= Lighthouse::Project.find(:all).find {|pr| pr.name == project_name}
212
+ end
213
+
214
+ def notify
215
+ puts "Starting BuildMeister Notify..."
216
+
217
+ loop do
218
+ title = "BuildMeister: #{Time.now.strftime("%m/%d %I:%M %p")}"
219
+
220
+ body = ''
221
+
222
+ bin_groups.each do |bin_group|
223
+ body += "#{bin_group[:name].titleize}\n"
224
+ body += "---------\n"
225
+
226
+ bin_group[:bin_names].each do |bin_name|
227
+ body += "#{bin_name}: #{display_value(bin_name)}\n"
228
+ end
229
+
230
+ body += "\n"
231
+ end
232
+
233
+ puts "Updated notification at #{Time.now.strftime("%m/%d %I:%M %p")}"
234
+
235
+ if changed?
236
+ Buildmeister.post_notification(title, body)
237
+ end
238
+
239
+ sleep notification_interval.minutes.to_i
240
+
241
+ reload_info
242
+ end
243
+ end
244
+
245
+ def display_value(bin_name)
246
+ # We're memoizing the display value on the bin objects
247
+ # so that when it comes time to reload, the previous value
248
+ # is kept.
249
+ send(bin_name.normalize).display_value ||=
250
+ if @options[:verbose]
251
+ send(bin_name.normalize).tickets.map(&:id).join(", ")
252
+ else
253
+ send(bin_name.normalize).tickets_count
254
+ end
255
+ end
256
+
257
+ def git_cleanup
258
+
259
+ end
260
+
261
+ # -----------------------------------------
262
+ # Class Methods
263
+ # -----------------------------------------
264
+
265
+ def self.post_notification(title, body)
266
+ `growlnotify -H localhost -s -n "Buildmeister" -d "Buildmeister" -t #{title} -m "#{body}"`
267
+ end
268
+
269
+ def self.normalize_bin_name(bin_name)
270
+ bin_name.squeeze(' ').gsub(' ', '_').gsub(/\W/, '').downcase
271
+ end
272
+
273
+ def self.load_config
274
+ YAML.load_file(File.expand_path('~/.buildmeister_config.yml'))
275
+ end
276
+ end
@@ -0,0 +1,179 @@
1
+ require 'rubygems'
2
+ require 'lighthouse'
3
+ require 'optparse'
4
+ require 'ostruct'
5
+ require File.expand_path(File.dirname(__FILE__) + "/../lib/buildmeister")
6
+
7
+ class GitCleanup
8
+
9
+ def initialize
10
+ @config = Buildmeister.load_config
11
+ Lighthouse.token = @config['token']
12
+ Lighthouse.account = @config['account']
13
+
14
+ @options = {:rules => {}}
15
+
16
+ OptionParser.new do |opts|
17
+ opts.banner = "Usage: git_cleanup --remote"
18
+
19
+ opts.on('-r', '--remote', 'Clean up remote branches') do |f|
20
+ @options[:mode] = 'remote'
21
+ end
22
+
23
+ opts.on('-l', '--local', 'Clean up local branches') do
24
+ @options[:mode] = 'local'
25
+ end
26
+
27
+ opts.on('-t', '--test', 'Test mode - no changes will be made') do
28
+ @options[:test_mode] = true
29
+ end
30
+
31
+ opts.on('-b', '--before date', 'Automatically delete branches last updated before') do |date|
32
+ @options[:rules].merge!(:before => eval(date))
33
+ end
34
+
35
+ opts.on('-s', '--state ticket_state', 'Automatically delete branches corresponding to a Lighthouse ticket state') do |state|
36
+ @options[:rules].merge!(:state => state)
37
+ end
38
+
39
+ opts.on('-m', '--matching string', 'Automatically delete branches with names matching the string') do |string|
40
+ @options[:rules].merge!(:matching => string)
41
+ end
42
+ end.parse!
43
+ end
44
+
45
+ def get_project(name)
46
+ projects = Lighthouse::Project.find(:all)
47
+ project = projects.find {|pr| pr.name == name}
48
+ end
49
+
50
+ def prune_these(local_or_remote, branches)
51
+ project = get_project(@config['project_name'])
52
+
53
+ branches.each do |branch|
54
+ branch_info = OpenStruct.new(:string => branch, :local_or_remote => local_or_remote)
55
+
56
+ puts "#{branch_info.string} (updated #{branch_modified(branch_info, :time_ago_in_words)})"
57
+
58
+ if project
59
+ get_lighthouse_status(branch_info, project)
60
+ puts branch_info.lighthouse_message
61
+ end
62
+
63
+ if @options[:rules].empty?
64
+ print "keep [return], delete [d]: "
65
+ user_input = gets
66
+ user_input.strip!
67
+
68
+ case user_input
69
+ when 'd'
70
+ send "delete_#{local_or_remote}", branch_info
71
+ end
72
+
73
+ puts "\n"
74
+ else
75
+ rules_matched = @options[:rules].map do |rule_name, rule_body|
76
+ send "match_#{rule_name}", branch_info, rule_body
77
+ end
78
+
79
+ send "delete_#{local_or_remote}", branch_info if rules_matched.all?
80
+ puts "\n"
81
+ end
82
+ end
83
+ end
84
+
85
+ def branch_modified(branch_info, format = :time_ago_in_words)
86
+ format_string =
87
+ case format
88
+ when :time_ago_in_words
89
+ "%ar"
90
+ when :absolute
91
+ "%aD"
92
+ end
93
+
94
+ `git show --pretty=format:#{format_string} #{branch_info.string}`.split("\n")[0]
95
+ end
96
+
97
+ # git_cleanup --before 1.month.ago
98
+ def match_before(branch_info, date)
99
+ last_updated = Time.parse(branch_modified(branch_info, :absolute))
100
+ last_updated < date
101
+ rescue
102
+ false
103
+ end
104
+
105
+ # git_cleanup --state resolved
106
+ def match_state(branch_info, state)
107
+ branch_info.lighthouse_state == state
108
+ end
109
+
110
+ # git_cleanup --matching hotfix
111
+ def match_matching(branch_info, string)
112
+ branch_info.string =~ /#{string}/
113
+ end
114
+
115
+ def delete_local(branch_info)
116
+ execute "git branch -D #{branch_info.string}"
117
+ end
118
+
119
+ def delete_remote(branch_info)
120
+ branch_info.string.gsub!(/(remotes\/)|(origin\/)/, '')
121
+ execute "git push origin :#{branch_info.string}", "git remote prune origin"
122
+ end
123
+
124
+ def execute(*instructions)
125
+ if test_mode?
126
+ puts "Test mode - The following instructions would be executed"
127
+
128
+ instructions.each do |instruction|
129
+ puts instruction
130
+ end
131
+ else
132
+ instructions.each do |instruction|
133
+ system instruction
134
+ end
135
+ end
136
+ end
137
+
138
+ def get_lighthouse_status(branch_info, project)
139
+ lighthouse_id =
140
+ (matches = branch_info.string.match(/(^|\/)(\d+)-/)) ? matches[2] : nil
141
+
142
+ if lighthouse_id
143
+ tickets = project.tickets :q => lighthouse_id
144
+ ticket = tickets.first
145
+
146
+ if ticket
147
+ branch_info.lighthouse_state = ticket.state
148
+ branch_info.lighthouse_message = "Lighthouse Info:\nTicket ##{lighthouse_id} state - #{ticket.state}"
149
+ return
150
+ end
151
+ end
152
+
153
+ branch_info.lighthouse_message = "No Lighthouse Info."
154
+ end
155
+
156
+ def cleanup
157
+ branches = `git branch -a`.split.reject { |name| name == "*" }
158
+
159
+ local_branches = branches.select do |branch|
160
+ !(branch =~ /^remotes\/origin/)
161
+ end
162
+
163
+ remote_branches = branches.select do |branch|
164
+ !local_branches.include?(branch)
165
+ end
166
+
167
+ local_or_remote = @options[:mode]
168
+
169
+ if local_or_remote == 'local'
170
+ prune_these(local_or_remote, local_branches)
171
+ else
172
+ prune_these(local_or_remote, remote_branches)
173
+ end
174
+ end
175
+
176
+ def test_mode?
177
+ @options[:test_mode]
178
+ end
179
+ end
File without changes
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: buildmeister
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.1
5
+ platform: ruby
6
+ authors:
7
+ - Leigh Caplan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-11 00:00:00 -07:00
13
+ default_executable: buildmeister
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.0.0
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: texel-lighthouse-api
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.1
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: hoe
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 1.8.3
44
+ version:
45
+ description: FIX (describe your package)
46
+ email:
47
+ - lcaplan@onehub.com
48
+ executables:
49
+ - buildmeister
50
+ - git_cleanup
51
+ extensions: []
52
+
53
+ extra_rdoc_files:
54
+ - History.txt
55
+ - Manifest.txt
56
+ - README.rdoc
57
+ files:
58
+ - History.txt
59
+ - Manifest.txt
60
+ - README.rdoc
61
+ - Rakefile
62
+ - bin/buildmeister
63
+ - bin/git_cleanup
64
+ - config/buildmeister_config.sample.yml
65
+ - lib/buildmeister.rb
66
+ - lib/git_cleanup.rb
67
+ - test/test_buildmeister.rb
68
+ has_rdoc: true
69
+ homepage: http://github.com/texel/buildmeister
70
+ licenses: []
71
+
72
+ post_install_message:
73
+ rdoc_options:
74
+ - --main
75
+ - README.txt
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: "0"
83
+ version:
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: "0"
89
+ version:
90
+ requirements: []
91
+
92
+ rubyforge_project: buildmeister
93
+ rubygems_version: 1.3.5
94
+ signing_key:
95
+ specification_version: 2
96
+ summary: FIX (describe your package)
97
+ test_files:
98
+ - test/test_buildmeister.rb