TicGit-ng 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE_GPL +681 -0
- data/LICENSE_MIT +29 -0
- data/README.mkd +425 -0
- data/bin/ti +13 -0
- data/bin/ticgitweb +313 -0
- data/lib/ticgit-ng/base.rb +365 -0
- data/lib/ticgit-ng/cli.rb +240 -0
- data/lib/ticgit-ng/command/assign.rb +43 -0
- data/lib/ticgit-ng/command/checkout.rb +14 -0
- data/lib/ticgit-ng/command/comment.rb +36 -0
- data/lib/ticgit-ng/command/list.rb +80 -0
- data/lib/ticgit-ng/command/milestone.rb +26 -0
- data/lib/ticgit-ng/command/new.rb +46 -0
- data/lib/ticgit-ng/command/points.rb +28 -0
- data/lib/ticgit-ng/command/recent.rb +29 -0
- data/lib/ticgit-ng/command/show.rb +18 -0
- data/lib/ticgit-ng/command/state.rb +45 -0
- data/lib/ticgit-ng/command/sync.rb +29 -0
- data/lib/ticgit-ng/command/tag.rb +31 -0
- data/lib/ticgit-ng/command.rb +77 -0
- data/lib/ticgit-ng/comment.rb +17 -0
- data/lib/ticgit-ng/ticket.rb +233 -0
- data/lib/ticgit-ng/version.rb +3 -0
- data/lib/ticgit-ng.rb +35 -0
- metadata +120 -0
data/bin/ticgitweb
ADDED
@@ -0,0 +1,313 @@
|
|
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
|
+
# Add the library from the source tree to the front of the load path.
|
12
|
+
# This allows ticgitweb to run without first installing a ticgit gem,
|
13
|
+
# which is important when testing multiple branches of development.
|
14
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
15
|
+
|
16
|
+
%w(rubygems sinatra git ticgit haml sass).each do |dependency|
|
17
|
+
begin
|
18
|
+
require dependency
|
19
|
+
rescue LoadError => e
|
20
|
+
puts "You need to install #{dependency} before we can proceed"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# !! TODO : if ARGV[1] is a path to a git repo, use that
|
25
|
+
# otherwise, look in ~/.ticgit
|
26
|
+
|
27
|
+
$ticgit = TicGit.open('.')
|
28
|
+
|
29
|
+
get('/_stylesheet.css') { Sass::Engine.new(File.read(__FILE__).gsub(/.*__END__/m, '')).render }
|
30
|
+
|
31
|
+
# ticket list view
|
32
|
+
get '/' do
|
33
|
+
@tickets = $ticgit.ticket_list(:order => 'date.desc')
|
34
|
+
haml(list('all'))
|
35
|
+
end
|
36
|
+
|
37
|
+
get '/fs/:state' do
|
38
|
+
@tickets = $ticgit.ticket_list(:state => params[:state], :order => 'date.desc')
|
39
|
+
haml(list(params[:state]))
|
40
|
+
end
|
41
|
+
|
42
|
+
get '/tag/:tag' do
|
43
|
+
@tickets = $ticgit.ticket_list(:tag => params[:tag], :order => 'date.desc')
|
44
|
+
haml(list(params[:tag]))
|
45
|
+
end
|
46
|
+
|
47
|
+
get '/sv/:saved_view' do
|
48
|
+
@tickets = $ticgit.ticket_list(:saved => params[:saved_view])
|
49
|
+
haml(list(params[:saved_view]))
|
50
|
+
end
|
51
|
+
|
52
|
+
# ticket single view
|
53
|
+
get '/ticket/:ticket' do
|
54
|
+
@ticket = $ticgit.ticket_show(params[:ticket])
|
55
|
+
haml(show)
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# add ticket
|
60
|
+
get '/t/new' do
|
61
|
+
haml(new_ticket)
|
62
|
+
end
|
63
|
+
|
64
|
+
# add ticket finalize
|
65
|
+
post '/t/new' do
|
66
|
+
title = params[:title].to_s.strip
|
67
|
+
if title.size > 1
|
68
|
+
tags = params[:tags].split(',').map { |t| t.strip } rescue nil
|
69
|
+
t = $ticgit.ticket_new(title, {:comment => params[:comment].strip, :tags => tags})
|
70
|
+
redirect '/ticket/' + t.ticket_id.to_s
|
71
|
+
else
|
72
|
+
redirect '/t/new'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
# add comment
|
78
|
+
post '/a/add_comment/:ticket' do
|
79
|
+
t = $ticgit.ticket_comment(params[:comment], params[:ticket])
|
80
|
+
redirect '/ticket/' + params[:ticket]
|
81
|
+
end
|
82
|
+
|
83
|
+
# add tag
|
84
|
+
post '/a/add_tags/:ticket' do
|
85
|
+
t = $ticgit.ticket_tag(params[:tags], params[:ticket])
|
86
|
+
redirect '/ticket/' + params[:ticket]
|
87
|
+
end
|
88
|
+
|
89
|
+
# change ticket state
|
90
|
+
get '/a/change_state/:ticket/:state' do
|
91
|
+
$ticgit.ticket_change(params[:state], params[:ticket])
|
92
|
+
redirect '/ticket/' + params[:ticket]
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
def layout(title, content)
|
97
|
+
@saved = $ticgit.config['list_options'].keys rescue []
|
98
|
+
%Q(
|
99
|
+
%html
|
100
|
+
%head
|
101
|
+
%title #{title}
|
102
|
+
%link{:rel => 'stylesheet', :href => '/_stylesheet.css', :type => 'text/css', :media => 'screen'}
|
103
|
+
%meta{'http-equiv' => 'Content-Type', :content => 'text/html; charset=utf-8'}
|
104
|
+
|
105
|
+
%body
|
106
|
+
#navigation
|
107
|
+
%a{:href => '/'} All
|
108
|
+
%a{:href => '/fs/open'} Open
|
109
|
+
%a{:href => '/fs/resolved'} Resolved
|
110
|
+
%a{:href => '/fs/hold'} Hold
|
111
|
+
%a{:href => '/fs/invalid'} Invalid
|
112
|
+
- if !@saved.empty?
|
113
|
+
| Saved:
|
114
|
+
- @saved.each do |s|
|
115
|
+
%a{:href => "/sv/\#{s}"}= s
|
116
|
+
#action
|
117
|
+
%a{:href => '/t/new'} New Ticket
|
118
|
+
|
119
|
+
#{content}
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
def new_ticket
|
124
|
+
layout('New Ticket', %q{
|
125
|
+
%h1 Create a New Ticket
|
126
|
+
%form{:action => '/t/new', :method => 'POST'}
|
127
|
+
%table
|
128
|
+
%tr
|
129
|
+
%th Title
|
130
|
+
%td
|
131
|
+
%input{:type => 'text', :name => 'title', :size => 30}
|
132
|
+
%tr
|
133
|
+
%th Tags
|
134
|
+
%td
|
135
|
+
%input{:name => 'tags', :size => 30}
|
136
|
+
%small (comma delimited)
|
137
|
+
%tr
|
138
|
+
%th Comment
|
139
|
+
%td
|
140
|
+
%textarea{:name => 'comment', :rows => 15, :cols => 30}
|
141
|
+
%tr
|
142
|
+
%td
|
143
|
+
%td
|
144
|
+
%input{:type => 'submit', :value => 'Create Ticket'}
|
145
|
+
})
|
146
|
+
end
|
147
|
+
|
148
|
+
def list(title = 'all')
|
149
|
+
@title = title
|
150
|
+
layout(title + ' tickets', %q{
|
151
|
+
%h1= "#{@title} tickets"
|
152
|
+
- if @tickets.empty?
|
153
|
+
%p No tickets found.
|
154
|
+
- else
|
155
|
+
%table.long
|
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&= t.title
|
163
|
+
%td{:class => t.state}= t.state
|
164
|
+
%td= t.opened.strftime("%m/%d")
|
165
|
+
%td= t.assigned_name
|
166
|
+
%td
|
167
|
+
- t.tags.each do |tag|
|
168
|
+
%a{:href => "/tag/#{tag}"}= tag
|
169
|
+
})
|
170
|
+
end
|
171
|
+
|
172
|
+
def show
|
173
|
+
layout('ticket', %q{
|
174
|
+
%center
|
175
|
+
%h1&= @ticket.title
|
176
|
+
|
177
|
+
%form{:action => "/a/add_tags/#{@ticket.ticket_id}", :method => 'POST'}
|
178
|
+
%table
|
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{: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
|
+
end
|
227
|
+
|
228
|
+
__END__
|
229
|
+
body
|
230
|
+
:font
|
231
|
+
family: Verdana, Arial, "Bitstream Vera Sans", Helvetica, sans-serif
|
232
|
+
color: black
|
233
|
+
line-height: 160%
|
234
|
+
background-color: white
|
235
|
+
margin: 2em
|
236
|
+
|
237
|
+
#navigation
|
238
|
+
a
|
239
|
+
background-color: #e0e0e0
|
240
|
+
color: black
|
241
|
+
text-decoration: none
|
242
|
+
padding: 2px
|
243
|
+
padding: 5px
|
244
|
+
border-bottom: 1px black solid
|
245
|
+
|
246
|
+
#action
|
247
|
+
text-align: right
|
248
|
+
|
249
|
+
.addtag
|
250
|
+
padding: 5px 0
|
251
|
+
|
252
|
+
h1
|
253
|
+
display: block
|
254
|
+
padding-bottom: 5px
|
255
|
+
|
256
|
+
a
|
257
|
+
color: black
|
258
|
+
a.exists
|
259
|
+
font-weight: bold
|
260
|
+
a.unknown
|
261
|
+
font-style: italic
|
262
|
+
|
263
|
+
.comments
|
264
|
+
margin: 10px 20px
|
265
|
+
.comment
|
266
|
+
.head
|
267
|
+
background: #eee
|
268
|
+
padding: 4px
|
269
|
+
.comment-text
|
270
|
+
padding: 10px
|
271
|
+
color: #333
|
272
|
+
|
273
|
+
table.long
|
274
|
+
width: 100%
|
275
|
+
|
276
|
+
table
|
277
|
+
tr.even
|
278
|
+
td
|
279
|
+
background: #eee
|
280
|
+
tr.odd
|
281
|
+
td
|
282
|
+
background: #fff
|
283
|
+
|
284
|
+
table
|
285
|
+
tr
|
286
|
+
th
|
287
|
+
text-align: left
|
288
|
+
padding: 3px
|
289
|
+
vertical-align: top
|
290
|
+
td.open
|
291
|
+
background: #ada
|
292
|
+
td.resolved
|
293
|
+
background: #abd
|
294
|
+
td.hold
|
295
|
+
background: #dda
|
296
|
+
td.invalid
|
297
|
+
background: #aaa
|
298
|
+
|
299
|
+
.submit
|
300
|
+
font-size: large
|
301
|
+
font-weight: bold
|
302
|
+
|
303
|
+
.page_title
|
304
|
+
font-size: xx-large
|
305
|
+
|
306
|
+
.edit_link
|
307
|
+
color: black
|
308
|
+
font-size: 14px
|
309
|
+
font-weight: bold
|
310
|
+
background-color: #e0e0e0
|
311
|
+
font-variant: small-caps
|
312
|
+
text-decoration: none
|
313
|
+
|
@@ -0,0 +1,365 @@
|
|
1
|
+
module TicGitNG
|
2
|
+
class NoRepoFound < StandardError;end
|
3
|
+
class Base
|
4
|
+
|
5
|
+
attr_reader :git, :logger
|
6
|
+
attr_reader :tic_working, :tic_index
|
7
|
+
attr_reader :last_tickets, :current_ticket # saved in state
|
8
|
+
attr_reader :config
|
9
|
+
attr_reader :state, :config_file
|
10
|
+
|
11
|
+
def initialize(git_dir, opts = {})
|
12
|
+
@git = Git.open(find_repo(git_dir))
|
13
|
+
@logger = opts[:logger] || Logger.new(STDOUT)
|
14
|
+
@last_tickets = []
|
15
|
+
|
16
|
+
proj = Ticket.clean_string(@git.dir.path)
|
17
|
+
|
18
|
+
@tic_dir = opts[:tic_dir] || '~/.ticgit-ng'
|
19
|
+
@tic_working = opts[:working_directory] || File.expand_path(File.join(@tic_dir, proj, 'working'))
|
20
|
+
@tic_index = opts[:index_file] || File.expand_path(File.join(@tic_dir, proj, 'index'))
|
21
|
+
|
22
|
+
# load config file
|
23
|
+
@config_file = File.expand_path(File.join(@tic_dir, proj, 'config.yml'))
|
24
|
+
if File.exists?(config_file)
|
25
|
+
@config = YAML.load(File.read(config_file))
|
26
|
+
else
|
27
|
+
@config = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
@state = File.expand_path(File.join(@tic_dir, proj, 'state'))
|
31
|
+
|
32
|
+
if File.file?(@state)
|
33
|
+
load_state
|
34
|
+
else
|
35
|
+
reset_ticgitng
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def find_repo(dir)
|
40
|
+
full = File.expand_path(dir)
|
41
|
+
ENV["GIT_WORKING_DIR"] || loop do
|
42
|
+
return full if File.directory?(File.join(full, ".git"))
|
43
|
+
raise NoRepoFound if full == full=File.dirname(full)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# marshal dump the internals
|
48
|
+
# save config file
|
49
|
+
def save_state
|
50
|
+
state_list = [@last_tickets, @current_ticket]
|
51
|
+
File.open(@state, 'w+'){|io| Marshal.dump(state_list, io) }
|
52
|
+
File.open(@config_file, 'w+'){|io| io.write(config.to_yaml) }
|
53
|
+
end
|
54
|
+
|
55
|
+
# read in the internals
|
56
|
+
def load_state
|
57
|
+
state_list = File.open(@state){|io| Marshal.load(io) }
|
58
|
+
garbage_data=nil
|
59
|
+
if state_list.length == 2
|
60
|
+
@last_tickets, @current_ticket = state_list
|
61
|
+
else
|
62
|
+
#This was left behind so that people can continue load_state-ing
|
63
|
+
#without having to delete their ~/.ticgit directory when
|
64
|
+
#updating to this version (rename to ticgit-ng)
|
65
|
+
garbage_data, @last_tickets, @current_ticket = state_list
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# returns new Ticket
|
70
|
+
def ticket_new(title, options = {})
|
71
|
+
t = TicGitNG::Ticket.create(self, title, options)
|
72
|
+
reset_ticgitng
|
73
|
+
TicGitNG::Ticket.open(self, t.ticket_name, tickets[t.ticket_name])
|
74
|
+
end
|
75
|
+
|
76
|
+
#This is a legacy function from back when ticgit-ng needed to have its
|
77
|
+
#cache reset in order to avoid cache corruption.
|
78
|
+
def reset_ticgitng
|
79
|
+
tickets
|
80
|
+
save_state
|
81
|
+
end
|
82
|
+
|
83
|
+
# returns new Ticket
|
84
|
+
def ticket_comment(comment, ticket_id = nil)
|
85
|
+
if t = ticket_revparse(ticket_id)
|
86
|
+
ticket = TicGitNG::Ticket.open(self, t, tickets[t])
|
87
|
+
ticket.add_comment(comment)
|
88
|
+
reset_ticgitng
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# returns array of Tickets
|
93
|
+
def ticket_list(options = {})
|
94
|
+
reset_ticgitng
|
95
|
+
ts = []
|
96
|
+
@last_tickets = []
|
97
|
+
@config['list_options'] ||= {}
|
98
|
+
|
99
|
+
tickets.to_a.each do |name, t|
|
100
|
+
ts << TicGitNG::Ticket.open(self, name, t)
|
101
|
+
end
|
102
|
+
|
103
|
+
if name = options[:saved]
|
104
|
+
if c = config['list_options'][name]
|
105
|
+
options = c.merge(options)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
if options[:list]
|
110
|
+
# TODO : this is a hack and i need to fix it
|
111
|
+
config['list_options'].each do |name, opts|
|
112
|
+
puts name + "\t" + opts.inspect
|
113
|
+
end
|
114
|
+
return false
|
115
|
+
end
|
116
|
+
|
117
|
+
if options.size == 0
|
118
|
+
# default list
|
119
|
+
options[:state] = 'open'
|
120
|
+
end
|
121
|
+
|
122
|
+
# :tag, :state, :assigned
|
123
|
+
if t = options[:tags]
|
124
|
+
t = {false => Set.new, true => Set.new}.merge t.classify { |x| x[0,1] != "-" }
|
125
|
+
t[false].map! { |x| x[1..-1] }
|
126
|
+
ts = ts.reject { |tic| t[true].intersection(tic.tags).empty? } unless t[true].empty?
|
127
|
+
ts = ts.select { |tic| t[false].intersection(tic.tags).empty? } unless t[false].empty?
|
128
|
+
end
|
129
|
+
if s = options[:states]
|
130
|
+
s = {false => Set.new, true => Set.new}.merge s.classify { |x| x[0,1] != "-" }
|
131
|
+
s[true].map! { |x| Regexp.new(x, Regexp::IGNORECASE) }
|
132
|
+
s[false].map! { |x| Regexp.new(x[1..-1], Regexp::IGNORECASE) }
|
133
|
+
ts = ts.select { |tic| s[true].any? { |st| tic.state =~ st } } unless s[true].empty?
|
134
|
+
ts = ts.reject { |tic| s[false].any? { |st| tic.state =~ st } } unless s[false].empty?
|
135
|
+
end
|
136
|
+
if a = options[:assigned]
|
137
|
+
ts = ts.select { |tic| tic.assigned =~ Regexp.new(a, Regexp::IGNORECASE) }
|
138
|
+
end
|
139
|
+
|
140
|
+
# SORTING
|
141
|
+
if field = options[:order]
|
142
|
+
field, type = field.split('.')
|
143
|
+
|
144
|
+
case field
|
145
|
+
when 'assigned'; ts = ts.sort_by{|a| a.assigned }
|
146
|
+
when 'state'; ts = ts.sort_by{|a| a.state }
|
147
|
+
when 'date'; ts = ts.sort_by{|a| a.opened }
|
148
|
+
when 'title'; ts = ts.sort_by{|a| a.title }
|
149
|
+
end
|
150
|
+
|
151
|
+
ts = ts.reverse if type == 'desc'
|
152
|
+
else
|
153
|
+
# default list
|
154
|
+
ts = ts.sort_by{|a| a.opened }
|
155
|
+
end
|
156
|
+
|
157
|
+
if options.size == 0
|
158
|
+
# default list
|
159
|
+
options[:state] = 'open'
|
160
|
+
end
|
161
|
+
|
162
|
+
# :tag, :state, :assigned
|
163
|
+
if t = options[:tag]
|
164
|
+
ts = ts.select { |tag| tag.tags.include?(t) }
|
165
|
+
end
|
166
|
+
if s = options[:state]
|
167
|
+
ts = ts.select { |tag| tag.state =~ /#{s}/ }
|
168
|
+
end
|
169
|
+
if a = options[:assigned]
|
170
|
+
ts = ts.select { |tag| tag.assigned =~ /#{a}/ }
|
171
|
+
end
|
172
|
+
|
173
|
+
if save = options[:save]
|
174
|
+
options.delete(:save)
|
175
|
+
@config['list_options'][save] = options
|
176
|
+
end
|
177
|
+
|
178
|
+
@last_tickets = ts.map{|t| t.ticket_name }
|
179
|
+
# :save
|
180
|
+
|
181
|
+
save_state
|
182
|
+
ts
|
183
|
+
end
|
184
|
+
|
185
|
+
# returns single Ticket
|
186
|
+
def ticket_show(ticket_id = nil)
|
187
|
+
# ticket_id can be index of last_tickets, partial sha or nil => last ticket
|
188
|
+
reset_ticgitng
|
189
|
+
if t = ticket_revparse(ticket_id)
|
190
|
+
return TicGitNG::Ticket.open(self, t, tickets[t])
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# returns recent ticgit-ng activity
|
195
|
+
# uses the git logs for this
|
196
|
+
def ticket_recent(ticket_id = nil)
|
197
|
+
if ticket_id
|
198
|
+
t = ticket_revparse(ticket_id)
|
199
|
+
return git.log.object('ticgit-ng').path(t)
|
200
|
+
else
|
201
|
+
return git.log.object('ticgit-ng')
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def ticket_revparse(ticket_id)
|
206
|
+
if ticket_id
|
207
|
+
ticket_id = ticket_id.strip
|
208
|
+
|
209
|
+
if /^[0-9]*$/ =~ ticket_id
|
210
|
+
if t = @last_tickets[ticket_id.to_i - 1]
|
211
|
+
return t
|
212
|
+
end
|
213
|
+
else # partial or full sha
|
214
|
+
regex = /^#{Regexp.escape(ticket_id)}/
|
215
|
+
ch = tickets.select{|name, t|
|
216
|
+
t['files'].assoc('TICKET_ID')[1] =~ regex }
|
217
|
+
ch.first[0] if ch.first
|
218
|
+
end
|
219
|
+
elsif(@current_ticket)
|
220
|
+
return @current_ticket
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def ticket_tag(tag, ticket_id = nil, options = OpenStruct.new)
|
225
|
+
if t = ticket_revparse(ticket_id)
|
226
|
+
ticket = TicGitNG::Ticket.open(self, t, tickets[t])
|
227
|
+
if options.remove
|
228
|
+
ticket.remove_tag(tag)
|
229
|
+
else
|
230
|
+
ticket.add_tag(tag)
|
231
|
+
end
|
232
|
+
reset_ticgitng
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def ticket_change(new_state, ticket_id = nil)
|
237
|
+
if t = ticket_revparse(ticket_id)
|
238
|
+
if tic_states.include?(new_state)
|
239
|
+
ticket = TicGitNG::Ticket.open(self, t, tickets[t])
|
240
|
+
ticket.change_state(new_state)
|
241
|
+
reset_ticgitng
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def ticket_assign(new_assigned = nil, ticket_id = nil)
|
247
|
+
if t = ticket_revparse(ticket_id)
|
248
|
+
ticket = TicGitNG::Ticket.open(self, t, tickets[t])
|
249
|
+
ticket.change_assigned(new_assigned)
|
250
|
+
reset_ticgitng
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def ticket_points(new_points = nil, ticket_id = nil)
|
255
|
+
if t = ticket_revparse(ticket_id)
|
256
|
+
ticket = TicGitNG::Ticket.open(self, t, tickets[t])
|
257
|
+
ticket.change_points(new_points)
|
258
|
+
reset_ticgitng
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def ticket_checkout(ticket_id)
|
263
|
+
if t = ticket_revparse(ticket_id)
|
264
|
+
ticket = TicGitNG::Ticket.open(self, t, tickets[t])
|
265
|
+
@current_ticket = ticket.ticket_name
|
266
|
+
save_state
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def comment_add(ticket_id, comment, options = {})
|
271
|
+
end
|
272
|
+
|
273
|
+
def comment_list(ticket_id)
|
274
|
+
end
|
275
|
+
|
276
|
+
def tic_states
|
277
|
+
['open', 'resolved', 'invalid', 'hold']
|
278
|
+
end
|
279
|
+
|
280
|
+
def sync_tickets(repo='origin', push=true, verbose=true )
|
281
|
+
in_branch(false) do
|
282
|
+
repo_g=git.remote(repo)
|
283
|
+
git.pull(repo_g, repo+'/ticgit-ng')
|
284
|
+
git.push(repo_g, 'ticgit-ng:ticgit-ng') if push
|
285
|
+
puts "Tickets synchronized." if verbose
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def tickets
|
290
|
+
read_tickets
|
291
|
+
end
|
292
|
+
|
293
|
+
def read_tickets
|
294
|
+
tickets = {}
|
295
|
+
|
296
|
+
bs = git.lib.branches_all.map{|b| b.first }
|
297
|
+
|
298
|
+
unless bs.include?('ticgit-ng') && File.directory?(@tic_working)
|
299
|
+
init_ticgitng_branch(bs.include?('ticgit-ng'))
|
300
|
+
end
|
301
|
+
|
302
|
+
tree = git.lib.full_tree('ticgit-ng')
|
303
|
+
tree.each do |t|
|
304
|
+
data, file = t.split("\t")
|
305
|
+
mode, type, sha = data.split(" ")
|
306
|
+
tic = file.split('/')
|
307
|
+
if tic.size == 2 # directory depth
|
308
|
+
ticket, info = tic
|
309
|
+
tickets[ticket] ||= { 'files' => [] }
|
310
|
+
tickets[ticket]['files'] << [info, sha]
|
311
|
+
end
|
312
|
+
end
|
313
|
+
tickets
|
314
|
+
end
|
315
|
+
|
316
|
+
def init_ticgitng_branch(ticgitng_branch = false)
|
317
|
+
@logger.info 'creating ticgit-ng repo branch'
|
318
|
+
|
319
|
+
in_branch(ticgitng_branch) do
|
320
|
+
#The .hold file seems to have little to no purpose aside from helping
|
321
|
+
#figure out if the branch should be checked out or not. It is created
|
322
|
+
#when the ticgit branch is created, and seems to exist for the lifetime
|
323
|
+
#of the ticgit branch. The purpose seems to be, to be able to tell if
|
324
|
+
#the ticgit branch is already checked out and not check it out again if
|
325
|
+
#it is. This might be superfluous after switching to grit.
|
326
|
+
new_file('.hold', 'hold')
|
327
|
+
|
328
|
+
unless ticgitng_branch
|
329
|
+
git.add
|
330
|
+
git.commit('creating the ticgit-ng branch')
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# temporarlily switches to ticgit branch for tic work
|
336
|
+
def in_branch(branch_exists = true)
|
337
|
+
needs_checkout = false
|
338
|
+
|
339
|
+
unless File.directory?(@tic_working)
|
340
|
+
FileUtils.mkdir_p(@tic_working)
|
341
|
+
needs_checkout = true
|
342
|
+
end
|
343
|
+
|
344
|
+
needs_checkout = true unless File.file?('.hold')
|
345
|
+
|
346
|
+
old_current = git.lib.branch_current
|
347
|
+
begin
|
348
|
+
git.lib.change_head_branch('ticgit-ng')
|
349
|
+
git.with_index(@tic_index) do
|
350
|
+
git.with_working(@tic_working) do |wd|
|
351
|
+
git.lib.checkout('ticgit-ng') if needs_checkout && branch_exists
|
352
|
+
yield wd
|
353
|
+
end
|
354
|
+
end
|
355
|
+
ensure
|
356
|
+
git.lib.change_head_branch(old_current)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
def new_file(name, contents)
|
361
|
+
File.open(name, 'w+'){|f| f.puts(contents) }
|
362
|
+
end
|
363
|
+
|
364
|
+
end
|
365
|
+
end
|