fogbugz 1.0.3 → 1.0.4

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