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