ursm-ditz 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.
data/lib/operator.rb ADDED
@@ -0,0 +1,549 @@
1
+ require 'fileutils'
2
+
3
+ module Ditz
4
+
5
+ class Operator
6
+ class Error < StandardError; end
7
+
8
+ class << self
9
+ def method_to_op meth; meth.to_s.gsub("_", "-") end
10
+ def op_to_method op; op.gsub("-", "_").intern end
11
+
12
+ def operation method, desc, *args_spec, &options_blk
13
+ @operations ||= {}
14
+ @operations[method] = { :desc => desc, :args_spec => args_spec,
15
+ :options_blk => options_blk }
16
+ end
17
+
18
+ def operations
19
+ @operations.map { |k, v| [method_to_op(k), v] }.sort_by { |k, v| k }
20
+ end
21
+ def has_operation? op; @operations.member? op_to_method(op) end
22
+
23
+ def build_opts method, args
24
+ options_blk = @operations[method][:options_blk]
25
+ options_blk and options args, &options_blk or nil
26
+ end
27
+
28
+ ## parse the specs, and the commandline arguments, and resolve them. does
29
+ ## typechecking but currently doesn't check for open_issues actually being
30
+ ## open, unstarted_issues being unstarted, etc. probably will check for
31
+ ## this in the future.
32
+ def build_args project, method, args
33
+ specs = @operations[method][:args_spec]
34
+ command = "command '#{method_to_op method}'"
35
+
36
+ if specs.empty? && args == ["<options>"]
37
+ die_with_completions project, method, nil
38
+ end
39
+
40
+ built_args = specs.map do |spec|
41
+ optional = spec.to_s =~ /^maybe_/
42
+ spec = spec.to_s.gsub(/^maybe_/, "").intern # :(
43
+ val = args.shift
44
+
45
+ case val
46
+ when nil
47
+ next if optional
48
+ specname = spec.to_s.gsub("_", " ")
49
+ article = specname =~ /^[aeiou]/ ? "an" : "a"
50
+ raise Error, "#{command} requires #{article} #{specname}"
51
+ when "<options>"
52
+ die_with_completions project, method, spec
53
+ end
54
+
55
+ case spec
56
+ when :issue, :open_issue, :unstarted_issue, :started_issue, :assigned_issue
57
+ ## issue completion sticks the title on there, so this will strip it off
58
+ valr = val.sub(/\A(\w+-\d+)_.*$/,'\1')
59
+ issues = project.issues_for valr
60
+ case issues.size
61
+ when 0; raise Error, "no issue with name #{val.inspect}"
62
+ when 1; issues.first
63
+ else
64
+ raise Error, "multiple issues matching name #{val.inspect}"
65
+ end
66
+ when :release, :unreleased_release
67
+ if val == "unassigned"
68
+ :unassigned
69
+ else
70
+ project.release_for(val) or raise Error, "no release with name #{val}"
71
+ end
72
+ when :component
73
+ project.component_for(val) or raise Error, "no component with name #{val}" if val
74
+ else
75
+ val # no translation for other types
76
+ end
77
+ end
78
+
79
+ raise Error, "too many arguments for #{command}" unless args.empty?
80
+ built_args
81
+ end
82
+
83
+ def die_with_completions project, method, spec
84
+ puts(case spec
85
+ when :issue, :open_issue, :unstarted_issue, :started_issue, :assigned_issue
86
+ m = { :issue => nil,
87
+ :open_issue => :open?,
88
+ :unstarted_issue => :unstarted?,
89
+ :started_issue => :in_progress?,
90
+ :assigned_issue => :assigned?,
91
+ }[spec]
92
+ project.issues.select { |i| m.nil? || i.send(m) }.sort_by { |i| i.creation_time }.reverse.map { |i| "#{i.name}_#{i.title.gsub(/\W+/, '-')}" }
93
+ when :release
94
+ project.releases.map { |r| r.name } + ["unassigned"]
95
+ when :unreleased_release
96
+ project.releases.select { |r| r.unreleased? }.map { |r| r.name }
97
+ when :component
98
+ project.components.map { |c| c.name }
99
+ when :command
100
+ operations.map { |name, _| name }
101
+ else
102
+ ""
103
+ end)
104
+ exit 0
105
+ end
106
+ private :die_with_completions
107
+ end
108
+
109
+ def do op, project, config, args
110
+ meth = self.class.op_to_method(op)
111
+
112
+ # Parse options, removing them from args
113
+ opts = self.class.build_opts meth, args
114
+ built_args = self.class.build_args project, meth, args
115
+
116
+ built_args.unshift opts if opts
117
+
118
+ send meth, project, config, *built_args
119
+ end
120
+
121
+ %w(operations has_operation?).each do |m|
122
+ define_method(m) { |*a| self.class.send m, *a }
123
+ end
124
+
125
+ operation :init, "Initialize the issue database for a new project"
126
+ def init
127
+ Project.create_interactively
128
+ end
129
+
130
+ operation :help, "List all registered commands", :maybe_command do
131
+ opt :cow, "Activate super cow powers", :default => false
132
+ end
133
+ def help project, config, opts, command
134
+ if opts[:cow]
135
+ puts "MOO!"
136
+ puts "All is well with the world now. A bit more methane though."
137
+ exit 0
138
+ end
139
+ return help_single(command) if command
140
+ puts <<EOS
141
+ Ditz commands:
142
+
143
+ EOS
144
+ ops = self.class.operations
145
+ len = ops.map { |name, op| name.to_s.length }.max
146
+ ops.each do |name, opts|
147
+ printf " %#{len}s: %s\n", name, opts[:desc]
148
+ end
149
+ puts <<EOS
150
+
151
+ Use 'ditz help <command>' for details.
152
+ EOS
153
+ end
154
+
155
+ def help_single command
156
+ name, opts = self.class.operations.find { |name, spec| name == command }
157
+ raise Error, "no such ditz command '#{command}'" unless name
158
+ args = opts[:args_spec].map do |spec|
159
+ case spec.to_s
160
+ when /^maybe_(.*)$/
161
+ "[#{$1}]"
162
+ else
163
+ "<#{spec.to_s}>"
164
+ end
165
+ end.join(" ")
166
+
167
+ puts <<EOS
168
+ #{opts[:desc]}.
169
+ Usage: ditz #{name} #{args}
170
+ EOS
171
+ end
172
+
173
+ operation :add, "Add an issue"
174
+ def add project, config
175
+ issue = Issue.create_interactively(:args => [config, project]) or return
176
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
177
+ issue.log "created", config.user, comment
178
+ project.add_issue issue
179
+ project.assign_issue_names!
180
+ puts "Added issue #{issue.name}."
181
+ end
182
+
183
+ operation :drop, "Drop an issue", :issue
184
+ def drop project, config, issue
185
+ project.drop_issue issue
186
+ puts "Dropped #{issue.name}. Note that other issue names may have changed."
187
+ end
188
+
189
+ operation :add_release, "Add a release", :maybe_name
190
+ def add_release project, config, maybe_name
191
+ puts "Adding release #{maybe_name}." if maybe_name
192
+ release = Release.create_interactively(:args => [project, config], :with => { :name => maybe_name }) or return
193
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
194
+ release.log "created", config.user, comment
195
+ project.add_release release
196
+ puts "Added release #{release.name}."
197
+ end
198
+
199
+ operation :add_component, "Add a component"
200
+ def add_component project, config
201
+ component = Component.create_interactively(:args => [project, config]) or return
202
+ project.add_component component
203
+ puts "Added component #{component.name}."
204
+ end
205
+
206
+ operation :add_reference, "Add a reference to an issue", :issue
207
+ def add_reference project, config, issue
208
+ puts "Adding a reference to #{issue.name}: #{issue.title}."
209
+ reference = ask "Reference"
210
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
211
+ issue.add_reference reference
212
+ issue.log "added reference #{issue.references.size}", config.user, comment
213
+ puts "Added reference to #{issue.name}."
214
+ end
215
+
216
+ operation :status, "Show project status", :maybe_release
217
+ def status project, config, releases
218
+ releases ||= project.unreleased_releases + [:unassigned]
219
+
220
+ if releases.empty?
221
+ puts "No releases."
222
+ return
223
+ end
224
+
225
+ entries = releases.map do |r|
226
+ title, issues = (r == :unassigned ? r.to_s : r.name), project.issues_for_release(r)
227
+
228
+ middle = Issue::TYPES.map do |type|
229
+ type_issues = issues.select { |i| i.type == type }
230
+ num = type_issues.size
231
+ nc = type_issues.count_of { |i| i.closed? }
232
+ pc = 100.0 * (type_issues.empty? ? 1.0 : nc.to_f / num)
233
+ "%2d/%2d %s" % [nc, num, type.to_s.pluralize(num, false)]
234
+ end
235
+
236
+ bar = if r != :unassigned && r.released?
237
+ "(released)"
238
+ elsif issues.empty?
239
+ "(no issues)"
240
+ elsif issues.all? { |i| i.closed? }
241
+ "(ready for release)"
242
+ else
243
+ status_bar_for(issues)
244
+ end
245
+
246
+ [title, middle, bar]
247
+ end
248
+
249
+ title_size = 0
250
+ middle_sizes = []
251
+
252
+ entries.each do |title, middle, bar|
253
+ title_size = [title_size, title.length].max
254
+ middle_sizes = middle.zip(middle_sizes).map do |e, s|
255
+ [s || 0, e.length].max
256
+ end
257
+ end
258
+
259
+ entries.each do |title, middle, bar|
260
+ printf "%-#{title_size}s ", title
261
+ middle.zip(middle_sizes).each_with_index do |(e, s), i|
262
+ sep = i < middle.size - 1 ? "," : ""
263
+ printf "%-#{s + sep.length}s ", e + sep
264
+ end
265
+ puts bar
266
+ end
267
+ end
268
+
269
+ def status_bar_for issues
270
+ Issue::STATUS_WIDGET.
271
+ sort_by { |k, v| -Issue::STATUS_SORT_ORDER[k] }.
272
+ map { |k, v| v * issues.count_of { |i| i.status == k } }.
273
+ join
274
+ end
275
+
276
+ def todo_list_for issues
277
+ return if issues.empty?
278
+ name_len = issues.max_of { |i| i.name.length }
279
+ issues.map do |i|
280
+ sprintf "%s %#{name_len}s: %s\n", i.status_widget, i.name, i.title
281
+ end.join
282
+ end
283
+
284
+ def print_todo_list_by_release_for project, issues
285
+ by_release = issues.inject({}) do |h, i|
286
+ r = project.release_for i.release
287
+ h[r] ||= []
288
+ h[r] << i
289
+ h
290
+ end
291
+
292
+ project.releases.each do |r|
293
+ next unless by_release.member? r
294
+ puts r == :unassigned ? "Unassigned:" : "#{r.name} (#{r.status}):"
295
+ print todo_list_for(by_release[r])
296
+ puts
297
+ end
298
+ end
299
+
300
+ operation :todo, "Generate todo list", :maybe_release
301
+ def todo project, config, releases
302
+ actually_do_todo project, config, releases, false
303
+ end
304
+
305
+ operation :todo_full, "Generate full todo list, including completed items", :maybe_release
306
+ def todo_full project, config, releases
307
+ actually_do_todo project, config, releases, true
308
+ end
309
+
310
+ def actually_do_todo project, config, releases, full
311
+ releases ||= project.unreleased_releases + [:unassigned]
312
+ releases = [*releases]
313
+ releases.each do |r|
314
+ puts r == :unassigned ? "Unassigned:" : "#{r.name} (#{r.status}):"
315
+ issues = project.issues_for_release r
316
+ issues = issues.select { |i| i.open? } unless full
317
+ puts(todo_list_for(issues.sort_by { |i| i.sort_order }) || "No open issues.")
318
+ puts
319
+ end
320
+ end
321
+
322
+ operation :show, "Describe a single issue", :issue
323
+ def show project, config, issue
324
+ ScreenView.new(project, config).render_issue issue
325
+ end
326
+
327
+ operation :start, "Start work on an issue", :unstarted_issue
328
+ def start project, config, issue
329
+ puts "Starting work on issue #{issue.name}: #{issue.title}."
330
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
331
+ issue.start_work config.user, comment
332
+ puts "Recorded start of work for #{issue.name}."
333
+ end
334
+
335
+ operation :stop, "Stop work on an issue", :started_issue
336
+ def stop project, config, issue
337
+ puts "Stopping work on issue #{issue.name}: #{issue.title}."
338
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
339
+ issue.stop_work config.user, comment
340
+ puts "Recorded work stop for #{issue.name}."
341
+ end
342
+
343
+ operation :close, "Close an issue", :open_issue
344
+ def close project, config, issue
345
+ puts "Closing issue #{issue.name}: #{issue.title}."
346
+ disp = ask_for_selection Issue::DISPOSITIONS, "disposition", lambda { |x| Issue::DISPOSITION_STRINGS[x] || x.to_s }
347
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
348
+ issue.close disp, config.user, comment
349
+ puts "Closed issue #{issue.name} with disposition #{issue.disposition_string}."
350
+ end
351
+
352
+ operation :assign, "Assign an issue to a release", :issue, :maybe_release
353
+ def assign project, config, issue, maybe_release
354
+ if maybe_release && maybe_release.name == issue.release
355
+ raise Error, "issue #{issue.name} already assigned to release #{issue.release}"
356
+ end
357
+
358
+ puts "Issue #{issue.name} currently " + if issue.release
359
+ "assigned to release #{issue.release}."
360
+ else
361
+ "not assigned to any release."
362
+ end
363
+
364
+ puts "Assigning to release #{maybe_release.name}." if maybe_release
365
+
366
+ release = maybe_release || begin
367
+ releases = project.releases.sort_by { |r| (r.release_time || 0).to_i }
368
+ releases -= [releases.find { |r| r.name == issue.release }] if issue.release
369
+ ask_for_selection(releases, "release") do |r|
370
+ r.name + if r.released?
371
+ " (released #{r.release_time.pretty_date})"
372
+ else
373
+ " (unreleased)"
374
+ end
375
+ end
376
+ end
377
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
378
+ issue.assign_to_release release, config.user, comment
379
+ puts "Assigned #{issue.name} to #{release.name}."
380
+ end
381
+
382
+ operation :set_component, "Set an issue's component", :issue, :maybe_component
383
+ def set_component project, config, issue, maybe_component
384
+ puts "Changing the component of issue #{issue.name}: #{issue.title}."
385
+
386
+ if project.components.size == 1
387
+ raise Error, "this project does not use multiple components"
388
+ end
389
+
390
+ if maybe_component && maybe_component.name == issue.component
391
+ raise Error, "issue #{issue.name} already assigned to component #{issue.component}"
392
+ end
393
+
394
+ component = maybe_component || begin
395
+ components = project.components
396
+ components -= [components.find { |r| r.name == issue.component }] if issue.component
397
+ ask_for_selection(components, "component") { |r| r.name }
398
+ end
399
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
400
+ issue.assign_to_component component, config.user, comment
401
+ oldname = issue.name
402
+ project.assign_issue_names!
403
+ puts <<EOS
404
+ Issue #{oldname} is now #{issue.name}. Note that the names of other issues may
405
+ have changed as well.
406
+ EOS
407
+ end
408
+
409
+ operation :unassign, "Unassign an issue from any releases", :assigned_issue
410
+ def unassign project, config, issue
411
+ puts "Unassigning issue #{issue.name}: #{issue.title}."
412
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
413
+ issue.unassign config.user, comment
414
+ puts "Unassigned #{issue.name}."
415
+ end
416
+
417
+ operation :comment, "Comment on an issue", :issue
418
+ def comment project, config, issue
419
+ puts "Commenting on issue #{issue.name}: #{issue.title}."
420
+ comment = ask_multiline "Comments"
421
+ if comment.blank?
422
+ puts "Empty comment, aborted."
423
+ else
424
+ issue.log "commented", config.user, comment
425
+ puts "Comments recorded for #{issue.name}."
426
+ end
427
+ end
428
+
429
+ operation :releases, "Show releases"
430
+ def releases project, config
431
+ a, b = project.releases.partition { |r| r.released? }
432
+ (b + a.sort_by { |r| r.release_time }).each do |r|
433
+ status = r.released? ? "released #{r.release_time.pretty_date}" : r.status
434
+ puts "#{r.name} (#{status})"
435
+ end
436
+ end
437
+
438
+ operation :release, "Release a release", :unreleased_release
439
+ def release project, config, release
440
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
441
+ release.release! project, config.user, comment
442
+ puts "Release #{release.name} released!"
443
+ end
444
+
445
+ operation :changelog, "Generate a changelog for a release", :release
446
+ def changelog project, config, r
447
+ puts "== #{r.name} / #{r.released? ? r.release_time.pretty_date : 'unreleased'}"
448
+ project.group_issues(project.issues_for_release(r)).each do |type, issues|
449
+ issues.select { |i| i.closed? }.each do |i|
450
+ if type == :bugfix
451
+ puts "* #{type}: #{i.title}"
452
+ else
453
+ puts "* #{i.title}"
454
+ end
455
+ end
456
+ end
457
+ end
458
+
459
+ operation :html, "Generate html status pages", :maybe_dir
460
+ def html project, config, dir
461
+ dir ||= "html"
462
+ HtmlView.new(project, config, dir).render_all
463
+ end
464
+
465
+ operation :validate, "Validate project status"
466
+ def validate project, config
467
+ ## a no-op
468
+ end
469
+
470
+ operation :grep, "Show issues matching a string or regular expression", :string
471
+ def grep project, config, match
472
+ re = /#{match}/
473
+ issues = project.issues.select do |i|
474
+ i.title =~ re || i.desc =~ re ||
475
+ i.log_events.map { |time, who, what, comments| comments }.join(" ") =~ re
476
+ end
477
+ puts(todo_list_for(issues) || "No matching issues.")
478
+ end
479
+
480
+ operation :log, "Show recent activity"
481
+ def log project, config
482
+ project.issues.map { |i| i.log_events.map { |e| [e, i] } }.
483
+ flatten_one_level.sort_by { |e| e.first.first }.reverse.
484
+ each do |(date, author, what, comment), i|
485
+ puts <<EOS
486
+ date : #{date.localtime} (#{date.ago} ago)
487
+ author: #{author}
488
+ issue: [#{i.name}] #{i.title}
489
+
490
+ #{what}
491
+ #{comment.gsub(/^/, " > ") unless comment =~ /^\A\s*\z/}
492
+ EOS
493
+ puts unless comment.blank?
494
+ end
495
+ end
496
+
497
+ operation :shortlog, "Show recent activity (short form)"
498
+ def shortlog project, config
499
+ project.issues.map { |i| i.log_events.map { |e| [e, i] } }.
500
+ flatten_one_level.sort_by { |e| e.first.first }.reverse.
501
+ each do |(date, author, what, comment), i|
502
+ shortauthor = if author =~ /<(.*?)@/
503
+ $1
504
+ else
505
+ author
506
+ end[0..15]
507
+ printf "%13s|%13s|%13s|%s\n", date.ago, i.name, shortauthor,
508
+ what
509
+ end
510
+ end
511
+
512
+ operation :archive, "Archive a release", :release, :maybe_dir
513
+ def archive project, config, release, dir
514
+ dir ||= "ditz-archive-#{release.name}"
515
+ FileUtils.mkdir dir
516
+ FileUtils.cp project.pathname, dir
517
+ project.issues_for_release(release).each do |i|
518
+ FileUtils.cp i.pathname, dir
519
+ project.drop_issue i
520
+ end
521
+ puts "Archived to #{dir}."
522
+ end
523
+
524
+ operation :edit, "Edit an issue", :issue
525
+ def edit project, config, issue
526
+ data = { :title => issue.title, :description => issue.desc,
527
+ :reporter => issue.reporter }
528
+
529
+ fn = run_editor { |f| f.puts data.to_yaml }
530
+
531
+ unless fn
532
+ puts "Aborted."
533
+ return
534
+ end
535
+
536
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
537
+
538
+ begin
539
+ edits = YAML.load_file fn
540
+ if issue.change edits, config.user, comment
541
+ puts "Change recorded."
542
+ else
543
+ puts "No changes."
544
+ end
545
+ end
546
+ end
547
+ end
548
+
549
+ end
@@ -0,0 +1,114 @@
1
+ require 'time'
2
+
3
+ module Ditz
4
+ class Issue
5
+ field :git_branch, :ask => false
6
+
7
+ def git_commits
8
+ return @git_commits if @git_commits
9
+
10
+ filters = ["--grep=\"Ditz-issue: #{id}\""]
11
+ filters << "master..#{git_branch}" if git_branch
12
+
13
+ output = filters.map do |f|
14
+ `git log --pretty=format:\"%aD\t%an <%ae>\t%h\t%s\" #{f}`
15
+ end.join
16
+
17
+ @git_commits = output.split(/\n/).map { |l| l.split("\t") }.
18
+ map { |date, email, hash, msg| [Time.parse(date).utc, email, hash, msg] }
19
+ end
20
+ end
21
+
22
+ class Config
23
+ field :git_commit_url_prefix, :prompt => "URL prefix to link git commits to", :default => ""
24
+ field :git_branch_url_prefix, :prompt => "URL prefix to link git branches to", :default => ""
25
+ end
26
+
27
+ class ScreenView
28
+ add_to_view :issue_summary do |issue, config|
29
+ " Git branch: #{issue.git_branch || 'none'}\n"
30
+ end
31
+
32
+ add_to_view :issue_details do |issue, config|
33
+ commits = issue.git_commits[0...5]
34
+ next if commits.empty?
35
+ "Recent commits:\n" + commits.map do |date, email, hash, msg|
36
+ "- #{msg} [#{hash}] (#{email.shortened_email}, #{date.ago} ago)\n"
37
+ end.join + "\n"
38
+ end
39
+ end
40
+
41
+ class HtmlView
42
+ add_to_view :issue_summary do |issue, config|
43
+ next unless issue.git_branch
44
+ [<<EOS, { :issue => issue, :url_prefix => config.git_branch_url_prefix }]
45
+ <tr>
46
+ <td class='attrname'>Git branch:</td>
47
+ <td class='attrval'><%= url_prefix && !url_prefix.blank? ? link_to([url_prefix, issue.git_branch].join, issue.git_branch) : h(issue.git_branch) %></td>
48
+ </tr>
49
+ EOS
50
+ end
51
+
52
+ add_to_view :issue_details do |issue, config|
53
+ commits = issue.git_commits
54
+ next if commits.empty?
55
+
56
+ [<<EOS, { :commits => commits, :url_prefix => config.git_commit_url_prefix }]
57
+ <h2>Commits for this issue</h2>
58
+ <table>
59
+ <% commits.each_with_index do |(time, who, hash, msg), i| %>
60
+ <% if i % 2 == 0 %>
61
+ <tr class="logentryeven">
62
+ <% else %>
63
+ <tr class="logentryodd">
64
+ <% end %>
65
+ <td class="logtime"><%=t time %></td>
66
+ <td class="logwho"><%=obscured_email who %></td>
67
+ <td class="logwhat"><%=h msg %> [<%= url_prefix && !url_prefix.blank? ? link_to([url_prefix, hash].join, hash) : hash %>]</td>
68
+ </tr>
69
+ <% end %>
70
+ </table>
71
+ EOS
72
+ end
73
+ end
74
+
75
+ class Operator
76
+ operation :set_branch, "Set the git feature branch of an issue", :issue, :maybe_string
77
+ def set_branch project, config, issue, maybe_string
78
+ puts "Issue #{issue.name} currently " + if issue.git_branch
79
+ "assigned to git branch #{issue.git_branch.inspect}."
80
+ else
81
+ "not assigned to any git branch."
82
+ end
83
+
84
+ branch = maybe_string || ask("Git feature branch name:")
85
+ return unless branch
86
+
87
+ if branch == issue.git_branch
88
+ raise Error, "issue #{issue.name} already assigned to branch #{issue.git_branch.inspect}"
89
+ end
90
+
91
+ puts "Assigning to branch #{branch.inspect}."
92
+ issue.git_branch = branch
93
+ end
94
+
95
+ operation :commit, "Runs git-commit and auto-fills the issue name in the commit message", :issue do
96
+ opt :all, "commit all changed files", :short => "-a", :default => false
97
+ opt :verbose, "show diff between HEAD and what would be committed", \
98
+ :short => "-v", :default => false
99
+ opt :message, "Use the given <s> as the commit message.", \
100
+ :short => "-m", :type => :string
101
+ end
102
+ def commit project, config, opts, issue
103
+ verbose_flag = opts[:verbose] ? "--verbose" : ""
104
+ all_flag = opts[:all] ? "--all" : ""
105
+ ditz_header = "Ditz-issue: #{issue.id}"
106
+ message = opts[:message] ? "#{opts[:message]}\n\n#{ditz_header}" : \
107
+ "#{ditz_header}"
108
+ edit_flag = opts[:message] ? "" : "--edit"
109
+ message_flag = %{--message="#{message}"}
110
+ exec "git commit #{all_flag} #{verbose_flag} #{message_flag} #{edit_flag}"
111
+ end
112
+ end
113
+
114
+ end