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.
- data/README +24 -0
- data/Rakefile +109 -0
- data/bin/git-trac +12 -0
- data/lib/git/trac.rb +15 -0
- data/lib/git/trac/attachment.rb +103 -0
- data/lib/git/trac/repository.rb +229 -0
- data/lib/git/trac/runner.rb +303 -0
- data/lib/git/trac/ticket.rb +183 -0
- data/setup.rb +1585 -0
- data/test/execution_test.rb +40 -0
- data/test/execution_test.rb~ +40 -0
- metadata +64 -0
data/README
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Run git-trac help for usage information.
|
2
|
+
|
3
|
+
Example for usage on the Ruby on Rails repository:
|
4
|
+
|
5
|
+
# Export the repository if you haven't already (start it and go to lunch)
|
6
|
+
$ git-svn clone --stdlayout http://dev.rubyonrails.org/svn/rails rails
|
7
|
+
$ cd rails
|
8
|
+
|
9
|
+
# This next step is optional but compresses the repository by a factor of 10
|
10
|
+
$ git gc
|
11
|
+
|
12
|
+
# Setup git-trac (only URL is mandatory)
|
13
|
+
$ git config trac.url http://dev.rubyonrails.org
|
14
|
+
$ git config trac.username myaccount
|
15
|
+
$ git config trac.password mypassword
|
16
|
+
|
17
|
+
# Have fun
|
18
|
+
$ git-trac download 1234567
|
19
|
+
$ git branch
|
20
|
+
$ git checkout some_attached_patch
|
21
|
+
$ git rebase trunk # if it's out of date
|
22
|
+
# EDIT EDIT EDIT
|
23
|
+
$ git commit -a
|
24
|
+
$ git-trac upload-patch 1234567
|
data/Rakefile
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
rescue LoadError
|
4
|
+
end
|
5
|
+
require 'rake'
|
6
|
+
require 'rake/testtask'
|
7
|
+
require 'rake/rdoctask'
|
8
|
+
require 'rake/packagetask'
|
9
|
+
require 'rake/gempackagetask'
|
10
|
+
require 'rake/contrib/sshpublisher'
|
11
|
+
require 'rake/contrib/rubyforgepublisher'
|
12
|
+
require File.join(File.dirname(__FILE__), 'lib', 'git', 'trac')
|
13
|
+
|
14
|
+
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
|
15
|
+
PKG_NAME = 'git-trac'
|
16
|
+
PKG_VERSION = Time.now.strftime("0.0.%Y%m%d")
|
17
|
+
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
18
|
+
# PKG_DESTINATION = ENV["PKG_DESTINATION"] || "../#{PKG_NAME}"
|
19
|
+
|
20
|
+
# RELEASE_NAME = "REL #{PKG_VERSION}"
|
21
|
+
|
22
|
+
RUBY_FORGE_PROJECT = PKG_NAME
|
23
|
+
RUBY_FORGE_USER = "tpope"
|
24
|
+
|
25
|
+
desc "Default task: test"
|
26
|
+
task :default => [ :test ]
|
27
|
+
|
28
|
+
|
29
|
+
# Run the unit tests
|
30
|
+
Rake::TestTask.new { |t|
|
31
|
+
t.libs << "test"
|
32
|
+
t.test_files = Dir['test/*_test.rb'] + Dir['test/test_*.rb']
|
33
|
+
t.verbose = true
|
34
|
+
}
|
35
|
+
|
36
|
+
|
37
|
+
# Generate the RDoc documentation
|
38
|
+
Rake::RDocTask.new { |rdoc|
|
39
|
+
rdoc.rdoc_dir = 'doc'
|
40
|
+
rdoc.rdoc_files.add('lib')
|
41
|
+
rdoc.main = "Git::Trac"
|
42
|
+
rdoc.title = rdoc.main
|
43
|
+
rdoc.options << '--inline-source'
|
44
|
+
}
|
45
|
+
|
46
|
+
|
47
|
+
# Create compressed packages
|
48
|
+
spec = Gem::Specification.new do |s|
|
49
|
+
s.platform = Gem::Platform::RUBY
|
50
|
+
s.name = PKG_NAME
|
51
|
+
s.summary = 'Interact with trac from a git repository pulled from svn'
|
52
|
+
s.description = 'git-trac takes the repetition out of working with trac and git-svn. Among other features is the ability to easily attach a patch to a ticket and to pull all patches from a ticket and turn them into git branches. Created for (but not limited to) work on the Ruby on Rails core.'
|
53
|
+
s.version = PKG_VERSION
|
54
|
+
|
55
|
+
s.author = 'Tim Pope'
|
56
|
+
s.email = 'ruby@tp0pe.inf0'.gsub(/0/,'o')
|
57
|
+
s.rubyforge_project = RUBY_FORGE_PROJECT
|
58
|
+
s.homepage = "http://#{PKG_NAME}.rubyforge.org"
|
59
|
+
|
60
|
+
s.has_rdoc = true
|
61
|
+
# s.requirements << 'none'
|
62
|
+
s.require_path = 'lib'
|
63
|
+
|
64
|
+
s.bindir = "bin"
|
65
|
+
s.executables = ["git-trac"]
|
66
|
+
s.default_executable = "git-trac"
|
67
|
+
|
68
|
+
s.add_dependency('mechanize', '>= 0.6.8')
|
69
|
+
|
70
|
+
s.files = [ "Rakefile", "README", "setup.rb" ]
|
71
|
+
s.files = s.files + Dir.glob( "lib/**/*.rb" )
|
72
|
+
s.files = s.files + Dir.glob( "test/**/*" ).reject { |item| item.include?( "\.svn" ) }
|
73
|
+
end
|
74
|
+
|
75
|
+
Rake::GemPackageTask.new(spec) do |p|
|
76
|
+
p.gem_spec = spec
|
77
|
+
p.need_tar = true
|
78
|
+
p.need_zip = true
|
79
|
+
end
|
80
|
+
|
81
|
+
# Publish documentation
|
82
|
+
desc "Publish the API documentation"
|
83
|
+
task :pdoc => [:rerdoc] do
|
84
|
+
# Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT,RUBY_FORGE_USER).upload
|
85
|
+
Rake::SshDirPublisher.new("rubyforge.org", "/var/www/gforge-projects/#{PKG_NAME}", "doc").upload
|
86
|
+
end
|
87
|
+
|
88
|
+
desc "Publish the release files to RubyForge."
|
89
|
+
task :release => [ :package ] do
|
90
|
+
`rubyforge login`
|
91
|
+
|
92
|
+
for ext in %w( gem tgz zip )
|
93
|
+
release_command = "rubyforge add_release #{PKG_NAME} #{PKG_NAME} 'REL #{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}"
|
94
|
+
puts release_command
|
95
|
+
system(release_command)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
begin
|
100
|
+
require 'rcov/rcovtask'
|
101
|
+
Rcov::RcovTask.new do |t|
|
102
|
+
t.test_files = Dir['test/*_test.rb'] + Dir['test/test_*.rb']
|
103
|
+
t.verbose = true
|
104
|
+
t.rcov_opts << "--text-report"
|
105
|
+
# t.rcov_opts << "--exclude \\\\A/var/lib/gems"
|
106
|
+
t.rcov_opts << "--exclude '/(active_record|active_support)\\b'"
|
107
|
+
end
|
108
|
+
rescue LoadError
|
109
|
+
end
|
data/bin/git-trac
ADDED
data/lib/git/trac.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Trac
|
5
|
+
class Error < ::RuntimeError
|
6
|
+
end
|
7
|
+
class ExecutionError < Error
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'git/trac/repository'
|
13
|
+
require 'git/trac/ticket'
|
14
|
+
require 'git/trac/attachment'
|
15
|
+
require 'git/trac/runner'
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Git
|
2
|
+
module Trac
|
3
|
+
|
4
|
+
class Attachment
|
5
|
+
|
6
|
+
attr_reader :ticket, :filename, :user, :time
|
7
|
+
attr_accessor :description
|
8
|
+
|
9
|
+
def initialize(ticket, filename, user, time)
|
10
|
+
@ticket, @filename, @user, @time = ticket, filename, user, time
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.from_hpricot(ticket, html)
|
14
|
+
collection = []
|
15
|
+
return collection unless html
|
16
|
+
(html.children).each do |element|
|
17
|
+
if !element.elem?
|
18
|
+
elsif element.name == "dd"
|
19
|
+
str = ""
|
20
|
+
element.traverse_text {|t| str << t.to_s}
|
21
|
+
collection.last.description = str
|
22
|
+
elsif element.name = "dt"
|
23
|
+
texts = []
|
24
|
+
element.traverse_text {|x| texts << x.to_s}
|
25
|
+
if texts.first =~ /\A(\w*)(\.\d+)?\.(diff|patch)\Z/
|
26
|
+
time = Time.parse("#{texts[3]} +0000").utc
|
27
|
+
collection << new(ticket, texts[0], texts[2], time)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
collection
|
32
|
+
end
|
33
|
+
|
34
|
+
def extension
|
35
|
+
File.extname(@filename)
|
36
|
+
end
|
37
|
+
|
38
|
+
def name
|
39
|
+
@filename[/^[^.]*/]
|
40
|
+
end
|
41
|
+
|
42
|
+
def number
|
43
|
+
@filename[/\.(\d+)\./,1]
|
44
|
+
end
|
45
|
+
|
46
|
+
def username
|
47
|
+
user.to_s[/[^\s@]+/]
|
48
|
+
end
|
49
|
+
|
50
|
+
def email
|
51
|
+
if user =~ /\S*@\S*/
|
52
|
+
return user
|
53
|
+
elsif user =~ /<(\S*@\S*)>/
|
54
|
+
return $1
|
55
|
+
else
|
56
|
+
[user, repository.url[%r{://([^/]*)},1]].compact.join("@")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def timestamp
|
61
|
+
time.utc.strftime("%Y-%m-%d_%H:%M:%S_+0000")
|
62
|
+
end
|
63
|
+
|
64
|
+
def url
|
65
|
+
"#{ticket.repository.url}/attachment/ticket/#{ticket.number}/#{filename}?format=raw"
|
66
|
+
end
|
67
|
+
|
68
|
+
def tag_name
|
69
|
+
"trac/#{ticket.number}/#{filename.gsub(/[^A-Za-z0-9]+/,"_")}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def tag_path
|
73
|
+
"#{ticket.repository.git_dir}/refs/remotes/#{tag_name}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def body
|
77
|
+
@body ||= repository.agent.get_file(url)
|
78
|
+
end
|
79
|
+
|
80
|
+
def p_level
|
81
|
+
if body[0,10] == "diff --git"
|
82
|
+
1
|
83
|
+
else
|
84
|
+
0
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def repository
|
89
|
+
ticket.repository
|
90
|
+
end
|
91
|
+
|
92
|
+
def apply!
|
93
|
+
repository.popen3("git-apply", "-p#{p_level}", :cached => true, :whitespace => "nowarn") do |inn,out,err|
|
94
|
+
inn.puts body
|
95
|
+
inn.close
|
96
|
+
err.read.empty?
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Trac
|
5
|
+
|
6
|
+
class Repository
|
7
|
+
|
8
|
+
attr_reader :git_dir, :options
|
9
|
+
|
10
|
+
def initialize(dir = nil, options = {})
|
11
|
+
options, dir = dir, nil if dir.kind_of?(Hash)
|
12
|
+
dir ||= ENV["GIT_DIR"]
|
13
|
+
dir ||= Dir.getwd
|
14
|
+
@git_dir = Dir.chdir(dir) do
|
15
|
+
Open3.popen3("git","rev-parse","--git-dir") do |i,o,e|
|
16
|
+
e.read.empty? ? o.read.chomp("\n") : nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
raise Git::Trac::Error, "Not a git repository" unless @git_dir
|
20
|
+
@git_dir = File.join(".",@git_dir) if @git_dir[0] == ?~
|
21
|
+
@git_dir = File.expand_path(@git_dir)
|
22
|
+
@options = (config("trac")||{}).merge(options)
|
23
|
+
unless url
|
24
|
+
# raise Git::Trac::Error, "missing trac url: consider `git config trac.url http://tracurl`"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def inspect
|
29
|
+
"#<#{self.class.inspect} #{git_dir} #{url}>"
|
30
|
+
end
|
31
|
+
|
32
|
+
# Call the block while chdired to the working tree.
|
33
|
+
def in_work_tree(&block)
|
34
|
+
Dir.chdir(File.dirname(@git_dir),&block)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Open3.popen3 in the current working tree. An optional hash in the last
|
38
|
+
# argument is converted to flags that precede the arguments.
|
39
|
+
# <tt>:foo_bar => "baz"</tt> becomes <tt>--foo-bar=baz</tt>
|
40
|
+
def popen3(*args,&block)
|
41
|
+
args.flatten!
|
42
|
+
cmd = args.shift
|
43
|
+
if args.last.kind_of?(Hash)
|
44
|
+
args.pop.each do |k,v|
|
45
|
+
k = k.to_s.tr('-_','_-')
|
46
|
+
opt = case v
|
47
|
+
when true then "#{k}"
|
48
|
+
when false then "no-#{k}"
|
49
|
+
else "#{k}=#{v}"
|
50
|
+
end
|
51
|
+
args.unshift "--#{opt}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
in_work_tree { Open3.popen3(cmd,*args,&block) }
|
55
|
+
end
|
56
|
+
|
57
|
+
# Invokes a command. If the command writes to stderr, said output is
|
58
|
+
# raised as an exception. Otherwise, read from stdout and return it as a
|
59
|
+
# string. If a block is given, each line is yielded to it.
|
60
|
+
def exec(*args,&block)
|
61
|
+
popen3(*args) do |i,o,e|
|
62
|
+
err = e.read
|
63
|
+
if err.empty?
|
64
|
+
if block_given?
|
65
|
+
o.each_line(&block)
|
66
|
+
nil
|
67
|
+
else
|
68
|
+
o.read
|
69
|
+
end
|
70
|
+
else
|
71
|
+
raise Git::Trac::ExecutionError, err.chomp
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def method_missing(method,*args,&block)
|
77
|
+
if method.to_s =~ /^git_/
|
78
|
+
exec(method.to_s.tr('_-','-_'),*args, &block)
|
79
|
+
else
|
80
|
+
super(method,*args,&block)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Call git-rev-parse on a revision, returning nil if nothing is found.
|
85
|
+
def rev_parse(rev)
|
86
|
+
if rev.kind_of?(String)
|
87
|
+
exec("git-rev-parse","--verify",rev).chomp
|
88
|
+
elsif rev.kind_of?(Array)
|
89
|
+
hash = {}
|
90
|
+
return hash if rev.empty?
|
91
|
+
exec("git-rev-parse",*rev) do |rev|
|
92
|
+
hash[name] = rev.chomp if rev.chomp =~ /^[0-9a-f]{40}$/
|
93
|
+
end
|
94
|
+
hash
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def each_ref(*args)
|
99
|
+
pattern = args.empty? ? "refs" : args.join("/")
|
100
|
+
exec("git-for-each-ref", pattern, :format => "%(objectname) %(refname)") do |line|
|
101
|
+
yield *line.chomp.split(" ",2)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def current_checkout
|
106
|
+
contents = File.read("#{git_dir}/HEAD").chomp
|
107
|
+
if contents =~ %r{^ref: refs/heads/(.*)}
|
108
|
+
$1
|
109
|
+
else
|
110
|
+
contents
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def with_index(file)
|
115
|
+
old_index = ENV["GIT_INDEX_FILE"]
|
116
|
+
ENV["GIT_INDEX_FILE"] = File.join(git_dir,file)
|
117
|
+
yield file
|
118
|
+
ensure
|
119
|
+
ENV["GIT_INDEX_FILE"] = old_index
|
120
|
+
File.unlink(file) rescue nil
|
121
|
+
end
|
122
|
+
|
123
|
+
# Parse the output of git-config and return it as a hash.
|
124
|
+
def config(arg1 = nil, arg2 = nil)
|
125
|
+
unless @config
|
126
|
+
@config = {}
|
127
|
+
exec("git-config","-l") do |line|
|
128
|
+
key, value = line.split("=",2)
|
129
|
+
superkey, subkey = key.match(/(.*)\.(.*)/).to_a[1,2]
|
130
|
+
@config[superkey] ||= {}
|
131
|
+
@config[superkey][subkey.tr('-_','_-').to_sym] = value
|
132
|
+
# hash[key] ||= []
|
133
|
+
# hash[key] << value
|
134
|
+
# @config[key] = value
|
135
|
+
end
|
136
|
+
end
|
137
|
+
if arg2
|
138
|
+
(@config[arg1.to_s]||{})[arg2.to_sym]
|
139
|
+
elsif arg1
|
140
|
+
@config[arg1]
|
141
|
+
else
|
142
|
+
@config
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# The trac url associated with the repository.
|
147
|
+
def url
|
148
|
+
options[:url] || config("svn-remote.svn",:url).to_s[%r{(http://.*)/svn},1]
|
149
|
+
end
|
150
|
+
|
151
|
+
def agent
|
152
|
+
unless defined?(@agent)
|
153
|
+
require 'mechanize'
|
154
|
+
@agent = WWW::Mechanize.new {|a| a.log = nil}
|
155
|
+
url = "#{url}/login"
|
156
|
+
if options[:password]
|
157
|
+
|
158
|
+
begin
|
159
|
+
page = @agent.get(url)
|
160
|
+
rescue WWW::Mechanize::ResponseCodeError => e
|
161
|
+
# 401 Forbidden, let's try HTTP authentication
|
162
|
+
raise e unless e.response_code == "401"
|
163
|
+
@agent.basic_auth(options[:username],options[:password])
|
164
|
+
begin
|
165
|
+
page = @agent.get(url)
|
166
|
+
return @agent
|
167
|
+
rescue WWW::Mechanize::ResponseCodeError => e
|
168
|
+
raise e unless e.response_code == "401"
|
169
|
+
raise Git::Trac::Error, "invalid username or password"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Form authentication
|
174
|
+
agent.redirect_ok = false
|
175
|
+
form = page.forms.last
|
176
|
+
form.fields.name("user").value = options[:username]
|
177
|
+
form.fields.name("password").value = options[:password]
|
178
|
+
form.action = url # Mechanize gets this wrong
|
179
|
+
begin
|
180
|
+
page = @agent.submit(form)
|
181
|
+
unless page.respond_to?(:code) && page.code == "303"
|
182
|
+
raise Git::Trac::Error, "invalid username or password"
|
183
|
+
end
|
184
|
+
ensure
|
185
|
+
agent.redirect_ok = true
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
@agent
|
191
|
+
end
|
192
|
+
|
193
|
+
# A hash of the form {commit_revision => ticket_number}
|
194
|
+
def generated_commits
|
195
|
+
hash = {}
|
196
|
+
each_ref("refs/remotes/trac") do |object, ref|
|
197
|
+
hash[object] = File.basename(File.dirname(ref)).to_i
|
198
|
+
end
|
199
|
+
hash
|
200
|
+
end
|
201
|
+
|
202
|
+
# See if the current commit or an ancestor was previously tagged as a
|
203
|
+
# trac ticket. Also check git config branch.branchname.tracticket
|
204
|
+
def guess_current_ticket_number
|
205
|
+
if number = config("branch.#{current_checkout}", :tracticket)
|
206
|
+
return number.to_i
|
207
|
+
else
|
208
|
+
hash = generated_commits
|
209
|
+
exec("git-rev-list", "HEAD", :maxcount => 25) do |line|
|
210
|
+
number = hash[line.chomp] and return number
|
211
|
+
end
|
212
|
+
nil
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Fetch a ticket object by number.
|
217
|
+
def ticket(number)
|
218
|
+
Ticket.new(self, number)
|
219
|
+
end
|
220
|
+
|
221
|
+
# Return ticket objects for each ticket referenced in the repository.
|
222
|
+
def working_tickets
|
223
|
+
Dir.entries("#{git_dir}/refs/remotes/trac").map {|number| ticket(number.to_i) unless number.to_i.zero? }.compact
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
end
|