ursm-ditz 0.4

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