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/git.rb CHANGED
@@ -9,6 +9,34 @@ module GitTopic::Git
9
9
 
10
10
  protected
11
11
 
12
+ def git_dir
13
+ @@git_dir ||= (
14
+ git_dir = capture_git( "rev-parse --git-dir" ).chomp;
15
+ raise "Unexpected gitdir: [#{git_dir}]" unless git_dir.index '.git'
16
+ git_dir
17
+ )
18
+ end
19
+
20
+ def git_editor
21
+ @@git_editor ||= capture_git( "var GIT_EDITOR" ).chomp
22
+ end
23
+
24
+ def git_author_name_short
25
+ @@git_author_name_short ||= (
26
+ full_name = capture_git( "config user.name" )
27
+ raise "
28
+ whatever
29
+ " unless $?.success?
30
+
31
+ parts = full_name.split( " " )
32
+ fname = parts.shift
33
+ fname = "#{fname[0..0].upcase}#{fname[1..-1]}"
34
+ suffix = parts.map{ |p| p[0..0].upcase }.join( "" )
35
+
36
+ "#{fname} #{suffix}".strip
37
+ )
38
+ end
39
+
12
40
  def working_tree_clean?
13
41
  git [ "diff --quiet", "diff --quiet --cached" ]
14
42
  $?.success?
@@ -18,6 +46,15 @@ module GitTopic::Git
18
46
  not working_tree_clean?
19
47
  end
20
48
 
49
+ def existing_comments?( branch=current_branch )
50
+ ref = notes_ref( branch )
51
+ not capture_git( "notes --ref #{ref} list" ).chomp.empty?
52
+ end
53
+
54
+ def existing_comments
55
+ capture_git( "notes --ref #{notes_ref} show" ).chomp
56
+ end
57
+
21
58
 
22
59
  def display_git_output?
23
60
  @@display_git_output ||= false
@@ -36,6 +73,10 @@ module GitTopic::Git
36
73
  end
37
74
  end
38
75
 
76
+ def invoke_git_editor( file )
77
+ system "#{git_editor} #{file}"
78
+ end
79
+
39
80
  def cmd_redirect_suffix( opts )
40
81
  if !opts[:show] && !display_git_output?
41
82
  "> /dev/null 2> /dev/null"
@@ -43,21 +84,43 @@ module GitTopic::Git
43
84
  end
44
85
 
45
86
  def git( cmds=[], opts={} )
87
+ opts.assert_valid_keys :must_succeed
88
+
46
89
  cmds = [cmds] if cmds.is_a? String
47
90
  redir = cmd_redirect_suffix( opts )
48
91
  cmd = cmds.map{|c| "git #{c} #{redir}"}.join( " && " )
49
92
 
50
93
  puts cmd if GitTopic::global_opts[:verbose]
51
- system cmd
94
+ result = system( cmd )
95
+
96
+ if opts[:must_succeed] && !$?.success?
97
+ raise "
98
+ Required git command failed:\n #{cmd}.\n re-run with --verbose to
99
+ see git output.
100
+ ".cleanup
101
+ end
102
+
103
+ result
52
104
  end
53
105
 
54
- def capture_git( cmds=[] )
106
+ def capture_git( cmds=[], opts={} )
107
+ opts.assert_valid_keys :must_succeed
108
+
55
109
  cmds = [cmds] if cmds.is_a? String
56
110
  redir = "2> /dev/null" unless display_git_output?
57
111
  cmd = "#{cmds.map{|c| "git #{c} #{redir}"}.join( " && " )}"
58
112
 
59
113
  puts cmd if GitTopic::global_opts[:verbose]
60
- `#{cmd}`
114
+ result = `#{cmd}`
115
+
116
+ if opts[:must_succeed] && !$?.success?
117
+ raise "
118
+ Required git command failed:\n #{cmd}.\n re-run with --verbose to
119
+ see git output.
120
+ ".cleanup
121
+ end
122
+
123
+ result
61
124
  end
62
125
 
63
126
  end
@@ -32,7 +32,7 @@ module GitTopic::Naming
32
32
  end
33
33
 
34
34
  def strip_namespace( ref )
35
- if ref =~ %r{(?:wip|rejected|review)/\S*/(.*)}
35
+ if ref =~ %r{(?:wip|rejected|review)/(?:(?:\S*)/)?(.*)}
36
36
  $1
37
37
  else
38
38
  ref
@@ -40,13 +40,20 @@ module GitTopic::Naming
40
40
  end
41
41
 
42
42
 
43
+ def notes_ref( branch=current_branch )
44
+ user, topic = user_topic_name( branch )
45
+ "refs/notes/reviews/#{user}/#{topic}"
46
+ end
47
+
48
+
43
49
  def user_topic_name( branch )
44
50
  if branch =~ %r{^origin}
45
51
  branch =~ %r{^\S*?/\S*?/(\S*?)/(\S*)}
46
52
  [$1, $2]
47
- else
48
- branch =~ %r{^\S*?/(\S*?)/(\S*)}
53
+ elsif branch =~ %r{^(?:\S*/)?(\S*?)/(\S*)}
49
54
  [$1, $2]
55
+ else
56
+ raise "Cannot compute user_topic for [#{branch}]"
50
57
  end
51
58
  end
52
59
 
@@ -54,6 +61,8 @@ module GitTopic::Naming
54
61
  p = {}
55
62
  parts = ref.split( '/' )
56
63
  case parts.size
64
+ when 3
65
+ _, p[:user], p[:topic] = parts
57
66
  when 2
58
67
  p[:user], p[:topic] = parts
59
68
  when 1
@@ -69,6 +78,11 @@ module GitTopic::Naming
69
78
  @@user ||= (ENV['USER'] || `whoami`)
70
79
  end
71
80
 
81
+ def current_namespace
82
+ current_branch =~ %r{(wip|review|rejected)/(\S*)}
83
+ $1
84
+ end
85
+
72
86
  def current_topic
73
87
  current_branch =~ %r{wip/\S*?/(\S*)}
74
88
  $1
@@ -120,9 +134,16 @@ module GitTopic::Naming
120
134
  end
121
135
 
122
136
  namespace_ut.symbolize_keys!
123
- namespace_ut[:review] ||= {}
137
+ namespace_ut[:review] ||= {}
124
138
  namespace_ut[:rejected] ||= {}
125
139
 
140
+ namespace_ut[:rejected].each do |user, topics|
141
+ topics.map! do |topic|
142
+ suffix = " (reviewer comments) "
143
+ "#{topic}#{suffix if existing_comments?( "#{user}/#{topic}" )}"
144
+ end
145
+ end
146
+
126
147
  namespace_ut[:review].reject!{|k,v| k == user}
127
148
  namespace_ut
128
149
  )
@@ -138,7 +159,7 @@ module GitTopic::Naming
138
159
 
139
160
  commits_by_age.find do |ref|
140
161
  # no ‘,’, i.e. only one ref matches the commit
141
- ref.index( ',' ).nil?
162
+ !ref.strip.empty? && ref.index( ',' ).nil?
142
163
  end.strip[ 1..-2 ] # chomp the leading and trailing parenthesis
143
164
  end
144
165
 
@@ -0,0 +1,28 @@
1
+
2
+
3
+
4
+ _git_work_on() {
5
+ __gitcomp "$(git-topic-completion work-on 2> /dev/null)"
6
+ return
7
+ }
8
+
9
+ _git_review() {
10
+ __gitcomp "$(git-topic-completion review 2> /dev/null)"
11
+ return
12
+ }
13
+
14
+
15
+ _git_done() {
16
+ __gitcomp "$(git-topic-completion done 2> /dev/null)"
17
+ return
18
+ }
19
+
20
+ _git_accept() {
21
+ __gitcomp "$(git-topic-completion accept 2> /dev/null)"
22
+ return
23
+ }
24
+
25
+ _git_reject() {
26
+ __gitcomp "$(git-topic-completion reject 2> /dev/null)"
27
+ return
28
+ }
@@ -0,0 +1,2 @@
1
+
2
+
@@ -0,0 +1,396 @@
1
+ require 'spec_helper'
2
+
3
+ DiffWithNonCommentAddition = %Q{
4
+ diff --git a/lib/git_topic.rb b/lib/git_topic.rb
5
+ index edb53f1..adceb44 100644
6
+ --- a/lib/git_topic.rb
7
+ +++ b/lib/git_topic.rb
8
+ @@ -9,0 +10 @@ require 'git_topic/naming'
9
+ +require 'git_topic/comment'
10
+ }.strip
11
+
12
+ DiffWithDeletion = %Q{
13
+ diff --git a/Gemfile b/Gemfile
14
+ index ece2952..9d2ccec 100644
15
+ --- a/Gemfile
16
+ +++ b/Gemfile
17
+ @@ -7 +6,0 @@ group :runtime do
18
+ - gem 'trollop'
19
+ }.strip
20
+
21
+ DiffWithOnlyComments = %Q{
22
+ diff --git a/Gemfile b/Gemfile
23
+ index ece2952..8485775 100644
24
+ --- a/Gemfile
25
+ +++ b/Gemfile
26
+ @@ -5,0 +6,2 @@ group :runtime do
27
+ + # what was the point of this?
28
+ + # surely it would be better with crazy doom stuff and fingles, no?
29
+ diff --git a/lib/git_topic.rb b/lib/git_topic.rb
30
+ index adceb44..920e512 100644
31
+ --- a/lib/git_topic.rb
32
+ +++ b/lib/git_topic.rb
33
+ @@ -17,0 +18 @@ module GitTopic
34
+ + # This is kind of ugly. Why can't we pull this into cli?
35
+ @@ -55,0 +57 @@ module GitTopic
36
+ + # Oh the night patty murphy died... yadda yadda.
37
+ @@ -107,0 +110,3 @@ module GitTopic
38
+ + # That's how they showed their respect for patty murphy! that's how they
39
+ + # showed their honour and their pride! They said it was a cryin' shame
40
+ + # and they winked at one another.
41
+ @@ -111,0 +117 @@ module GitTopic
42
+ + # and so on and so forth. The point is, there was a lot of wailing.
43
+ @@ -118,0 +125 @@ module GitTopic
44
+ + # They put the bottle with the corpse to keep that whiskey cold!
45
+ diff --git a/Rakefile b/Rakefile
46
+ index 309d6df..73428b2 100644
47
+ --- a/Rakefile
48
+ +++ b/Rakefile
49
+ @@ -64,0 +65,4 @@ rescue LoadError
50
+ + # Awesome I guess. But why not something like:
51
+ + # Crazy good.
52
+ + # Or something vaguely similar.
53
+ + # It would seem better that way.
54
+ }.strip
55
+
56
+
57
+ EditorBufferResultFromClean = %Q{
58
+ ./Gemfile
59
+
60
+ Line 6
61
+
62
+ David: what was the point of this? surely it would be better with
63
+ crazy doom stuff and fingles, no?
64
+
65
+
66
+ ./lib/git_topic.rb
67
+
68
+ Line 18
69
+
70
+ David: This is kind of ugly. Why can't we pull this into cli?
71
+
72
+ Line 56
73
+
74
+ David: Oh the night patty murphy died... yadda yadda.
75
+
76
+ Line 108
77
+
78
+ David: That's how they showed their respect for patty murphy!
79
+ that's how they showed their honour and their pride! They
80
+ said it was a cryin' shame and they winked at one another.
81
+
82
+ Line 112
83
+
84
+ David: and so on and so forth. The point is, there was a lot of
85
+ wailing.
86
+
87
+ Line 119
88
+
89
+ David: They put the bottle with the corpse to keep that whiskey
90
+ cold!
91
+
92
+
93
+ ./Rakefile
94
+
95
+ Line 65
96
+
97
+ David: Awesome I guess. But why not something like:
98
+ Crazy good.
99
+ Or something vaguely similar. It would seem better that way.
100
+ }.strip
101
+
102
+
103
+ ExistingComment = %Q{
104
+ Spec 123: I have some general comments, mostly relating to the quality
105
+ of our zombie-control policies. Basically, they're not
106
+ working.
107
+
108
+ ./zombies
109
+
110
+ Line 2
111
+ Spec 123: I suggest we do the following instead:
112
+ zombies.each{ |z| reason_with( z )}
113
+ zomies.select do |z|
114
+ z.unconvinced?
115
+ end.each do |z|
116
+ destroy z
117
+ end
118
+ This should take care of our issues with zombies.
119
+
120
+
121
+ ./lib/foo/zombie_repellant
122
+
123
+ Line 6
124
+ Spec 123: Does this stuff really work? I'm not convinced.
125
+
126
+ Line 40
127
+ Spec 123: Please explain this.
128
+ }.strip
129
+
130
+ LegalReply = %Q{
131
+ # Spec 123: I have some general comments, mostly relating to the quality
132
+ # of our zombie-control policies. Basically, they're not
133
+ # working.
134
+
135
+ Sorry about that. I have fixed the branch to have better zombie-control policies.
136
+
137
+ #
138
+ # ./zombies
139
+ #
140
+ # Line 2
141
+ # Spec 123: I suggest we do the following instead:
142
+ # zombies.each{ |z| reason_with( z )}
143
+ # zomies.select do |z|
144
+ # z.unconvinced?
145
+ # end.each do |z|
146
+ # destroy z
147
+ # end
148
+ # This should take care of our issues with zombies.
149
+
150
+ Excellent suggestion. It's done.
151
+ #
152
+ # ./lib/foo/zombie_repellant
153
+ #
154
+ # Line 6
155
+ # Spec 123: Does this stuff really work? I'm not convinced.
156
+
157
+ You can look at the tests yourself. It seems to work.
158
+ #
159
+ # Line 40
160
+ # Spec 123: Please explain this.
161
+ }.strip
162
+
163
+ CommentWithReply = %Q{
164
+ Spec 123: I have some general comments, mostly relating to the quality
165
+ of our zombie-control policies. Basically, they're not
166
+ working.
167
+ Spec 456: Sorry about that. I have fixed the branch to have better
168
+ zombie-control policies.
169
+
170
+ ./zombies
171
+
172
+ Line 2
173
+ Spec 123: I suggest we do the following instead:
174
+ zombies.each{ |z| reason_with( z )}
175
+ zomies.select do |z|
176
+ z.unconvinced?
177
+ end.each do |z|
178
+ destroy z
179
+ end
180
+ This should take care of our issues with zombies.
181
+ Spec 456: Excellent suggestion. It's done.
182
+
183
+
184
+ ./lib/foo/zombie_repellant
185
+
186
+ Line 6
187
+ Spec 123: Does this stuff really work? I'm not convinced.
188
+ Spec 456: You can look at the tests yourself. It seems to work.
189
+
190
+ Line 40
191
+ Spec 123: Please explain this.
192
+ }.strip
193
+
194
+ MalformedReplyBadLines = %Q{
195
+ # Spec 123: I have some general comments, mostly relating to the quality
196
+ # of our zombie-control policies. Basically, they're not
197
+ # working.
198
+ #
199
+ # ./zombies
200
+
201
+ Uh oh. a malformed reply. This is not a legal spot for comments!
202
+
203
+ #
204
+ # Line 2
205
+ # Spec 123: I suggest we do the following instead:
206
+ # zombies.each{ |z| reason_with( z )}
207
+ # zomies.select do |z|
208
+ # z.unconvinced?
209
+ # end.each do |z|
210
+ # destroy z
211
+ # end
212
+ # This should take care of our issues with zombies.
213
+ #
214
+ # ./lib/foo/zombie_repellant
215
+ #
216
+ # Line 6
217
+ # Spec 123: Does this stuff really work? I'm not convinced.
218
+ #
219
+ # Line 40
220
+ # Spec 123: Please explain this.
221
+ }.strip
222
+
223
+ MalformedReplyNewFiles = %Q{
224
+ # Spec 123: I have some general comments, mostly relating to the quality
225
+ # of our zombie-control policies. Basically, they're not
226
+ # working.
227
+ #
228
+ # ./zombies
229
+ #
230
+ # Line 2
231
+ # Spec 123: I suggest we do the following instead:
232
+ # zombies.each{ |z| reason_with( z )}
233
+ # zomies.select do |z|
234
+ # z.unconvinced?
235
+ # end.each do |z|
236
+ # destroy z
237
+ # end
238
+ # This should take care of our issues with zombies.
239
+ #
240
+ # ./anew/and/illegal/path
241
+ Uh oh. This appears to be an invalid reply! Madness!
242
+ #
243
+ # ./lib/foo/zombie_repellant
244
+ #
245
+ # Line 6
246
+ # Spec 123: Does this stuff really work? I'm not convinced.
247
+ #
248
+ # Line 40
249
+ # Spec 123: Please explain this.
250
+ }.strip
251
+
252
+
253
+ describe GitTopic::Comment do
254
+ include GitTopic::Comment::ClassMethods
255
+
256
+ describe "#diff_to_file_specific_notes" do
257
+
258
+ it "should fail with non-comment additions" do
259
+ lambda do
260
+ diff_to_file_specific_notes DiffWithNonCommentAddition,
261
+ :author => "David"
262
+ end.should raise_error
263
+ end
264
+
265
+ it "should fail with deletions" do
266
+ lambda do
267
+ diff_to_file_specific_notes DiffWithDeletion,
268
+ :author => "David"
269
+ end.should raise_error
270
+ end
271
+
272
+ it "should fail if :author is not supplied" do
273
+ lambda do
274
+ diff_to_file_specific_notes DiffWithOnlyComments
275
+ end.should raise_error
276
+ end
277
+
278
+ describe "when there are no existing comments" do
279
+
280
+ it "should format the diff correctly for no existing comments" do
281
+ initial_contents = diff_to_file_specific_notes(
282
+ DiffWithOnlyComments,
283
+ :author => "David"
284
+ )
285
+
286
+ # Not interested in exact formatting: just the contents.
287
+ initial_contents.gsub( /\s+/, "\n").strip.should ==
288
+ EditorBufferResultFromClean.gsub( /\s+/, "\n" ).strip
289
+ end
290
+
291
+ end
292
+ end
293
+
294
+
295
+ describe "#notes_from_reply_to_comments" do
296
+
297
+ it "should fail if there are no existing comments" do
298
+ self.should_receive( :existing_comments? ).and_return( false )
299
+ lambda{ notes_from_reply_to_comments }.should raise_error
300
+ end
301
+
302
+ it "
303
+ should complain if the user malforms the file by responding to ‘new’
304
+ files
305
+ ".oneline do
306
+
307
+ self.should_receive( :existing_comments? ). and_return( true )
308
+ self.should_receive( :existing_comments ). and_return( ExistingComment )
309
+ self.should_receive( :git_dir ). and_return( "." )
310
+ self.should_receive( :notes_ref ).exactly( 0 ).times
311
+ self.stub!( :invoke_git_editor ) do |path|
312
+ File.open( path, 'w' ){ |f| f.puts MalformedReplyNewFiles }
313
+ end
314
+ self.should_receive( :invoke_git_editor )
315
+
316
+ lambda{ notes_from_reply_to_comments }.should raise_error
317
+ end
318
+
319
+ it "
320
+ should complain if the user malforms the file by responding to
321
+ file-specific comments before the line sections
322
+ ".oneline do
323
+
324
+ self.should_receive( :existing_comments? ). and_return( true )
325
+ self.should_receive( :existing_comments ). and_return( ExistingComment )
326
+ self.should_receive( :git_dir ). and_return( "." )
327
+ self.should_receive( :notes_ref ).exactly( 0 ).times
328
+ self.stub!( :invoke_git_editor ) do |path|
329
+ File.open( path, 'w' ){ |f| f.puts MalformedReplyBadLines }
330
+ end
331
+ self.should_receive( :invoke_git_editor )
332
+
333
+ lambda{ notes_from_reply_to_comments }.should raise_error
334
+ end
335
+
336
+ it "should add the users' comments as replies to the originals" do
337
+ self.should_receive( :existing_comments? ). and_return( true )
338
+ self.should_receive( :existing_comments ). and_return( ExistingComment )
339
+ self.should_receive( :git_dir ). and_return( "." )
340
+ self.should_receive( :git_author_name_short ). and_return( "Spec 456" )
341
+ self.should_receive( :notes_ref ).
342
+ and_return( "refs/notes/reviews/#{@user}/topic" )
343
+ self.stub!( :invoke_git_editor ) do |path|
344
+ File.open( path, 'w' ){ |f| f.write LegalReply }
345
+ end
346
+ self.should_receive( :invoke_git_editor )
347
+ self.should_receive( :git ) do
348
+ File.read( "./COMMENT_EDITMSG" ).should == CommentWithReply
349
+ end
350
+
351
+ lambda{ notes_from_reply_to_comments }.should_not raise_error
352
+ end
353
+
354
+ end
355
+
356
+
357
+ describe "#notes_to_hash" do
358
+ it "should convert notes to a hash" do
359
+ hash = notes_to_hash( ExistingComment )
360
+ hash.should_not be_nil
361
+ hash.keys.should == [ :general,
362
+ "./zombies",
363
+ "./lib/foo/zombie_repellant" ]
364
+ hash[ :general ].should == 3
365
+ hash[ "./zombies" ].should be_a Hash
366
+ hash[ "./lib/foo/zombie_repellant" ].should be_a Hash
367
+
368
+ zombie_hash = hash[ "./zombies" ]
369
+ repellant_hash = hash[ "./lib/foo/zombie_repellant" ]
370
+
371
+ zombie_hash.keys.should == [ 2 ]
372
+ repellant_hash.keys.should == [ 6, 40 ]
373
+
374
+ zombie_hash[ 2 ].should == 15
375
+ repellant_hash[ 6 ].should == 21
376
+ repellant_hash[ 40 ].should == 24
377
+ end
378
+ end
379
+
380
+ describe "#attrib" do
381
+
382
+ it "should return a 16-length string for short author names" do
383
+ attrib( "David" ).should == "David: "
384
+ end
385
+
386
+ it "should truncate very long author names" do
387
+ attrib( "DavidVonSuperLongNameStuff" ).
388
+ should == "DavidVonSuperLo:"
389
+ end
390
+
391
+ it "should respect padding for short author names" do
392
+ attrib( "David", 4 ).should == " David: "
393
+ end
394
+ end
395
+
396
+ end