git-trac 0.0.20080205 → 0.0.20080206
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/git/trac/attachment.rb +11 -24
- data/lib/git/trac/pager.rb +2 -0
- data/lib/git/trac/patch.rb +18 -16
- data/lib/git/trac/runner.rb +17 -229
- data/lib/git/trac/runner/apply.rb +65 -0
- data/lib/git/trac/runner/cleanup.rb +44 -0
- data/lib/git/trac/runner/download.rb +44 -0
- data/lib/git/trac/runner/fetch.rb +63 -0
- data/lib/git/trac/runner/show.rb +45 -0
- data/lib/git/trac/runner/upload_patch.rb +62 -0
- data/lib/git/trac/ticket.rb +34 -7
- data/test/attachment_test.rb +31 -0
- metadata +9 -2
data/lib/git/trac/attachment.rb
CHANGED
@@ -6,33 +6,20 @@ module Git
|
|
6
6
|
attr_reader :ticket, :filename, :user, :time
|
7
7
|
attr_accessor :description
|
8
8
|
|
9
|
-
def initialize(ticket, filename, user, time)
|
9
|
+
def initialize(ticket, filename, user, time, description)
|
10
10
|
@ticket, @filename, @user, @time = ticket, filename, user, time
|
11
|
+
@description = description
|
11
12
|
end
|
12
13
|
|
13
|
-
def self.
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
element.traverse_text {|t| str << t.to_s}
|
22
|
-
last.description = str if last
|
23
|
-
elsif element.name = "dt"
|
24
|
-
texts = []
|
25
|
-
element.traverse_text {|x| texts << x.to_s}
|
26
|
-
if texts.first =~ /./
|
27
|
-
time = Time.parse("#{texts[3]} +0000").utc
|
28
|
-
collection << new(ticket, texts[0], texts[2], time)
|
29
|
-
last = collection.last
|
30
|
-
else
|
31
|
-
last = nil
|
32
|
-
end
|
33
|
-
end
|
14
|
+
def self.from_html(ticket, html)
|
15
|
+
require 'time'
|
16
|
+
unesc = lambda do |string|
|
17
|
+
string.gsub(/&(\w+);/) { {"lt"=>"<","rt"=>">","amp"=>"&"}[$1]||$& }
|
18
|
+
end
|
19
|
+
html.scan(%r{<dt><a .*?>(.*?)</a>.*? <em>(.*?)</em>\s*on\s+(.*?)\.</dt>\s*(?:<dd>(.*?)</dd>)?}).map do |(filename, user, time, desc)|
|
20
|
+
time = Time.parse("#{time} +0000").utc
|
21
|
+
new(ticket, unesc[filename], unesc[user], time, desc && unesc[desc])
|
34
22
|
end
|
35
|
-
collection
|
36
23
|
end
|
37
24
|
|
38
25
|
def extension
|
@@ -103,7 +90,7 @@ module Git
|
|
103
90
|
repository.with_index("tracindex#{$$}") do
|
104
91
|
parent = repository.exec("git","rev-list","--max-count=1","--before=#{timestamp}",options[:branch] || 'trunk').chomp
|
105
92
|
repository.exec("git","read-tree",parent)
|
106
|
-
applied = patch.apply(options) or return [nil,applied]
|
93
|
+
applied = patch.apply(options.merge(:cached => true)) or return [nil,applied]
|
107
94
|
tree = repository.exec("git","write-tree").chomp
|
108
95
|
ENV["GIT_AUTHOR_NAME"] = ENV["GIT_COMMITTER_NAME"] = username
|
109
96
|
ENV["GIT_AUTHOR_EMAIL"] = ENV["GIT_COMMITTER_EMAIL"] = email
|
data/lib/git/trac/pager.rb
CHANGED
data/lib/git/trac/patch.rb
CHANGED
@@ -10,24 +10,20 @@ module Git
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def body
|
13
|
-
@body.respond_to?(:call) ? @body.call : body
|
13
|
+
@body.respond_to?(:call) ? @body.call : @body
|
14
14
|
end
|
15
15
|
|
16
16
|
alias to_s body
|
17
17
|
|
18
|
-
def
|
19
|
-
|
20
|
-
1
|
21
|
-
else
|
22
|
-
0
|
23
|
-
end
|
18
|
+
def git?
|
19
|
+
body[0,10] == "diff --git" || body[0,6] == "--- a/"
|
24
20
|
end
|
25
21
|
|
26
22
|
# Rewrite filenames in a patch to be relative to +root+. For each .. at
|
27
23
|
# the end of +root+, a path is stripped off the original filename. The
|
28
24
|
# rewritten patch is returned as a string.
|
29
25
|
def with_root(root = nil)
|
30
|
-
if root && !root.empty? &&
|
26
|
+
if root && !root.empty? && !git?
|
31
27
|
patch = ""
|
32
28
|
body.each_line do |line|
|
33
29
|
line.sub!(/^([+-]{3} |Index: )(?!\/dev\/null)(.*)$/) do
|
@@ -46,7 +42,9 @@ module Git
|
|
46
42
|
end
|
47
43
|
|
48
44
|
def apply(options = {})
|
49
|
-
|
45
|
+
command = %W(git apply -p#{git? ? 1 : 0} --whitespace=nowarn)
|
46
|
+
command << "--cached" if options[:cached]
|
47
|
+
repository.popen3(*command) do |inn,out,err|
|
50
48
|
inn.puts with_root(options[:root])
|
51
49
|
inn.close
|
52
50
|
if err.read.empty?
|
@@ -57,27 +55,31 @@ module Git
|
|
57
55
|
end
|
58
56
|
end
|
59
57
|
end
|
60
|
-
if options[:depth].to_i > 0 &&
|
58
|
+
if options[:depth].to_i > 0 && !git?
|
61
59
|
repository.in_work_tree do
|
62
60
|
last_roots = [options[:root]]
|
63
|
-
|
64
|
-
1.upto(
|
65
|
-
parent = File.join(*(%w(..)*d+[
|
66
|
-
result = apply(
|
61
|
+
opts = options.dup
|
62
|
+
1.upto(opts.delete(:depth).to_i) do |d|
|
63
|
+
parent = File.join(*(%w(..)*d+[opts[:root]]).compact)
|
64
|
+
result = apply(opts.merge(:root => parent)) and return result
|
67
65
|
roots = []
|
68
66
|
last_roots.each do |root|
|
69
67
|
Dir.entries(root || ".").each do |dir|
|
70
68
|
path = File.join(*[root,dir].compact)
|
71
69
|
next if dir =~ /\A\./ || !File.directory?(path)
|
72
70
|
roots << path
|
73
|
-
result = apply(
|
71
|
+
result = apply(opts.merge(:root => path)) and return result
|
74
72
|
end
|
75
73
|
end
|
76
74
|
last_roots = roots
|
77
75
|
end
|
78
76
|
end
|
79
77
|
end
|
80
|
-
|
78
|
+
if body.include?("\r")
|
79
|
+
Patch.new(@repository, body.delete("\r")).apply(options)
|
80
|
+
else
|
81
|
+
false
|
82
|
+
end
|
81
83
|
end
|
82
84
|
|
83
85
|
end
|
data/lib/git/trac/runner.rb
CHANGED
@@ -29,6 +29,7 @@ module Git
|
|
29
29
|
Usage: git-trac <command> [options] [arguments]
|
30
30
|
|
31
31
|
Available commands:
|
32
|
+
apply Apply a patch directly to the work tree
|
32
33
|
cleanup Remove old branches for a ticket
|
33
34
|
download Download all patches for a ticket to the cwd
|
34
35
|
fetch Create brances for all patches of a ticket
|
@@ -141,145 +142,18 @@ Available commands:
|
|
141
142
|
end
|
142
143
|
end
|
143
144
|
|
144
|
-
def
|
145
|
+
def fetch_unless_local
|
145
146
|
unless options[:local]
|
146
|
-
@repository.
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
end
|
155
|
-
|
156
|
-
class Download < Base #:nodoc:
|
157
|
-
|
158
|
-
def description
|
159
|
-
<<-EOF
|
160
|
-
Download all attachments that look like patches to the current working
|
161
|
-
directory.
|
162
|
-
EOF
|
163
|
-
end
|
164
|
-
|
165
|
-
def banner_arguments
|
166
|
-
"[options] [ticket[/filename]] ..."
|
167
|
-
end
|
168
|
-
|
169
|
-
def add_options(opts)
|
170
|
-
require_ticket_number
|
171
|
-
opts.separator("Options:")
|
172
|
-
opts.on("--root DIR","prefix patch paths with DIR") do |dir|
|
173
|
-
options[:root] = dir
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
def run
|
178
|
-
each_ticket_argument do |number, filename|
|
179
|
-
@repository.ticket(number).attachments.each do |attach|
|
180
|
-
next if filename && attach.filename != filename
|
181
|
-
File.open(attach.filename, "w") do |f|
|
182
|
-
f.puts attach.patch.with_root(options[:root])
|
183
|
-
end
|
184
|
-
end
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
end
|
189
|
-
|
190
|
-
class Show < Base #:nodoc:
|
191
|
-
|
192
|
-
def banner_arguments
|
193
|
-
"[ticket[/filename]]"
|
194
|
-
end
|
195
|
-
|
196
|
-
def description
|
197
|
-
<<-EOF
|
198
|
-
Show a crude ticket summary or a patch.
|
199
|
-
EOF
|
200
|
-
end
|
201
|
-
|
202
|
-
def add_options(opts)
|
203
|
-
require_ticket_number
|
204
|
-
end
|
205
|
-
|
206
|
-
def run
|
207
|
-
each_ticket_argument do |number, filename|
|
208
|
-
ticket = @repository.ticket(number)
|
209
|
-
attachments = ticket.attachments
|
210
|
-
if filename
|
211
|
-
attachment = attachments.detect {|a|a.filename == filename}
|
212
|
-
abort "no such attachment" unless attachment
|
213
|
-
@repository.pager(attachment.body, true)
|
147
|
+
if @repository.config("svn-remote.svn")
|
148
|
+
command = "git svn fetch"
|
149
|
+
elsif @repository.config("origin")
|
150
|
+
command = "git fetch origin"
|
214
151
|
else
|
215
|
-
|
216
|
-
csv = ticket.csv
|
217
|
-
csv.reject {|k,v| k == "description"}.sort.each do |(k,v)|
|
218
|
-
body << "#{k}: #{v}\n" if v
|
219
|
-
end
|
220
|
-
body << "attachments:\n"
|
221
|
-
attachments.each do |attachment|
|
222
|
-
body << "#{number}/#{attachment.filename}"
|
223
|
-
body << " (#{attachment.description})" if attachment.description
|
224
|
-
body << "\n"
|
225
|
-
end
|
226
|
-
body << "description:\n"
|
227
|
-
body << csv["description"]
|
228
|
-
body << "\n" unless body =~ /\n\z/
|
229
|
-
@repository.pager(body)
|
230
|
-
end
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
|
-
end
|
235
|
-
|
236
|
-
class Fetch < Base #:nodoc:
|
237
|
-
|
238
|
-
def banner_arguments
|
239
|
-
"[options] [ticket[/filename]] ..."
|
240
|
-
end
|
241
|
-
|
242
|
-
def description
|
243
|
-
<<-EOF
|
244
|
-
Download all branches that look like patches. For each patch, find the
|
245
|
-
revision of trunk that most recently proceeds the the time of upload, apply
|
246
|
-
the patch to it, create a new commit, and add a remote head of the form
|
247
|
-
refs/remotes/trac/ticketnumber/file_name.ext. For each unique base name
|
248
|
-
(filename without extension), a branch is created pointing to the newest
|
249
|
-
patch. Existing branches will not be overridden, but there is an implied
|
250
|
-
`git cleanup <patch>` that runs beforehand which could potentially remove
|
251
|
-
conflicting branches first.
|
252
|
-
EOF
|
253
|
-
end
|
254
|
-
|
255
|
-
def add_options(opts)
|
256
|
-
require_ticket_number
|
257
|
-
opts.on("--branch BRANCH","apply against branch BRANCH") do |b|
|
258
|
-
options[:branch] = b
|
259
|
-
end
|
260
|
-
opts.on("--depth NUM","search NUM directories deep for a root") do |n|
|
261
|
-
options[:depth] = n
|
262
|
-
end
|
263
|
-
opts.on("--root DIR","apply patches relative to DIR") do |dir|
|
264
|
-
options[:root] = dir
|
265
|
-
end
|
266
|
-
add_local_option(opts)
|
267
|
-
end
|
268
|
-
|
269
|
-
def run
|
270
|
-
svn_fetch_unless_local
|
271
|
-
each_ticket_argument do |number, filename|
|
272
|
-
loop_opts = options.dup
|
273
|
-
if filename
|
274
|
-
loop_opts[:filter] = "\\A#{Regexp.escape(filename)}"
|
152
|
+
return
|
275
153
|
end
|
276
|
-
@repository.
|
277
|
-
|
278
|
-
|
279
|
-
elsif dir
|
280
|
-
puts "#{attachment.tag_name} (#{dir})"
|
281
|
-
else
|
282
|
-
$stderr.puts "#{attachment.tag_name} FAILED"
|
154
|
+
@repository.in_work_tree do
|
155
|
+
unless system(command)
|
156
|
+
abort "#{command} failed (use --local to skip)"
|
283
157
|
end
|
284
158
|
end
|
285
159
|
end
|
@@ -287,99 +161,13 @@ conflicting branches first.
|
|
287
161
|
|
288
162
|
end
|
289
163
|
|
290
|
-
class Cleanup < Base #:nodoc:
|
291
|
-
|
292
|
-
def banner_arguments
|
293
|
-
"[options] [ticket[/filename]] ..."
|
294
|
-
end
|
295
|
-
|
296
|
-
def description
|
297
|
-
<<-EOF
|
298
|
-
Remove remote heads for a given ticket (e.g., trac/12345/work_patch). Also
|
299
|
-
removes branches that point to one of these heads. Branches that have been
|
300
|
-
committed to will not be removed. The default is to target tickets that have
|
301
|
-
been closed, but you can also specify ticket numbers explicitly or use --all.
|
302
|
-
EOF
|
303
|
-
end
|
304
|
-
|
305
|
-
def add_options(opts)
|
306
|
-
opts.separator("Options:")
|
307
|
-
opts.on("-a","--all", "cleanup all tickets") { options[:all] = true }
|
308
|
-
end
|
309
|
-
|
310
|
-
def run
|
311
|
-
if options[:all]
|
312
|
-
@repository.working_tickets.each do |t|
|
313
|
-
t.cleanup
|
314
|
-
end
|
315
|
-
elsif @argv.any?
|
316
|
-
each_ticket_argument do |number, filename|
|
317
|
-
@repository.ticket(number).cleanup(:attachment => filename)
|
318
|
-
end
|
319
|
-
else
|
320
|
-
@repository.working_tickets.each do |t|
|
321
|
-
t.cleanup unless t.open?
|
322
|
-
end
|
323
|
-
end
|
324
|
-
end
|
325
|
-
end
|
326
|
-
|
327
|
-
class UploadPatch < Base #:nodoc:
|
328
|
-
|
329
|
-
def banner_arguments
|
330
|
-
"[options] [ticket]"
|
331
|
-
end
|
332
|
-
|
333
|
-
def description
|
334
|
-
<<-EOF
|
335
|
-
Do a `git diff` against trunk (or another branch) and upload the result as an
|
336
|
-
attachment to a ticket. This command is experimental to take care when using
|
337
|
-
it against a production trac server.
|
338
|
-
EOF
|
339
|
-
end
|
340
|
-
|
341
|
-
def add_options(opts)
|
342
|
-
require_ticket_number
|
343
|
-
opts.on("--branch BRANCH", "git diff BRANCH (default trunk...HEAD)") do |b|
|
344
|
-
options[:branch] = b
|
345
|
-
end
|
346
|
-
opts.on("--description TEXT", "use TEXT as description") do |text|
|
347
|
-
options[:description] = text
|
348
|
-
end
|
349
|
-
opts.on("--[no-]force", "do not prompt before uploading") do |force|
|
350
|
-
options[:force] = force
|
351
|
-
end
|
352
|
-
add_local_option(opts)
|
353
|
-
end
|
354
|
-
|
355
|
-
def run
|
356
|
-
number = get_ticket_number
|
357
|
-
svn_fetch_unless_local
|
358
|
-
ticket = @repository.ticket(number)
|
359
|
-
if $stdin.tty? && !options[:force]
|
360
|
-
block = lambda do
|
361
|
-
@repository.in_work_tree do
|
362
|
-
system("git","diff", options[:branch] || "trunk...HEAD")
|
363
|
-
end
|
364
|
-
description = "##{number} (#{ticket.csv["summary"]}"
|
365
|
-
cols = ENV["COLUMNS"].to_i
|
366
|
-
cols = 80 if cols.zero?
|
367
|
-
description.sub!(/^(.{#{cols-22}}).{4,}/,"\\1...")
|
368
|
-
print "#{description}) Proceed? [yN] "
|
369
|
-
$stdin.gets[0,1] == "y"
|
370
|
-
end
|
371
|
-
else
|
372
|
-
block = lambda { true }
|
373
|
-
end
|
374
|
-
if uri = ticket.upload_patch(options,&block)
|
375
|
-
puts uri
|
376
|
-
else
|
377
|
-
exit 1
|
378
|
-
end
|
379
|
-
end
|
380
|
-
end
|
381
|
-
|
382
164
|
end
|
383
|
-
|
384
165
|
end
|
385
166
|
end
|
167
|
+
|
168
|
+
require 'git/trac/runner/apply'
|
169
|
+
require 'git/trac/runner/cleanup'
|
170
|
+
require 'git/trac/runner/download'
|
171
|
+
require 'git/trac/runner/fetch'
|
172
|
+
require 'git/trac/runner/show'
|
173
|
+
require 'git/trac/runner/upload_patch'
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Git
|
2
|
+
module Trac
|
3
|
+
class Runner
|
4
|
+
|
5
|
+
class Apply < Base #:nodoc:
|
6
|
+
|
7
|
+
def banner_arguments
|
8
|
+
"[ticket[/filename]]"
|
9
|
+
end
|
10
|
+
|
11
|
+
def description
|
12
|
+
<<-EOF
|
13
|
+
Apply a patch directly to the work tree. This command is a building block used
|
14
|
+
by other, more powerful commands and most users will never need to invoke it
|
15
|
+
directly.
|
16
|
+
|
17
|
+
The argument can be either a "ticket/filename" specifiying the patch to apply,
|
18
|
+
or simply ticket, in which case the tickets's last patch is applied. If no
|
19
|
+
argument is given, the patch is read from stdin.
|
20
|
+
|
21
|
+
The --depth option specifies how many directories deep to recursively attempt
|
22
|
+
to apply the patch in. For a depth of 2, the patch will be attempted in the
|
23
|
+
repositority root, foo, foo/bar, but not foo/baz. It will also try stripping
|
24
|
+
off up to 2 directories off of the paths inside the patch, the same as -p0,
|
25
|
+
-p1, and -p2 with patch(1). The option is ignored if the patch appears to have
|
26
|
+
been generated with git. The default value is 0, but this can be changed with
|
27
|
+
the trac.depth configuration option.
|
28
|
+
|
29
|
+
If the patch fails to apply even after a depth search, and the patch contains
|
30
|
+
carriage returns, a second pass is made with those carriage returns stripped.
|
31
|
+
EOF
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_options(opts)
|
35
|
+
require_ticket_number
|
36
|
+
opts.on("--depth NUM","search NUM directories deep for a root") do |n|
|
37
|
+
options[:depth] = n
|
38
|
+
end
|
39
|
+
opts.on("--root DIR","apply patches relative to DIR") do |dir|
|
40
|
+
options[:root] = dir
|
41
|
+
end
|
42
|
+
add_local_option(opts)
|
43
|
+
end
|
44
|
+
|
45
|
+
def run
|
46
|
+
if @argv.empty?
|
47
|
+
patch = Patch.new(@repository,$stdin.read)
|
48
|
+
elsif @argv.size > 1
|
49
|
+
abort "too many arguments"
|
50
|
+
else
|
51
|
+
each_ticket_argument do |number, filename|
|
52
|
+
patch = @repository.ticket(number).attachment(filename).patch
|
53
|
+
end
|
54
|
+
end
|
55
|
+
abort "no changes" if patch.body.empty?
|
56
|
+
unless patch.apply(options)
|
57
|
+
abort "patch failed to apply"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Git
|
2
|
+
module Trac
|
3
|
+
class Runner
|
4
|
+
|
5
|
+
class Cleanup < Base #:nodoc:
|
6
|
+
|
7
|
+
def banner_arguments
|
8
|
+
"[options] [ticket[/filename]] ..."
|
9
|
+
end
|
10
|
+
|
11
|
+
def description
|
12
|
+
<<-EOF
|
13
|
+
Remove remote heads for a given ticket (e.g., trac/12345/work_patch). Also
|
14
|
+
removes branches that point to one of these heads. Branches that have been
|
15
|
+
committed to will not be removed. The default is to target tickets that have
|
16
|
+
been closed, but you can also specify ticket numbers explicitly or use --all.
|
17
|
+
EOF
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_options(opts)
|
21
|
+
opts.separator("Options:")
|
22
|
+
opts.on("-a","--all", "cleanup all tickets") { options[:all] = true }
|
23
|
+
end
|
24
|
+
|
25
|
+
def run
|
26
|
+
if options[:all]
|
27
|
+
@repository.working_tickets.each do |t|
|
28
|
+
t.cleanup
|
29
|
+
end
|
30
|
+
elsif @argv.any?
|
31
|
+
each_ticket_argument do |number, filename|
|
32
|
+
@repository.ticket(number).cleanup(:attachment => filename)
|
33
|
+
end
|
34
|
+
else
|
35
|
+
@repository.working_tickets.each do |t|
|
36
|
+
t.cleanup unless t.open?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Git
|
2
|
+
module Trac
|
3
|
+
class Runner
|
4
|
+
|
5
|
+
class Download < Base #:nodoc:
|
6
|
+
|
7
|
+
def description
|
8
|
+
<<-EOF
|
9
|
+
Download all attachments that look like patches to the current working
|
10
|
+
directory.
|
11
|
+
|
12
|
+
This command is a candidate for removal. Individual patches can be downloaded
|
13
|
+
by redirecting git-trac show to a file.
|
14
|
+
EOF
|
15
|
+
end
|
16
|
+
|
17
|
+
def banner_arguments
|
18
|
+
"[options] [ticket[/filename]] ..."
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_options(opts)
|
22
|
+
require_ticket_number
|
23
|
+
opts.separator("Options:")
|
24
|
+
opts.on("--root DIR","prefix patch paths with DIR") do |dir|
|
25
|
+
options[:root] = dir
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def run
|
30
|
+
each_ticket_argument do |number, filename|
|
31
|
+
@repository.ticket(number).attachments.each do |attach|
|
32
|
+
next if filename && attach.filename != filename
|
33
|
+
File.open(attach.filename, "w") do |f|
|
34
|
+
f.puts attach.patch.with_root(options[:root])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Git
|
2
|
+
module Trac
|
3
|
+
class Runner
|
4
|
+
|
5
|
+
class Fetch < Base #:nodoc:
|
6
|
+
|
7
|
+
def banner_arguments
|
8
|
+
"[options] [ticket[/filename]] ..."
|
9
|
+
end
|
10
|
+
|
11
|
+
def description
|
12
|
+
<<-EOF
|
13
|
+
Download all branches that look like patches. For each patch, find the
|
14
|
+
revision of trunk that most recently proceeds the the time of upload, apply
|
15
|
+
the patch to it, create a new commit, and add a remote head of the form
|
16
|
+
refs/remotes/trac/ticketnumber/file_name.ext.
|
17
|
+
|
18
|
+
For each unique base name (filename without extension), a branch is created
|
19
|
+
pointing to the newest patch. Existing branches will not be overridden, but
|
20
|
+
there is an implied `git cleanup <patch>` that runs beforehand which could
|
21
|
+
potentially remove conflicting branches first. Automatic branch creation is
|
22
|
+
informally deprecated and may be removed in a future release.
|
23
|
+
EOF
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_options(opts)
|
27
|
+
require_ticket_number
|
28
|
+
opts.on("--branch BRANCH","apply against branch BRANCH") do |b|
|
29
|
+
options[:branch] = b
|
30
|
+
end
|
31
|
+
opts.on("--depth NUM","search depth (see git-trac help apply)") do |n|
|
32
|
+
options[:depth] = n
|
33
|
+
end
|
34
|
+
opts.on("--root DIR","apply patches relative to DIR") do |dir|
|
35
|
+
options[:root] = dir
|
36
|
+
end
|
37
|
+
add_local_option(opts)
|
38
|
+
end
|
39
|
+
|
40
|
+
def run
|
41
|
+
fetch_unless_local
|
42
|
+
each_ticket_argument do |number, filename|
|
43
|
+
loop_opts = options.dup
|
44
|
+
if filename
|
45
|
+
loop_opts[:filter] = "\\A#{Regexp.escape(filename)}"
|
46
|
+
end
|
47
|
+
@repository.ticket(number).fetch(loop_opts) do |attachment, dir|
|
48
|
+
if dir == "."
|
49
|
+
puts "#{attachment.tag_name}"
|
50
|
+
elsif dir
|
51
|
+
puts "#{attachment.tag_name} (#{dir})"
|
52
|
+
else
|
53
|
+
$stderr.puts "#{attachment.tag_name} FAILED"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Git
|
2
|
+
module Trac
|
3
|
+
class Runner
|
4
|
+
|
5
|
+
class Show < Base #:nodoc:
|
6
|
+
|
7
|
+
def banner_arguments
|
8
|
+
"[ticket[/filename]]"
|
9
|
+
end
|
10
|
+
|
11
|
+
def description
|
12
|
+
<<-EOF
|
13
|
+
Show the given ticket attachment. The patch will be highlighted in a manner
|
14
|
+
If the filename is omitted, show a list of attachments for the ticket.
|
15
|
+
|
16
|
+
Attachments can be downloaded by redirecting to a file. The following example
|
17
|
+
shows how one might download all patches for a given ticket:
|
18
|
+
|
19
|
+
for patch in $(git-trac show 123); do
|
20
|
+
git-trac show $patch > $(basename $patch)
|
21
|
+
done
|
22
|
+
EOF
|
23
|
+
end
|
24
|
+
|
25
|
+
def run
|
26
|
+
each_ticket_argument do |number, filename|
|
27
|
+
ticket = @repository.ticket(number)
|
28
|
+
if filename
|
29
|
+
attachment = ticket.attachment(filename)
|
30
|
+
@repository.pager(attachment.body, filename =~ /\.(diff|patch)$/)
|
31
|
+
else
|
32
|
+
body = ticket.attachments.map do |attachment|
|
33
|
+
"#{number}/#{attachment.filename}\n"
|
34
|
+
end.join
|
35
|
+
exit(1) if body.empty?
|
36
|
+
@repository.pager(body)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Git
|
2
|
+
module Trac
|
3
|
+
class Runner
|
4
|
+
|
5
|
+
class UploadPatch < Base #:nodoc:
|
6
|
+
|
7
|
+
def banner_arguments
|
8
|
+
"[options] [ticket]"
|
9
|
+
end
|
10
|
+
|
11
|
+
def description
|
12
|
+
<<-EOF
|
13
|
+
Do a `git diff` against trunk (or another branch) and upload the result as an
|
14
|
+
attachment to a ticket. The potential patch will be shown in a pager and you
|
15
|
+
will be given the opportunity to cancel.
|
16
|
+
EOF
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_options(opts)
|
20
|
+
require_ticket_number
|
21
|
+
opts.on("--branch BRANCH", "git diff BRANCH (default trunk...HEAD)") do |b|
|
22
|
+
options[:branch] = b
|
23
|
+
end
|
24
|
+
opts.on("--description TEXT", "use TEXT as description") do |text|
|
25
|
+
options[:description] = text
|
26
|
+
end
|
27
|
+
opts.on("--[no-]force", "do not prompt before uploading") do |force|
|
28
|
+
options[:force] = force
|
29
|
+
end
|
30
|
+
add_local_option(opts)
|
31
|
+
end
|
32
|
+
|
33
|
+
def run
|
34
|
+
number = get_ticket_number
|
35
|
+
fetch_unless_local
|
36
|
+
ticket = @repository.ticket(number)
|
37
|
+
if $stdin.tty? && !options[:force]
|
38
|
+
block = lambda do
|
39
|
+
@repository.in_work_tree do
|
40
|
+
system("git","diff", options[:branch] || "trunk...HEAD")
|
41
|
+
end
|
42
|
+
description = "##{number} (#{ticket.csv["summary"]}"
|
43
|
+
cols = ENV["COLUMNS"].to_i
|
44
|
+
cols = 80 if cols.zero?
|
45
|
+
description.sub!(/^(.{#{cols-22}}).{4,}/,"\\1...")
|
46
|
+
print "#{description}) Proceed? [yN] "
|
47
|
+
$stdin.gets[0,1] == "y"
|
48
|
+
end
|
49
|
+
else
|
50
|
+
block = lambda { true }
|
51
|
+
end
|
52
|
+
if uri = ticket.upload_patch(options,&block)
|
53
|
+
puts uri
|
54
|
+
else
|
55
|
+
exit 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/git/trac/ticket.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'hpricot'
|
2
1
|
require 'fileutils'
|
3
2
|
|
4
3
|
module Git
|
@@ -25,10 +24,17 @@ module Git
|
|
25
24
|
"#{@repository.url}/attachment/ticket/#{@number}"
|
26
25
|
end
|
27
26
|
|
28
|
-
def
|
27
|
+
def get_response(uri)
|
29
28
|
require 'net/http'
|
30
29
|
require 'uri'
|
31
|
-
|
30
|
+
Net::HTTP.get_response(URI.parse(uri))
|
31
|
+
end
|
32
|
+
|
33
|
+
def csv
|
34
|
+
response = get_response(url(:tab))
|
35
|
+
no_such_ticket if response.kind_of?(Net::HTTPInternalServerError)
|
36
|
+
response.error! unless response.kind_of?(Net::HTTPSuccess)
|
37
|
+
body = response.body
|
32
38
|
headers, values = body.split(/\r?\n/).map do |line|
|
33
39
|
line.split("\t").map do |column|
|
34
40
|
column.gsub(/\\(.)/) do
|
@@ -46,10 +52,25 @@ module Git
|
|
46
52
|
|
47
53
|
attr_reader :number
|
48
54
|
|
49
|
-
def attachments
|
50
|
-
|
51
|
-
|
52
|
-
|
55
|
+
def attachments
|
56
|
+
response = get_response(attachment_url)
|
57
|
+
if html = response.body[/<dl class="attachments">.*?<\/dl>/m]
|
58
|
+
return Attachment.from_html(self,html)
|
59
|
+
elsif response.kind_of?(Net::HTTPSuccess)
|
60
|
+
return []
|
61
|
+
else
|
62
|
+
response.error!
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def attachment(name)
|
67
|
+
if name
|
68
|
+
attachments.detect {|a| a.filename == name} or
|
69
|
+
raise Git::Trac::Error, "no such attachment #{number}/#{name}"
|
70
|
+
else
|
71
|
+
attachments.last or
|
72
|
+
raise Git::Trac::Error, "no attachments for #{number}"
|
73
|
+
end
|
53
74
|
end
|
54
75
|
|
55
76
|
def trac_dir
|
@@ -148,6 +169,12 @@ module Git
|
|
148
169
|
end
|
149
170
|
end
|
150
171
|
|
172
|
+
private
|
173
|
+
|
174
|
+
def no_such_ticket
|
175
|
+
raise Git::Trac::Error, "no such ticket #{number}", caller
|
176
|
+
end
|
177
|
+
|
151
178
|
end
|
152
179
|
|
153
180
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
$: << File.join(File.dirname(File.dirname(__FILE__)),'lib')
|
4
|
+
require 'git/trac/attachment'
|
5
|
+
|
6
|
+
class AttacmentTest < Test::Unit::TestCase
|
7
|
+
|
8
|
+
HTML = <<-EOF
|
9
|
+
<dl class="attachments">
|
10
|
+
<dd>red herring description</dd>
|
11
|
+
<dt><a href="/attachment/ticket/123/first.patch" title="View attachment">first.patch</a> (456 bytes) - added by <em>tpope</em> on 01/02/08 12:34:56.</dt>
|
12
|
+
<dd>foo & bar</dd>
|
13
|
+
<dt><a href="/attachment/ticket/123/second.diff" title="View attachment">second.diff</a> (7 kB) - added by <em>jhope</em> on 02/01/08 23:45:01.</dt>
|
14
|
+
</dl>
|
15
|
+
EOF
|
16
|
+
|
17
|
+
def test_should_parse_html
|
18
|
+
attachments = Git::Trac::Attachment.from_html(nil, HTML)
|
19
|
+
assert_equal 2, attachments.size
|
20
|
+
first, second = *attachments
|
21
|
+
assert_equal "first.patch", first.filename
|
22
|
+
assert_equal "tpope", first.user
|
23
|
+
assert_equal Time.utc(2008,1,2,12,34,56), first.time
|
24
|
+
assert_equal "foo & bar", first.description
|
25
|
+
assert_equal "second.diff", second.filename
|
26
|
+
assert_equal "jhope", second.user
|
27
|
+
assert_equal "2008-02-01_23:45:01_+0000", second.timestamp
|
28
|
+
assert_nil second.description
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: git-trac
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.20080206
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tim Pope
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-02-
|
12
|
+
date: 2008-02-06 00:00:00 -06:00
|
13
13
|
default_executable: git-trac
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -41,7 +41,14 @@ files:
|
|
41
41
|
- lib/git/trac/ticket.rb
|
42
42
|
- lib/git/trac/pager.rb
|
43
43
|
- lib/git/trac/patch.rb
|
44
|
+
- lib/git/trac/runner/download.rb
|
45
|
+
- lib/git/trac/runner/show.rb
|
46
|
+
- lib/git/trac/runner/fetch.rb
|
47
|
+
- lib/git/trac/runner/cleanup.rb
|
48
|
+
- lib/git/trac/runner/upload_patch.rb
|
49
|
+
- lib/git/trac/runner/apply.rb
|
44
50
|
- test/execution_test.rb
|
51
|
+
- test/attachment_test.rb
|
45
52
|
has_rdoc: true
|
46
53
|
homepage: http://git-trac.rubyforge.org
|
47
54
|
post_install_message:
|