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.
@@ -4,8 +4,12 @@ module Git
4
4
 
5
5
  class Apply < Base #:nodoc:
6
6
 
7
+ def self.summary
8
+ "Apply a patch directly to the work tree"
9
+ end
10
+
7
11
  def banner_arguments
8
- "[ticket[/filename]]"
12
+ "[<attachment>]"
9
13
  end
10
14
 
11
15
  def description
@@ -14,43 +18,35 @@ Apply a patch directly to the work tree. This command is a building block used
14
18
  by other, more powerful commands and most users will never need to invoke it
15
19
  directly.
16
20
 
17
- The argument can be either a "ticket/filename" specifiying the patch to apply,
18
- or simply ticket, in which case the tickets's last patch is applied. If no
19
- argument is given, the patch is read from stdin.
20
-
21
21
  The --depth option specifies how many directories deep to recursively attempt
22
22
  to apply the patch in. For a depth of 2, the patch will be attempted in the
23
- repositority root, foo, foo/bar, but not foo/baz. It will also try stripping
24
- off up to 2 directories off of the paths inside the patch, the same as -p0,
25
- -p1, and -p2 with patch(1). The option is ignored if the patch appears to have
26
- been generated with git. The default value is 0, but this can be changed with
27
- the trac.depth configuration option.
23
+ repositority root, foo, foo/bar, but not foo/bar/baz. It will also try
24
+ stripping off up to 2 directories off of the paths inside the patch, the same
25
+ as -p0, -p1, and -p2 with patch(1). The option is ignored if the patch appears
26
+ to have been generated with git. The default value is 0, but this can be
27
+ changed with the trac.depth configuration option.
28
28
 
29
29
  If the patch fails to apply even after a depth search, and the patch contains
30
30
  carriage returns, a second pass is made with those carriage returns stripped.
31
+
32
+ If no argument is given, the patch is read from stdin.
31
33
  EOF
32
34
  end
33
35
 
34
36
  def add_options(opts)
35
- require_ticket_number
36
37
  opts.on("--depth NUM","search NUM directories deep for a root") do |n|
37
38
  options[:depth] = n
38
39
  end
39
40
  opts.on("--root DIR","apply patches relative to DIR") do |dir|
40
41
  options[:root] = dir
41
42
  end
42
- add_local_option(opts)
43
43
  end
44
44
 
45
45
  def run
46
46
  if @argv.empty?
47
- patch = Patch.new(@repository,$stdin.read)
48
- elsif @argv.size > 1
49
- abort "too many arguments"
47
+ patch = Patch.new(repository,$stdin.read)
50
48
  else
51
- each_ticket_argument do |number, filename|
52
- patch = @repository.ticket(number).attachment(filename).patch
53
- end
49
+ patch = one_attachment.patch
54
50
  end
55
51
  abort "no changes" if patch.body.empty?
56
52
  unless patch.apply(options)
@@ -0,0 +1,81 @@
1
+ module Git
2
+ module Trac
3
+ class Runner
4
+
5
+ class Checkout < Base #:nodoc:
6
+
7
+ include Fetchable
8
+
9
+ def self.summary
10
+ "Fetch an attachment and check it out into a branch"
11
+ end
12
+
13
+ def banner_arguments
14
+ "[options] <attachment> [<branchname>]"
15
+ end
16
+
17
+ def description
18
+ <<-EOF
19
+ Fetch the specified attachment and check it out into a new branch. If no
20
+ branch name is given, one is generated automatically from the attachment
21
+ filename.
22
+
23
+ If the branch already exists, it will be deleted if it contains either the same
24
+ exact changes as the attachment, or no changes at all, with respect to
25
+ upstream.
26
+ EOF
27
+ end
28
+
29
+ def add_options(opts)
30
+ @checkout_options = []
31
+ super
32
+ opts.on("--rebase","rebase after checkout") do
33
+ options[:rebase] = true
34
+ end
35
+ opts.on("-q","git checkout -q") do
36
+ @checkout_options << "-f"
37
+ end
38
+ opts.on("-f","git checkout -f") do
39
+ @checkout_options << "-f"
40
+ end
41
+ opts.on("-m","git checkout -m") do
42
+ @checkout_options << "-m"
43
+ end
44
+ end
45
+
46
+ def run
47
+ options[:upstream] ||= repository.guess_upstream || "refs/remotes/trunk"
48
+ fetch_unless_local
49
+ attachment = one_attachment(false)
50
+ branch = @argv.shift || attachment.name
51
+ too_many_arguments if @argv.any?
52
+ fetch_or_abort(attachment)
53
+
54
+ to_be_deleted = nil
55
+ repository.each_ref("refs/heads/#{branch}") do |object,ref|
56
+ hash = repository.diff_hash(object)
57
+ if hash == Digest::SHA1.digest("") || hash == repository.diff_hash(attachment.tag_name)
58
+ to_be_deleted = "#{branch}_tmp_git-trac_#{$$}"
59
+ repository.exec("git","branch","-m",branch,to_be_deleted)
60
+ end
61
+ end
62
+
63
+ repository.in_work_tree do
64
+ unless Kernel.system("git","checkout",*@checkout_options + ["-b",branch,attachment.tag_name])
65
+ exitstatus = $?.exitstatus
66
+ if to_be_deleted
67
+ repository.exec("git","branch","-m",to_be_deleted,branch)
68
+ end
69
+ exit exitstatus
70
+ end
71
+ end
72
+
73
+ repository.exec("git","branch","-D",to_be_deleted) if to_be_deleted
74
+ system("git","rebase",options[:upstream]) if options[:rebase]
75
+ end
76
+
77
+ end
78
+
79
+ end
80
+ end
81
+ end
@@ -4,13 +4,17 @@ module Git
4
4
 
5
5
  class Cleanup < Base #:nodoc:
6
6
 
7
+ def self.summary
8
+ "Remove old branches for a ticket"
9
+ end
10
+
7
11
  def banner_arguments
8
- "[options] [ticket[/filename]] ..."
12
+ "[options] [<attachment>...]"
9
13
  end
10
14
 
11
15
  def description
12
16
  <<-EOF
13
- Remove remote heads for a given ticket (e.g., trac/12345/work_patch). Also
17
+ Remove remote heads for a given ticket (e.g., trac/12345/work.patch). Also
14
18
  removes branches that point to one of these heads. Branches that have been
15
19
  committed to will not be removed. The default is to target tickets that have
16
20
  been closed, but you can also specify ticket numbers explicitly or use --all.
@@ -18,22 +22,27 @@ been closed, but you can also specify ticket numbers explicitly or use --all.
18
22
  end
19
23
 
20
24
  def add_options(opts)
21
- opts.separator("Options:")
22
25
  opts.on("-a","--all", "cleanup all tickets") { options[:all] = true }
26
+ opts.on("--only-branches", "cleanup branches only, not remote heads") do |b|
27
+ options[:only_branches] = true
28
+ end
29
+ opts.on("--[no-]rebased","also cleanup rebased attachments") do |b|
30
+ options[:rebased] = b
31
+ end
23
32
  end
24
33
 
25
34
  def run
26
35
  if options[:all]
27
- @repository.working_tickets.each do |t|
28
- t.cleanup
36
+ repository.working_tickets.each do |t|
37
+ t.cleanup(options)
29
38
  end
30
39
  elsif @argv.any?
31
- each_ticket_argument do |number, filename|
32
- @repository.ticket(number).cleanup(:attachment => filename)
40
+ each_ticket_or_attachment do |ticket_or_attachment|
41
+ ticket_or_attachment.cleanup(options)
33
42
  end
34
43
  else
35
- @repository.working_tickets.each do |t|
36
- t.cleanup unless t.open?
44
+ repository.working_tickets.each do |t|
45
+ t.cleanup(options) unless t.open?
37
46
  end
38
47
  end
39
48
  end
@@ -4,54 +4,60 @@ module Git
4
4
 
5
5
  class Fetch < Base #:nodoc:
6
6
 
7
+ include Fetchable
8
+
9
+ def self.summary
10
+ "Create remote heads for ticket attachments"
11
+ end
12
+
7
13
  def banner_arguments
8
- "[options] [ticket[/filename]] ..."
14
+ "<attachment>..."
9
15
  end
10
16
 
11
17
  def description
12
18
  <<-EOF
13
- Download all branches that look like patches. For each patch, find the
14
- revision of trunk that most recently proceeds the the time of upload, apply
15
- the patch to it, create a new commit, and add a remote head of the form
16
- refs/remotes/trac/ticketnumber/file_name.ext.
17
-
18
- For each unique base name (filename without extension), a branch is created
19
- pointing to the newest patch. Existing branches will not be overridden, but
20
- there is an implied `git cleanup <patch>` that runs beforehand which could
21
- potentially remove conflicting branches first. Automatic branch creation is
22
- informally deprecated and may be removed in a future release.
19
+ For each attachment argument, find the revision that most recently preceeds the
20
+ the time of upload, apply the patch to it, create a new commit, and add a
21
+ remote head of the form refs/remotes/trac/ticketnumber/filename.ext.
22
+
23
+ If the --auto-branch option is given, a unique branch is created for each
24
+ unique base name (filename without extension) of each attachment. If multiple
25
+ attachments have the same base name, the newest is used. Existing branches
26
+ will not be overridden, but there is an implied `git cleanup` that runs
27
+ beforehand which could potentially remove conflicting branches first.
28
+ Automatic branch creation used to be the default, but now the behavior is
29
+ deprecated and may be removed in a future release. Consider using `git-trac
30
+ checkout` instead.
23
31
  EOF
24
32
  end
25
33
 
26
34
  def add_options(opts)
27
- require_ticket_number
28
- opts.on("--branch BRANCH","apply against branch BRANCH") do |b|
29
- options[:branch] = b
30
- end
31
- opts.on("--depth NUM","search depth (see git-trac help apply)") do |n|
32
- options[:depth] = n
35
+ super
36
+ opts.on("--auto-branch","create branches for attachments") do
37
+ options[:auto_branch] = true
33
38
  end
34
- opts.on("--root DIR","apply patches relative to DIR") do |dir|
35
- options[:root] = dir
36
- end
37
- add_local_option(opts)
38
39
  end
39
40
 
40
41
  def run
41
42
  fetch_unless_local
42
- each_ticket_argument do |number, filename|
43
- loop_opts = options.dup
44
- if filename
45
- loop_opts[:filter] = "\\A#{Regexp.escape(filename)}"
46
- end
47
- @repository.ticket(number).fetch(loop_opts) do |attachment, dir|
48
- if dir == "."
43
+ each_ticket_or_attachment do |ticket|
44
+ seen = {}
45
+ ticket.fetch(options) do |attachment, dir, commit|
46
+ if dir == "." || dir == true
49
47
  puts "#{attachment.tag_name}"
50
48
  elsif dir
51
49
  puts "#{attachment.tag_name} (#{dir})"
52
50
  else
53
51
  $stderr.puts "#{attachment.tag_name} FAILED"
54
52
  end
53
+ seen[attachment.name] = commit if commit
54
+ end
55
+ if options[:auto_branch]
56
+ seen.each do |k,v|
57
+ if !File.exists?(path = "#{repository.git_dir}/refs/heads/#{k}")
58
+ File.open(path, "w") {|f| f.puts v}
59
+ end
60
+ end
55
61
  end
56
62
  end
57
63
  end
@@ -0,0 +1,141 @@
1
+ module Git
2
+ module Trac
3
+ class Runner
4
+
5
+ class Help < Base #:nodoc:
6
+
7
+ def self.summary
8
+ "Help for a command or topic"
9
+ end
10
+
11
+ def banner_arguments
12
+ "[<command> | --commands | <topic> | --topics]"
13
+ end
14
+
15
+ def add_options(opts)
16
+ @options = {} # Don't try to read the repository config
17
+ opts.on("--commands","list all commands") do |commands|
18
+ options[:commands] = commands
19
+ end
20
+ opts.on("--topics","list all topics") do |topics|
21
+ options[:topics] = topics
22
+ end
23
+ end
24
+
25
+ def description
26
+ <<-EOF
27
+ Show help for a given topic or command.
28
+ EOF
29
+ end
30
+
31
+ def commands
32
+ Runner.commands.map do |runner|
33
+ [runner.command, runner.summary]
34
+ end
35
+ end
36
+
37
+ def topics
38
+ TOPICS.keys.compact.sort.map do |key|
39
+ [key, TOPICS[key].first]
40
+ end
41
+ end
42
+
43
+ def run
44
+ topic = @argv.first
45
+ if options[:commands]
46
+ puts commands.map {|t| "%-15s %s\n" % t}.join
47
+ elsif options[:topics]
48
+ puts topics.map {|t| "%-15s %s\n" % t}.join
49
+ elsif !topic
50
+ Pager.new.page(default)
51
+ elsif klass = Runner.for_command(topic)
52
+ return klass.new(["--help"],nil)
53
+ elsif TOPICS[topic]
54
+ body = TOPICS[topic].last
55
+ body = body.call(self) if body.respond_to?(:call)
56
+ Pager.new.page(body)
57
+ else
58
+ abort "no such topic #{topic}"
59
+ end
60
+ end
61
+
62
+ def default
63
+ out = "Usage: git-trac <command> [options] [arguments]\n"
64
+ out << "\nCommands:\n"
65
+ out << commands.map {|t| " %-15s %s\n" % t}.join
66
+ out << "\nHelp topics:\n"
67
+ out << topics.map {|t| " %-15s %s\n" % t}.join
68
+ out << "\nUse git-trac help <command> or git-trac help <topic> for further information.\n"
69
+ end
70
+
71
+ TOPICS = {}
72
+
73
+ TOPICS["config"] = ["Configuration"] << <<-EOF
74
+ The following configuration options control git-trac:
75
+ trac.url The root URL to the Trac website
76
+ trac.username Username (if required by site)
77
+ trac.password Password (if required by site)
78
+ trac.depth Default --depth for commands which accept it
79
+ trac.fetch-command Update command to use in lieu of git svn fetch
80
+
81
+ They may be set either by editing the git config file or with git config.
82
+ $ vim .git/config
83
+ $ git config trac.url http://my.trac.url
84
+
85
+ Additionally, the following options can affect git-trac's behavior:
86
+ core.pager
87
+ color.diff
88
+ color.pager
89
+
90
+ Tip: `git-trac` can be called as `git trac` with the following alias:
91
+ $ git config alias.trac '!git-trac'
92
+ EOF
93
+
94
+ TOPICS["rails"] = ["Quick start guide for Rails"] << <<-EOF
95
+ Example for usage on the Ruby on Rails repository:
96
+
97
+ # Export the repository if you haven't already (start it and go to lunch)
98
+ $ git svn clone --stdlayout http://dev.rubyonrails.org/svn/rails rails
99
+ $ cd rails
100
+
101
+ # This next step is optional but compresses the repository by a factor of 10
102
+ $ git gc
103
+
104
+ # Setup git-trac (see git-trac help config)
105
+ $ git config trac.url http://dev.rubyonrails.org
106
+ $ git config trac.username myaccount
107
+ $ git config trac.password mypassword
108
+
109
+ # Have fun
110
+ $ git-trac checkout 1234567/new_feature_with_failing_tests.patch
111
+ $ rake
112
+ $ git-trac checkout --rebase 1234567/new_feature_with_passing_tests.patch
113
+ # EDIT EDIT EDIT
114
+ $ git commit -a
115
+ $ git-trac push 1234567/improved_feature.patch
116
+ EOF
117
+
118
+ TOPICS["arguments"] = ["Definitions for <ticket> and <attachment>"] << <<-EOF
119
+ A <ticket> is specified by number, which may be optionally prefixed by a slash
120
+ terminated string.
121
+ 123456
122
+ trac/123456
123
+ http://dev.rubyonrails.org/ticket/123456
124
+
125
+ An <attachment> is a <ticket> followed by a slash and a filename. Anything
126
+ in this filename after a slash or a question mark is ignored.
127
+ 123456/foo.patch
128
+ trac/123456/foo.patch
129
+ http://dev.rubyonrails.org/attachment/ticket/123456/foo.patch
130
+ http://dev.rubyonrails.org/attachment/ticket/123456/foo.patch?format=raw
131
+
132
+ If a ticket is given to a command expecting a multiple attachments, all the
133
+ ticket's attachments are used. If the command accepts just one attachment,
134
+ just the ticket's last attachment is used.
135
+ EOF
136
+
137
+ end
138
+
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,79 @@
1
+ module Git
2
+ module Trac
3
+ class Runner
4
+
5
+ class Push < Base #:nodoc:
6
+
7
+ def self.summary
8
+ "Upload the current diff against upstream to a ticket"
9
+ end
10
+
11
+ def banner_arguments
12
+ "[options] <ticket>[/<filename>]"
13
+ end
14
+
15
+ def description
16
+ <<-EOF
17
+ Do a `git diff` and upload the result as an attachment to a ticket. The
18
+ potential patch will be shown in a pager and you will be given the opportunity
19
+ to cancel. If no filename is given, the name of the current branch plus
20
+ ".patch" is used.
21
+
22
+ This command was formerly known as upload-patch.
23
+ EOF
24
+ end
25
+
26
+ def add_options(opts)
27
+ opts.on("--against BRANCH", "git diff BRANCH...HEAD") do |b|
28
+ options[:upstream] = b
29
+ end
30
+ opts.on("--description TEXT", "use TEXT as description") do |text|
31
+ options[:description] = text
32
+ end
33
+ opts.on("--[no-]force", "do not prompt before uploading") do |force|
34
+ options[:force] = force
35
+ end
36
+ add_local_option(opts)
37
+ end
38
+
39
+ def run
40
+ if @argv.size > 1
41
+ too_many_arguments
42
+ elsif @argv.empty?
43
+ number = repository.guess_current_ticket_number or
44
+ missing_argument "ticket"
45
+ else
46
+ number, options[:filename] = parse_attachment(@argv.shift)
47
+ end
48
+ fetch_unless_local
49
+ ticket = repository.ticket(number)
50
+ options[:upstream] ||= repository.guess_upstream || "refs/remotes/trunk"
51
+ options[:upstream] += "...HEAD" unless options[:upstream].include?(".")
52
+ if $stdin.tty? && !options[:force]
53
+ block = lambda do
54
+ repository.in_work_tree do
55
+ system("git","diff", options[:upstream])
56
+ end
57
+ description = "##{number} (#{ticket.csv["summary"]}"
58
+ cols = ENV["COLUMNS"].to_i
59
+ cols = 80 if cols.zero?
60
+ description.sub!(/^(.{#{cols-22}}).{4,}/,"\\1...")
61
+ print "#{description}) Proceed? [yN] "
62
+ $stdin.gets[0,1] == "y"
63
+ end
64
+ else
65
+ block = lambda { true }
66
+ end
67
+ if uri = ticket.upload_patch(options,&block)
68
+ puts uri
69
+ else
70
+ exit 1
71
+ end
72
+ end
73
+ end
74
+
75
+ UploadPatch = Push
76
+
77
+ end
78
+ end
79
+ end