git-trac 0.0.20080206 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -1,24 +1,39 @@
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
1
+ = git-trac
2
+
3
+ git-trac takes the repetition out of working with trac and git-svn. Easily
4
+ download a patch, apply it at the right point in time, turn it into a commit
5
+ with useful metadata, and check it out into a branch, all in one command.
6
+ Created for (but not limited to) work on the Ruby on Rails core.
7
+
8
+ == Usage
9
+
10
+ Running `git-trac help` gives you access to usage information, including
11
+ information about how to use the help command to get more help.
12
+
13
+ == Examples
14
+
15
+ Run `git-trac help rails` for a quick start guide for the Ruby on Rails core.
16
+
17
+ == License
18
+
19
+ git-trac is available under an MIT-style license.
20
+
21
+ :include: MIT-LICENSE
22
+
23
+ == History
24
+
25
+ See the NEWS file for a version history.
26
+
27
+ == About
28
+
29
+ Author:: Tim Pope <ruby at tpope. info>
30
+ Home Page:: http://git-trac.rubyforge.org
31
+
32
+ == Feedback
33
+
34
+ Feedback welcome and appreciated. While I intend for git-trac to be
35
+ applicable to a wide variety of projects and setups, in practice it has been
36
+ tested primarily on a git-svn clone of the Rails core and the official Rails
37
+ trac while running Debian with one of a few recent versions of git. If
38
+ altering any combination of these variables does not work for you, please
39
+ report it.
data/Rakefile CHANGED
@@ -13,7 +13,7 @@ require File.join(File.dirname(__FILE__), 'lib', 'git', 'trac')
13
13
 
14
14
  PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
15
15
  PKG_NAME = 'git-trac'
16
- PKG_VERSION = Time.now.strftime("0.0.%Y%m%d")
16
+ PKG_VERSION = Git::Trac::VERSION::STRING
17
17
  PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
18
18
  # PKG_DESTINATION = ENV["PKG_DESTINATION"] || "../#{PKG_NAME}"
19
19
 
@@ -39,6 +39,7 @@ Rake::RDocTask.new { |rdoc|
39
39
  rdoc.rdoc_dir = 'doc'
40
40
  rdoc.rdoc_files.add('lib')
41
41
  rdoc.rdoc_files.add('README')
42
+ rdoc.rdoc_files.add('NEWS')
42
43
  rdoc.main = "README"
43
44
  rdoc.title = "git-trac"
44
45
  rdoc.options << '--inline-source'
@@ -50,7 +51,7 @@ spec = Gem::Specification.new do |s|
50
51
  s.platform = Gem::Platform::RUBY
51
52
  s.name = PKG_NAME
52
53
  s.summary = 'Interact with trac from a git repository pulled from svn'
53
- 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.'
54
+ s.description = 'git-trac takes the repetition out of working with trac and git-svn. Easily download a patch, apply it at the right point in time, turn it into a commit with useful metadata, and check it out into a branch, all in one command. Created for (but not limited to) work on the Ruby on Rails core.'
54
55
  s.version = PKG_VERSION
55
56
 
56
57
  s.author = 'Tim Pope'
@@ -14,3 +14,4 @@ require 'git/trac/ticket'
14
14
  require 'git/trac/attachment'
15
15
  require 'git/trac/patch'
16
16
  require 'git/trac/runner'
17
+ require 'git/trac/version'
@@ -39,10 +39,10 @@ module Git
39
39
  end
40
40
 
41
41
  def email
42
- if user =~ /\S*@\S*/
43
- return user
44
- elsif user =~ /<(\S*@\S*)>/
42
+ if user =~ /<(\S*@\S*)>/
45
43
  return $1
44
+ elsif user =~ /\S*@\S*/
45
+ return user
46
46
  else
47
47
  [user, repository.url[%r{://([^/]*)},1]].compact.join("@")
48
48
  end
@@ -52,8 +52,9 @@ module Git
52
52
  time.utc.strftime("%Y-%m-%d_%H:%M:%S_+0000")
53
53
  end
54
54
 
55
- def url
56
- "#{ticket.repository.url}/attachment/ticket/#{ticket.number}/#{filename}?format=raw"
55
+ def url(format = :raw)
56
+ query = "?format=#{format}" if format
57
+ "#{ticket.repository.url}/attachment/ticket/#{ticket.number}/#{filename}#{query}"
57
58
  end
58
59
 
59
60
  def tag_name
@@ -65,7 +66,7 @@ module Git
65
66
  end
66
67
 
67
68
  def body
68
- @body ||= repository.agent.get_file(url)
69
+ @body ||= repository.agent.get_file(url(:raw))
69
70
  end
70
71
 
71
72
  def repository
@@ -78,8 +79,8 @@ module Git
78
79
 
79
80
  def cleanup(options = {})
80
81
  repository.each_ref("refs/remotes/#{tag_name}") do |object, ref|
81
- repository.exec("git","update-ref","-d",ref,object)
82
- repository.cleanup_branches(object)
82
+ repository.exec("git","update-ref","-d",ref,object) unless options[:only_branches]
83
+ repository.cleanup_branches(options, object)
83
84
  return object
84
85
  end
85
86
  end
@@ -88,9 +89,13 @@ module Git
88
89
  cleanup(options)
89
90
  FileUtils.mkdir_p(ticket.trac_dir)
90
91
  repository.with_index("tracindex#{$$}") do
91
- parent = repository.exec("git","rev-list","--max-count=1","--before=#{timestamp}",options[:branch] || 'trunk').chomp
92
+ parent = repository.exec("git","rev-list","--max-count=1","--before=#{timestamp}",options[:upstream] || 'refs/remotes/trunk').chomp
92
93
  repository.exec("git","read-tree",parent)
93
- applied = patch.apply(options.merge(:cached => true)) or return [nil,applied]
94
+ applied = patch.apply(options.merge(:cached => true))
95
+ unless applied
96
+ yield self, nil, nil
97
+ return
98
+ end
94
99
  tree = repository.exec("git","write-tree").chomp
95
100
  ENV["GIT_AUTHOR_NAME"] = ENV["GIT_COMMITTER_NAME"] = username
96
101
  ENV["GIT_AUTHOR_EMAIL"] = ENV["GIT_COMMITTER_EMAIL"] = email
@@ -104,7 +109,8 @@ module Git
104
109
  File.open(tag_path,"w") do |f|
105
110
  f.puts commit
106
111
  end
107
- return [commit, applied]
112
+ yield self, applied, commit
113
+ return commit
108
114
  end
109
115
  ensure
110
116
  %w(GIT_AUTHOR_NAME GIT_COMMITTER_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_EMAIL GIT_AUTHOR_DATE GIT_COMMITTER_DATE).each {|e| ENV[e] = nil}
@@ -2,6 +2,18 @@ module Git
2
2
  module Trac
3
3
 
4
4
  class Repository
5
+ def pager(body, diff = false)
6
+ pager = Pager.new(ENV["GIT_PAGER"] || config("core","pager"))
7
+ if pager.command
8
+ diff &&= !%w(false no 0).include?(config("color","pager").to_s.downcase)
9
+ end
10
+ diff &&= %w(true yes 1 always auto).include?(config("color","diff").to_s.downcase)
11
+ pager.page(body, diff)
12
+ end
13
+
14
+ end
15
+
16
+ class Pager
5
17
 
6
18
  DIFF_COLORS = {
7
19
  :reset => "\033[m",
@@ -14,27 +26,37 @@ module Git
14
26
  :whitespace => "\033[41m"
15
27
  }
16
28
 
17
- def pager(body, diff = false)
18
- pager = ENV["GIT_PAGER"] || config("core","pager") || ENV["PAGER"] || "less"
19
- return colorize_diff_output($stdout, body) if pager.empty? && diff
20
- return $stdout.puts(body) if !$stdout.tty? || pager.empty?
29
+ attr_reader :command
30
+
31
+ def initialize(command = nil)
32
+ @command = command || ENV["PAGER"] || "less"
33
+ @command = nil if @command.empty?
34
+ end
35
+
36
+ def page(body, diff = false)
37
+ less = ENV["LESS"]
21
38
  ENV["LESS"] ||= "FRSX"
22
39
 
23
- IO.popen(pager, "w") do |io|
24
- if diff && !%w(false no 0).include?(config("color","pager").to_s.downcase)
25
- colorize_diff_output(io, body)
26
- else
27
- io.puts body
40
+ return $stdout.puts(body) unless $stdout.tty?
41
+
42
+ if command.nil?
43
+ output_to_io($stdout, body, diff)
44
+ else
45
+ IO.popen(command, "w") do |io|
46
+ output_to_io(io, body, diff)
28
47
  end
29
48
  end
49
+
30
50
  rescue Errno::EPIPE
31
51
  # Pager was terminated
52
+ ensure
53
+ ENV["LESS"] = less
32
54
  end
33
55
 
34
56
  private
35
57
 
36
- def colorize_diff_output(io, body)
37
- if %w(true yes 1 always auto).include?(config("color","diff").to_s.downcase)
58
+ def output_to_io(io, body, diff)
59
+ if diff
38
60
  state = nil
39
61
  body.each_line do |line|
40
62
  if state.nil? && line !~ /^(diff|Index:) /
@@ -138,6 +138,12 @@ module Git
138
138
  options[:url] || config("svn-remote.svn",:url).to_s[%r{(http://.*)/svn},1]
139
139
  end
140
140
 
141
+ def get_response(uri)
142
+ require 'net/http'
143
+ require 'uri'
144
+ Net::HTTP.get_response(URI.parse(uri))
145
+ end
146
+
141
147
  def agent
142
148
  unless defined?(@agent)
143
149
  unless url
@@ -192,15 +198,32 @@ module Git
192
198
  hash
193
199
  end
194
200
 
195
- def cleanup_branches(*revs)
201
+ def cleanup_branches(options, *revs)
196
202
  return if revs.empty?
203
+ diff_hashes = revs.map {|r| diff_hash(r)}.compact if options[:rebased]
204
+ current = current_checkout
197
205
  each_ref("refs/heads") do |object, ref|
198
- if revs.include?(object) && current_checkout != File.basename(ref)
199
- exec("git","branch","-D",File.basename(ref))
206
+ if current != File.basename(ref)
207
+ if revs.include?(object)
208
+ exec("git","branch","-D",File.basename(ref))
209
+ elsif diff_hashes && diff_hashes.include?(dh = diff_hash(ref))
210
+ p "wooo!"
211
+ exec("git","branch","-D",File.basename(ref))
212
+ end
200
213
  end
201
214
  end
202
215
  end
203
216
 
217
+ def diff_hash(head)
218
+ require 'digest/sha1'
219
+ guess = guess_upstream(head)
220
+ return unless guess
221
+ diff = exec("git","diff","#{guess}...#{head}")
222
+ diff.gsub!(/^(?:index |@@ -).*\n/,'')
223
+ diff.gsub!(/[[:space:]]/,'')
224
+ Digest::SHA1.digest(diff)
225
+ end
226
+
204
227
  # See if the current commit or an ancestor was previously tagged as a
205
228
  # trac ticket. Also check git config branch.branchname.tracticket
206
229
  def guess_current_ticket_number
@@ -215,6 +238,34 @@ module Git
215
238
  end
216
239
  end
217
240
 
241
+ def guess_upstream(head = "HEAD")
242
+ svn = config("svn-remote.svn")
243
+ return unless (svn||{})[:url]
244
+ branch = popen3("git","log","--no-color",head) do |i,o,e|
245
+ o.each_line do |line|
246
+ next unless line.gsub!(/^\s*git-svn-id:\s*/,'')
247
+ url, uuid = line.chomp.split(" ")
248
+ url.sub!(/^#{Regexp.escape(svn[:url])}\/*/,'')
249
+ branch, revision = url.split("@")
250
+ break branch
251
+ end
252
+ end
253
+ if branch
254
+ trunk = svn[:trunk] || "trunk:refs/remotes/trunk"
255
+ branches = svn[:branches] || "branches/*:refs/remotes/*"
256
+ tags = svn[:tags] || "tags/*:refs/remotes/tags/*"
257
+ [trunk, branches, tags].each do |pair|
258
+ remote, local = pair.split(":",2)
259
+ next if local.to_s.empty?
260
+ before, after = remote.split("*",2)
261
+ if branch =~ /^#{Regexp.escape(before)}(.*)#{Regexp.escape(after.to_s)}/
262
+ return local.sub(/\*/,$1)
263
+ end
264
+ end
265
+ svn["branches"].to_s
266
+ end
267
+ end
268
+
218
269
  # Fetch a ticket object by number.
219
270
  def ticket(number)
220
271
  Ticket.new(self, number)
@@ -10,7 +10,50 @@ module Git
10
10
 
11
11
  class Runner #:nodoc:
12
12
 
13
- attr_reader :options
13
+ def self.for_command(command)
14
+ klass_name = command.to_s.capitalize.gsub(/-(.)/) { $1.upcase }
15
+ if klass_name =~ /^[A-Z]\w*$/ && const_defined?(klass_name)
16
+ klass = const_get(klass_name)
17
+ if klass < Base
18
+ return klass
19
+ end
20
+ end
21
+ end
22
+
23
+ def self.commands
24
+ Runner.constants.map {|c| Runner.const_get(c)}.select {|c| c < Runner::Base}.sort_by {|r| r.command}.uniq
25
+ end
26
+
27
+ module Fetchable #:nodoc:
28
+
29
+ def add_options(opts)
30
+ super
31
+ opts.on("--against BRANCH","apply against branch BRANCH") do |b|
32
+ options[:upstream] = b
33
+ end
34
+ opts.on("--depth NUM","search depth (see git-trac help apply)") do |n|
35
+ options[:depth] = n
36
+ end
37
+ opts.on("--root DIR","apply patches relative to DIR") do |dir|
38
+ options[:root] = dir
39
+ end
40
+ add_local_option(opts)
41
+ end
42
+
43
+ def run
44
+ options[:upstream] ||= repository.guess_upstream || "refs/remotes/trunk"
45
+ fetch_unless_local
46
+ attachment = one_attachment
47
+ fetch_or_abort(attachment)
48
+ repository.in_work_tree do
49
+ after_fetch(attachment)
50
+ end
51
+ end
52
+
53
+ def after_fetch(attachment)
54
+ end
55
+
56
+ end
14
57
 
15
58
  def initialize(argv)
16
59
  @argv = argv.dup
@@ -18,6 +61,8 @@ module Git
18
61
  rescue Git::Trac::Error
19
62
  $stderr.puts "Error: " + $!.message
20
63
  exit 1
64
+ rescue Interrupt
65
+ $stderr.puts "Interrupted!"
21
66
  end
22
67
 
23
68
  def abort(message)
@@ -27,43 +72,22 @@ module Git
27
72
  def general_usage
28
73
  $stderr.puts <<-EOS
29
74
  Usage: git-trac <command> [options] [arguments]
30
-
31
- Available commands:
32
- apply Apply a patch directly to the work tree
33
- cleanup Remove old branches for a ticket
34
- download Download all patches for a ticket to the cwd
35
- fetch Create brances for all patches of a ticket
36
- show Show a summary of a ticket
37
- upload-patch Upload the current diff against trunk to a ticket
75
+ See `git-trac help` for details.
38
76
  EOS
39
77
  exit 1
40
78
  end
41
79
 
42
80
  def run
43
81
 
44
- command = @argv.shift
82
+ command = @argv.shift unless %w(-v --version).include?(@argv.first)
45
83
  repo = nil
46
84
  if command == "--repository"
47
85
  repo, command = @argv.shift, @argv.shift
48
86
  end
87
+ command = "help" if %w(-h --help).include?(command)
49
88
 
50
- if command == "help"
51
- command = @argv.shift
52
- @argv.unshift "--help"
53
- end
54
- general_usage unless command
55
-
56
- @repository = Git::Trac::Repository.new(repo)
57
- unless @repository.url || @argv.first == "--help"
58
- abort "no URL. Try `git config trac.url http://trac-url`"
59
- end
60
-
61
- klass_name = command.capitalize.gsub(/-(.)/) { $1.upcase }
62
- if klass_name =~ /^[A-Z]\w*$/ && self.class.const_defined?(klass_name)
63
- klass = self.class.const_get(klass_name)
64
- if klass < Base
65
- return klass.new(@argv, @repository)
66
- end
89
+ if klass = Runner.for_command(command || "help")
90
+ return klass.new(@argv, repo)
67
91
  end
68
92
 
69
93
  general_usage
@@ -72,13 +96,17 @@ Available commands:
72
96
 
73
97
  class Base #:nodoc:
74
98
 
75
- attr_reader :options
76
-
77
99
  def initialize(argv, repo)
78
- @argv, @repository = argv, repo
79
- @options = repo.config("trac") || {}
100
+ @argv, @repository_option = argv, repo
80
101
  @opts = OptionParser.new
81
- @opts.banner = "Usage: git-trac #{command} #{banner_arguments}\n#{"\n" if description}#{description}"
102
+ @opts.banner = "Usage: git-trac #{self.class.command} #{banner_arguments}"
103
+ @opts.version = Git::Trac::VERSION::STRING
104
+ @opts.base.long["help"] = OptionParser::Switch::NoArgument.new do
105
+ help = @opts.help.chomp.chomp + "\n"
106
+ help += "\n#{description}" if description
107
+ Pager.new.page(help)
108
+ exit
109
+ end
82
110
  @opts.separator("")
83
111
  add_options(@opts)
84
112
  begin
@@ -89,35 +117,63 @@ Available commands:
89
117
  run
90
118
  end
91
119
 
120
+ def repository
121
+ @repository ||= Git::Trac::Repository.new(@repository_option)
122
+ unless @repository.url
123
+ abort "no URL. Try `git config trac.url http://trac-url`"
124
+ end
125
+ @repository
126
+ end
127
+
128
+ def options
129
+ @options ||= repository.config("trac") || {}
130
+ end
131
+
92
132
  def description
93
133
  end
94
134
 
95
- def get_ticket_number
96
- if @argv.first =~ /^\d+$/
97
- Integer(@argv.shift)
98
- elsif number = @repository.guess_current_ticket_number
99
- number
100
- elsif block_given?
101
- yield
102
- else
103
- abort "ticket number required"
135
+ def system(*args)
136
+ repository.in_work_tree do
137
+ Kernel.system(*args) or exit $?.exitstatus
104
138
  end
105
139
  end
106
140
 
107
- def each_ticket_argument
108
- if @argv.empty?
109
- yield get_ticket_number, nil
110
- elsif @argv.any? {|a| a !~ /\b\d+\b/}
111
- abort "ticket number required"
141
+ def parse_attachment(arg)
142
+ if match_data = arg.to_s.match(/\b(\d+)\b(?:\/([^?\/]+))?/)
143
+ return Integer(match_data[1]), match_data[2]
144
+ else
145
+ abort "invalid argument '#{arg}'"
112
146
  end
113
- @argv.each do |arg|
114
- match_data = arg.match(/\b(\d+)\b(?:\/([^?\/]+))?/)
115
- yield Integer(match_data[1]), match_data[2]
147
+ end
148
+
149
+ def one_attachment(abort_when_extra = true)
150
+ missing_argument if @argv.empty?
151
+ number, filename = parse_attachment(@argv.shift)
152
+ too_many_arguments if abort_when_extra && @argv.any?
153
+ return repository.ticket(number).attachment(filename)
154
+ end
155
+
156
+ def each_ticket_or_attachment(abort_when_missing = true)
157
+ missing_argument if abort_when_missing && @argv.empty?
158
+ until @argv.empty?
159
+ number, filename = parse_attachment(@argv.shift)
160
+ ticket = repository.ticket(number)
161
+ if filename
162
+ yield ticket.attachment(filename)
163
+ else
164
+ yield ticket
165
+ end
116
166
  end
117
167
  end
118
168
 
119
- def require_ticket_number
120
- @opts.separator("Ticket number is required unless it can be derived from the current branch.\n")
169
+ def each_attachment(abort_when_missing = true)
170
+ each_ticket_or_attachment(abort_when_missing) do |object|
171
+ if object.respond_to?(:attachments)
172
+ object.attachments.each {|a| yield a}
173
+ else
174
+ yield attachment
175
+ end
176
+ end
121
177
  end
122
178
 
123
179
  def abort(message)
@@ -125,6 +181,24 @@ Available commands:
125
181
  exit(1)
126
182
  end
127
183
 
184
+ def too_many_arguments
185
+ abort "too many arguments"
186
+ end
187
+
188
+ def missing_argument(label = "attachment")
189
+ abort "#{label} expected but none given"
190
+ end
191
+
192
+ def fetch_or_abort(attachment)
193
+ branch = options[:upstream] || repository.guess_upstream || "refs/remotes/trunk"
194
+ attachment.fetch(options) do |a, dir, commit|
195
+ unless dir
196
+ abort "failed to apply #{attachment.tag_name} to #{branch.sub(/^refs\/(?:remotes\/)?/,'')}"
197
+ end
198
+ end
199
+ attachment
200
+ end
201
+
128
202
  def add_options(opts)
129
203
  end
130
204
 
@@ -132,8 +206,12 @@ Available commands:
132
206
  "[options]"
133
207
  end
134
208
 
209
+ def self.command
210
+ name[/[^:]*$/].gsub(/(.)([A-Z])/) { $1+"-"+$2 }.downcase
211
+ end
212
+
135
213
  def command
136
- self.class.name[/[^:]*$/].gsub(/(.)([A-Z])/) { $1+"-"+$2 }.downcase
214
+ self.class.command
137
215
  end
138
216
 
139
217
  def add_local_option(opts)
@@ -144,17 +222,12 @@ Available commands:
144
222
 
145
223
  def fetch_unless_local
146
224
  unless options[:local]
147
- if @repository.config("svn-remote.svn")
148
- command = "git svn fetch"
149
- elsif @repository.config("origin")
150
- command = "git fetch origin"
151
- else
152
- return
153
- end
154
- @repository.in_work_tree do
155
- unless system(command)
156
- abort "#{command} failed (use --local to skip)"
157
- end
225
+ if options[:fetch_command]
226
+ system(options[:fetch_command])
227
+ elsif repository.config("svn-remote.svn")
228
+ system("git svn fetch")
229
+ elsif repository.config("origin")
230
+ system("git fetch origin")
158
231
  end
159
232
  end
160
233
  end
@@ -166,8 +239,9 @@ Available commands:
166
239
  end
167
240
 
168
241
  require 'git/trac/runner/apply'
242
+ require 'git/trac/runner/checkout'
169
243
  require 'git/trac/runner/cleanup'
170
- require 'git/trac/runner/download'
171
244
  require 'git/trac/runner/fetch'
245
+ require 'git/trac/runner/help'
246
+ require 'git/trac/runner/push'
172
247
  require 'git/trac/runner/show'
173
- require 'git/trac/runner/upload_patch'