git-topic 0.1.6.4 → 0.2.1
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/.vimspell.utf8.add +9 -0
- data/.vimspell.utf8.add.spl +0 -0
- data/README.rdoc +57 -27
- data/Rakefile +10 -0
- data/VERSION.yml +3 -3
- data/bin/git-topic-completion +58 -0
- data/lib/core_ext.rb +74 -12
- data/lib/git_topic.rb +145 -12
- data/lib/git_topic/cli.rb +105 -13
- data/lib/git_topic/comment.rb +273 -0
- data/lib/git_topic/git.rb +66 -3
- data/lib/git_topic/naming.rb +26 -5
- data/share/completion.bash +28 -0
- data/spec/bash_completion.rb +2 -0
- data/spec/comment_spec.rb +396 -0
- data/spec/git_topic_accept_spec.rb +78 -0
- data/spec/git_topic_comment_spec.rb +393 -0
- data/spec/git_topic_comments_spec.rb +47 -0
- data/spec/git_topic_done_spec.rb +94 -0
- data/spec/git_topic_install_aliases_spec.rb +32 -0
- data/spec/git_topic_reject_spec.rb +112 -0
- data/spec/git_topic_review_spec.rb +115 -0
- data/spec/git_topic_status_spec.rb +63 -0
- data/spec/git_topic_work_on_spec.rb +121 -0
- data/spec/spec_helper.rb +136 -0
- metadata +34 -11
- data/git-topic +0 -13
- data/git-topic.gemspec +0 -190
- data/spec/git_topic_spec.rb +0 -502
data/lib/git_topic/cli.rb
CHANGED
@@ -10,7 +10,9 @@ require 'git_topic'
|
|
10
10
|
|
11
11
|
|
12
12
|
module GitTopic
|
13
|
-
SubCommands = %w(
|
13
|
+
SubCommands = %w(
|
14
|
+
work-on done status review comment comments accept reject install-aliases
|
15
|
+
)
|
14
16
|
Version = lambda {
|
15
17
|
h = YAML::load_file( "#{File.dirname( __FILE__ )}/../../VERSION.yml" )
|
16
18
|
if h.is_a? Hash
|
@@ -35,30 +37,57 @@ module GitTopic
|
|
35
37
|
".cleanup
|
36
38
|
version Version
|
37
39
|
|
38
|
-
opt :verbose,
|
40
|
+
opt :verbose,
|
41
|
+
"Verbose output, including complete traces on errors."
|
42
|
+
opt :completion_help,
|
43
|
+
"View instructions for setting up autocompletion."
|
44
|
+
|
39
45
|
stop_on SubCommands
|
40
46
|
end
|
41
47
|
|
48
|
+
|
49
|
+
if global_opts[:completion_help]
|
50
|
+
root = File.expand_path( "#{File.dirname( __FILE__ )}/../.." )
|
51
|
+
completion_bash_file = "#{root}/share/completion.bash"
|
52
|
+
completion_bin_file = "#{root}/bin/git-topic-completion"
|
53
|
+
gem_bin_dir = Gem.default_bindir
|
54
|
+
puts %Q{
|
55
|
+
To make use of bash autocompletion, you must do the following:
|
56
|
+
|
57
|
+
1. Make sure you source #{completion_bash_file} before you source
|
58
|
+
git's completion.
|
59
|
+
2. Optionally, copy #{completion_bin_file} to #{gem_bin_dir}.
|
60
|
+
This is to sidestep ruby issue 3465 which makes loading gems far too
|
61
|
+
slow for autocompletion. For more information, see:
|
62
|
+
|
63
|
+
http://redmine.ruby-lang.org/issues/show/3465
|
64
|
+
}.cleanup
|
65
|
+
exit 0
|
66
|
+
end
|
67
|
+
|
68
|
+
|
42
69
|
cmd = ARGV.shift
|
43
70
|
cmd_opts = Trollop::options do
|
44
71
|
case cmd
|
45
72
|
when "work-on"
|
46
73
|
banner "
|
47
|
-
git work-on <topic>
|
48
|
-
git-topic work-on <topic>
|
74
|
+
git[-topic] work-on <topic>
|
49
75
|
|
50
76
|
Switches to a local work-in-progress (wip) branch for <topic>. The
|
51
77
|
branch (and a matching remote branch) is created if necessary.
|
52
78
|
|
53
79
|
If this is a rejected topic, work will continue from the state of
|
54
|
-
the rejected topic branch.
|
80
|
+
the rejected topic branch. Similarly, if this is a review topic,
|
81
|
+
the review will be pulled and work will continue on that topic.
|
82
|
+
|
83
|
+
<topic>'s branches HEAD will point to <upstream>. If <upstream> is
|
84
|
+
omitted, it will default to the current HEAD.
|
55
85
|
|
56
86
|
Options:
|
57
87
|
".cleanup
|
58
88
|
when /done(-with)?/
|
59
89
|
banner "
|
60
|
-
git done
|
61
|
-
git-topic done
|
90
|
+
git[-topic] done
|
62
91
|
|
63
92
|
Indicate that this topic branch is ready for review. Push to a
|
64
93
|
remote review branch and switch back to master.
|
@@ -83,17 +112,66 @@ module GitTopic
|
|
83
112
|
:default => false
|
84
113
|
when "review"
|
85
114
|
banner "
|
86
|
-
git review [<topic>]
|
87
|
-
git-topic reivew [<topic>]
|
115
|
+
git[-topic] review [<topic>]
|
88
116
|
|
89
117
|
Review <topic>. If <topic> is unspecified, review the oldest (by HEAD) topic.
|
90
118
|
|
119
|
+
Options:
|
120
|
+
".cleanup
|
121
|
+
when "comment"
|
122
|
+
banner "
|
123
|
+
git[-topic] comment
|
124
|
+
|
125
|
+
Add your comments to the current topic. If this is the first time
|
126
|
+
you are reviwing <topic> you can set initial comments (see
|
127
|
+
INITIAL_COMMENTS below). Otherwise, your GIT_EDITOR will open to
|
128
|
+
let you enter your replies to the comments.
|
129
|
+
|
130
|
+
Similarly, if you are working on a rejected branch, git-topic
|
131
|
+
comment will open your GIT_EDITOR so you can reply to the reviewer's
|
132
|
+
comments.
|
133
|
+
|
134
|
+
INITIAL_COMMENTS
|
135
|
+
|
136
|
+
For the initial set of comments, you can edit the files in your
|
137
|
+
working tree to include any file specific comments. Simply ensure
|
138
|
+
that all such comments are prefixed with a ‘#’. git-topic comment
|
139
|
+
will convert your changes to a list of file-specific comments.
|
140
|
+
|
141
|
+
In order to use this feature, there are several requirements about
|
142
|
+
the output of git diff.
|
143
|
+
|
144
|
+
1. It must only have file modifications. i.e., no deletions,
|
145
|
+
additions or mode changes.
|
146
|
+
|
147
|
+
2. Those modifications must only have line additions. i.e. no line
|
148
|
+
deletions.
|
149
|
+
|
150
|
+
3. Those line additions must all begin with any amount of
|
151
|
+
whitespace followed by a ‘#’ character. i.e. they should be
|
152
|
+
comments.
|
153
|
+
|
154
|
+
Options:
|
155
|
+
".cleanup
|
156
|
+
|
157
|
+
opt :force_update,
|
158
|
+
"
|
159
|
+
If you are commenting on the initial review and you wish to
|
160
|
+
edit your comments, you can pass this flag to do so.
|
161
|
+
".oneline
|
162
|
+
when "comments"
|
163
|
+
banner "
|
164
|
+
git[-topic] comments
|
165
|
+
|
166
|
+
View the comments for the current topic. If your branch was
|
167
|
+
rejected, you should read these comments so you know what to do to
|
168
|
+
appease the reviewer.
|
169
|
+
|
91
170
|
Options:
|
92
171
|
".cleanup
|
93
172
|
when "accept"
|
94
173
|
banner "
|
95
|
-
git accept
|
96
|
-
git-topic accept
|
174
|
+
git[-topic] accept
|
97
175
|
|
98
176
|
Accept the current in-review topic, merging it to master and
|
99
177
|
cleaning up the remote branch. This will fail if the branch does
|
@@ -104,13 +182,19 @@ module GitTopic
|
|
104
182
|
".cleanup
|
105
183
|
when "reject"
|
106
184
|
banner "
|
107
|
-
git reject
|
108
|
-
git-topic reject
|
185
|
+
git[-topic] reject
|
109
186
|
|
110
187
|
Reject the current in-review topic.
|
111
188
|
|
112
189
|
Options:
|
113
190
|
".cleanup
|
191
|
+
|
192
|
+
opt :save_comments,
|
193
|
+
"
|
194
|
+
If the current diff includes your comments (see git-topic
|
195
|
+
comment --help), this flag will autosave those comments before
|
196
|
+
rejecting the branch.
|
197
|
+
".oneline
|
114
198
|
when "install-aliases"
|
115
199
|
banner "
|
116
200
|
git-topic install-aliases
|
@@ -143,6 +227,10 @@ module GitTopic
|
|
143
227
|
case cmd
|
144
228
|
when "work-on"
|
145
229
|
topic = ARGV.shift
|
230
|
+
upstream = ARGV.shift
|
231
|
+
opts.merge!({
|
232
|
+
:upstream => upstream
|
233
|
+
})
|
146
234
|
work_on topic, opts
|
147
235
|
when /done(-with)?/
|
148
236
|
topic = ARGV.shift
|
@@ -152,6 +240,10 @@ module GitTopic
|
|
152
240
|
when "review"
|
153
241
|
spec = ARGV.shift
|
154
242
|
review spec, opts
|
243
|
+
when "comment"
|
244
|
+
comment opts
|
245
|
+
when "comments"
|
246
|
+
comments opts
|
155
247
|
when "accept"
|
156
248
|
topic = ARGV.shift
|
157
249
|
accept topic, opts
|
@@ -0,0 +1,273 @@
|
|
1
|
+
|
2
|
+
module GitTopic; end
|
3
|
+
module GitTopic::Comment
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
|
7
|
+
def diff_to_file_specific_notes( diff, opts={} )
|
8
|
+
raise "
|
9
|
+
Must specify :author to attribute diffs to.
|
10
|
+
" unless opts.has_key? :author
|
11
|
+
|
12
|
+
initial_buffer = StringIO.new
|
13
|
+
comment_buffer = ''
|
14
|
+
flush_comment_buffer = lambda do
|
15
|
+
next if comment_buffer.empty?
|
16
|
+
formatted_comment = comment_buffer.wrap(
|
17
|
+
80,
|
18
|
+
4,
|
19
|
+
attrib( opts[:author], 4 ))
|
20
|
+
initial_buffer.puts formatted_comment
|
21
|
+
comment_buffer = ''
|
22
|
+
end
|
23
|
+
|
24
|
+
diff.each_line do |line|
|
25
|
+
flush_comment_buffer.call unless line =~ %r{^\+[^+]}
|
26
|
+
case line
|
27
|
+
when %r{^diff --git a/(.*) }
|
28
|
+
path = $1
|
29
|
+
initial_buffer.puts "\n\n" unless initial_buffer.size == 0
|
30
|
+
initial_buffer.puts "./#{path}"
|
31
|
+
when %r{^@@ -(\d+),}
|
32
|
+
line = $1.to_i + 1
|
33
|
+
initial_buffer.puts ""
|
34
|
+
initial_buffer.puts " Line #{line}"
|
35
|
+
when %r{^\+[^+]}
|
36
|
+
raise "
|
37
|
+
Diff includes non-comment additions. Each added line must be
|
38
|
+
prefixed with any amount of whitespace and then a ‘#’ character.
|
39
|
+
".oneline unless line =~ %r{^\+\s*#(.*)}
|
40
|
+
|
41
|
+
comment_part = "#{$1}\n"
|
42
|
+
comment_buffer << comment_part
|
43
|
+
when %r{^old mode}
|
44
|
+
raise "Diff includes mode changes."
|
45
|
+
when %r{^\- }
|
46
|
+
raise "Diff includes deletions."
|
47
|
+
end
|
48
|
+
end
|
49
|
+
flush_comment_buffer.call
|
50
|
+
initial_buffer.string
|
51
|
+
end
|
52
|
+
|
53
|
+
def collect_comments( edit_file )
|
54
|
+
author = git_author_name_short
|
55
|
+
diff = capture_git(
|
56
|
+
"diff --diff-filter=M -U0 --no-color --no-renames -B",
|
57
|
+
:must_succeed => true )
|
58
|
+
|
59
|
+
# file specific notes computed from diff
|
60
|
+
fs_notes = diff_to_file_specific_notes( diff, :author => author )
|
61
|
+
edit_file = "#{git_dir}/COMMENT_EDITMSG"
|
62
|
+
|
63
|
+
# solicit the user (via GIT_EDITOR) for any general notes beyond the file
|
64
|
+
# specific ones
|
65
|
+
File.open( edit_file, 'w' ) do |f|
|
66
|
+
f.puts %Q{
|
67
|
+
# Edit this file to include whatever general comments you would like.
|
68
|
+
# The file specific comments, shown below, will be appended to
|
69
|
+
# anything you put here. Your comments will automatically be
|
70
|
+
# attributed.
|
71
|
+
#
|
72
|
+
# Any lines beginning with a ‘#’ character will be ignored.
|
73
|
+
#
|
74
|
+
#
|
75
|
+
# Ceterum censeo, Carthaginem esse delendam.
|
76
|
+
#
|
77
|
+
#
|
78
|
+
}.cleanup
|
79
|
+
f.puts( fs_notes.lines.map do |line|
|
80
|
+
"# #{line}"
|
81
|
+
end.join )
|
82
|
+
end
|
83
|
+
invoke_git_editor( edit_file )
|
84
|
+
|
85
|
+
# combine the general and file_specific notes
|
86
|
+
general_notes = File.readlines( edit_file ).reject do |line|
|
87
|
+
line =~ %r{^\s*#}
|
88
|
+
end.join( "" ).strip
|
89
|
+
|
90
|
+
unless general_notes.empty?
|
91
|
+
general_notes.wrap!( 80, 4, attrib( author ))
|
92
|
+
end
|
93
|
+
|
94
|
+
notes = [general_notes, fs_notes].reject{ |n| n.empty? }.join( "\n\n" )
|
95
|
+
|
96
|
+
notes
|
97
|
+
end
|
98
|
+
|
99
|
+
def notes_from_initial_comments( mode="add" )
|
100
|
+
raise %Q{
|
101
|
+
Illegal mode [#{mode}]. Specify either “add” or “edit”
|
102
|
+
} unless ["add", "edit"].include? mode
|
103
|
+
|
104
|
+
edit_file = "#{git_dir}/COMMENT_EDITMSG"
|
105
|
+
notes = collect_comments( edit_file )
|
106
|
+
|
107
|
+
# Write the complete set of comments to a git note at the appropriate ref
|
108
|
+
File.open( edit_file, 'w' ){ |f| f.write( notes )}
|
109
|
+
git "notes --ref #{notes_ref} #{mode} -F #{edit_file}",
|
110
|
+
:must_succeed => true
|
111
|
+
|
112
|
+
# If all has gone well so far, clear the diff
|
113
|
+
git "reset --hard"
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
def notes_to_hash( notes )
|
118
|
+
result = {}
|
119
|
+
current_hash = result
|
120
|
+
key = :general
|
121
|
+
pos = -1
|
122
|
+
|
123
|
+
update_key = lambda do
|
124
|
+
current_hash[ key ] = pos + 1 unless key.nil?
|
125
|
+
end
|
126
|
+
|
127
|
+
notes.lines.each_with_index do |line, line_no|
|
128
|
+
case line
|
129
|
+
when %r{^(./.*)}
|
130
|
+
# new file
|
131
|
+
update_key.call
|
132
|
+
path = $1
|
133
|
+
result[ path ] = {}
|
134
|
+
current_hash = result[ path ]
|
135
|
+
key = nil
|
136
|
+
when %r{^\s*Line (\d+)\s*$}
|
137
|
+
# new line in existing file
|
138
|
+
update_key.call
|
139
|
+
comment_line_no = $1.to_i
|
140
|
+
key = comment_line_no
|
141
|
+
end
|
142
|
+
pos = line_no unless line =~ %r{^\s*$}
|
143
|
+
end
|
144
|
+
update_key.call
|
145
|
+
|
146
|
+
result
|
147
|
+
end
|
148
|
+
|
149
|
+
def append_reply( notes, content, opts={} )
|
150
|
+
raise "
|
151
|
+
Must specify :author to attribute diffs to.
|
152
|
+
" unless opts.has_key? :author
|
153
|
+
|
154
|
+
notes_hash = notes_to_hash( notes )
|
155
|
+
result = notes.split( "\n" )
|
156
|
+
replies = {}
|
157
|
+
reply_at_line = notes_hash[ :general ]
|
158
|
+
context_file = nil
|
159
|
+
context_line = nil
|
160
|
+
reply = nil
|
161
|
+
|
162
|
+
add_reply = lambda do
|
163
|
+
break if reply.nil?
|
164
|
+
|
165
|
+
unless context_file.nil?
|
166
|
+
raise "
|
167
|
+
Unexpected reply to file [#{context_file}]
|
168
|
+
".oneline unless notes_hash.has_key? context_file
|
169
|
+
|
170
|
+
raise "
|
171
|
+
Unexpected reply to file [#{context_file}] without a line context.
|
172
|
+
".oneline if context_line.nil?
|
173
|
+
|
174
|
+
raise "
|
175
|
+
Unexpected reply to context line [#{context_line}] of file
|
176
|
+
[#{context_file}]
|
177
|
+
".oneline unless notes_hash[ context_file ].has_key? context_line
|
178
|
+
end
|
179
|
+
|
180
|
+
attrib_indent =
|
181
|
+
reply_at_line == notes_hash[ :general ] ? 0 : 4
|
182
|
+
|
183
|
+
replies[ reply_at_line ] =
|
184
|
+
reply.strip.wrap( 80, 4, attrib( opts[:author], attrib_indent ))
|
185
|
+
|
186
|
+
reply = nil
|
187
|
+
end
|
188
|
+
|
189
|
+
content.each_line do |line|
|
190
|
+
case line
|
191
|
+
when %r{^# (./.*)$}
|
192
|
+
# Context switched to new file
|
193
|
+
add_reply.call
|
194
|
+
context_file = $1
|
195
|
+
context_line = nil
|
196
|
+
reply_at_line = nil
|
197
|
+
when %r{^#\s*Line (\d+).*$}
|
198
|
+
add_reply.call
|
199
|
+
context_line = $1.to_i
|
200
|
+
file_hash = notes_hash[ context_file ]
|
201
|
+
reply_at_line = file_hash && file_hash[ context_line ]
|
202
|
+
when %r{^\s*#}
|
203
|
+
# non-signposting comment, ignore it
|
204
|
+
else
|
205
|
+
(reply ||= '' ) << line
|
206
|
+
end
|
207
|
+
end
|
208
|
+
add_reply.call
|
209
|
+
|
210
|
+
replies.keys.sort.reverse.each do |reply_at_line|
|
211
|
+
reply_content = replies[ reply_at_line ]
|
212
|
+
i = reply_at_line
|
213
|
+
result[ i..i ] = reply_content, result[ i ]
|
214
|
+
end
|
215
|
+
|
216
|
+
result.join( "\n" )
|
217
|
+
end
|
218
|
+
|
219
|
+
def notes_from_reply_to_comments
|
220
|
+
raise "There is nothing to reply to." unless existing_comments?
|
221
|
+
|
222
|
+
notes = existing_comments
|
223
|
+
edit_file = "#{git_dir}/COMMENT_EDITMSG"
|
224
|
+
File.open( edit_file, 'w' ) do |f|
|
225
|
+
f.puts %Q{
|
226
|
+
# Edit this file to include your replies. Place your replies below
|
227
|
+
# either the general comments, or below a comment on a specific line
|
228
|
+
# in a specific file. Do not remove any of the existing lines.
|
229
|
+
#
|
230
|
+
# Any lines beginning with a ‘#’ character will be ignored.
|
231
|
+
#
|
232
|
+
#
|
233
|
+
# In the beginning the Universe was created. This has made a lot of
|
234
|
+
# people very angry and been widely regarded as a bad move.
|
235
|
+
# Douglas Adams
|
236
|
+
#
|
237
|
+
#
|
238
|
+
}.cleanup
|
239
|
+
f.puts( notes.lines.map do |line|
|
240
|
+
"# #{line}"
|
241
|
+
end.join )
|
242
|
+
end
|
243
|
+
invoke_git_editor( edit_file )
|
244
|
+
content = File.read( edit_file )
|
245
|
+
|
246
|
+
notes_with_reply = append_reply(
|
247
|
+
notes,
|
248
|
+
content,
|
249
|
+
:author => git_author_name_short )
|
250
|
+
|
251
|
+
File.open( edit_file, 'w' ){ |f| f.write( notes_with_reply )}
|
252
|
+
git "notes --ref #{notes_ref} edit -F #{edit_file}",
|
253
|
+
:must_succeed => true
|
254
|
+
end
|
255
|
+
|
256
|
+
def attrib( author, indent=0, max_w=16 )
|
257
|
+
w = max_w - indent
|
258
|
+
attrib =
|
259
|
+
if author.size < w
|
260
|
+
"#{author}:"
|
261
|
+
else
|
262
|
+
fname = author.split.first
|
263
|
+
"#{fname[0...w-1]}:"
|
264
|
+
end
|
265
|
+
sprintf "%s%-#{w}.#{w}s", (' ' * indent), attrib
|
266
|
+
end
|
267
|
+
|
268
|
+
end
|
269
|
+
|
270
|
+
def self.included( base )
|
271
|
+
base.extend ClassMethods
|
272
|
+
end
|
273
|
+
end
|