git-trac 0.0.20071102

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.
@@ -0,0 +1,303 @@
1
+ require 'optparse'
2
+ require 'ostruct'
3
+
4
+ module Git
5
+ module Trac
6
+
7
+ def self.run(argv)
8
+ Runner.new(argv)
9
+ end
10
+
11
+ class Runner
12
+
13
+ attr_reader :options
14
+
15
+ def initialize(argv)
16
+ @argv = argv.dup
17
+ run
18
+ rescue Git::Trac::Error
19
+ $stderr.puts "Error: " + $!.message
20
+ exit 1
21
+ end
22
+
23
+ def abort(message)
24
+ raise Git::Trac::Error, message
25
+ end
26
+
27
+ def general_usage
28
+ $stderr.puts <<-EOS
29
+ Usage: git-trac <command> [options] [arguments]
30
+
31
+ Available commands:
32
+ cleanup Remove old branches for a ticket
33
+ download Download all patches for a ticket to the cwd
34
+ fetch Create brances for all patches of a ticket
35
+ show Show a summary of a ticket
36
+ upload-patch Upload the current diff against trunk to a ticket
37
+ EOS
38
+ exit 1
39
+ end
40
+
41
+ def run
42
+
43
+ command = @argv.shift
44
+ repo = nil
45
+ if command == "--repository"
46
+ repo, command = @argv.shift, @argv.shift
47
+ end
48
+
49
+ if command == "help"
50
+ command = @argv.shift
51
+ @argv.unshift "--help"
52
+ end
53
+ general_usage unless command
54
+
55
+ @repository = Git::Trac::Repository.new(repo)
56
+ unless @repository.url || @argv.first == "--help"
57
+ abort "no URL. Try `git config trac.url http://trac-url`"
58
+ end
59
+
60
+ klass_name = command.capitalize.gsub(/-(.)/) { $1.upcase }
61
+ if self.class.const_defined?(klass_name)
62
+ klass = self.class.const_get(klass_name)
63
+ if klass < Base
64
+ return klass.new(@argv, @repository)
65
+ end
66
+ end
67
+
68
+ general_usage
69
+
70
+ end
71
+
72
+ class Base
73
+
74
+ attr_reader :options
75
+
76
+ def initialize(argv, repo)
77
+ @argv, @repository = argv, repo
78
+ @options = {}
79
+ @opts = OptionParser.new
80
+ @opts.banner = "Usage: git-trac #{command} #{banner_arguments}\n#{"\n" if description}#{description}"
81
+ @opts.separator("")
82
+ add_options(@opts)
83
+ begin
84
+ @opts.parse!(@argv)
85
+ rescue OptionParser::InvalidOption
86
+ abort $!.message
87
+ end
88
+ run
89
+ end
90
+
91
+ def description
92
+ end
93
+
94
+ def get_ticket_number
95
+ if @argv.first =~ /^\d+$/
96
+ Integer(@argv.shift)
97
+ elsif number = @repository.guess_current_ticket_number
98
+ number
99
+ elsif block_given?
100
+ yield
101
+ else
102
+ abort "ticket number required"
103
+ end
104
+ end
105
+
106
+ def require_ticket_number
107
+ @opts.separator("Ticket number is required unless it can be derived from the current branch.\n")
108
+ end
109
+
110
+ def abort(message)
111
+ $stderr.puts "Error: #{message}"
112
+ # $stderr.puts @opts
113
+ exit(1)
114
+ end
115
+
116
+ def add_options(opts)
117
+ end
118
+
119
+ def banner_arguments
120
+ "[options]"
121
+ end
122
+
123
+ def command
124
+ self.class.name[/[^:]*$/].gsub(/(.)([A-Z])/) { $1+"-"+$2 }.downcase
125
+ end
126
+
127
+ end
128
+
129
+ class Download < Base
130
+
131
+ def description
132
+ <<-EOF
133
+ Download all attachments that look like patches to the current working
134
+ directory.
135
+ EOF
136
+ end
137
+
138
+ def banner_arguments
139
+ "[options] [ticket]"
140
+ end
141
+
142
+ def add_options(opts)
143
+ require_ticket_number
144
+ opts.separator("Options:")
145
+ opts.on("--filter PATTERN","only download patches matching PATTERN") do |pattern|
146
+ options[:filter] = pattern
147
+ end
148
+ end
149
+
150
+ def run
151
+ number = get_ticket_number
152
+ @repository.ticket(number).attachments.each do |attach|
153
+ next if options[:filter] && attach.filename !~ /#{options[:filter]}/
154
+ File.open(attach.filename, "w") do |f|
155
+ f.puts attach.body
156
+ end
157
+ end
158
+ end
159
+
160
+ end
161
+
162
+ class Show < Base
163
+
164
+ def banner_arguments
165
+ "[ticket]"
166
+ end
167
+
168
+ def description
169
+ <<-EOF
170
+ Show a crude ticket summary.
171
+ EOF
172
+ end
173
+
174
+ def add_options(opts)
175
+ require_ticket_number
176
+ end
177
+
178
+ def run
179
+ number = get_ticket_number
180
+ csv = @repository.ticket(number).csv
181
+ csv.reject {|k,v| k == "description"}.sort.each do |(k,v)|
182
+ puts "#{k}: #{v}" if v
183
+ end
184
+ puts "description:"
185
+ puts csv["description"]
186
+ end
187
+
188
+ end
189
+
190
+ class Fetch < Base
191
+
192
+ def banner_arguments
193
+ "[ticket]"
194
+ end
195
+
196
+ def description
197
+ <<-EOF
198
+ Download all branches that look like patches. For each patch, find the
199
+ revision of trunk that most recently proceeds the the time of upload, apply
200
+ the patch to it, create a new commit, and add a remote head of the form
201
+ refs/remotes/trac/ticketnumber/file_name_ext. Patches with the same root
202
+ name (filename.diff, filename.2.diff, filename.patch, etc.) will committed in
203
+ parent child relationships based on upload time. This means filename.2.diff
204
+ might cite filename.diff as a parent. For each root name, a branch is created
205
+ pointing to the newest patch. Existing branches will not be overridden, but
206
+ there is an implied `git cleanup <patch>` that runs beforehand which could
207
+ potentially remove conflicted branches first.
208
+ EOF
209
+ end
210
+
211
+ def add_options(opts)
212
+ require_ticket_number
213
+ end
214
+
215
+ def run
216
+ number = get_ticket_number
217
+ @repository.ticket(number).fetch do |attachment, branch|
218
+ if branch
219
+ puts "#{attachment.filename}: #{branch}"
220
+ else
221
+ $stderr.puts "#{attachment.filename} FAILED"
222
+ end
223
+ end
224
+ end
225
+
226
+ end
227
+
228
+ class Cleanup < Base
229
+
230
+ def banner_arguments
231
+ "[options] [ticket] [ticket] ..."
232
+ end
233
+
234
+ def description
235
+ <<-EOF
236
+ Remove remote heads for a given ticket (e.g., trac/12345/work_patch). Also
237
+ removes branches that point to one of these heads. Branches that have been
238
+ committed to will not be removed. The default is to target tickets that have
239
+ been closed, but you can also specify ticket numbers explicitly or use --all.
240
+ EOF
241
+ end
242
+
243
+ def add_options(opts)
244
+ opts.separator("Options:")
245
+ opts.on("-a","--all", "cleanup all tickets") { options[:all] = true }
246
+ end
247
+
248
+ def run
249
+ if options[:all]
250
+ @repository.working_tickets.each do |t|
251
+ t.cleanup
252
+ end
253
+ elsif @argv.any?
254
+ begin
255
+ @argv.each do |a|
256
+ @repository.ticket(Integer(a)).cleanup
257
+ end
258
+ rescue TypeError
259
+ abort "invalid ticket number"
260
+ end
261
+ else
262
+ @repository.working_tickets.each do |t|
263
+ t.cleanup unless t.open?
264
+ end
265
+ end
266
+ end
267
+ end
268
+
269
+ class UploadPatch < Base
270
+
271
+ def banner_arguments
272
+ "[options] [ticket]"
273
+ end
274
+
275
+ def description
276
+ <<-EOF
277
+ Do a `git diff` against trunk (or another branch) and upload the result as an
278
+ attachment to a ticket. This command is experimental to take care when using
279
+ it against a production trac server.
280
+ EOF
281
+ end
282
+
283
+ def add_options(opts)
284
+ require_ticket_number
285
+ opts.on("--branch BRANCH", "git diff BRANCH (default trunk)") do |b|
286
+ options[:branch] = b
287
+ end
288
+ end
289
+
290
+ def run
291
+ number = get_ticket_number
292
+ if uri = @repository.ticket(number).upload_patch(options)
293
+ puts uri
294
+ else
295
+ exit 1
296
+ end
297
+ end
298
+ end
299
+
300
+ end
301
+
302
+ end
303
+ end
@@ -0,0 +1,183 @@
1
+ require 'hpricot'
2
+ require 'fileutils'
3
+
4
+ module Git
5
+ module Trac
6
+
7
+ class Ticket
8
+
9
+ attr_reader :repository, :number
10
+
11
+ def initialize(repo, number)
12
+ @repository, @number = repo, Integer(number)
13
+ end
14
+
15
+ def inspect
16
+ "#<#{self.class.inspect} #{url}>"
17
+ end
18
+
19
+ def url(format = nil)
20
+ query = "?format=#{format}" if format
21
+ "#{@repository.url}/ticket/#{@number}#{query}"
22
+ end
23
+
24
+ def attachment_url
25
+ "#{@repository.url}/attachment/ticket/#{@number}"
26
+ end
27
+
28
+ def csv
29
+ require 'net/http'
30
+ require 'uri'
31
+ body = Net::HTTP.get_response(URI.parse(url(:tab))).body
32
+ headers, values = body.split(/\r?\n/).map do |line|
33
+ line.split("\t").map do |column|
34
+ column.gsub(/\\(.)/) do
35
+ case $1
36
+ when "r" then ""
37
+ when "n" then "\n"
38
+ when "t" then "\t"
39
+ else $1
40
+ end
41
+ end
42
+ end
43
+ end
44
+ return headers.zip(values).inject({}) {|h,(k,v)| h[k] = v; h}
45
+ end
46
+
47
+ attr_reader :number
48
+
49
+ def attachments(&block)
50
+ entries = repository.agent.get(attachment_url).at("dl.attachments")
51
+ require 'git/trac/attachment'
52
+ Attachment.from_hpricot(self,entries)
53
+ end
54
+
55
+ def trac_dir
56
+ "#{repository.git_dir}/refs/remotes/trac/#{number}"
57
+ end
58
+
59
+ class Form
60
+ def initialize(mech_form)
61
+ @mech_form = mech_form
62
+ end
63
+
64
+ def submit(*args)
65
+ @mech_form.submit
66
+ end
67
+
68
+ def method_missing(method,*args,&block)
69
+ if method.to_s[-1] == "=" && @mech_form.fields.name(method.to_s[0..-2]).any?
70
+ @mech_form.fields.name(method.to_s[0..-2]).send(:value=,*args,&block)
71
+ elsif @mech_form.fields.name(method.to_s).any?
72
+ @mech_form.fields.name(method.to_s).send(:value,*args,&block)
73
+ else
74
+ super(method,*args,&block)
75
+ end
76
+ end
77
+ end
78
+
79
+ def form
80
+ repository.agent.get(url).forms.last
81
+ end
82
+
83
+ def comment!(body)
84
+ form = form()
85
+ form.fields.name("comment").value = body
86
+ form.submit
87
+ end
88
+
89
+ def upload_attachment(description, filename, body)
90
+ form = repository.agent.get("#{attachment_url}?action-new").forms.last
91
+ author = form.fields.name("author")
92
+ if author.any? && author.value == "anonymous"
93
+ author.value = repository.options[:user].to_s
94
+ end
95
+ form.fields.name("description").value = description.to_s
96
+ attachment = form.file_uploads.name("attachment").first
97
+ attachment.file_name = filename
98
+ attachment.file_data = body
99
+ form.submit.instance_variable_get(:@uri)
100
+ end
101
+
102
+ def upload_patch(options = {})
103
+ filename = options[:filename] || "#{File.basename(repository.current_checkout)}.patch"
104
+ # diff = repository.exec("git-diff","#{options[:branch] || "trunk"}...HEAD")
105
+ diff = repository.exec("git-diff", options[:branch] || "trunk")
106
+ return false if diff.empty?
107
+ # Don't upload the exact same patch that was pulled down
108
+ return false if repository.generated_commits[repository.rev_parse("HEAD")] == number
109
+ upload_attachment(options[:description], filename, diff)
110
+ end
111
+
112
+ def open?
113
+ c = csv
114
+ %w(new reopened).include?(c["status"])
115
+ end
116
+
117
+ def cleanup
118
+ revs = []
119
+ repository.each_ref("refs/remotes/trac/#{number}") do |object, ref|
120
+ revs << object
121
+ repository.exec("git-update-ref","-d",ref,object)
122
+ end
123
+ unless revs.empty?
124
+ repository.each_ref("refs/heads") do |object, ref|
125
+ if revs.include?(object) && repository.current_checkout != File.basename(ref)
126
+ repository.exec("git-branch","-D",File.basename(ref))
127
+ end
128
+ end
129
+ true
130
+ end
131
+ end
132
+
133
+ def fetch(options = {})
134
+ cleanup
135
+ seen = {}
136
+ repository.with_index("tracindex#{$$}") do
137
+ FileUtils.mkdir_p(trac_dir)
138
+ attachments.map do |attachment|
139
+ parent = repository.exec("git-rev-list","--max-count=1","--before=#{attachment.timestamp}",options[:branch] || 'trunk').chomp
140
+ repository.exec("git-read-tree",parent)
141
+ unless attachment.apply!
142
+ yield attachment, false if block_given?
143
+ next
144
+ end
145
+ tree = repository.exec("git-write-tree").chomp
146
+ ENV["GIT_AUTHOR_NAME"] = ENV["GIT_COMMITTER_NAME"] = attachment.username
147
+ ENV["GIT_AUTHOR_EMAIL"] = ENV["GIT_COMMITTER_EMAIL"] = attachment.email
148
+ ENV["GIT_AUTHOR_DATE"] = ENV["GIT_COMMITTER_DATE"] = attachment.timestamp
149
+ if repeated = seen[attachment.name]
150
+ if repeated.last == parent
151
+ parents = " -p #{repeated.first}"
152
+ else
153
+ parents = " -p #{repeated.first} -p #{parent}"
154
+ end
155
+ else
156
+ parents = " -p #{parent}"
157
+ end
158
+ commit = repository.popen3("git-commit-tree #{tree}#{parents}") do |i,o,e|
159
+ i.puts attachment.description || attachment.filename
160
+ i.close
161
+ o.read.chomp
162
+ end
163
+ File.open(attachment.tag_path,"w") do |f|
164
+ f.puts commit
165
+ end
166
+ yield attachment, attachment.tag_name if block_given?
167
+ seen[attachment.name] = [commit, parent]
168
+ commit
169
+ end
170
+ end
171
+ seen.each do |k,v|
172
+ if !File.exists?(path = "#{repository.git_dir}/refs/heads/#{k}")
173
+ File.open(path, "w") {|f| f.puts v.first}
174
+ end
175
+ end
176
+ ensure
177
+ %w(GIT_AUTHOR_NAME GIT_COMMITTER_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_EMAIL GIT_AUTHOR_DATE GIT_COMMITTER_DATE).each {|e| ENV[e] = nil}
178
+ end
179
+
180
+ end
181
+
182
+ end
183
+ end