TicGit-ng 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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