ticgit 0.3.6
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/ti +11 -0
- data/bin/ticgitweb +308 -0
- data/lib/ticgit.rb +28 -0
- data/lib/ticgit/base.rb +301 -0
- data/lib/ticgit/cli.rb +396 -0
- data/lib/ticgit/comment.rb +17 -0
- data/lib/ticgit/ticket.rb +214 -0
- metadata +63 -0
data/bin/ti
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# This is a command line client that does all the actual tic commands
|
4
|
+
#
|
5
|
+
# author : Scott Chacon (schacon@gmail.com)
|
6
|
+
#
|
7
|
+
|
8
|
+
require 'rubygems'
|
9
|
+
require 'ticgit'
|
10
|
+
#require File.dirname(__FILE__) + '/../lib/ticgit'
|
11
|
+
TicGit::CLI.execute
|
data/bin/ticgitweb
ADDED
@@ -0,0 +1,308 @@
|
|
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 : Scott Chacon (schacon@gmail.com)
|
9
|
+
#
|
10
|
+
|
11
|
+
%w(rubygems sinatra git ticgit haml).each do |dependency|
|
12
|
+
begin
|
13
|
+
require dependency
|
14
|
+
rescue LoadError => e
|
15
|
+
puts "You need to install #{dependency} before we can proceed"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# !! TODO : if ARGV[1] is a path to a git repo, use that
|
20
|
+
# otherwise, look in ~/.ticgit
|
21
|
+
|
22
|
+
$ticgit = TicGit.open('.')
|
23
|
+
|
24
|
+
get('/_stylesheet.css') { Sass::Engine.new(File.read(__FILE__).gsub(/.*__END__/m, '')).render }
|
25
|
+
|
26
|
+
# ticket list view
|
27
|
+
get '/' do
|
28
|
+
@tickets = $ticgit.ticket_list(:order => 'date.desc')
|
29
|
+
haml(list('all'))
|
30
|
+
end
|
31
|
+
|
32
|
+
get '/fs/:state' do
|
33
|
+
@tickets = $ticgit.ticket_list(:state => params[:state], :order => 'date.desc')
|
34
|
+
haml(list(params[:state]))
|
35
|
+
end
|
36
|
+
|
37
|
+
get '/tag/:tag' do
|
38
|
+
@tickets = $ticgit.ticket_list(:tag => params[:tag], :order => 'date.desc')
|
39
|
+
haml(list(params[:tag]))
|
40
|
+
end
|
41
|
+
|
42
|
+
get '/sv/:saved_view' do
|
43
|
+
@tickets = $ticgit.ticket_list(:saved => params[:saved_view])
|
44
|
+
haml(list(params[:saved_view]))
|
45
|
+
end
|
46
|
+
|
47
|
+
# ticket single view
|
48
|
+
get '/ticket/:ticket' do
|
49
|
+
@ticket = $ticgit.ticket_show(params[:ticket])
|
50
|
+
haml(show)
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
# add ticket
|
55
|
+
get '/t/new' do
|
56
|
+
haml(new_ticket)
|
57
|
+
end
|
58
|
+
|
59
|
+
# add ticket finalize
|
60
|
+
post '/t/new' do
|
61
|
+
title = params[:title].to_s.strip
|
62
|
+
if title.size > 1
|
63
|
+
tags = params[:tags].split(',').map { |t| t.strip } rescue nil
|
64
|
+
t = $ticgit.ticket_new(title, {:comment => params[:comment].strip, :tags => tags})
|
65
|
+
redirect '/ticket/' + t.ticket_id.to_s
|
66
|
+
else
|
67
|
+
redirect '/t/new'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
# add comment
|
73
|
+
post '/a/add_comment/:ticket' do
|
74
|
+
t = $ticgit.ticket_comment(params[:comment], params[:ticket])
|
75
|
+
redirect '/ticket/' + params[:ticket]
|
76
|
+
end
|
77
|
+
|
78
|
+
# add tag
|
79
|
+
post '/a/add_tags/:ticket' do
|
80
|
+
t = $ticgit.ticket_tag(params[:tags], params[:ticket])
|
81
|
+
redirect '/ticket/' + params[:ticket]
|
82
|
+
end
|
83
|
+
|
84
|
+
# change ticket state
|
85
|
+
get '/a/change_state/:ticket/:state' do
|
86
|
+
$ticgit.ticket_change(params[:state], params[:ticket])
|
87
|
+
redirect '/ticket/' + params[:ticket]
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
def layout(title, content)
|
92
|
+
@saved = $ticgit.config['list_options'].keys rescue []
|
93
|
+
%Q(
|
94
|
+
%html
|
95
|
+
%head
|
96
|
+
%title #{title}
|
97
|
+
%link{:rel => 'stylesheet', :href => '/_stylesheet.css', :type => 'text/css', :media => 'screen'}
|
98
|
+
%meta{'http-equiv' => 'Content-Type', :content => 'text/html; charset=utf-8'}
|
99
|
+
|
100
|
+
%body
|
101
|
+
#navigation
|
102
|
+
%a{:href => '/'} All
|
103
|
+
%a{:href => '/fs/open'} Open
|
104
|
+
%a{:href => '/fs/resolved'} Resolved
|
105
|
+
%a{:href => '/fs/hold'} Hold
|
106
|
+
%a{:href => '/fs/invalid'} Invalid
|
107
|
+
- if !@saved.empty?
|
108
|
+
| Saved:
|
109
|
+
- @saved.each do |s|
|
110
|
+
%a{:href => "/sv/\#{s}"}= s
|
111
|
+
#action
|
112
|
+
%a{:href => '/t/new'} New Ticket
|
113
|
+
|
114
|
+
#{content}
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
def new_ticket
|
119
|
+
layout('New Ticket', %q{
|
120
|
+
%h1 Create a New Ticket
|
121
|
+
%form{:action => '/t/new', :method => 'POST'}
|
122
|
+
%table
|
123
|
+
%tr
|
124
|
+
%th Title
|
125
|
+
%td
|
126
|
+
%input{:type => 'text', :name => 'title', :size => 30}
|
127
|
+
%tr
|
128
|
+
%th Tags
|
129
|
+
%td
|
130
|
+
%input{:name => 'tags', :size => 30}
|
131
|
+
%small (comma delimited)
|
132
|
+
%tr
|
133
|
+
%th Comment
|
134
|
+
%td
|
135
|
+
%textarea{:name => 'comment', :rows => 15, :cols => 30}
|
136
|
+
%tr
|
137
|
+
%td
|
138
|
+
%td
|
139
|
+
%input{:type => 'submit', :value => 'Create Ticket'}
|
140
|
+
})
|
141
|
+
end
|
142
|
+
|
143
|
+
def list(title = 'all')
|
144
|
+
@title = title
|
145
|
+
layout(title + ' tickets', %q{
|
146
|
+
%h1= "#{@title} tickets"
|
147
|
+
- if @tickets.empty?
|
148
|
+
%p No tickets found.
|
149
|
+
- else
|
150
|
+
%table.long
|
151
|
+
- c = 'even'
|
152
|
+
- @tickets.each do |t|
|
153
|
+
%tr{:class => (c == 'even' ? c = 'odd' : c = 'even') }
|
154
|
+
%td
|
155
|
+
%a{:href => "/ticket/#{t.ticket_id}" }
|
156
|
+
%code= t.ticket_id[0,6]
|
157
|
+
%td= t.title
|
158
|
+
%td{:class => t.state}= t.state
|
159
|
+
%td= t.opened.strftime("%m/%d")
|
160
|
+
%td= t.assigned_name
|
161
|
+
%td
|
162
|
+
- t.tags.each do |tag|
|
163
|
+
%a{:href => "/tag/#{tag}"}= tag
|
164
|
+
})
|
165
|
+
end
|
166
|
+
|
167
|
+
def show
|
168
|
+
layout('ticket', %q{
|
169
|
+
%center
|
170
|
+
%h1= @ticket.title
|
171
|
+
|
172
|
+
%form{:action => "/a/add_tags/#{@ticket.ticket_id}", :method => 'POST'}
|
173
|
+
%table
|
174
|
+
%tr
|
175
|
+
%th TicId
|
176
|
+
%td
|
177
|
+
%code= @ticket.ticket_id
|
178
|
+
%tr
|
179
|
+
%th Assigned
|
180
|
+
%td= @ticket.assigned
|
181
|
+
%tr
|
182
|
+
%th Opened
|
183
|
+
%td= @ticket.opened
|
184
|
+
%tr
|
185
|
+
%th State
|
186
|
+
%td{:class => @ticket.state}
|
187
|
+
%table{:width => '300'}
|
188
|
+
%tr
|
189
|
+
%td{:width=>'90%'}= @ticket.state
|
190
|
+
- $ticgit.tic_states.select { |s| s != @ticket.state}.each do |st|
|
191
|
+
%td{:class => st}
|
192
|
+
%a{:href => "/a/change_state/#{@ticket.ticket_id}/#{st}"}= st[0,2]
|
193
|
+
%tr
|
194
|
+
%th Tags
|
195
|
+
%td
|
196
|
+
- @ticket.tags.each do |t|
|
197
|
+
%a{:href => "/tag/#{t}"}= t
|
198
|
+
%div.addtag
|
199
|
+
%input{:name => 'tags'}
|
200
|
+
%input{:type => 'submit', :value => 'add tag'}
|
201
|
+
|
202
|
+
%h3 Comments
|
203
|
+
%form{:action => "/a/add_comment/#{@ticket.ticket_id}", :method => 'POST'}
|
204
|
+
%div
|
205
|
+
%textarea{:name => 'comment', :cols => 50}
|
206
|
+
%br
|
207
|
+
%input{:type => 'submit', :value => 'add comment'}
|
208
|
+
|
209
|
+
%div.comments
|
210
|
+
- @ticket.comments.reverse.each do |t|
|
211
|
+
%div.comment
|
212
|
+
%span.head
|
213
|
+
Added
|
214
|
+
= t.added.strftime("%m/%d %H:%M")
|
215
|
+
by
|
216
|
+
= t.user
|
217
|
+
%div.comment-text
|
218
|
+
= t.comment
|
219
|
+
%br
|
220
|
+
})
|
221
|
+
end
|
222
|
+
|
223
|
+
__END__
|
224
|
+
body
|
225
|
+
:font
|
226
|
+
family: Verdana, Arial, "Bitstream Vera Sans", Helvetica, sans-serif
|
227
|
+
color: black
|
228
|
+
line-height: 160%
|
229
|
+
background-color: white
|
230
|
+
margin: 2em
|
231
|
+
|
232
|
+
#navigation
|
233
|
+
a
|
234
|
+
background-color: #e0e0e0
|
235
|
+
color: black
|
236
|
+
text-decoration: none
|
237
|
+
padding: 2px
|
238
|
+
padding: 5px
|
239
|
+
border-bottom: 1px black solid
|
240
|
+
|
241
|
+
#action
|
242
|
+
text-align: right
|
243
|
+
|
244
|
+
.addtag
|
245
|
+
padding: 5px 0
|
246
|
+
|
247
|
+
h1
|
248
|
+
display: block
|
249
|
+
padding-bottom: 5px
|
250
|
+
|
251
|
+
a
|
252
|
+
color: black
|
253
|
+
a.exists
|
254
|
+
font-weight: bold
|
255
|
+
a.unknown
|
256
|
+
font-style: italic
|
257
|
+
|
258
|
+
.comments
|
259
|
+
margin: 10px 20px
|
260
|
+
.comment
|
261
|
+
.head
|
262
|
+
background: #eee
|
263
|
+
padding: 4px
|
264
|
+
.comment-text
|
265
|
+
padding: 10px
|
266
|
+
color: #333
|
267
|
+
|
268
|
+
table.long
|
269
|
+
width: 100%
|
270
|
+
|
271
|
+
table
|
272
|
+
tr.even
|
273
|
+
td
|
274
|
+
background: #eee
|
275
|
+
tr.odd
|
276
|
+
td
|
277
|
+
background: #fff
|
278
|
+
|
279
|
+
table
|
280
|
+
tr
|
281
|
+
th
|
282
|
+
text-align: left
|
283
|
+
padding: 3px
|
284
|
+
vertical-align: top
|
285
|
+
td.open
|
286
|
+
background: #ada
|
287
|
+
td.resolved
|
288
|
+
background: #abd
|
289
|
+
td.hold
|
290
|
+
background: #dda
|
291
|
+
td.invalid
|
292
|
+
background: #aaa
|
293
|
+
|
294
|
+
.submit
|
295
|
+
font-size: large
|
296
|
+
font-weight: bold
|
297
|
+
|
298
|
+
.page_title
|
299
|
+
font-size: xx-large
|
300
|
+
|
301
|
+
.edit_link
|
302
|
+
color: black
|
303
|
+
font-size: 14px
|
304
|
+
font-weight: bold
|
305
|
+
background-color: #e0e0e0
|
306
|
+
font-variant: small-caps
|
307
|
+
text-decoration: none
|
308
|
+
|
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,301 @@
|
|
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
|
+
case field
|
113
|
+
when 'assigned'
|
114
|
+
ts = ts.sort { |a, b| a.assigned <=> b.assigned }
|
115
|
+
when 'state'
|
116
|
+
ts = ts.sort { |a, b| a.state <=> b.state }
|
117
|
+
when 'date'
|
118
|
+
ts = ts.sort { |a, b| a.opened <=> b.opened }
|
119
|
+
end
|
120
|
+
ts = ts.reverse if type == 'desc'
|
121
|
+
else
|
122
|
+
# default list
|
123
|
+
ts = ts.sort { |a, b| a.opened <=> b.opened }
|
124
|
+
end
|
125
|
+
|
126
|
+
if options.size == 0
|
127
|
+
# default list
|
128
|
+
options[:state] = 'open'
|
129
|
+
end
|
130
|
+
|
131
|
+
# :tag, :state, :assigned
|
132
|
+
if t = options[:tag]
|
133
|
+
ts = ts.select { |tag| tag.tags.include?(t) }
|
134
|
+
end
|
135
|
+
if s = options[:state]
|
136
|
+
ts = ts.select { |tag| tag.state =~ /#{s}/ }
|
137
|
+
end
|
138
|
+
if a = options[:assigned]
|
139
|
+
ts = ts.select { |tag| tag.assigned =~ /#{a}/ }
|
140
|
+
end
|
141
|
+
|
142
|
+
if save = options[:save]
|
143
|
+
options.delete(:save)
|
144
|
+
@config['list_options'][save] = options
|
145
|
+
end
|
146
|
+
|
147
|
+
@last_tickets = ts.map { |t| t.ticket_name }
|
148
|
+
# :save
|
149
|
+
|
150
|
+
save_state
|
151
|
+
ts
|
152
|
+
end
|
153
|
+
|
154
|
+
# returns single Ticket
|
155
|
+
def ticket_show(ticket_id = nil)
|
156
|
+
# ticket_id can be index of last_tickets, partial sha or nil => last ticket
|
157
|
+
if t = ticket_revparse(ticket_id)
|
158
|
+
return TicGit::Ticket.open(self, t, @tickets[t])
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# returns recent ticgit activity
|
163
|
+
# uses the git logs for this
|
164
|
+
def ticket_recent(ticket_id = nil)
|
165
|
+
if ticket_id
|
166
|
+
t = ticket_revparse(ticket_id)
|
167
|
+
return git.log.object('ticgit').path(t)
|
168
|
+
else
|
169
|
+
return git.log.object('ticgit')
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def ticket_revparse(ticket_id)
|
174
|
+
if ticket_id
|
175
|
+
if /^[0-9]*$/ =~ ticket_id
|
176
|
+
if t = @last_tickets[ticket_id.to_i - 1]
|
177
|
+
return t
|
178
|
+
end
|
179
|
+
else
|
180
|
+
# partial or full sha
|
181
|
+
if ch = @tickets.select { |name, t| t['files'].assoc('TICKET_ID')[1] =~ /^#{ticket_id}/ }
|
182
|
+
return ch.first[0]
|
183
|
+
end
|
184
|
+
end
|
185
|
+
elsif(@current_ticket)
|
186
|
+
return @current_ticket
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def ticket_tag(tag, ticket_id = nil, options = {})
|
191
|
+
if t = ticket_revparse(ticket_id)
|
192
|
+
ticket = TicGit::Ticket.open(self, t, @tickets[t])
|
193
|
+
if options[:remove]
|
194
|
+
ticket.remove_tag(tag)
|
195
|
+
else
|
196
|
+
ticket.add_tag(tag)
|
197
|
+
end
|
198
|
+
reset_ticgit
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def ticket_change(new_state, ticket_id = nil)
|
203
|
+
if t = ticket_revparse(ticket_id)
|
204
|
+
if tic_states.include?(new_state)
|
205
|
+
ticket = TicGit::Ticket.open(self, t, @tickets[t])
|
206
|
+
ticket.change_state(new_state)
|
207
|
+
reset_ticgit
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def ticket_assign(new_assigned = nil, ticket_id = nil)
|
213
|
+
if t = ticket_revparse(ticket_id)
|
214
|
+
ticket = TicGit::Ticket.open(self, t, @tickets[t])
|
215
|
+
ticket.change_assigned(new_assigned)
|
216
|
+
reset_ticgit
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def ticket_checkout(ticket_id)
|
221
|
+
if t = ticket_revparse(ticket_id)
|
222
|
+
ticket = TicGit::Ticket.open(self, t, @tickets[t])
|
223
|
+
@current_ticket = ticket.ticket_name
|
224
|
+
save_state
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def comment_add(ticket_id, comment, options = {})
|
229
|
+
end
|
230
|
+
|
231
|
+
def comment_list(ticket_id)
|
232
|
+
end
|
233
|
+
|
234
|
+
def tic_states
|
235
|
+
['open', 'resolved', 'invalid', 'hold']
|
236
|
+
end
|
237
|
+
|
238
|
+
def load_tickets
|
239
|
+
@tickets = {}
|
240
|
+
|
241
|
+
bs = git.lib.branches_all.map { |b| b[0] }
|
242
|
+
init_ticgit_branch(bs.include?('ticgit')) if !(bs.include?('ticgit') && File.directory?(@tic_working))
|
243
|
+
|
244
|
+
tree = git.lib.full_tree('ticgit')
|
245
|
+
tree.each do |t|
|
246
|
+
data, file = t.split("\t")
|
247
|
+
mode, type, sha = data.split(" ")
|
248
|
+
tic = file.split('/')
|
249
|
+
if tic.size == 2 # directory depth
|
250
|
+
ticket, info = tic
|
251
|
+
@tickets[ticket] ||= { 'files' => [] }
|
252
|
+
@tickets[ticket]['files'] << [info, sha]
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def init_ticgit_branch(ticgit_branch = false)
|
258
|
+
@logger.info 'creating ticgit repo branch'
|
259
|
+
|
260
|
+
in_branch(ticgit_branch) do
|
261
|
+
new_file('.hold', 'hold')
|
262
|
+
if !ticgit_branch
|
263
|
+
git.add
|
264
|
+
git.commit('creating the ticgit branch')
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# temporarlily switches to ticgit branch for tic work
|
270
|
+
def in_branch(branch_exists = true)
|
271
|
+
needs_checkout = false
|
272
|
+
if !File.directory?(@tic_working)
|
273
|
+
FileUtils.mkdir_p(@tic_working)
|
274
|
+
needs_checkout = true
|
275
|
+
end
|
276
|
+
if !File.exists?('.hold')
|
277
|
+
needs_checkout = true
|
278
|
+
end
|
279
|
+
|
280
|
+
old_current = git.lib.branch_current
|
281
|
+
begin
|
282
|
+
git.lib.change_head_branch('ticgit')
|
283
|
+
git.with_index(@tic_index) do
|
284
|
+
git.with_working(@tic_working) do |wd|
|
285
|
+
git.lib.checkout('ticgit') if needs_checkout && branch_exists
|
286
|
+
yield wd
|
287
|
+
end
|
288
|
+
end
|
289
|
+
ensure
|
290
|
+
git.lib.change_head_branch(old_current)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def new_file(name, contents)
|
295
|
+
File.open(name, 'w') do |f|
|
296
|
+
f.puts contents
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
end
|
301
|
+
end
|
data/lib/ticgit/cli.rb
ADDED
@@ -0,0 +1,396 @@
|
|
1
|
+
require 'ticgit'
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
# used Cap as a model for this - thanks Jamis
|
5
|
+
|
6
|
+
module TicGit
|
7
|
+
class CLI
|
8
|
+
# The array of (unparsed) command-line options
|
9
|
+
attr_reader :action, :options, :args, :tic
|
10
|
+
|
11
|
+
def self.execute
|
12
|
+
parse(ARGV).execute!
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.parse(args)
|
16
|
+
cli = new(args)
|
17
|
+
cli.parse_options!
|
18
|
+
cli
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(args)
|
22
|
+
@args = args.dup
|
23
|
+
@tic = TicGit.open('.', :keep_state => true)
|
24
|
+
$stdout.sync = true # so that Net::SSH prompts show up
|
25
|
+
rescue NoRepoFound
|
26
|
+
puts "No repo found"
|
27
|
+
exit
|
28
|
+
end
|
29
|
+
|
30
|
+
def execute!
|
31
|
+
case action
|
32
|
+
when 'list':
|
33
|
+
handle_ticket_list
|
34
|
+
when 'state'
|
35
|
+
handle_ticket_state
|
36
|
+
when 'assign'
|
37
|
+
handle_ticket_assign
|
38
|
+
when 'show'
|
39
|
+
handle_ticket_show
|
40
|
+
when 'new'
|
41
|
+
handle_ticket_new
|
42
|
+
when 'checkout', 'co'
|
43
|
+
handle_ticket_checkout
|
44
|
+
when 'comment'
|
45
|
+
handle_ticket_comment
|
46
|
+
when 'tag'
|
47
|
+
handle_ticket_tag
|
48
|
+
when 'recent'
|
49
|
+
handle_ticket_recent
|
50
|
+
when 'milestone'
|
51
|
+
handle_ticket_milestone
|
52
|
+
else
|
53
|
+
puts 'not a command'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# tic milestone
|
58
|
+
# tic milestone migration1 (list tickets)
|
59
|
+
# tic milestone -n migration1 3/4/08 (new milestone)
|
60
|
+
# tic milestone -a {1} (add ticket to milestone)
|
61
|
+
# tic milestone -d migration1 (delete)
|
62
|
+
def parse_ticket_milestone
|
63
|
+
@options = {}
|
64
|
+
OptionParser.new do |opts|
|
65
|
+
opts.banner = "Usage: ti milestone [milestone_name] [options] [date]"
|
66
|
+
opts.on("-n MILESTONE", "--new MILESTONE", "Add a new milestone to this project") do |v|
|
67
|
+
@options[:new] = v
|
68
|
+
end
|
69
|
+
opts.on("-a TICKET", "--new TICKET", "Add a ticket to this milestone") do |v|
|
70
|
+
@options[:add] = v
|
71
|
+
end
|
72
|
+
opts.on("-d MILESTONE", "--delete MILESTONE", "Remove a milestone") do |v|
|
73
|
+
@options[:remove] = v
|
74
|
+
end
|
75
|
+
end.parse!
|
76
|
+
end
|
77
|
+
|
78
|
+
def handle_ticket_recent
|
79
|
+
tic.ticket_recent(ARGV[1]).each do |commit|
|
80
|
+
puts commit.sha[0, 7] + " " + commit.date.strftime("%m/%d %H:%M") + "\t" + commit.message
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def parse_ticket_tag
|
85
|
+
@options = {}
|
86
|
+
OptionParser.new do |opts|
|
87
|
+
opts.banner = "Usage: ti tag [tic_id] [options] [tag_name] "
|
88
|
+
opts.on("-d", "Remove this tag from the ticket") do |v|
|
89
|
+
@options[:remove] = v
|
90
|
+
end
|
91
|
+
end.parse!
|
92
|
+
end
|
93
|
+
|
94
|
+
def handle_ticket_tag
|
95
|
+
parse_ticket_tag
|
96
|
+
|
97
|
+
if options[:remove]
|
98
|
+
puts 'remove'
|
99
|
+
end
|
100
|
+
|
101
|
+
tid = nil
|
102
|
+
if ARGV.size > 2
|
103
|
+
tid = ARGV[1].chomp
|
104
|
+
tic.ticket_tag(ARGV[2].chomp, tid, options)
|
105
|
+
elsif ARGV.size > 1
|
106
|
+
tic.ticket_tag(ARGV[1], nil, options)
|
107
|
+
else
|
108
|
+
puts 'You need to at least specify one tag to add'
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def parse_ticket_comment
|
113
|
+
@options = {}
|
114
|
+
OptionParser.new do |opts|
|
115
|
+
opts.banner = "Usage: ti comment [tic_id] [options]"
|
116
|
+
opts.on("-m MESSAGE", "--message MESSAGE", "Message you would like to add as a comment") do |v|
|
117
|
+
@options[:message] = v
|
118
|
+
end
|
119
|
+
opts.on("-f FILE", "--file FILE", "A file that contains the comment you would like to add") do |v|
|
120
|
+
raise ArgumentError, "Only 1 of -f/--file and -m/--message can be specified" if @options[:message]
|
121
|
+
raise ArgumentError, "File #{v} doesn't exist" unless File.file?(v)
|
122
|
+
raise ArgumentError, "File #{v} must be <= 2048 bytes" unless File.size(v) <= 2048
|
123
|
+
@options[:file] = v
|
124
|
+
end
|
125
|
+
end.parse!
|
126
|
+
end
|
127
|
+
|
128
|
+
def handle_ticket_comment
|
129
|
+
parse_ticket_comment
|
130
|
+
|
131
|
+
tid = nil
|
132
|
+
tid = ARGV[1].chomp if ARGV[1]
|
133
|
+
|
134
|
+
if(m = options[:message])
|
135
|
+
tic.ticket_comment(m, tid)
|
136
|
+
elsif(f = options[:file])
|
137
|
+
tic.ticket_comment(File.read(options[:file]), tid)
|
138
|
+
else
|
139
|
+
if message = get_editor_message
|
140
|
+
tic.ticket_comment(message.join(''), tid)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
def handle_ticket_checkout
|
147
|
+
tid = ARGV[1].chomp
|
148
|
+
tic.ticket_checkout(tid)
|
149
|
+
end
|
150
|
+
|
151
|
+
def handle_ticket_state
|
152
|
+
if ARGV.size > 2
|
153
|
+
tid = ARGV[1].chomp
|
154
|
+
new_state = ARGV[2].chomp
|
155
|
+
if valid_state(new_state)
|
156
|
+
tic.ticket_change(new_state, tid)
|
157
|
+
else
|
158
|
+
puts 'Invalid State - please choose from : ' + tic.tic_states.join(", ")
|
159
|
+
end
|
160
|
+
elsif ARGV.size > 1
|
161
|
+
# new state
|
162
|
+
new_state = ARGV[1].chomp
|
163
|
+
if valid_state(new_state)
|
164
|
+
tic.ticket_change(new_state)
|
165
|
+
else
|
166
|
+
puts 'Invalid State - please choose from : ' + tic.tic_states.join(", ")
|
167
|
+
end
|
168
|
+
else
|
169
|
+
puts 'You need to at least specify a new state for the current ticket'
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def valid_state(state)
|
174
|
+
tic.tic_states.include?(state)
|
175
|
+
end
|
176
|
+
|
177
|
+
def parse_ticket_assign
|
178
|
+
@options = {}
|
179
|
+
OptionParser.new do |opts|
|
180
|
+
opts.banner = "Usage: ti assign [options] [ticket_id]"
|
181
|
+
opts.on("-u USER", "--user USER", "Assign the ticket to this user") do |v|
|
182
|
+
@options[:user] = v
|
183
|
+
end
|
184
|
+
opts.on("-c TICKET", "--checkout TICKET", "Checkout this ticket") do |v|
|
185
|
+
@options[:checkout] = v
|
186
|
+
end
|
187
|
+
end.parse!
|
188
|
+
end
|
189
|
+
|
190
|
+
# Assigns a ticket to someone
|
191
|
+
#
|
192
|
+
# Usage:
|
193
|
+
# ti assign (assign checked out ticket to current user)
|
194
|
+
# ti assign {1} (assign ticket to current user)
|
195
|
+
# ti assign -c {1} (assign ticket to current user and checkout the ticket)
|
196
|
+
# ti assign -u {name} (assign ticket to specified user)
|
197
|
+
def handle_ticket_assign
|
198
|
+
parse_ticket_assign
|
199
|
+
|
200
|
+
tic.ticket_checkout(options[:checkout]) if options[:checkout]
|
201
|
+
|
202
|
+
tic_id = ARGV.size > 1 ? ARGV[1].chomp : nil
|
203
|
+
tic.ticket_assign(options[:user], tic_id)
|
204
|
+
end
|
205
|
+
|
206
|
+
## LIST TICKETS ##
|
207
|
+
def parse_ticket_list
|
208
|
+
@options = {}
|
209
|
+
OptionParser.new do |opts|
|
210
|
+
opts.banner = "Usage: ti list [options]"
|
211
|
+
opts.on("-o ORDER", "--order ORDER", "Field to order by - one of : assigned,state,date") do |v|
|
212
|
+
@options[:order] = v
|
213
|
+
end
|
214
|
+
opts.on("-t TAG", "--tag TAG", "List only tickets with specific tag") do |v|
|
215
|
+
@options[:tag] = v
|
216
|
+
end
|
217
|
+
opts.on("-s STATE", "--state STATE", "List only tickets in a specific state") do |v|
|
218
|
+
@options[:state] = v
|
219
|
+
end
|
220
|
+
opts.on("-a ASSIGNED", "--assigned ASSIGNED", "List only tickets assigned to someone") do |v|
|
221
|
+
@options[:assigned] = v
|
222
|
+
end
|
223
|
+
opts.on("-S SAVENAME", "--saveas SAVENAME", "Save this list as a saved name") do |v|
|
224
|
+
@options[:save] = v
|
225
|
+
end
|
226
|
+
opts.on("-l", "--list", "Show the saved queries") do |v|
|
227
|
+
@options[:list] = true
|
228
|
+
end
|
229
|
+
end.parse!
|
230
|
+
end
|
231
|
+
|
232
|
+
def handle_ticket_list
|
233
|
+
parse_ticket_list
|
234
|
+
|
235
|
+
options[:saved] = ARGV[1] if ARGV[1]
|
236
|
+
|
237
|
+
if tickets = tic.ticket_list(options)
|
238
|
+
counter = 0
|
239
|
+
|
240
|
+
puts
|
241
|
+
puts [' ', just('#', 4, 'r'),
|
242
|
+
just('TicId', 6),
|
243
|
+
just('Title', 25),
|
244
|
+
just('State', 5),
|
245
|
+
just('Date', 5),
|
246
|
+
just('Assgn', 8),
|
247
|
+
just('Tags', 20) ].join(" ")
|
248
|
+
|
249
|
+
a = []
|
250
|
+
80.times { a << '-'}
|
251
|
+
puts a.join('')
|
252
|
+
|
253
|
+
tickets.each do |t|
|
254
|
+
counter += 1
|
255
|
+
tic.current_ticket == t.ticket_name ? add = '*' : add = ' '
|
256
|
+
puts [add, just(counter, 4, 'r'),
|
257
|
+
t.ticket_id[0,6],
|
258
|
+
just(t.title, 25),
|
259
|
+
just(t.state, 5),
|
260
|
+
t.opened.strftime("%m/%d"),
|
261
|
+
just(t.assigned_name, 8),
|
262
|
+
just(t.tags.join(','), 20) ].join(" ")
|
263
|
+
end
|
264
|
+
puts
|
265
|
+
end
|
266
|
+
|
267
|
+
end
|
268
|
+
|
269
|
+
## SHOW TICKETS ##
|
270
|
+
|
271
|
+
def handle_ticket_show
|
272
|
+
if t = @tic.ticket_show(ARGV[1])
|
273
|
+
ticket_show(t)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def ticket_show(t)
|
278
|
+
days_ago = ((Time.now - t.opened) / (60 * 60 * 24)).round.to_s
|
279
|
+
puts
|
280
|
+
puts just('Title', 10) + ': ' + t.title
|
281
|
+
puts just('TicId', 10) + ': ' + t.ticket_id
|
282
|
+
puts
|
283
|
+
puts just('Assigned', 10) + ': ' + t.assigned.to_s
|
284
|
+
puts just('Opened', 10) + ': ' + t.opened.to_s + ' (' + days_ago + ' days)'
|
285
|
+
puts just('State', 10) + ': ' + t.state.upcase
|
286
|
+
if !t.tags.empty?
|
287
|
+
puts just('Tags', 10) + ': ' + t.tags.join(', ')
|
288
|
+
end
|
289
|
+
puts
|
290
|
+
if !t.comments.empty?
|
291
|
+
puts 'Comments (' + t.comments.size.to_s + '):'
|
292
|
+
t.comments.reverse.each do |c|
|
293
|
+
puts ' * Added ' + c.added.strftime("%m/%d %H:%M") + ' by ' + c.user
|
294
|
+
|
295
|
+
wrapped = c.comment.split("\n").collect do |line|
|
296
|
+
line.length > 80 ? line.gsub(/(.{1,80})(\s+|$)/, "\\1\n").strip : line
|
297
|
+
end * "\n"
|
298
|
+
|
299
|
+
wrapped = wrapped.split("\n").map { |line| "\t" + line }
|
300
|
+
if wrapped.size > 6
|
301
|
+
puts wrapped[0, 6].join("\n")
|
302
|
+
puts "\t** more... **"
|
303
|
+
else
|
304
|
+
puts wrapped.join("\n")
|
305
|
+
end
|
306
|
+
puts
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
## NEW TICKETS ##
|
312
|
+
|
313
|
+
def parse_ticket_new
|
314
|
+
@options = {}
|
315
|
+
OptionParser.new do |opts|
|
316
|
+
opts.banner = "Usage: ti new [options]"
|
317
|
+
opts.on("-t TITLE", "--title TITLE", "Title to use for the name of the new ticket") do |v|
|
318
|
+
@options[:title] = v
|
319
|
+
end
|
320
|
+
end.parse!
|
321
|
+
end
|
322
|
+
|
323
|
+
def handle_ticket_new
|
324
|
+
parse_ticket_new
|
325
|
+
if(t = options[:title])
|
326
|
+
ticket_show(@tic.ticket_new(t, options))
|
327
|
+
else
|
328
|
+
# interactive
|
329
|
+
message_file = Tempfile.new('ticgit_message').path
|
330
|
+
File.open(message_file, 'w') do |f|
|
331
|
+
f.puts "\n# ---"
|
332
|
+
f.puts "tags:"
|
333
|
+
f.puts "# first line will be the title of the tic, the rest will be the first comment"
|
334
|
+
f.puts "# if you would like to add initial tags, put them on the 'tags:' line, comma delim"
|
335
|
+
end
|
336
|
+
if message = get_editor_message(message_file)
|
337
|
+
title = message.shift
|
338
|
+
if title && title.chomp.length > 0
|
339
|
+
title = title.chomp
|
340
|
+
if message.last[0, 5] == 'tags:'
|
341
|
+
tags = message.pop
|
342
|
+
tags = tags.gsub('tags:', '')
|
343
|
+
tags = tags.split(',').map { |t| t.strip }
|
344
|
+
end
|
345
|
+
if message.size > 0
|
346
|
+
comment = message.join("")
|
347
|
+
end
|
348
|
+
ticket_show(@tic.ticket_new(title, :comment => comment, :tags => tags))
|
349
|
+
else
|
350
|
+
puts "You need to at least enter a title"
|
351
|
+
end
|
352
|
+
else
|
353
|
+
puts "It seems you wrote nothing"
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
def get_editor_message(message_file = nil)
|
359
|
+
message_file = Tempfile.new('ticgit_message').path if !message_file
|
360
|
+
|
361
|
+
editor = ENV["EDITOR"] || 'vim'
|
362
|
+
system("#{editor} #{message_file}");
|
363
|
+
message = File.readlines(message_file)
|
364
|
+
message = message.select { |line| line[0, 1] != '#' } # removing comments
|
365
|
+
if message.empty?
|
366
|
+
return false
|
367
|
+
else
|
368
|
+
return message
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def parse_options! #:nodoc:
|
373
|
+
if args.empty?
|
374
|
+
warn "Please specify at least one action to execute."
|
375
|
+
puts " list state show new checkout comment tag assign "
|
376
|
+
exit
|
377
|
+
end
|
378
|
+
|
379
|
+
@action = args.first
|
380
|
+
end
|
381
|
+
|
382
|
+
|
383
|
+
def just(value, size, side = 'l')
|
384
|
+
value = value.to_s
|
385
|
+
if value.size > size
|
386
|
+
value = value[0, size]
|
387
|
+
end
|
388
|
+
if side == 'r'
|
389
|
+
return value.rjust(size)
|
390
|
+
else
|
391
|
+
return value.ljust(size)
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
end
|
396
|
+
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,214 @@
|
|
1
|
+
module TicGit
|
2
|
+
class Ticket
|
3
|
+
|
4
|
+
attr_reader :base, :opts
|
5
|
+
attr_accessor :ticket_id, :ticket_name
|
6
|
+
attr_accessor :title, :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
|
+
@state = 'open' # by default
|
17
|
+
@comments = []
|
18
|
+
@tags = []
|
19
|
+
@attachments = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.create(base, title, options = {})
|
23
|
+
t = Ticket.new(base, options)
|
24
|
+
t.title = title
|
25
|
+
t.ticket_name = self.create_ticket_name(title)
|
26
|
+
t.save_new
|
27
|
+
t
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.open(base, ticket_name, ticket_hash, options = {})
|
31
|
+
tid = nil
|
32
|
+
|
33
|
+
t = Ticket.new(base, options)
|
34
|
+
t.ticket_name = ticket_name
|
35
|
+
|
36
|
+
title, date = self.parse_ticket_name(ticket_name)
|
37
|
+
|
38
|
+
t.title = title
|
39
|
+
t.opened = date
|
40
|
+
|
41
|
+
ticket_hash['files'].each do |fname, value|
|
42
|
+
if fname == 'TICKET_ID'
|
43
|
+
tid = value
|
44
|
+
else
|
45
|
+
# matching
|
46
|
+
data = fname.split('_')
|
47
|
+
if data[0] == 'ASSIGNED'
|
48
|
+
t.assigned = data[1]
|
49
|
+
end
|
50
|
+
if data[0] == 'COMMENT'
|
51
|
+
t.comments << TicGit::Comment.new(base, fname, value)
|
52
|
+
end
|
53
|
+
if data[0] == 'TAG'
|
54
|
+
t.tags << data[1]
|
55
|
+
end
|
56
|
+
if data[0] == 'STATE'
|
57
|
+
t.state = data[1]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
t.ticket_id = tid
|
63
|
+
t
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
def self.parse_ticket_name(name)
|
68
|
+
epoch, title, rand = name.split('_')
|
69
|
+
title = title.gsub('-', ' ')
|
70
|
+
return [title, Time.at(epoch.to_i)]
|
71
|
+
end
|
72
|
+
|
73
|
+
# write this ticket to the git database
|
74
|
+
def save_new
|
75
|
+
base.in_branch do |wd|
|
76
|
+
base.logger.info "saving #{ticket_name}"
|
77
|
+
|
78
|
+
Dir.mkdir(ticket_name)
|
79
|
+
Dir.chdir(ticket_name) do
|
80
|
+
base.new_file('TICKET_ID', ticket_name)
|
81
|
+
base.new_file('ASSIGNED_' + email, email)
|
82
|
+
base.new_file('STATE_' + state, state)
|
83
|
+
|
84
|
+
# add initial comment
|
85
|
+
#COMMENT_080315060503045__schacon_at_gmail
|
86
|
+
base.new_file(comment_name(email), opts[:comment]) if opts[:comment]
|
87
|
+
|
88
|
+
# add initial tags
|
89
|
+
if opts[:tags] && opts[:tags].size > 0
|
90
|
+
opts[:tags] = opts[:tags].map { |t| t.strip }.compact
|
91
|
+
opts[:tags].each do |tag|
|
92
|
+
if tag.size > 0
|
93
|
+
tag_filename = 'TAG_' + Ticket.clean_string(tag)
|
94
|
+
if !File.exists?(tag_filename)
|
95
|
+
base.new_file(tag_filename, tag_filename)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
base.git.add
|
103
|
+
base.git.commit("added ticket #{ticket_name}")
|
104
|
+
end
|
105
|
+
# ticket_id
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.clean_string(string)
|
109
|
+
string.downcase.gsub(/[^a-z0-9]+/i, '-')
|
110
|
+
end
|
111
|
+
|
112
|
+
def add_comment(comment)
|
113
|
+
return false if !comment
|
114
|
+
base.in_branch do |wd|
|
115
|
+
Dir.chdir(ticket_name) do
|
116
|
+
base.new_file(comment_name(email), comment)
|
117
|
+
end
|
118
|
+
base.git.add
|
119
|
+
base.git.commit("added comment to ticket #{ticket_name}")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def change_state(new_state)
|
124
|
+
return false if !new_state
|
125
|
+
return false if new_state == state
|
126
|
+
|
127
|
+
base.in_branch do |wd|
|
128
|
+
Dir.chdir(ticket_name) do
|
129
|
+
base.new_file('STATE_' + new_state, new_state)
|
130
|
+
end
|
131
|
+
base.git.remove(File.join(ticket_name,'STATE_' + state))
|
132
|
+
base.git.add
|
133
|
+
base.git.commit("added state (#{new_state}) to ticket #{ticket_name}")
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def change_assigned(new_assigned)
|
138
|
+
new_assigned ||= email
|
139
|
+
return false if new_assigned == assigned
|
140
|
+
|
141
|
+
base.in_branch do |wd|
|
142
|
+
Dir.chdir(ticket_name) do
|
143
|
+
base.new_file('ASSIGNED_' + new_assigned, new_assigned)
|
144
|
+
end
|
145
|
+
base.git.remove(File.join(ticket_name,'ASSIGNED_' + assigned))
|
146
|
+
base.git.add
|
147
|
+
base.git.commit("assigned #{new_assigned} to ticket #{ticket_name}")
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def add_tag(tag)
|
152
|
+
return false if !tag
|
153
|
+
added = false
|
154
|
+
tags = tag.split(',').map { |t| t.strip }
|
155
|
+
base.in_branch do |wd|
|
156
|
+
Dir.chdir(ticket_name) do
|
157
|
+
tags.each do |add_tag|
|
158
|
+
if add_tag.size > 0
|
159
|
+
tag_filename = 'TAG_' + Ticket.clean_string(add_tag)
|
160
|
+
if !File.exists?(tag_filename)
|
161
|
+
base.new_file(tag_filename, tag_filename)
|
162
|
+
added = true
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
if added
|
168
|
+
base.git.add
|
169
|
+
base.git.commit("added tags (#{tag}) to ticket #{ticket_name}")
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def remove_tag(tag)
|
175
|
+
return false if !tag
|
176
|
+
removed = false
|
177
|
+
tags = tag.split(',').map { |t| t.strip }
|
178
|
+
base.in_branch do |wd|
|
179
|
+
tags.each do |add_tag|
|
180
|
+
tag_filename = File.join(ticket_name, 'TAG_' + Ticket.clean_string(add_tag))
|
181
|
+
if File.exists?(tag_filename)
|
182
|
+
base.git.remove(tag_filename)
|
183
|
+
removed = true
|
184
|
+
end
|
185
|
+
end
|
186
|
+
if removed
|
187
|
+
base.git.commit("removed tags (#{tag}) from ticket #{ticket_name}")
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def path
|
193
|
+
File.join(state, ticket_name)
|
194
|
+
end
|
195
|
+
|
196
|
+
def comment_name(email)
|
197
|
+
'COMMENT_' + Time.now.to_i.to_s + '_' + email
|
198
|
+
end
|
199
|
+
|
200
|
+
def email
|
201
|
+
opts[:user_email] || 'anon'
|
202
|
+
end
|
203
|
+
|
204
|
+
def assigned_name
|
205
|
+
assigned.split('@').first rescue ''
|
206
|
+
end
|
207
|
+
|
208
|
+
def self.create_ticket_name(title)
|
209
|
+
[Time.now.to_i.to_s, Ticket.clean_string(title), rand(999).to_i.to_s].join('_')
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
end
|
214
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ticgit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.6
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Scott Chacon
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-05-10 00:00:00 -07:00
|
13
|
+
default_executable: ti
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: schacon@gmail.com
|
18
|
+
executables:
|
19
|
+
- ti
|
20
|
+
- ticgitweb
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files: []
|
24
|
+
|
25
|
+
files:
|
26
|
+
- lib/ticgit/base.rb
|
27
|
+
- lib/ticgit/cli.rb
|
28
|
+
- lib/ticgit/comment.rb
|
29
|
+
- lib/ticgit/ticket.rb
|
30
|
+
- lib/ticgit.rb
|
31
|
+
- bin/ti
|
32
|
+
- bin/ticgitweb
|
33
|
+
has_rdoc: true
|
34
|
+
homepage: http://github.com/schacon/ticgit
|
35
|
+
licenses: []
|
36
|
+
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
- bin
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: "0"
|
48
|
+
version:
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 1.3.5
|
59
|
+
signing_key:
|
60
|
+
specification_version: 2
|
61
|
+
summary: A distributed ticketing system for Git projects.
|
62
|
+
test_files: []
|
63
|
+
|