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 +39 -24
- data/Rakefile +3 -2
- data/lib/git/trac.rb +1 -0
- data/lib/git/trac/attachment.rb +17 -11
- data/lib/git/trac/pager.rb +33 -11
- data/lib/git/trac/repository.rb +54 -3
- data/lib/git/trac/runner.rb +139 -65
- data/lib/git/trac/runner/apply.rb +14 -18
- data/lib/git/trac/runner/checkout.rb +81 -0
- data/lib/git/trac/runner/cleanup.rb +18 -9
- data/lib/git/trac/runner/fetch.rb +34 -28
- data/lib/git/trac/runner/help.rb +141 -0
- data/lib/git/trac/runner/push.rb +79 -0
- data/lib/git/trac/runner/show.rb +55 -8
- data/lib/git/trac/ticket.rb +14 -48
- data/lib/git/trac/version.rb +11 -0
- metadata +14 -12
- data/lib/git/trac/runner/download.rb +0 -44
- data/lib/git/trac/runner/upload_patch.rb +0 -62
@@ -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
|
-
"[
|
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
|
24
|
-
off up to 2 directories off of the paths inside the patch, the same
|
25
|
-
-p1, and -p2 with patch(1). The option is ignored if the patch appears
|
26
|
-
been generated with git. The default value is 0, but this can be
|
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(
|
48
|
-
elsif @argv.size > 1
|
49
|
-
abort "too many arguments"
|
47
|
+
patch = Patch.new(repository,$stdin.read)
|
50
48
|
else
|
51
|
-
|
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] [
|
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/
|
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
|
-
|
28
|
-
t.cleanup
|
36
|
+
repository.working_tickets.each do |t|
|
37
|
+
t.cleanup(options)
|
29
38
|
end
|
30
39
|
elsif @argv.any?
|
31
|
-
|
32
|
-
|
40
|
+
each_ticket_or_attachment do |ticket_or_attachment|
|
41
|
+
ticket_or_attachment.cleanup(options)
|
33
42
|
end
|
34
43
|
else
|
35
|
-
|
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
|
-
"
|
14
|
+
"<attachment>..."
|
9
15
|
end
|
10
16
|
|
11
17
|
def description
|
12
18
|
<<-EOF
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
there is an implied `git cleanup
|
21
|
-
potentially remove conflicting branches first.
|
22
|
-
|
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
|
-
|
28
|
-
opts.on("--branch
|
29
|
-
options[:
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|