TicGit-ng 1.0.0
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/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
|