fogbugz 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ basename = File.basename(__FILE__)
4
+ absolute = File.expand_path(__FILE__)
5
+
6
+ usage = <<HERE
7
+ usage: #{basename} <subcommand> [options]
8
+ #{basename} <case>
9
+
10
+ The subcommands are:
11
+ areas List active areas.
12
+ assign Assign a case.
13
+ categories List available categories.
14
+ close Close a resolved case.
15
+ edit Edit an existing case.
16
+ filter Set the current default filter.
17
+ filters List available filters.
18
+ list List cases by the current filter or a search query.
19
+ login Create an API token.
20
+ logoff Invalidate an API token.
21
+ milestones List active milestones.
22
+ new Create a new case.
23
+ people List active users.
24
+ priorities List available priorities.
25
+ projects List active projects.
26
+ reactivate Reactivate a resolved case.
27
+ reopen Reopen a closed case.
28
+ resolve Resolve a case.
29
+ show Show details on a case.
30
+ start Start working on a case.
31
+ statuses List available statuses.
32
+ stop Stop working on a case.
33
+
34
+ See '#{basename} help <commands>' for more information on a \
35
+ specific command.
36
+ HERE
37
+
38
+ if ARGV.length == 0
39
+ puts usage
40
+ exit 1
41
+ end
42
+
43
+ case ARGV[0]
44
+ when 'help'
45
+ if ARGV.length == 1
46
+ puts usage
47
+ exit 1
48
+ else
49
+ case ARGV[1]
50
+ when 'areas', 'assign', 'categories', 'close', 'edit', 'filter', 'filters',
51
+ 'list', 'login', 'logoff', 'milestones', 'new', 'people', 'priorities',
52
+ 'projects', 'reactivate', 'reopen', 'resolve', 'show', 'start',
53
+ 'statuses', 'stop'
54
+ subcommand = ARGV[1]
55
+ ARGV.clear
56
+ ARGV.push '--help'
57
+ load "#{absolute}-#{subcommand}"
58
+ exit 0
59
+ else
60
+ puts usage
61
+ exit 1
62
+ end
63
+ end
64
+ when 'areas', 'assign', 'categories', 'close', 'edit', 'filter', 'filters',
65
+ 'list', 'login', 'logoff', 'milestones', 'new', 'people', 'priorities',
66
+ 'projects', 'reactivate', 'reopen', 'resolve', 'show', 'start',
67
+ 'statuses', 'stop'
68
+ load "#{absolute}-#{ARGV.shift}"
69
+ exit 0
70
+ else
71
+ if ARGV[0].to_i.to_s == ARGV[0]
72
+ load "#{absolute}-show"
73
+ exit 0
74
+ else
75
+ puts usage
76
+ exit 1
77
+ end
78
+ end
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'typhoeus'
5
+ require 'xml'
6
+ require 'optparse'
7
+
8
+ api_url = ENV['FOGBUGZ_API_URL']
9
+ unless api_url
10
+ puts "Environment variable FOGBUGZ_API_URL must be set."
11
+ exit 1
12
+ end
13
+
14
+ api_token = ENV['FOGBUGZ_API_TOKEN']
15
+ unless api_url
16
+ puts "Environment variable FOGBUGZ_API_TOKEN must be set."
17
+ exit 1
18
+ end
19
+
20
+ options = {}
21
+ optparse = OptionParser.new do |opts|
22
+ opts.banner = "usage: #{File::basename(__FILE__)} [options]"
23
+
24
+ options[:verbose] = false
25
+ opts.on('-v', '--verbose', 'Output verbose debugging information') do
26
+ options[:verbose] = true
27
+ end
28
+
29
+ opts.on_tail('-h', '--help') do
30
+ puts optparse.help
31
+ exit 1
32
+ end
33
+
34
+ options[:project] = nil
35
+ opts.on('--project=<project>', 'Filter by project.') do |project|
36
+ options[:project] = project
37
+ end
38
+ end
39
+ optparse.parse!
40
+
41
+ response = Typhoeus::Request.get(api_url,
42
+ :verbose => options[:verbose],
43
+ :params => {
44
+ :cmd => 'listAreas',
45
+ :token => api_token,
46
+ :ixProject => options[:project] })
47
+ if response.code != 200
48
+ puts "HTTP request to #{api_url} failed with code #{response.code}."
49
+ exit 1
50
+ end
51
+
52
+ result = XML::Parser.string(response.body).parse
53
+ error = result.find_first('/response/error')
54
+ if error
55
+ puts "Failed with error: #{error.content}."
56
+ exit 1
57
+ end
58
+
59
+ result.find('/response/areas/area').each do |status|
60
+ puts format("%-20.20s %-20.20s %s\n",
61
+ status.find_first('sPersonOwner').content,
62
+ status.find_first('sProject').content,
63
+ status.find_first('sArea').content).strip!
64
+ end
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'typhoeus'
5
+ require 'xml'
6
+ require 'optparse'
7
+
8
+ api_url = ENV['FOGBUGZ_API_URL']
9
+ unless api_url
10
+ puts "Environment variable FOGBUGZ_API_URL must be set."
11
+ exit 1
12
+ end
13
+
14
+ api_token = ENV['FOGBUGZ_API_TOKEN']
15
+ unless api_url
16
+ puts "Environment variable FOGBUGZ_API_TOKEN must be set."
17
+ exit 1
18
+ end
19
+
20
+ options = {}
21
+ optparse = OptionParser.new do |opts|
22
+ opts.banner = "usage: #{File::basename(__FILE__)} [options] <case> <assignee>"
23
+
24
+ options[:verbose] = false
25
+ opts.on('-v', '--verbose', 'Output verbose debugging information.') do
26
+ options[:verbose] = true
27
+ end
28
+
29
+ opts.on_tail('-h', '--help') do
30
+ puts optparse.help
31
+ exit 1
32
+ end
33
+ end
34
+ optparse.parse!
35
+
36
+ unless ARGV.length == 2
37
+ puts optparse.help
38
+ exit 1
39
+ end
40
+
41
+ response = Typhoeus::Request.get(api_url,
42
+ :verbose => options[:verbose],
43
+ :params => {
44
+ :cmd => 'assign',
45
+ :token => api_token,
46
+ :ixBug => ARGV[0],
47
+ :sPersonAssignedTo => ARGV[1] })
48
+ if response.code != 200
49
+ puts "HTTP request to #{api_url} failed with code #{response.code}."
50
+ exit 1
51
+ end
52
+
53
+ result = XML::Parser.string(response.body).parse
54
+ error = result.find_first('/response/error')
55
+ if error
56
+ puts "Failed with error: #{error.content}."
57
+ exit 1
58
+ end
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'typhoeus'
5
+ require 'xml'
6
+ require 'optparse'
7
+
8
+ api_url = ENV['FOGBUGZ_API_URL']
9
+ unless api_url
10
+ puts "Environment variable FOGBUGZ_API_URL must be set."
11
+ exit 1
12
+ end
13
+
14
+ api_token = ENV['FOGBUGZ_API_TOKEN']
15
+ unless api_url
16
+ puts "Environment variable FOGBUGZ_API_TOKEN must be set."
17
+ exit 1
18
+ end
19
+
20
+ options = {}
21
+ optparse = OptionParser.new do |opts|
22
+ opts.banner = "usage: #{File::basename(__FILE__)} [options]"
23
+
24
+ options[:verbose] = false
25
+ opts.on('-v', '--verbose', 'Output verbose debugging information') do
26
+ options[:verbose] = true
27
+ end
28
+
29
+ opts.on_tail('-h', '--help') do
30
+ puts optparse.help
31
+ exit 1
32
+ end
33
+ end
34
+ optparse.parse!
35
+
36
+ response = Typhoeus::Request.get(api_url,
37
+ :verbose => options[:verbose],
38
+ :params => {
39
+ :cmd => 'listCategories',
40
+ :token => api_token })
41
+ if response.code != 200
42
+ puts "HTTP request to #{api_url} failed with code #{response.code}."
43
+ exit 1
44
+ end
45
+
46
+ result = XML::Parser.string(response.body).parse
47
+ error = result.find_first('/response/error')
48
+ if error
49
+ puts "Failed with error: #{error.content}."
50
+ exit 1
51
+ end
52
+
53
+ result.find('/response/categories/category').each do |status|
54
+ puts status.find_first('sCategory').content
55
+ end
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'typhoeus'
5
+ require 'xml'
6
+ require 'optparse'
7
+
8
+ api_url = ENV['FOGBUGZ_API_URL']
9
+ unless api_url
10
+ puts "Environment variable FOGBUGZ_API_URL must be set."
11
+ exit 1
12
+ end
13
+
14
+ api_token = ENV['FOGBUGZ_API_TOKEN']
15
+ unless api_url
16
+ puts "Environment variable FOGBUGZ_API_TOKEN must be set."
17
+ exit 1
18
+ end
19
+
20
+ options = {}
21
+ optparse = OptionParser.new do |opts|
22
+ opts.banner = "usage: #{File::basename(__FILE__)} [options] <case>"
23
+
24
+ options[:verbose] = false
25
+ opts.on('-v', '--verbose', 'Output verbose debugging information.') do
26
+ options[:verbose] = true
27
+ end
28
+
29
+ opts.on_tail('-h', '--help') do
30
+ puts optparse.help
31
+ exit 1
32
+ end
33
+ end
34
+ optparse.parse!
35
+
36
+ unless ARGV[0]
37
+ puts optparse.help
38
+ exit 1
39
+ end
40
+
41
+ response = Typhoeus::Request.get(api_url,
42
+ :verbose => options[:verbose],
43
+ :params => {
44
+ :cmd => 'listAreas',
45
+ :token => api_token,
46
+ :ixBug => ARGV[0] })
47
+ if response.code != 200
48
+ puts "HTTP request to #{api_url} failed with code #{response.code}."
49
+ exit 1
50
+ end
51
+
52
+ result = XML::Parser.string(response.body).parse
53
+ error = result.find_first('/response/error')
54
+ if error
55
+ puts "Failed with error: #{error.content}."
56
+ exit 1
57
+ end
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'typhoeus'
5
+ require 'xml'
6
+ require 'tempfile'
7
+ require 'optparse'
8
+ require 'yaml'
9
+ require 'English'
10
+
11
+ api_url = ENV['FOGBUGZ_API_URL']
12
+ unless api_url
13
+ puts "Environment variable FOGBUGZ_API_URL must be set."
14
+ exit 1
15
+ end
16
+
17
+ api_token = ENV['FOGBUGZ_API_TOKEN']
18
+ unless api_url
19
+ puts "Environment variable FOGBUGZ_API_TOKEN must be set."
20
+ exit 1
21
+ end
22
+
23
+ editor = ENV['EDITOR'] || 'vim'
24
+
25
+ options = {}
26
+ optparse = OptionParser.new do |opts|
27
+ opts.banner = "usage: #{File::basename(__FILE__)} [options] <case>"
28
+
29
+ options[:verbose] = false
30
+ opts.on('-v', '--verbose', 'Output verbose debugging information') do
31
+ options[:verbose] = true
32
+ end
33
+
34
+ opts.on_tail('-h', '--help') do
35
+ puts optparse.help
36
+ exit 1
37
+ end
38
+
39
+ options[:file] = nil
40
+ opts.on('-f', '--file=<file>',
41
+ 'Take the case content from the given file. Use - to read from STDIN.') do |file|
42
+ options[:file] = file
43
+ end
44
+
45
+ options[:template] = nil
46
+ opts.on('-t', '--template=<template>',
47
+ 'Use the file content or - for STDIN as the initial case content.') do |template|
48
+ options[:template] = template
49
+ end
50
+ end
51
+ optparse.parse!
52
+
53
+ unless ARGV.length == 1
54
+ puts optparse.help
55
+ exit 1
56
+ end
57
+ case_id = ARGV[0]
58
+
59
+ response = Typhoeus::Request.get(api_url,
60
+ :verbose => options[:verbose],
61
+ :params => {
62
+ :cmd => 'search',
63
+ :token => api_token,
64
+ :cols => 'ixBug,ixBugParent,tags,sTitle,sProject,sArea,sFixFor,sCategory,sPersonAssignedTo,sPriority,hrsCurrEst,events',
65
+ :q => case_id })
66
+ if response.code != 200
67
+ puts "HTTP request to #{api_url} failed with code #{response.code}."
68
+ exit 1
69
+ end
70
+
71
+ result = XML::Parser.string(response.body).parse
72
+ error = result.find_first('/response/error')
73
+ if error
74
+ puts "Failed with error: #{error.content}."
75
+ exit 1
76
+ end
77
+
78
+ bug = result.find_first("/response/cases/case[@ixBug='#{case_id}']")
79
+ unless bug
80
+ puts "Case #{case_id} does not exist."
81
+ exit 1
82
+ end
83
+
84
+ invoke_editor = true
85
+ if options[:file]
86
+ if options[:file] == '-'
87
+ ARGV.replace []
88
+ else
89
+ ARGV.replace [options[:file]]
90
+ end
91
+ template = ARGF.read
92
+ invoke_editor = false
93
+ elsif options[:template]
94
+ if options[:template] == '-'
95
+ ARGV.replace []
96
+ else
97
+ ARGV.replace [options[:template]]
98
+ end
99
+ template = ARGF.read
100
+ else
101
+ template = ''
102
+ template << <<HERE
103
+ # Fill in metadata for the case.
104
+ HERE
105
+
106
+ sTitle = bug.find_first('sTitle')
107
+ if sTitle and sTitle.content and not sTitle.content.empty?
108
+ template << "# title: #{sTitle.content}\n"
109
+ else
110
+ template << "# title: <title>\n"
111
+ end
112
+
113
+ sPersonAssignedTo = bug.find_first('sPersonAssignedTo')
114
+ if sPersonAssignedTo and sPersonAssignedTo.content and
115
+ not sPersonAssignedTo.content.empty?
116
+ template << "# assignee: #{sPersonAssignedTo.content}\n"
117
+ else
118
+ template << "# assignee: <person>\n"
119
+ end
120
+
121
+ ixBugParent = bug.find_first('ixBugParent')
122
+ if ixBugParent and ixBugParent.content and not ixBugParent.content.empty? and
123
+ ixBugParent.content != '0'
124
+ template << "# parent: #{ixBugParent.content}\n"
125
+ else
126
+ template << "# parent: <case>\n"
127
+ end
128
+
129
+ tags = bug.find('tags/tag').collect { |tag| tag.content }
130
+ if tags and not tags.empty?
131
+ template << "# tags: [#{tags.join(', ')}]\n"
132
+ else
133
+ template << "# tags: [bug, enhancement]\n"
134
+ end
135
+
136
+ sProject = bug.find_first('sProject')
137
+ if sProject and sProject.content and not sProject.content.empty?
138
+ template << "# project: #{sProject.content}\n"
139
+ else
140
+ template << "# project: <project>\n"
141
+ end
142
+
143
+ sArea = bug.find_first('sArea')
144
+ if sArea and sArea.content and not sArea.content.empty?
145
+ template << "# area: #{sArea.content}\n"
146
+ else
147
+ template << "# area: <area>\n"
148
+ end
149
+
150
+ sFixFor = bug.find_first('sFixFor')
151
+ if sFixFor and sFixFor.content and not sFixFor.content.empty?
152
+ template << "# milestone: #{sFixFor.content}\n"
153
+ else
154
+ template << "# milestone: <milestone>"
155
+ end
156
+
157
+ sCategory = bug.find_first('sCategory')
158
+ if sCategory and sCategory.content and not sCategory.content.empty?
159
+ template << "# category: #{sCategory.content}\n"
160
+ else
161
+ template << "# category: <category>"
162
+ end
163
+
164
+ sPriority = bug.find_first('sPriority')
165
+ if sPriority and sPriority.content and not sPriority.content.empty?
166
+ template << "# priority: #{sPriority.content}\n"
167
+ else
168
+ template << "# priority: <priority>"
169
+ end
170
+
171
+ hrsCurrEst = bug.find_first('hrsCurrEst')
172
+ if hrsCurrEst and hrsCurrEst.content and not hrsCurrEst.content.empty? and
173
+ hrsCurrEst.content != '0'
174
+ template << "# estimate: #{hrsCurrEst.content}\n"
175
+ else
176
+ template << "# estimate: <estimate>\n"
177
+ end
178
+
179
+ template << "\n"
180
+ bug.find('events/event').each do |event|
181
+ evtDescription = event.find_first('evtDescription')
182
+ if evtDescription and evtDescription.content and
183
+ not evtDescription.content.empty?
184
+ time = Time.parse(event.find_first('dt').content).localtime
185
+ template << format("# %s at %s on %s.\n", evtDescription.content,
186
+ time.strftime('%-l:%M %p'),
187
+ time.strftime('%A, %B %e %Y'))
188
+ end
189
+
190
+ sChanges = event.find_first('sChanges')
191
+ if sChanges and sChanges.content and not sChanges.content.empty?
192
+ commented = sChanges.content.gsub(/\n/, "\n# ")
193
+ template << "# #{commented}\n"
194
+ end
195
+
196
+ summary = event.find_first('s')
197
+ if summary and summary.content and not summary.content.empty?
198
+ commented = summary.content.gsub(/\n/, "\n# ")
199
+ template << "# #{commented}\n"
200
+ end
201
+ template << "\n"
202
+ end
203
+ template << <<HERE
204
+ # Enter an additional note for the case after the dashes.
205
+ ---
206
+ HERE
207
+ end
208
+
209
+ content = template
210
+ if invoke_editor
211
+ tempfile = Tempfile.new ['case', '.md']
212
+ tempfile.write template
213
+ tempfile.close
214
+ rc = system "#{editor} #{tempfile.path}"
215
+ unless rc
216
+ puts "Editor exited with non-zero status. Aborting."
217
+ exit 1
218
+ end
219
+ tempfile.open
220
+ content = tempfile.read
221
+ tempfile.close
222
+ tempfile.delete
223
+ end
224
+
225
+ data = {}
226
+ if content =~ /(.*?\n?)^(---\s*$\n?)/m
227
+ # Combined YAML front matter with text content.
228
+ begin
229
+ data = YAML.load($1) || {}
230
+ data['body'] = $POSTMATCH if $POSTMATCH and not $POSTMATCH.empty?
231
+ rescue => e
232
+ puts "Exception reading YAML front matter. #{e.inspect}"
233
+ exit 1
234
+ end
235
+ else
236
+ begin
237
+ # YAML only content.
238
+ data = YAML.load(content)
239
+ was_yaml = true if data and not data.empty?
240
+ if data.instance_of? String
241
+ # Text only content.
242
+ data = { 'body' => data }
243
+ end
244
+ rescue => e
245
+ data = {}
246
+ end
247
+ end
248
+
249
+ if not data or data.empty?
250
+ puts "No changes to case. Aborting."
251
+ exit 1
252
+ end
253
+
254
+ response = Typhoeus::Request.get(api_url,
255
+ :verbose => options[:verbose],
256
+ :params => {
257
+ :cmd => 'edit',
258
+ :token => api_token,
259
+ :ixBug => case_id,
260
+ :sTitle => data['title'],
261
+ :ixBugParent => data['parent'],
262
+ :sTags => data['tags'] ? data['tags'].join(',') : nil,
263
+ :sProject => data['project'],
264
+ :sArea => data['area'],
265
+ :sFixFor => data['milestone'],
266
+ :sCategory => data['category'],
267
+ :sPersonAssignedTo => data['assignee'],
268
+ :sPriority => data['priority'],
269
+ :hrsCurrEst => data['estimate'],
270
+ :sEvent => data['body'] })
271
+ if response.code != 200
272
+ puts "HTTP request to #{api_url} failed with code #{response.code}."
273
+ exit 1
274
+ end
275
+
276
+ result = XML::Parser.string(response.body).parse
277
+ error = result.find_first('/response/error')
278
+ if error
279
+ puts "Failed with error: #{error.content}."
280
+ exit 1
281
+ end