git-trac 0.0.20080206 → 0.1.0

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 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'