buildmeister 0.8.1

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/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