git-topic 0.1.6.4 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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(work-on done status review accept reject install-aliases)
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, "Verbose output, including complete traces on errors."
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