mislav-ticgit 0.3.6
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/ti +10 -0
- data/bin/ticgitweb +403 -0
- data/lib/ticgit.rb +28 -0
- data/lib/ticgit/base.rb +308 -0
- data/lib/ticgit/cli.rb +280 -0
- data/lib/ticgit/comment.rb +17 -0
- data/lib/ticgit/ticket.rb +224 -0
- metadata +78 -0
data/bin/ti
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# This is a command line client that does all the actual tic commands
|
4
|
+
#
|
5
|
+
# authors: Scott Chacon <schacon@gmail.com>
|
6
|
+
# Mislav Marohnić <mislav.marohnic@gmail.com>
|
7
|
+
|
8
|
+
require File.dirname(__FILE__) + "/../lib/ticgit"
|
9
|
+
|
10
|
+
TicGit::CLI::start()
|
data/bin/ticgitweb
ADDED
@@ -0,0 +1,403 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# starts a sinatra based web server that provides an interface to
|
4
|
+
# your ticgit tickets
|
5
|
+
#
|
6
|
+
# some of the sinatra code borrowed from sr's git-wiki
|
7
|
+
#
|
8
|
+
# author: Flurin Egger
|
9
|
+
# original author : Scott Chacon (schacon@gmail.com)
|
10
|
+
#
|
11
|
+
|
12
|
+
%w(rubygems sinatra git ticgit haml sass).each do |dependency|
|
13
|
+
begin
|
14
|
+
require dependency
|
15
|
+
rescue LoadError => e
|
16
|
+
puts "You need to install #{dependency} before we can proceed"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# !! TODO : if ARGV[1] is a path to a git repo, use that
|
21
|
+
# otherwise, look in ~/.ticgit
|
22
|
+
|
23
|
+
$ticgit = TicGit.open('.')
|
24
|
+
|
25
|
+
# Always load saved searches. (used in navigation)
|
26
|
+
before do
|
27
|
+
@saved = $ticgit.config['list_options'].keys rescue []
|
28
|
+
end
|
29
|
+
|
30
|
+
# Stylesheets
|
31
|
+
get('/_stylesheet.css') do
|
32
|
+
header("Content-Type" => "text/css;charset=utf-8")
|
33
|
+
sass :stylesheet_all
|
34
|
+
end
|
35
|
+
get('/_print.css') do
|
36
|
+
header("Content-Type" => "text/css;charset=utf-8")
|
37
|
+
sass :stylesheet_print
|
38
|
+
end
|
39
|
+
|
40
|
+
# ticket list view
|
41
|
+
get '/' do
|
42
|
+
@tickets = $ticgit.ticket_list(:order => 'date.desc')
|
43
|
+
haml :list, :locals => {:title => "All tickets"}
|
44
|
+
end
|
45
|
+
|
46
|
+
get '/fs/:state' do
|
47
|
+
@tickets = $ticgit.ticket_list(:state => params[:state], :order => 'date.desc')
|
48
|
+
haml :list, :locals => {:title => "#{params[:state].to_s.capitalize} tickets"}
|
49
|
+
end
|
50
|
+
|
51
|
+
get '/tag/:tag' do
|
52
|
+
@tickets = $ticgit.ticket_list(:tag => params[:tag], :order => 'date.desc')
|
53
|
+
haml :list, :locals => {:title => "All tickets with tag '#{params[:tag]}'"}
|
54
|
+
end
|
55
|
+
|
56
|
+
get '/sv/:saved_view' do
|
57
|
+
@tickets = $ticgit.ticket_list(:saved => params[:saved_view])
|
58
|
+
haml :list, :locals => {:title => "All tickets in view '#{params[:saved_view]}'"}
|
59
|
+
end
|
60
|
+
|
61
|
+
# ticket single view
|
62
|
+
get '/ticket/:ticket' do
|
63
|
+
@ticket = $ticgit.ticket_show(params[:ticket])
|
64
|
+
haml :show, :locals => {:title => "Ticket #{@ticket.ticket_id}"}
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
# add ticket
|
69
|
+
get '/t/new' do
|
70
|
+
haml :new, :locals => {:title => "Create new ticket"}
|
71
|
+
end
|
72
|
+
|
73
|
+
# add ticket finalize
|
74
|
+
post '/t/new' do
|
75
|
+
title = params[:title].to_s.strip
|
76
|
+
if title.size > 1
|
77
|
+
tags = params[:tags].split(',').map { |t| t.strip } rescue nil
|
78
|
+
t = $ticgit.ticket_new(title, {:description => params[:description].strip, :tags => tags})
|
79
|
+
redirect '/ticket/' + t.ticket_id.to_s
|
80
|
+
else
|
81
|
+
redirect '/t/new'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
# add comment
|
87
|
+
post '/a/add_comment/:ticket' do
|
88
|
+
t = $ticgit.ticket_comment(params[:comment], params[:ticket])
|
89
|
+
redirect '/ticket/' + params[:ticket]
|
90
|
+
end
|
91
|
+
|
92
|
+
# add tag
|
93
|
+
post '/a/add_tags/:ticket' do
|
94
|
+
t = $ticgit.ticket_tag(params[:tags], params[:ticket])
|
95
|
+
redirect '/ticket/' + params[:ticket]
|
96
|
+
end
|
97
|
+
|
98
|
+
# change ticket state
|
99
|
+
get '/a/change_state/:ticket/:state' do
|
100
|
+
$ticgit.ticket_change(params[:state], params[:ticket])
|
101
|
+
redirect '/ticket/' + params[:ticket]
|
102
|
+
end
|
103
|
+
|
104
|
+
use_in_file_templates!
|
105
|
+
|
106
|
+
__END__
|
107
|
+
## layout
|
108
|
+
:plain
|
109
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
110
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
111
|
+
%html{:xmlns => "http://www.w3.org/1999/xhtml", "xml:lang" => "en", :lang=>"en"}
|
112
|
+
%head
|
113
|
+
%title= title
|
114
|
+
%link{:rel => 'stylesheet', :href => '/_stylesheet.css', :type => 'text/css', :media => 'all'}
|
115
|
+
%link{:rel => 'stylesheet', :href => '/_print.css', :type => 'text/css', :media => 'print'}
|
116
|
+
%meta{'http-equiv' => 'Content-Type', :content => 'text/html; charset=utf-8'}
|
117
|
+
%body
|
118
|
+
#wrapper
|
119
|
+
#action
|
120
|
+
%a{:href => '/t/new'} New Ticket
|
121
|
+
%ul#navigation
|
122
|
+
%li
|
123
|
+
%a{:href => '/'} All
|
124
|
+
%li
|
125
|
+
%a{:href => '/fs/open'} Open
|
126
|
+
%li
|
127
|
+
%a{:href => '/fs/resolved'} Resolved
|
128
|
+
%li
|
129
|
+
%a{:href => '/fs/hold'} Hold
|
130
|
+
%li
|
131
|
+
%a{:href => '/fs/invalid'} Invalid
|
132
|
+
- if @saved && !@saved.empty?
|
133
|
+
%li | Saved:
|
134
|
+
%ul.saved
|
135
|
+
- @saved.each do |s|
|
136
|
+
%li
|
137
|
+
%a{:href => "/sv/\#{s}"}= s
|
138
|
+
|
139
|
+
= yield
|
140
|
+
|
141
|
+
## list
|
142
|
+
%h1= title
|
143
|
+
- if @tickets.empty?
|
144
|
+
%p No tickets found.
|
145
|
+
- else
|
146
|
+
%table.long
|
147
|
+
%thead
|
148
|
+
%tr
|
149
|
+
%th SHA
|
150
|
+
%th.ticket Ticket
|
151
|
+
%th State
|
152
|
+
%th Created at
|
153
|
+
%th Created by
|
154
|
+
%th Tags
|
155
|
+
%tbody
|
156
|
+
- c = 'even'
|
157
|
+
- @tickets.each do |t|
|
158
|
+
%tr{:class => (c == 'even' ? c = 'odd' : c = 'even') }
|
159
|
+
%td
|
160
|
+
%a{:href => "/ticket/#{t.ticket_id}" }
|
161
|
+
%code= t.ticket_id[0,6]
|
162
|
+
%td
|
163
|
+
%strong
|
164
|
+
%a{:href => "/ticket/#{t.ticket_id}" }= t.title
|
165
|
+
.content~ t.description
|
166
|
+
%td{:class => t.state}= t.state
|
167
|
+
%td= t.opened.strftime("%m/%d")
|
168
|
+
%td= t.assigned_name
|
169
|
+
%td
|
170
|
+
- t.tags.each do |tag|
|
171
|
+
%a.tag{:href => "/tag/#{tag}"}= tag
|
172
|
+
|
173
|
+
## show
|
174
|
+
%h1= @ticket.title
|
175
|
+
.content~ @ticket.description
|
176
|
+
%hr/
|
177
|
+
%form{:action => "/a/add_tags/#{@ticket.ticket_id}", :method => 'post'}
|
178
|
+
%table.twocol
|
179
|
+
%tr
|
180
|
+
%th TicId
|
181
|
+
%td
|
182
|
+
%code= @ticket.ticket_id
|
183
|
+
%tr
|
184
|
+
%th Assigned
|
185
|
+
%td= @ticket.assigned
|
186
|
+
%tr
|
187
|
+
%th Opened
|
188
|
+
%td= @ticket.opened
|
189
|
+
%tr
|
190
|
+
%th State
|
191
|
+
%td{:class => @ticket.state}
|
192
|
+
%table{:width => '300'}
|
193
|
+
%tr
|
194
|
+
%td{:width=>'90%'}= @ticket.state
|
195
|
+
- $ticgit.tic_states.select { |s| s != @ticket.state}.each do |st|
|
196
|
+
%td{:class => st}
|
197
|
+
%a{:href => "/a/change_state/#{@ticket.ticket_id}/#{st}"}= st[0,2]
|
198
|
+
%tr
|
199
|
+
%th Tags
|
200
|
+
%td
|
201
|
+
- @ticket.tags.each do |t|
|
202
|
+
%a.tag{:href => "/tag/#{t}"}= t
|
203
|
+
%div.addtag
|
204
|
+
%input{:name => 'tags'}
|
205
|
+
%input{:type => 'submit', :value => 'add tag'}
|
206
|
+
|
207
|
+
%h3 Comments
|
208
|
+
%form{:action => "/a/add_comment/#{@ticket.ticket_id}", :method => 'post'}
|
209
|
+
%div
|
210
|
+
%textarea{:name => 'comment', :cols => 50}
|
211
|
+
%br
|
212
|
+
%input{:type => 'submit', :value => 'add comment'}
|
213
|
+
|
214
|
+
%div.comments
|
215
|
+
- @ticket.comments.reverse.each do |t|
|
216
|
+
%div.comment
|
217
|
+
%span.head
|
218
|
+
Added
|
219
|
+
= t.added.strftime("%m/%d %H:%M")
|
220
|
+
by
|
221
|
+
= t.user
|
222
|
+
%div.comment-text
|
223
|
+
= t.comment
|
224
|
+
%br
|
225
|
+
|
226
|
+
## new
|
227
|
+
%h1 Create a New Ticket
|
228
|
+
%form{:action => '/t/new', :method => 'post'}
|
229
|
+
%table
|
230
|
+
%tr
|
231
|
+
%th Title
|
232
|
+
%td
|
233
|
+
%input{:type => 'text', :name => 'title', :size => 30}
|
234
|
+
%tr
|
235
|
+
%th Description
|
236
|
+
%td
|
237
|
+
%textarea{:rows => 15, :cols => 30, :name => 'description'}
|
238
|
+
%tr
|
239
|
+
%th Tags
|
240
|
+
%td
|
241
|
+
%input{:name => 'tags', :size => 30}
|
242
|
+
%small (comma delimited)
|
243
|
+
%tr
|
244
|
+
%td
|
245
|
+
%td
|
246
|
+
%input{:type => 'submit', :value => 'Create Ticket'}
|
247
|
+
|
248
|
+
## stylesheet_all
|
249
|
+
body
|
250
|
+
:font
|
251
|
+
family: Verdana, Arial, "Bitstream Vera Sans", Helvetica, sans-serif
|
252
|
+
color: black
|
253
|
+
size: 62.5%
|
254
|
+
line-height: 1.2
|
255
|
+
background-color: white
|
256
|
+
margin: 2em
|
257
|
+
|
258
|
+
#wrapper
|
259
|
+
font-size: 1.2em
|
260
|
+
width: 90%
|
261
|
+
margin: 0 auto
|
262
|
+
|
263
|
+
// Autoclearing
|
264
|
+
#navigation:after
|
265
|
+
content: "."
|
266
|
+
visibility: hidden
|
267
|
+
clear: both
|
268
|
+
display: block
|
269
|
+
height: 0px
|
270
|
+
|
271
|
+
// IE autoclearing
|
272
|
+
#navigation
|
273
|
+
zoom: 1
|
274
|
+
|
275
|
+
#navigation
|
276
|
+
li
|
277
|
+
float: left
|
278
|
+
margin-right: 0.5em
|
279
|
+
a
|
280
|
+
background-color: #e0e0e0
|
281
|
+
color: black
|
282
|
+
text-decoration: none
|
283
|
+
padding: 2px
|
284
|
+
margin: 0
|
285
|
+
list-style: none
|
286
|
+
padding: 5px
|
287
|
+
border-bottom: 1px black solid
|
288
|
+
|
289
|
+
#action
|
290
|
+
text-align: right
|
291
|
+
float: right
|
292
|
+
a
|
293
|
+
background: #005
|
294
|
+
padding: 5px 10px
|
295
|
+
font-weight: bold
|
296
|
+
color: #fff
|
297
|
+
float: left
|
298
|
+
|
299
|
+
.addtag
|
300
|
+
padding: 5px 0
|
301
|
+
|
302
|
+
h1
|
303
|
+
display: block
|
304
|
+
padding-bottom: 5px
|
305
|
+
|
306
|
+
a
|
307
|
+
color: black
|
308
|
+
a.exists
|
309
|
+
font-weight: bold
|
310
|
+
a.unknown
|
311
|
+
font-style: italic
|
312
|
+
|
313
|
+
a.tag
|
314
|
+
padding: 2px 5px
|
315
|
+
background: #888
|
316
|
+
color: #fff
|
317
|
+
font-weight: normal
|
318
|
+
font-size: 80%
|
319
|
+
float: left
|
320
|
+
margin: 1px 2px
|
321
|
+
text-decoration: none
|
322
|
+
|
323
|
+
.comments
|
324
|
+
margin: 10px 20px
|
325
|
+
.comment
|
326
|
+
.head
|
327
|
+
background: #eee
|
328
|
+
padding: 4px
|
329
|
+
.comment-text
|
330
|
+
padding: 10px
|
331
|
+
color: #333
|
332
|
+
|
333
|
+
table.long
|
334
|
+
width: 100%
|
335
|
+
|
336
|
+
table.twocol
|
337
|
+
background: #f2f2f2
|
338
|
+
th
|
339
|
+
width: 30%
|
340
|
+
|
341
|
+
table
|
342
|
+
font-size: 100%
|
343
|
+
border-collapse: collapse
|
344
|
+
td,th
|
345
|
+
vertical-align: top
|
346
|
+
tr.even
|
347
|
+
td
|
348
|
+
background: #eee
|
349
|
+
tr.odd
|
350
|
+
td
|
351
|
+
background: #fff
|
352
|
+
|
353
|
+
table
|
354
|
+
tr
|
355
|
+
td,th
|
356
|
+
padding: 3px 5px
|
357
|
+
border-bottom: 1px solid #fff
|
358
|
+
th
|
359
|
+
text-align: left
|
360
|
+
vertical-align: top
|
361
|
+
th.ticket
|
362
|
+
width: 50%
|
363
|
+
td.open
|
364
|
+
background: #ada
|
365
|
+
td.resolved
|
366
|
+
background: #abd
|
367
|
+
td.hold
|
368
|
+
background: #dda
|
369
|
+
td.invalid
|
370
|
+
background: #aaa
|
371
|
+
|
372
|
+
strong a
|
373
|
+
text-decoration: none
|
374
|
+
|
375
|
+
table
|
376
|
+
thead
|
377
|
+
tr
|
378
|
+
td,th
|
379
|
+
border-bottom: 1px solid #000
|
380
|
+
|
381
|
+
.submit
|
382
|
+
font-size: large
|
383
|
+
font-weight: bold
|
384
|
+
|
385
|
+
.page_title
|
386
|
+
font-size: xx-large
|
387
|
+
|
388
|
+
.edit_link
|
389
|
+
color: black
|
390
|
+
font-size: 14px
|
391
|
+
font-weight: bold
|
392
|
+
background-color: #e0e0e0
|
393
|
+
font-variant: small-caps
|
394
|
+
text-decoration: none
|
395
|
+
|
396
|
+
## stylesheet_print
|
397
|
+
#navigation, #action
|
398
|
+
display: none
|
399
|
+
|
400
|
+
table
|
401
|
+
tr.odd, tr.even
|
402
|
+
td
|
403
|
+
border-bottom: 1px solid #ddd
|
data/lib/ticgit.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Add the directory containing this file to the start of the load path if it
|
2
|
+
# isn't there already.
|
3
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
4
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
# requires git >= 1.0.5
|
8
|
+
require 'git'
|
9
|
+
require 'ticgit/base'
|
10
|
+
require 'ticgit/ticket'
|
11
|
+
require 'ticgit/comment'
|
12
|
+
|
13
|
+
require 'ticgit/cli'
|
14
|
+
|
15
|
+
# TicGit Library
|
16
|
+
#
|
17
|
+
# This library implements a git based ticketing system in a git repo
|
18
|
+
#
|
19
|
+
# Author:: Scott Chacon (mailto:schacon@gmail.com)
|
20
|
+
# License:: MIT License
|
21
|
+
#
|
22
|
+
module TicGit
|
23
|
+
# options
|
24
|
+
# :logger => Logger.new(STDOUT)
|
25
|
+
def self.open(git_dir, options = {})
|
26
|
+
Base.new(git_dir, options)
|
27
|
+
end
|
28
|
+
end
|
data/lib/ticgit/base.rb
ADDED
@@ -0,0 +1,308 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module TicGit
|
6
|
+
class NoRepoFound < StandardError;end
|
7
|
+
class Base
|
8
|
+
|
9
|
+
attr_reader :git, :logger
|
10
|
+
attr_reader :tic_working, :tic_index
|
11
|
+
attr_reader :tickets, :last_tickets, :current_ticket # saved in state
|
12
|
+
attr_reader :config
|
13
|
+
attr_reader :state, :config_file
|
14
|
+
|
15
|
+
def initialize(git_dir, opts = {})
|
16
|
+
@git = Git.open(find_repo(git_dir))
|
17
|
+
@logger = opts[:logger] || Logger.new(STDOUT)
|
18
|
+
|
19
|
+
proj = Ticket.clean_string(@git.dir.path)
|
20
|
+
|
21
|
+
@tic_dir = opts[:tic_dir] || '~/.ticgit'
|
22
|
+
@tic_working = opts[:working_directory] || File.expand_path(File.join(@tic_dir, proj, 'working'))
|
23
|
+
@tic_index = opts[:index_file] || File.expand_path(File.join(@tic_dir, proj, 'index'))
|
24
|
+
|
25
|
+
# load config file
|
26
|
+
@config_file = File.expand_path(File.join(@tic_dir, proj, 'config.yml'))
|
27
|
+
if File.exists?(config_file)
|
28
|
+
@config = YAML.load(File.read(config_file))
|
29
|
+
else
|
30
|
+
@config = {}
|
31
|
+
end
|
32
|
+
|
33
|
+
@state = File.expand_path(File.join(@tic_dir, proj, 'state'))
|
34
|
+
|
35
|
+
if File.exists?(@state)
|
36
|
+
load_state
|
37
|
+
else
|
38
|
+
reset_ticgit
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_repo(dir)
|
43
|
+
full = File.expand_path(dir)
|
44
|
+
ENV["GIT_WORKING_DIR"] || loop do
|
45
|
+
return full if File.directory?(File.join(full, ".git"))
|
46
|
+
raise NoRepoFound if full == full=File.dirname(full)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def save_state
|
51
|
+
# marshal dump the internals
|
52
|
+
File.open(@state, 'w') { |f| Marshal.dump([@tickets, @last_tickets, @current_ticket], f) } rescue nil
|
53
|
+
# save config file
|
54
|
+
File.open(@config_file, 'w') { |f| f.write(config.to_yaml) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def load_state
|
58
|
+
# read in the internals
|
59
|
+
if(File.exists?(@state))
|
60
|
+
@tickets, @last_tickets, @current_ticket = File.open(@state) { |f| Marshal.load(f) } rescue nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# returns new Ticket
|
65
|
+
def ticket_new(title, options = {})
|
66
|
+
t = TicGit::Ticket.create(self, title, options)
|
67
|
+
reset_ticgit
|
68
|
+
TicGit::Ticket.open(self, t.ticket_name, @tickets[t.ticket_name])
|
69
|
+
end
|
70
|
+
|
71
|
+
def reset_ticgit
|
72
|
+
load_tickets
|
73
|
+
save_state
|
74
|
+
end
|
75
|
+
|
76
|
+
# returns new Ticket
|
77
|
+
def ticket_comment(comment, ticket_id = nil)
|
78
|
+
if t = ticket_revparse(ticket_id)
|
79
|
+
ticket = TicGit::Ticket.open(self, t, @tickets[t])
|
80
|
+
ticket.add_comment(comment)
|
81
|
+
reset_ticgit
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# returns array of Tickets
|
86
|
+
def ticket_list(options = {})
|
87
|
+
ts = []
|
88
|
+
@last_tickets = []
|
89
|
+
@config['list_options'] ||= {}
|
90
|
+
|
91
|
+
@tickets.to_a.each do |name, t|
|
92
|
+
ts << TicGit::Ticket.open(self, name, t)
|
93
|
+
end
|
94
|
+
|
95
|
+
if name = options[:saved]
|
96
|
+
if c = config['list_options'][name]
|
97
|
+
options = c.merge(options)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
if options[:list]
|
102
|
+
# TODO : this is a hack and i need to fix it
|
103
|
+
config['list_options'].each do |name, opts|
|
104
|
+
puts name + "\t" + opts.inspect
|
105
|
+
end
|
106
|
+
return false
|
107
|
+
end
|
108
|
+
|
109
|
+
# SORTING
|
110
|
+
if field = options[:order]
|
111
|
+
field, type = field.split('.')
|
112
|
+
field = "opened" if field == "date" || field.nil?
|
113
|
+
ts = sort_list_by_keys(ts,[field,:ticket_id])
|
114
|
+
ts = ts.reverse if type == 'desc'
|
115
|
+
else
|
116
|
+
# default list
|
117
|
+
ts = ts.sort { |a, b| a.opened <=> b.opened }
|
118
|
+
end
|
119
|
+
|
120
|
+
if options.size == 0
|
121
|
+
# default list
|
122
|
+
options[:state] = 'open'
|
123
|
+
end
|
124
|
+
|
125
|
+
# :tag, :state, :assigned
|
126
|
+
if t = options[:tag]
|
127
|
+
ts = ts.select { |tag| tag.tags.include?(t) }
|
128
|
+
end
|
129
|
+
if s = options[:state]
|
130
|
+
ts = ts.select { |tag| tag.state =~ /#{s}/ }
|
131
|
+
end
|
132
|
+
if a = options[:assigned]
|
133
|
+
ts = ts.select { |tag| tag.assigned =~ /#{a}/ }
|
134
|
+
end
|
135
|
+
|
136
|
+
if save = options[:save]
|
137
|
+
options.delete(:save)
|
138
|
+
@config['list_options'][save] = options
|
139
|
+
end
|
140
|
+
|
141
|
+
@last_tickets = ts.map { |t| t.ticket_name }
|
142
|
+
# :save
|
143
|
+
|
144
|
+
save_state
|
145
|
+
ts
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
def sort_list_by_keys(list,keys)
|
150
|
+
return list if keys.find{|k| !list.first.respond_to?(k) }
|
151
|
+
list.sort do |a,b|
|
152
|
+
value,i = 0,0
|
153
|
+
while (keys.size > i && value == 0) do
|
154
|
+
value = a.send(keys[i]) <=> b.send(keys[i])
|
155
|
+
i += 1
|
156
|
+
end
|
157
|
+
value
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# returns single Ticket
|
162
|
+
def ticket_show(ticket_id = nil)
|
163
|
+
# ticket_id can be index of last_tickets, partial sha or nil => last ticket
|
164
|
+
if t = ticket_revparse(ticket_id)
|
165
|
+
return TicGit::Ticket.open(self, t, @tickets[t])
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# returns recent ticgit activity
|
170
|
+
# uses the git logs for this
|
171
|
+
def ticket_recent(ticket_id = nil)
|
172
|
+
if ticket_id
|
173
|
+
t = ticket_revparse(ticket_id)
|
174
|
+
return git.log.object('ticgit').path(t)
|
175
|
+
else
|
176
|
+
return git.log.object('ticgit')
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def ticket_revparse(ticket_id)
|
181
|
+
if ticket_id
|
182
|
+
if /^[0-9]*$/ =~ ticket_id
|
183
|
+
if t = @last_tickets[ticket_id.to_i - 1]
|
184
|
+
return t
|
185
|
+
end
|
186
|
+
else
|
187
|
+
# partial or full sha
|
188
|
+
if ch = @tickets.select { |name, t| t['files'].assoc('TICKET_ID')[1] =~ /^#{ticket_id}/ }
|
189
|
+
return ch.first[0]
|
190
|
+
end
|
191
|
+
end
|
192
|
+
elsif(@current_ticket)
|
193
|
+
return @current_ticket
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def ticket_tag(tag, ticket_id = nil, options = {})
|
198
|
+
if t = ticket_revparse(ticket_id)
|
199
|
+
ticket = TicGit::Ticket.open(self, t, @tickets[t])
|
200
|
+
if options[:remove]
|
201
|
+
ticket.remove_tag(tag)
|
202
|
+
else
|
203
|
+
ticket.add_tag(tag)
|
204
|
+
end
|
205
|
+
reset_ticgit
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def ticket_change(new_state, ticket_id = nil)
|
210
|
+
if t = ticket_revparse(ticket_id)
|
211
|
+
if tic_states.include?(new_state)
|
212
|
+
ticket = TicGit::Ticket.open(self, t, @tickets[t])
|
213
|
+
ticket.change_state(new_state)
|
214
|
+
reset_ticgit
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def ticket_assign(new_assigned = nil, ticket_id = nil)
|
220
|
+
if t = ticket_revparse(ticket_id)
|
221
|
+
ticket = TicGit::Ticket.open(self, t, @tickets[t])
|
222
|
+
ticket.change_assigned(new_assigned)
|
223
|
+
reset_ticgit
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def ticket_checkout(ticket_id)
|
228
|
+
if t = ticket_revparse(ticket_id)
|
229
|
+
ticket = TicGit::Ticket.open(self, t, @tickets[t])
|
230
|
+
@current_ticket = ticket.ticket_name
|
231
|
+
save_state
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def comment_add(ticket_id, comment, options = {})
|
236
|
+
end
|
237
|
+
|
238
|
+
def comment_list(ticket_id)
|
239
|
+
end
|
240
|
+
|
241
|
+
def tic_states
|
242
|
+
['open', 'resolved', 'invalid', 'hold']
|
243
|
+
end
|
244
|
+
|
245
|
+
def load_tickets
|
246
|
+
@tickets = {}
|
247
|
+
|
248
|
+
bs = git.lib.branches_all.map { |b| b[0] }
|
249
|
+
init_ticgit_branch(bs.include?('ticgit')) if !(bs.include?('ticgit') && File.directory?(@tic_working))
|
250
|
+
|
251
|
+
tree = git.lib.full_tree('ticgit')
|
252
|
+
tree.each do |t|
|
253
|
+
data, file = t.split("\t")
|
254
|
+
mode, type, sha = data.split(" ")
|
255
|
+
tic = file.split('/')
|
256
|
+
if tic.size == 2 # directory depth
|
257
|
+
ticket, info = tic
|
258
|
+
@tickets[ticket] ||= { 'files' => [] }
|
259
|
+
@tickets[ticket]['files'] << [info, sha]
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def init_ticgit_branch(ticgit_branch = false)
|
265
|
+
@logger.info 'creating ticgit repo branch'
|
266
|
+
|
267
|
+
in_branch(ticgit_branch) do
|
268
|
+
new_file('.hold', 'hold')
|
269
|
+
if !ticgit_branch
|
270
|
+
git.add
|
271
|
+
git.commit('creating the ticgit branch')
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
# temporarlily switches to ticgit branch for tic work
|
277
|
+
def in_branch(branch_exists = true)
|
278
|
+
needs_checkout = false
|
279
|
+
if !File.directory?(@tic_working)
|
280
|
+
FileUtils.mkdir_p(@tic_working)
|
281
|
+
needs_checkout = true
|
282
|
+
end
|
283
|
+
if !File.exists?('.hold')
|
284
|
+
needs_checkout = true
|
285
|
+
end
|
286
|
+
|
287
|
+
old_current = git.lib.branch_current
|
288
|
+
begin
|
289
|
+
git.lib.change_head_branch('ticgit')
|
290
|
+
git.with_index(@tic_index) do
|
291
|
+
git.with_working(@tic_working) do |wd|
|
292
|
+
git.lib.checkout('ticgit') if needs_checkout && branch_exists
|
293
|
+
yield wd
|
294
|
+
end
|
295
|
+
end
|
296
|
+
ensure
|
297
|
+
git.lib.change_head_branch(old_current)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def new_file(name, contents)
|
302
|
+
File.open(name, 'w') do |f|
|
303
|
+
f.puts contents
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
end
|
308
|
+
end
|
data/lib/ticgit/cli.rb
ADDED
@@ -0,0 +1,280 @@
|
|
1
|
+
require 'ticgit'
|
2
|
+
require 'thor'
|
3
|
+
|
4
|
+
module TicGit
|
5
|
+
class CLI < Thor
|
6
|
+
attr_reader :tic
|
7
|
+
|
8
|
+
def initialize(opts = {}, *args)
|
9
|
+
@tic = TicGit.open('.', :keep_state => true)
|
10
|
+
$stdout.sync = true # so that Net::SSH prompts show up
|
11
|
+
rescue NoRepoFound
|
12
|
+
puts "No repo found"
|
13
|
+
exit
|
14
|
+
end
|
15
|
+
|
16
|
+
# tic milestone
|
17
|
+
# tic milestone migration1 (list tickets)
|
18
|
+
# tic milestone -n migration1 3/4/08 (new milestone)
|
19
|
+
# tic milestone -a {1} (add ticket to milestone)
|
20
|
+
# tic milestone -d migration1 (delete)
|
21
|
+
desc "milestone [<name>]", %(Add a new milestone to this project)
|
22
|
+
method_options :new => :optional, :add => :optional, :delete => :optional
|
23
|
+
def milestone(name = nil)
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "recent [<ticket-id>]", %(Recent ticgit activity)
|
28
|
+
def recent(ticket_id = nil)
|
29
|
+
tic.ticket_recent(ticket_id).each do |commit|
|
30
|
+
puts commit.sha[0, 7] + " " + commit.date.strftime("%m/%d %H:%M") + "\t" + commit.message
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
desc "tag [<ticket-id>] [<tag1,tag2...>]", %(Add or remove ticket tags)
|
35
|
+
method_options %w(--remove -d) => :boolean
|
36
|
+
def tag(*args)
|
37
|
+
puts 'remove' if options[:remove]
|
38
|
+
|
39
|
+
tid = args.size > 1 && args.shift
|
40
|
+
tags = args.first
|
41
|
+
|
42
|
+
if tags
|
43
|
+
tic.ticket_tag(tags, tid, options)
|
44
|
+
else
|
45
|
+
puts 'You need to at least specify one tag to add'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
desc "comment [<ticket-id>]", %(Comment on a ticket)
|
50
|
+
method_options :message => :optional, :file => :optional
|
51
|
+
def comment(ticket_id = nil)
|
52
|
+
if options[:file]
|
53
|
+
raise ArgumentError, "Only 1 of -f/--file and -m/--message can be specified" if options[:message]
|
54
|
+
file = options[:file]
|
55
|
+
raise ArgumentError, "File #{file} doesn't exist" unless File.file?(file)
|
56
|
+
raise ArgumentError, "File #{file} must be <= 2048 bytes" unless File.size(file) <= 2048
|
57
|
+
tic.ticket_comment(File.read(file), ticket_id)
|
58
|
+
elsif m = options[:message]
|
59
|
+
tic.ticket_comment(m, ticket_id)
|
60
|
+
else
|
61
|
+
message, meta = get_editor_message
|
62
|
+
if message
|
63
|
+
tic.ticket_comment(message.join(''), ticket_id)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
desc "checkout <ticket-id>", %(Checkout a ticket)
|
69
|
+
def checkout(ticket_id)
|
70
|
+
tic.ticket_checkout(ticket_id)
|
71
|
+
end
|
72
|
+
|
73
|
+
desc "state [<ticket-id>] <state>", %(Change state of a ticket)
|
74
|
+
def state(id_or_state, state = nil)
|
75
|
+
if state.nil?
|
76
|
+
state = id_or_state
|
77
|
+
ticket_id = nil
|
78
|
+
else
|
79
|
+
ticket_id = id_or_state
|
80
|
+
end
|
81
|
+
|
82
|
+
if valid_state(state)
|
83
|
+
tic.ticket_change(state, ticket_id)
|
84
|
+
else
|
85
|
+
puts 'Invalid State - please choose from : ' + tic.tic_states.join(", ")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Assigns a ticket to someone
|
90
|
+
#
|
91
|
+
# Usage:
|
92
|
+
# ti assign (assign checked out ticket to current user)
|
93
|
+
# ti assign {1} (assign ticket to current user)
|
94
|
+
# ti assign -c {1} (assign ticket to current user and checkout the ticket)
|
95
|
+
# ti assign -u {name} (assign ticket to specified user)
|
96
|
+
desc "assign [<ticket-id>]", %(Assign ticket to user)
|
97
|
+
method_options :user => :optional, :checkout => :optional
|
98
|
+
def assign(ticket_id = nil)
|
99
|
+
tic.ticket_checkout(options[:checkout]) if options[:checkout]
|
100
|
+
tic.ticket_assign(options[:user], ticket_id)
|
101
|
+
end
|
102
|
+
|
103
|
+
# "-o ORDER", "--order ORDER", "Field to order by - one of : assigned,state,date"
|
104
|
+
# "-t TAG", "--tag TAG", "List only tickets with specific tag"
|
105
|
+
# "-s STATE", "--state STATE", "List only tickets in a specific state"
|
106
|
+
# "-a ASSIGNED", "--assigned ASSIGNED", "List only tickets assigned to someone"
|
107
|
+
# "-S SAVENAME", "--saveas SAVENAME", "Save this list as a saved name"
|
108
|
+
# "-l", "--list", "Show the saved queries"
|
109
|
+
desc "list [<saved-query>]", %(Show existing tickets)
|
110
|
+
method_options :order => :optional, :tag => :optional, :state => :optional,
|
111
|
+
:assigned => :optional, :list => :optional, %w(--save-as -S) => :optional
|
112
|
+
|
113
|
+
def list(saved_query = nil)
|
114
|
+
opts = options.dup
|
115
|
+
opts[:saved] = saved_query if saved_query
|
116
|
+
|
117
|
+
if tickets = tic.ticket_list(opts)
|
118
|
+
output_ticket_list(tickets)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
## SHOW TICKETS ##
|
123
|
+
|
124
|
+
desc 'show <ticket-id>', %(Show a single ticket)
|
125
|
+
def show(ticket_id = nil)
|
126
|
+
if t = @tic.ticket_show(ticket_id)
|
127
|
+
ticket_show(t)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
desc 'new', %(Create a new ticket)
|
132
|
+
method_options :title => :optional
|
133
|
+
def new
|
134
|
+
if title = options[:title]
|
135
|
+
ticket_show(@tic.ticket_new(title, options))
|
136
|
+
else
|
137
|
+
# interactive
|
138
|
+
message_file = Tempfile.new('ticgit_message').path
|
139
|
+
File.open(message_file, 'w') do |f|
|
140
|
+
f.puts "\n# ---"
|
141
|
+
f.puts "tags:"
|
142
|
+
f.puts "# The first line will be the title of the ticket,"
|
143
|
+
f.puts "# the rest will be the description. If you would like to add initial tags,"
|
144
|
+
f.puts "# put them on the 'tags:' line, comma delimited"
|
145
|
+
end
|
146
|
+
|
147
|
+
message, meta = get_editor_message(message_file)
|
148
|
+
if message
|
149
|
+
title, description, tags = parse_editor_message(message,meta)
|
150
|
+
if title and !title.empty?
|
151
|
+
ticket_show(@tic.ticket_new(title, :description => description, :tags => tags))
|
152
|
+
else
|
153
|
+
puts "You need to at least enter a title"
|
154
|
+
end
|
155
|
+
else
|
156
|
+
puts "It seems you wrote nothing"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
protected
|
162
|
+
|
163
|
+
def valid_state(state)
|
164
|
+
tic.tic_states.include?(state)
|
165
|
+
end
|
166
|
+
|
167
|
+
def ticket_show(t)
|
168
|
+
days_ago = ((Time.now - t.opened) / (60 * 60 * 24)).round.to_s
|
169
|
+
puts
|
170
|
+
puts just('Title', 10) + ': ' + t.title
|
171
|
+
puts just('TicId', 10) + ': ' + t.ticket_id
|
172
|
+
if t.description
|
173
|
+
desc = t.description.split("\n").map{|l| " "*12 + l.strip}.join("\n").lstrip
|
174
|
+
puts just('Descr.',10) + ': ' + desc
|
175
|
+
end
|
176
|
+
puts
|
177
|
+
puts just('Assigned', 10) + ': ' + t.assigned.to_s
|
178
|
+
puts just('Opened', 10) + ': ' + t.opened.to_s + ' (' + days_ago + ' days)'
|
179
|
+
puts just('State', 10) + ': ' + t.state.upcase
|
180
|
+
if !t.tags.empty?
|
181
|
+
puts just('Tags', 10) + ': ' + t.tags.join(', ')
|
182
|
+
end
|
183
|
+
puts
|
184
|
+
if !t.comments.empty?
|
185
|
+
puts 'Comments (' + t.comments.size.to_s + '):'
|
186
|
+
t.comments.reverse.each do |c|
|
187
|
+
puts ' * Added ' + c.added.strftime("%m/%d %H:%M") + ' by ' + c.user
|
188
|
+
|
189
|
+
wrapped = c.comment.split("\n").collect do |line|
|
190
|
+
line.length > 80 ? line.gsub(/(.{1,80})(\s+|$)/, "\\1\n").strip : line
|
191
|
+
end * "\n"
|
192
|
+
|
193
|
+
wrapped = wrapped.split("\n").map { |line| "\t" + line }
|
194
|
+
if wrapped.size > 6
|
195
|
+
puts wrapped[0, 6].join("\n")
|
196
|
+
puts "\t** more... **"
|
197
|
+
else
|
198
|
+
puts wrapped.join("\n")
|
199
|
+
end
|
200
|
+
puts
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def get_editor_message(message_file = nil)
|
206
|
+
message_file = Tempfile.new('ticgit_message').path if !message_file
|
207
|
+
|
208
|
+
editor = ENV["EDITOR"] || 'vim'
|
209
|
+
system("#{editor} #{message_file}");
|
210
|
+
message = File.read(message_file)
|
211
|
+
|
212
|
+
# We must have some content before the # --- line
|
213
|
+
message, meta = message.split("# ---")
|
214
|
+
if message =~ /[^\s]+/m
|
215
|
+
return [message,meta]
|
216
|
+
else
|
217
|
+
return [false,false]
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def parse_editor_message(message, meta)
|
222
|
+
message.strip!
|
223
|
+
puts message.inspect
|
224
|
+
title, description = message.split(/\r?\n/,2).map { |t| t.strip }
|
225
|
+
tags = []
|
226
|
+
# Strip comments from meta block
|
227
|
+
meta.gsub!(/^\s*#.*$/, "")
|
228
|
+
meta.split("\n").each do |line|
|
229
|
+
if line[0, 5] == 'tags:'
|
230
|
+
tags = line.gsub('tags:', '')
|
231
|
+
tags = tags.split(',').map { |t| t.strip }
|
232
|
+
end
|
233
|
+
end
|
234
|
+
[title, description, tags]
|
235
|
+
end
|
236
|
+
|
237
|
+
def just(value, size, side = 'l')
|
238
|
+
value = value.to_s
|
239
|
+
if value.size > size
|
240
|
+
value = value[0, size]
|
241
|
+
end
|
242
|
+
if side == 'r'
|
243
|
+
return value.rjust(size)
|
244
|
+
else
|
245
|
+
return value.ljust(size)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def output_ticket_list(tickets)
|
250
|
+
counter = 0
|
251
|
+
|
252
|
+
puts
|
253
|
+
puts [' ', just('#', 4, 'r'),
|
254
|
+
just('TicId', 6),
|
255
|
+
just('Title', 25),
|
256
|
+
just('State', 5),
|
257
|
+
just('Date', 5),
|
258
|
+
just('Assgn', 8),
|
259
|
+
just('Tags', 20) ].join(" ")
|
260
|
+
|
261
|
+
a = []
|
262
|
+
80.times { a << '-'}
|
263
|
+
puts a.join('')
|
264
|
+
|
265
|
+
tickets.each do |t|
|
266
|
+
counter += 1
|
267
|
+
tic.current_ticket == t.ticket_name ? add = '*' : add = ' '
|
268
|
+
puts [add, just(counter, 4, 'r'),
|
269
|
+
t.ticket_id[0,6],
|
270
|
+
just(t.title, 25),
|
271
|
+
just(t.state, 5),
|
272
|
+
t.opened.strftime("%m/%d"),
|
273
|
+
just(t.assigned_name, 8),
|
274
|
+
just(t.tags.join(','), 20) ].join(" ")
|
275
|
+
end
|
276
|
+
puts
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
280
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module TicGit
|
2
|
+
class Comment
|
3
|
+
|
4
|
+
attr_reader :base, :user, :added, :comment
|
5
|
+
|
6
|
+
def initialize(base, file_name, sha)
|
7
|
+
@base = base
|
8
|
+
@comment = base.git.gblob(sha).contents rescue nil
|
9
|
+
|
10
|
+
type, date, user = file_name.split('_')
|
11
|
+
|
12
|
+
@added = Time.at(date.to_i)
|
13
|
+
@user = user
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
module TicGit
|
2
|
+
class Ticket
|
3
|
+
|
4
|
+
attr_reader :base, :opts
|
5
|
+
attr_accessor :ticket_id, :ticket_name
|
6
|
+
attr_accessor :title, :description, :state, :milestone, :assigned, :opened
|
7
|
+
attr_accessor :comments, :tags, :attachments # arrays
|
8
|
+
|
9
|
+
def initialize(base, options = {})
|
10
|
+
options[:user_name] ||= base.git.config('user.name')
|
11
|
+
options[:user_email] ||= base.git.config('user.email')
|
12
|
+
|
13
|
+
@base = base
|
14
|
+
@opts = options || {}
|
15
|
+
|
16
|
+
@description = options.delete(:description)
|
17
|
+
@state = 'open' # by default
|
18
|
+
@comments = []
|
19
|
+
@tags = []
|
20
|
+
@attachments = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.create(base, title, options = {})
|
24
|
+
t = Ticket.new(base, options)
|
25
|
+
t.title = title
|
26
|
+
t.ticket_name = self.create_ticket_name(title)
|
27
|
+
t.save_new
|
28
|
+
t
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.open(base, ticket_name, ticket_hash, options = {})
|
32
|
+
tid = nil
|
33
|
+
|
34
|
+
t = Ticket.new(base, options)
|
35
|
+
t.ticket_name = ticket_name
|
36
|
+
|
37
|
+
basic_title, date = self.parse_ticket_name(ticket_name)
|
38
|
+
|
39
|
+
# t.title will be overridden below if the file TICKET_TITLE exists
|
40
|
+
t.title = basic_title
|
41
|
+
t.opened = date
|
42
|
+
|
43
|
+
ticket_hash['files'].each do |fname, value|
|
44
|
+
if fname == 'TICKET_ID'
|
45
|
+
t.ticket_id = value
|
46
|
+
elsif fname == 'TICKET_TITLE'
|
47
|
+
t.title = base.git.gblob(value).contents rescue nil
|
48
|
+
elsif fname == 'TICKET_DESCRIPTION'
|
49
|
+
t.description = base.git.gblob(value).contents rescue nil
|
50
|
+
else
|
51
|
+
# matching
|
52
|
+
data = fname.split('_')
|
53
|
+
if data[0] == 'ASSIGNED'
|
54
|
+
t.assigned = data[1]
|
55
|
+
end
|
56
|
+
if data[0] == 'COMMENT'
|
57
|
+
t.comments << TicGit::Comment.new(base, fname, value)
|
58
|
+
end
|
59
|
+
if data[0] == 'TAG'
|
60
|
+
t.tags << data[1]
|
61
|
+
end
|
62
|
+
if data[0] == 'STATE'
|
63
|
+
t.state = data[1]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
t
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
def self.parse_ticket_name(name)
|
74
|
+
unless name =~ /^([^_]+)_(.+)_([^_]+)$/
|
75
|
+
raise "invalid ticket name #{name.inspect}"
|
76
|
+
end
|
77
|
+
epoch, title, rand = $1, $2.gsub('-', ' '), $3
|
78
|
+
return [title, Time.at(epoch.to_i)]
|
79
|
+
end
|
80
|
+
|
81
|
+
# write this ticket to the git database
|
82
|
+
def save_new
|
83
|
+
base.in_branch do |wd|
|
84
|
+
base.logger.info "saving #{ticket_name}"
|
85
|
+
|
86
|
+
Dir.mkdir(ticket_name)
|
87
|
+
Dir.chdir(ticket_name) do
|
88
|
+
base.new_file('TICKET_ID', ticket_name)
|
89
|
+
base.new_file('ASSIGNED_' + email, email)
|
90
|
+
base.new_file('STATE_' + state, state)
|
91
|
+
base.new_file('TICKET_TITLE',self.title)
|
92
|
+
base.new_file('TICKET_DESCRIPTION',self.description) if self.description
|
93
|
+
|
94
|
+
# add initial comment
|
95
|
+
#COMMENT_080315060503045__schacon_at_gmail
|
96
|
+
base.new_file(comment_name(email), opts[:comment]) if opts[:comment]
|
97
|
+
|
98
|
+
# add initial tags
|
99
|
+
if opts[:tags] && opts[:tags].size > 0
|
100
|
+
opts[:tags] = opts[:tags].map { |t| t.strip }.compact
|
101
|
+
opts[:tags].each do |tag|
|
102
|
+
if tag.size > 0
|
103
|
+
tag_filename = 'TAG_' + Ticket.clean_string(tag)
|
104
|
+
if !File.exists?(tag_filename)
|
105
|
+
base.new_file(tag_filename, tag_filename)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
base.git.add
|
113
|
+
base.git.commit("added ticket #{ticket_name}")
|
114
|
+
end
|
115
|
+
# ticket_id
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.clean_string(string)
|
119
|
+
string.downcase.gsub(/[^a-zA-Z0-9_:;,.]+/, '-')
|
120
|
+
end
|
121
|
+
|
122
|
+
def add_comment(comment)
|
123
|
+
return false if !comment
|
124
|
+
base.in_branch do |wd|
|
125
|
+
Dir.chdir(ticket_name) do
|
126
|
+
base.new_file(comment_name(email), comment)
|
127
|
+
end
|
128
|
+
base.git.add
|
129
|
+
base.git.commit("added comment to ticket #{ticket_name}")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def change_state(new_state)
|
134
|
+
return false if !new_state
|
135
|
+
return false if new_state == state
|
136
|
+
|
137
|
+
base.in_branch do |wd|
|
138
|
+
Dir.chdir(ticket_name) do
|
139
|
+
base.new_file('STATE_' + new_state, new_state)
|
140
|
+
end
|
141
|
+
base.git.remove(File.join(ticket_name,'STATE_' + state))
|
142
|
+
base.git.add
|
143
|
+
base.git.commit("added state (#{new_state}) to ticket #{ticket_name}")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def change_assigned(new_assigned)
|
148
|
+
new_assigned ||= email
|
149
|
+
return false if new_assigned == assigned
|
150
|
+
|
151
|
+
base.in_branch do |wd|
|
152
|
+
Dir.chdir(ticket_name) do
|
153
|
+
base.new_file('ASSIGNED_' + new_assigned, new_assigned)
|
154
|
+
end
|
155
|
+
base.git.remove(File.join(ticket_name,'ASSIGNED_' + assigned))
|
156
|
+
base.git.add
|
157
|
+
base.git.commit("assigned #{new_assigned} to ticket #{ticket_name}")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def add_tag(tag)
|
162
|
+
return false if !tag
|
163
|
+
added = false
|
164
|
+
tags = tag.split(',').map { |t| t.strip }
|
165
|
+
base.in_branch do |wd|
|
166
|
+
Dir.chdir(ticket_name) do
|
167
|
+
tags.each do |add_tag|
|
168
|
+
if add_tag.size > 0
|
169
|
+
tag_filename = 'TAG_' + Ticket.clean_string(add_tag)
|
170
|
+
if !File.exists?(tag_filename)
|
171
|
+
base.new_file(tag_filename, tag_filename)
|
172
|
+
added = true
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
if added
|
178
|
+
base.git.add
|
179
|
+
base.git.commit("added tags (#{tag}) to ticket #{ticket_name}")
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def remove_tag(tag)
|
185
|
+
return false if !tag
|
186
|
+
removed = false
|
187
|
+
tags = tag.split(',').map { |t| t.strip }
|
188
|
+
base.in_branch do |wd|
|
189
|
+
tags.each do |add_tag|
|
190
|
+
tag_filename = File.join(ticket_name, 'TAG_' + Ticket.clean_string(add_tag))
|
191
|
+
if File.exists?(tag_filename)
|
192
|
+
base.git.remove(tag_filename)
|
193
|
+
removed = true
|
194
|
+
end
|
195
|
+
end
|
196
|
+
if removed
|
197
|
+
base.git.commit("removed tags (#{tag}) from ticket #{ticket_name}")
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def path
|
203
|
+
File.join(state, ticket_name)
|
204
|
+
end
|
205
|
+
|
206
|
+
def comment_name(email)
|
207
|
+
'COMMENT_' + Time.now.to_i.to_s + '_' + email
|
208
|
+
end
|
209
|
+
|
210
|
+
def email
|
211
|
+
opts[:user_email] || 'anon'
|
212
|
+
end
|
213
|
+
|
214
|
+
def assigned_name
|
215
|
+
assigned.split('@').first rescue ''
|
216
|
+
end
|
217
|
+
|
218
|
+
def self.create_ticket_name(title)
|
219
|
+
[Time.now.to_i.to_s, Ticket.clean_string(title), rand(999).to_i.to_s].join('_')
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
end
|
224
|
+
end
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mislav-ticgit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.6
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Scott Chacon
|
8
|
+
- "Mislav Marohni\xC4\x87"
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2008-09-11 00:00:00 -07:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: schacon-git
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ~>
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.0.5
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: wycats-thor
|
27
|
+
version_requirement:
|
28
|
+
version_requirements: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.9.5
|
33
|
+
version:
|
34
|
+
description:
|
35
|
+
email: schacon@gmail.com
|
36
|
+
executables:
|
37
|
+
- ti
|
38
|
+
- ticgitweb
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files: []
|
42
|
+
|
43
|
+
files:
|
44
|
+
- lib/ticgit/base.rb
|
45
|
+
- lib/ticgit/cli.rb
|
46
|
+
- lib/ticgit/comment.rb
|
47
|
+
- lib/ticgit/ticket.rb
|
48
|
+
- lib/ticgit.rb
|
49
|
+
- bin/ti
|
50
|
+
- bin/ticgitweb
|
51
|
+
has_rdoc: false
|
52
|
+
homepage: http://github.com/schacon/ticgit/wikis
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: "0"
|
63
|
+
version:
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
version:
|
70
|
+
requirements: []
|
71
|
+
|
72
|
+
rubyforge_project:
|
73
|
+
rubygems_version: 1.2.0
|
74
|
+
signing_key:
|
75
|
+
specification_version: 2
|
76
|
+
summary: A distributed ticketing system for Git projects.
|
77
|
+
test_files: []
|
78
|
+
|