fogbugz 1.0.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.
@@ -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