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