fogbugz 1.0.3 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)