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 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
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'rubygems'
5
+ rescue LoadError
6
+ end
7
+
8
+ $:.unshift(File.join(File.dirname(File.dirname(__FILE__)),"lib"))
9
+
10
+ require 'git/trac'
11
+
12
+ Git::Trac.run(ARGV)
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