git-topic 0.1.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.
Files changed (54) hide show
  1. data/.gitignore +4 -0
  2. data/.gvimrc +23 -0
  3. data/.rspec +1 -0
  4. data/.rvmrc +1 -0
  5. data/.vimproject +26 -0
  6. data/.vimrc +1 -0
  7. data/Gemfile +19 -0
  8. data/Gemfile.lock +63 -0
  9. data/History.txt +6 -0
  10. data/LICENSE +20 -0
  11. data/README.rdoc +192 -0
  12. data/Rakefile +62 -0
  13. data/VERSION +1 -0
  14. data/autotest/discover.rb +1 -0
  15. data/bin/git-topic +143 -0
  16. data/git-topic.gemspec +127 -0
  17. data/lib/git-topic.rb +375 -0
  18. data/lib/util.rb +34 -0
  19. data/spec/git-topic_spec.rb +378 -0
  20. data/spec/spec_helper.rb +56 -0
  21. data/spec/template/origin/HEAD +1 -0
  22. data/spec/template/origin/config +7 -0
  23. data/spec/template/origin/description +1 -0
  24. data/spec/template/origin/hooks/applypatch-msg.sample +15 -0
  25. data/spec/template/origin/hooks/commit-msg.sample +24 -0
  26. data/spec/template/origin/hooks/post-commit.sample +8 -0
  27. data/spec/template/origin/hooks/post-receive.sample +15 -0
  28. data/spec/template/origin/hooks/post-update.sample +8 -0
  29. data/spec/template/origin/hooks/pre-applypatch.sample +14 -0
  30. data/spec/template/origin/hooks/pre-commit.sample +46 -0
  31. data/spec/template/origin/hooks/pre-rebase.sample +169 -0
  32. data/spec/template/origin/hooks/prepare-commit-msg.sample +36 -0
  33. data/spec/template/origin/hooks/update.sample +128 -0
  34. data/spec/template/origin/info/exclude +6 -0
  35. data/spec/template/origin/objects/0a/da6d051b94cd0df50f5a0b7229aec26f0d2cdf +0 -0
  36. data/spec/template/origin/objects/0c/e06c616769768f09f5e629cfcc68eabe3dee81 +0 -0
  37. data/spec/template/origin/objects/20/049991cdafdce826f5a3c01e10ffa84d6997ec +0 -0
  38. data/spec/template/origin/objects/33/1d827fd47fb234af54e3a4bbf8c6705e9116cc +3 -0
  39. data/spec/template/origin/objects/41/51899b742fd6b1c873b177b9d13451682089bc +0 -0
  40. data/spec/template/origin/objects/44/ffd9c9c8b52b201659e3ad318cdad6ec836b46 +0 -0
  41. data/spec/template/origin/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 +0 -0
  42. data/spec/template/origin/objects/55/eeb01bdf874d1a35870bcf24a970c475c63344 +0 -0
  43. data/spec/template/origin/objects/8d/09f9b8d80ce282218125cb0cbf53cccf022203 +0 -0
  44. data/spec/template/origin/objects/b4/8e68d5cac189af36abe48e893d11c24b7b2a19 +0 -0
  45. data/spec/template/origin/objects/c0/838ed2ee8f2e83c8bda859fc5e332b92f0a5a3 +1 -0
  46. data/spec/template/origin/objects/cd/f7b9dbc4911a0d1404db54cde2ed448f6a6afd +0 -0
  47. data/spec/template/origin/objects/d2/6b33daea1ed9823a189992bba38fbc913483c1 +0 -0
  48. data/spec/template/origin/objects/fe/4e254557e19f338f40ccfdc00a7517771db880 +0 -0
  49. data/spec/template/origin/refs/heads/master +1 -0
  50. data/spec/template/origin/refs/heads/rejected/davidjh/krakens +1 -0
  51. data/spec/template/origin/refs/heads/review/davidjh/pirates +1 -0
  52. data/spec/template/origin/refs/heads/review/user24601/ninja-basic +1 -0
  53. data/spec/template/origin/refs/heads/review/user24601/zombie-basic +1 -0
  54. metadata +158 -0
data/git-topic.gemspec ADDED
@@ -0,0 +1,127 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{git-topic}
8
+ s.version = "0.1.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["David J. Hamilton"]
12
+ s.date = %q{2010-07-08}
13
+ s.default_executable = %q{git-topic}
14
+ s.description = %q{
15
+ gem command around reviewed topic branches. Supports workflow of the form:
16
+
17
+ # alexander:
18
+ git work-on <topic>
19
+ git done
20
+
21
+ # bismarck:
22
+ git status # notice a review branch
23
+ git review <topic>
24
+ # happy, merge into master, push and cleanup
25
+ git accept
26
+
27
+ git review <topic2>
28
+ # unhappy
29
+ git reject
30
+
31
+ # alexander:
32
+ git status # notice rejected topic
33
+ git work-on <topic>
34
+
35
+ see README.rdoc for more (any) details.
36
+ }
37
+ s.email = %q{git-topic@hjdivad.com}
38
+ s.executables = ["git-topic"]
39
+ s.extra_rdoc_files = [
40
+ "LICENSE",
41
+ "README.rdoc"
42
+ ]
43
+ s.files = [
44
+ ".gitignore",
45
+ ".gvimrc",
46
+ ".rspec",
47
+ ".rvmrc",
48
+ ".vimproject",
49
+ ".vimrc",
50
+ "Gemfile",
51
+ "Gemfile.lock",
52
+ "History.txt",
53
+ "LICENSE",
54
+ "README.rdoc",
55
+ "Rakefile",
56
+ "VERSION",
57
+ "autotest/discover.rb",
58
+ "bin/git-topic",
59
+ "git-topic.gemspec",
60
+ "lib/git-topic.rb",
61
+ "lib/util.rb",
62
+ "spec/git-topic_spec.rb",
63
+ "spec/spec_helper.rb",
64
+ "spec/template/origin/HEAD",
65
+ "spec/template/origin/config",
66
+ "spec/template/origin/description",
67
+ "spec/template/origin/hooks/applypatch-msg.sample",
68
+ "spec/template/origin/hooks/commit-msg.sample",
69
+ "spec/template/origin/hooks/post-commit.sample",
70
+ "spec/template/origin/hooks/post-receive.sample",
71
+ "spec/template/origin/hooks/post-update.sample",
72
+ "spec/template/origin/hooks/pre-applypatch.sample",
73
+ "spec/template/origin/hooks/pre-commit.sample",
74
+ "spec/template/origin/hooks/pre-rebase.sample",
75
+ "spec/template/origin/hooks/prepare-commit-msg.sample",
76
+ "spec/template/origin/hooks/update.sample",
77
+ "spec/template/origin/info/exclude",
78
+ "spec/template/origin/objects/0a/da6d051b94cd0df50f5a0b7229aec26f0d2cdf",
79
+ "spec/template/origin/objects/0c/e06c616769768f09f5e629cfcc68eabe3dee81",
80
+ "spec/template/origin/objects/20/049991cdafdce826f5a3c01e10ffa84d6997ec",
81
+ "spec/template/origin/objects/33/1d827fd47fb234af54e3a4bbf8c6705e9116cc",
82
+ "spec/template/origin/objects/41/51899b742fd6b1c873b177b9d13451682089bc",
83
+ "spec/template/origin/objects/44/ffd9c9c8b52b201659e3ad318cdad6ec836b46",
84
+ "spec/template/origin/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904",
85
+ "spec/template/origin/objects/55/eeb01bdf874d1a35870bcf24a970c475c63344",
86
+ "spec/template/origin/objects/8d/09f9b8d80ce282218125cb0cbf53cccf022203",
87
+ "spec/template/origin/objects/b4/8e68d5cac189af36abe48e893d11c24b7b2a19",
88
+ "spec/template/origin/objects/c0/838ed2ee8f2e83c8bda859fc5e332b92f0a5a3",
89
+ "spec/template/origin/objects/cd/f7b9dbc4911a0d1404db54cde2ed448f6a6afd",
90
+ "spec/template/origin/objects/d2/6b33daea1ed9823a189992bba38fbc913483c1",
91
+ "spec/template/origin/objects/fe/4e254557e19f338f40ccfdc00a7517771db880",
92
+ "spec/template/origin/refs/heads/master",
93
+ "spec/template/origin/refs/heads/rejected/davidjh/krakens",
94
+ "spec/template/origin/refs/heads/review/davidjh/pirates",
95
+ "spec/template/origin/refs/heads/review/user24601/ninja-basic",
96
+ "spec/template/origin/refs/heads/review/user24601/zombie-basic"
97
+ ]
98
+ s.homepage = %q{http://github.com/hjdivad/git-topic}
99
+ s.rdoc_options = ["--charset=UTF-8"]
100
+ s.require_paths = ["lib"]
101
+ s.rubygems_version = %q{1.3.7}
102
+ s.summary = %q{git command around reviewed topic branches}
103
+ s.test_files = [
104
+ "spec/spec_helper.rb",
105
+ "spec/git-topic_spec.rb"
106
+ ]
107
+
108
+ if s.respond_to? :specification_version then
109
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
110
+ s.specification_version = 3
111
+
112
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
113
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
114
+ s.add_development_dependency(%q<yard>, [">= 0"])
115
+ s.add_development_dependency(%q<cucumber>, [">= 0"])
116
+ else
117
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
118
+ s.add_dependency(%q<yard>, [">= 0"])
119
+ s.add_dependency(%q<cucumber>, [">= 0"])
120
+ end
121
+ else
122
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
123
+ s.add_dependency(%q<yard>, [">= 0"])
124
+ s.add_dependency(%q<cucumber>, [">= 0"])
125
+ end
126
+ end
127
+
data/lib/git-topic.rb ADDED
@@ -0,0 +1,375 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require 'active_support'
5
+ require 'active_support/core_ext/hash/keys'
6
+
7
+ require 'util'
8
+
9
+
10
+ module GitTopic
11
+ GlobalOptKeys = [ :verbose, :help, :verbose_given ]
12
+
13
+ class << self
14
+
15
+ # Switch to a branch for the given topic.
16
+ def work_on( topic, opts={} )
17
+ raise "Topic must be specified" if topic.nil?
18
+
19
+ # setup a remote branch, if necessary
20
+ wb = wip_branch( topic )
21
+ git(
22
+ "push origin HEAD:#{wb}"
23
+ ) unless remote_branches.include? "origin/#{wb}"
24
+ # switch to the new branch
25
+ git [ switch_to_branch( wb, "origin/#{wb}" )]
26
+
27
+ # Check for rejected branch
28
+ rej_branch = rejected_branch( topic )
29
+ if remote_branches.include? "origin/#{rej_branch}"
30
+ git [
31
+ "reset --hard origin/#{rej_branch}",
32
+ "push origin :#{rej_branch} HEAD:#{wb}",
33
+ ]
34
+ end
35
+ end
36
+
37
+ # Done with the given topic. If none is specified, then topic is assumed to
38
+ # be the current branch (if it's a topic branch).
39
+ def done( topic=nil, opts={} )
40
+ raise(
41
+ "Branch must be a topic branch"
42
+ ) unless current_branch =~ %r{^wip/}
43
+ raise(
44
+ "Working tree must be clean"
45
+ ) unless working_tree_clean?
46
+
47
+ topic = current_topic if topic.nil?
48
+
49
+ wb = wip_branch( topic )
50
+ rb = review_branch( topic )
51
+ git [
52
+ "push origin #{wb}:#{rb} :#{wb}",
53
+ "checkout master",
54
+ "branch -D #{wip_branch( topic )}"
55
+ ]
56
+ end
57
+
58
+ # Produce status like
59
+ #
60
+ # # There are 2 topics you can review.
61
+ # #
62
+ # # from davidjh:
63
+ # # zombies
64
+ # # pirates
65
+ # # from king-julian:
66
+ # # fish
67
+ # # whales
68
+ # #
69
+ # # 2 of your topics were rejected.
70
+ # # dragons
71
+ # # liches
72
+ def status( opts={} )
73
+ opts.assert_valid_keys :prepended, :prepended_given, *GlobalOptKeys
74
+
75
+ sb = ''
76
+ rb = remote_branches_organized
77
+ review_ut = rb[:review]
78
+ rejected_ut = rb[:rejected]
79
+
80
+ unless review_ut.empty?
81
+ prep = review_ut.size == 1 ? "is 1" : "are #{review_ut.size}"
82
+ sb << "# There #{prep} #{'topic'.pluralize( review_ut.size )} you can review.\n\n"
83
+
84
+ sb << review_ut.map do |user, topics|
85
+ sb2 = " from #{user}:\n"
86
+ sb2 << topics.map{|t| " #{t}"}.join( "\n" )
87
+ sb2
88
+ end.join( "\n" )
89
+ end
90
+
91
+ rejected_topics = rejected_ut[ user ] || []
92
+ unless rejected_topics.empty?
93
+ sb << "\n" unless review_ut.empty?
94
+ verb = rejected_topics.size == 1 ? 'is' : 'are'
95
+ sb << "\n#{rejected_topics.size} of your topics #{verb} rejected.\n "
96
+ sb << rejected_topics.join( "\n " )
97
+ end
98
+
99
+ sb.gsub! "\n", "\n# "
100
+ sb << "\n" unless sb.empty?
101
+ print sb
102
+
103
+ if opts[ :prepended ]
104
+ print "#\n" unless sb.empty?
105
+ git "status", :show => true
106
+ end
107
+ end
108
+
109
+ # Switch to a review branch to check somebody else's code.
110
+ def review( spec=nil, opts={} )
111
+ rb = remote_branches_organized
112
+ review_branches = rb[:review]
113
+
114
+ if spec.nil?
115
+ # select the oldest (by HEAD) topic, if any exist
116
+ if review_branches.empty?
117
+ puts "nothing to review."
118
+ return
119
+ end
120
+
121
+ user, topic = oldest_review_user_topic
122
+ else
123
+ user, topic = spec.split( '/' )
124
+ end
125
+
126
+ if remote_topic_branch = find_remote_review_branch( topic )
127
+ git [
128
+ switch_to_branch(
129
+ review_branch( topic, user ),
130
+ remote_topic_branch )]
131
+ else
132
+ raise "No review topic found matching ‘#{spec}’"
133
+ end
134
+ end
135
+
136
+ # Accept the branch currently being reviewed.
137
+ def accept( topic=nil, opts={} )
138
+ raise "Must be on a review branch." unless on_review_branch?
139
+
140
+ # switch to master
141
+ # merge review branch, assuming FF
142
+ # push master, destroy remote
143
+ # destroy local
144
+ user, topic = user_topic_name( current_branch )
145
+
146
+ local_review_branch = current_branch
147
+ ff_merge = git [
148
+ "checkout master",
149
+ "merge --ff-only #{local_review_branch}",
150
+ ]
151
+
152
+ unless ff_merge
153
+ git "checkout #{local_review_branch}"
154
+ raise "
155
+ review branch is not up to date: merge not a fast-forward. Either
156
+ rebase or reject this branch.
157
+ ".cleanup
158
+ end
159
+
160
+ rem_review_branch = find_remote_review_branch( topic ).gsub( %r{^origin/}, '' )
161
+ git [
162
+ "push origin :#{rem_review_branch}",
163
+ "branch -d #{local_review_branch}"
164
+ ]
165
+ end
166
+
167
+ # Reject the branch currently being reviewed.
168
+ def reject( topic=nil, opts={} )
169
+ raise "Must be on a review branch." unless on_review_branch?
170
+
171
+ # switch to master
172
+ # push to rejected, destroy remote
173
+ # destroy local
174
+ user, topic = user_topic_name( current_branch )
175
+
176
+ rem_review_branch = find_remote_review_branch( topic ).gsub( %r{^origin/}, '' )
177
+ rem_rej_branch = remote_rejected_branch( topic, user )
178
+ git [
179
+ "checkout master",
180
+ "push origin #{current_branch}:#{rem_rej_branch} :#{rem_review_branch}",
181
+ "branch -D #{current_branch}"
182
+ ]
183
+ end
184
+
185
+ def install_aliases( opts={} )
186
+ opts.assert_valid_keys :local, :local_given, *GlobalOptKeys
187
+
188
+ flags = "--global" unless opts[:local]
189
+
190
+ git [
191
+ "config #{flags} alias.work-on 'topic work-on'",
192
+ "config #{flags} alias.done 'topic done'",
193
+ "config #{flags} alias.review 'topic review'",
194
+ "config #{flags} alias.accept 'topic accept'",
195
+ "config #{flags} alias.reject 'topic reject'",
196
+
197
+ "config #{flags} alias.w 'topic work-on'",
198
+ "config #{flags} alias.r 'topic review'",
199
+ "config #{flags} alias.st 'topic status --prepended'",
200
+ ]
201
+ end
202
+
203
+
204
+ private
205
+
206
+
207
+ def backup_branch( topic )
208
+ "backup/#{user}/#{topic}"
209
+ end
210
+
211
+ def wip_branch( topic )
212
+ "wip/#{user}/#{topic}"
213
+ end
214
+
215
+ def rejected_branch( topic )
216
+ "rejected/#{user}/#{topic}"
217
+ end
218
+
219
+ def review_branch( topic, user=user )
220
+ "review/#{user}/#{topic}"
221
+ end
222
+
223
+ def remote_rejected_branch( topic, user=user )
224
+ "rejected/#{user}/#{topic}"
225
+ end
226
+
227
+
228
+ def find_remote_review_branch( topic )
229
+ others_review_branches.find{|b| b.index topic}
230
+ end
231
+
232
+
233
+ def user_topic_name( branch )
234
+ if branch =~ %r{^origin}
235
+ branch =~ %r{^\S*?/\S*?/(\S*?)/(\S*)}
236
+ [$1, $2]
237
+ else
238
+ branch =~ %r{^\S*?/(\S*?)/(\S*)}
239
+ [$1, $2]
240
+ end
241
+ end
242
+
243
+
244
+ def user
245
+ @@user ||= (ENV['USER'] || `whoami`)
246
+ end
247
+
248
+ def current_topic
249
+ current_branch =~ %r{wip/\S*?/(\S*)}
250
+ $1
251
+ end
252
+
253
+ def current_branch
254
+ @@current_branch ||= capture_git( "branch --no-color" ).split( "\n" ).find do |b|
255
+ b =~ %r{^\*}
256
+ end[ 2..-1 ]
257
+ end
258
+
259
+ def branches
260
+ @@branches ||= capture_git( "branch --no-color" ).split( "\n" ).map{|b| b[2..-1]}
261
+ end
262
+
263
+ def remote_branches
264
+ @@remote_branches ||= capture_git( "branch -r --no-color" ).split( "\n" ).map{|b| b[2..-1]}
265
+ end
266
+
267
+ def others_review_branches
268
+ remote_branches.select do
269
+ |b| b =~ %r{/review/}
270
+ end.reject do |b|
271
+ b =~ %r{/#{user}/}
272
+ end
273
+ end
274
+
275
+ def remote_branches_organized
276
+ @@remote_branches_organized ||= (
277
+ rb = remote_branches.dup
278
+ # Convert a bunch of remote branch names, like
279
+ # origin/HEAD -> origin/masterr
280
+ # origin/master
281
+ # origin/review/user1/topic1
282
+ # origin/something-else
283
+ # origin/rejected/user2/topic2
284
+ #
285
+ # Into a hash with keys 'review' and 'rejected' pointing to hashes of
286
+ # user-topic(s) pairs.
287
+ rb.map!{|s| s.gsub( /->.*/, '')}
288
+ rb.map!{|s| s.strip.split( '/' )}
289
+ namespace_ut = rb.group_by{|remote, namespace, user, topic| namespace if topic}
290
+ namespace_ut.reject!{|k,v| not %w(rejected review).include? k}
291
+
292
+ namespace_ut.each do |k,v|
293
+ v.each{|a| a.shift( 2 )}
294
+ v = namespace_ut[k] = v.group_by{|user, topic| user if topic}
295
+ v.each{|kk,vv| vv.each(&:shift); vv.flatten!}
296
+ end
297
+
298
+ namespace_ut.symbolize_keys!
299
+ namespace_ut[:review] ||= {}
300
+ namespace_ut[:rejected] ||= {}
301
+
302
+ namespace_ut[:review].reject!{|k,v| k == user}
303
+ namespace_ut
304
+ )
305
+ end
306
+
307
+ def oldest_review_branch
308
+ return nil if others_review_branches.empty?
309
+
310
+ commits_by_age = capture_git([
311
+ "log --date-order --reverse --pretty=format:%d",
312
+ "^origin/master #{others_review_branches.join( ' ' )}",
313
+ ].join( " " )).split( "\n" )
314
+
315
+ commits_by_age.find do |ref|
316
+ # no ‘,’, i.e. only one ref matches the commit
317
+ ref.index( ',' ).nil?
318
+ end.strip[ 1..-2 ] # chomp the leading and trailing parenthesis
319
+ end
320
+
321
+ def oldest_review_user_topic
322
+ user_topic_name( oldest_review_branch )
323
+ end
324
+
325
+ def on_review_branch?
326
+ current_branch =~ %r{^review/}
327
+ end
328
+
329
+ def working_tree_clean?
330
+ git [ "diff --quiet", "diff --quiet --cached" ]
331
+ $?.success?
332
+ end
333
+
334
+ def working_tree_dirty?
335
+ not working_tree_clean?
336
+ end
337
+
338
+
339
+ def display_git_output?
340
+ @@display_git_output ||= false
341
+ end
342
+
343
+ def display_git_output!
344
+ @@display_git_output = true
345
+ end
346
+
347
+
348
+ def switch_to_branch( branch, tracking=nil )
349
+ if branches.include?( branch )
350
+ "checkout #{branch}"
351
+ else
352
+ "checkout -b #{branch} #{tracking}"
353
+ end
354
+ end
355
+
356
+ def cmd_redirect_suffix( opts )
357
+ if !opts[:show] && !display_git_output?
358
+ "> /dev/null 2> /dev/null"
359
+ end
360
+ end
361
+
362
+ def git( cmds=[], opts={} )
363
+ cmds = [cmds] if cmds.is_a? String
364
+ redir = cmd_redirect_suffix( opts )
365
+ system cmds.map{|c| "git #{c} #{redir}"}.join( " && " )
366
+ end
367
+
368
+ def capture_git( cmds=[] )
369
+ cmds = [cmds] if cmds.is_a? String
370
+ redir = "2> /dev/null" unless display_git_output?
371
+ `#{cmds.map{|c| "git #{c} #{redir}"}.join( " && " )}`
372
+ end
373
+ end
374
+ end
375
+
data/lib/util.rb ADDED
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+
3
+ require 'active_support'
4
+ require 'active_support/inflector'
5
+ require 'active_support/core_ext/module/aliasing'
6
+ require 'active_support/core_ext/string/inflections'
7
+
8
+
9
+ class String
10
+ def cleanup
11
+ indent = (index /^([ \t]+)/; $1) || ''
12
+ regex = /^#{Regexp::escape( indent )}/
13
+ strip.gsub regex, ''
14
+ end
15
+
16
+ def oneline
17
+ strip.gsub( /\n\s+/, '' )
18
+ end
19
+
20
+ # Annoyingly, the useful version of pluralize in texthelpers isn't in the
21
+ # string core extensions.
22
+ def pluralize_with_count( count )
23
+ count > 1 ? pluralize_without_count : singularize
24
+ end
25
+ alias_method_chain :pluralize, :count
26
+ end
27
+
28
+
29
+ class Object
30
+ # Deep duplicate via remarshaling. Not always applicable.
31
+ def ddup
32
+ Marshal.load( Marshal.dump( self ))
33
+ end
34
+ end