git-trac 0.0.20071102

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