mislav-ticgit 0.3.6
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/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
|
+
|