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 +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
|
+
|