daq_flow 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +62 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +165 -0
  8. data/Rakefile +10 -0
  9. data/bin/daq_flow +23 -0
  10. data/flash_flow.gemspec +28 -0
  11. data/flash_flow.yml.erb.example +42 -0
  12. data/lib/flash_flow.rb +7 -0
  13. data/lib/flash_flow/branch_merger.rb +55 -0
  14. data/lib/flash_flow/cmd_runner.rb +54 -0
  15. data/lib/flash_flow/config.rb +84 -0
  16. data/lib/flash_flow/data.rb +6 -0
  17. data/lib/flash_flow/data/base.rb +89 -0
  18. data/lib/flash_flow/data/bitbucket.rb +152 -0
  19. data/lib/flash_flow/data/branch.rb +124 -0
  20. data/lib/flash_flow/data/collection.rb +211 -0
  21. data/lib/flash_flow/data/github.rb +140 -0
  22. data/lib/flash_flow/data/store.rb +44 -0
  23. data/lib/flash_flow/git.rb +267 -0
  24. data/lib/flash_flow/install.rb +19 -0
  25. data/lib/flash_flow/lock.rb +23 -0
  26. data/lib/flash_flow/merge.rb +6 -0
  27. data/lib/flash_flow/merge/acceptance.rb +154 -0
  28. data/lib/flash_flow/merge/base.rb +116 -0
  29. data/lib/flash_flow/merge_order.rb +27 -0
  30. data/lib/flash_flow/notifier.rb +23 -0
  31. data/lib/flash_flow/options.rb +34 -0
  32. data/lib/flash_flow/resolve.rb +143 -0
  33. data/lib/flash_flow/shadow_repo.rb +44 -0
  34. data/lib/flash_flow/time_helper.rb +32 -0
  35. data/lib/flash_flow/version.rb +4 -0
  36. data/log/.keep +0 -0
  37. data/test/lib/data/test_base.rb +10 -0
  38. data/test/lib/data/test_branch.rb +206 -0
  39. data/test/lib/data/test_collection.rb +308 -0
  40. data/test/lib/data/test_store.rb +70 -0
  41. data/test/lib/lock/test_github.rb +74 -0
  42. data/test/lib/merge/test_acceptance.rb +230 -0
  43. data/test/lib/test_branch_merger.rb +78 -0
  44. data/test/lib/test_config.rb +63 -0
  45. data/test/lib/test_git.rb +73 -0
  46. data/test/lib/test_merge_order.rb +71 -0
  47. data/test/lib/test_notifier.rb +33 -0
  48. data/test/lib/test_resolve.rb +69 -0
  49. data/test/minitest_helper.rb +41 -0
  50. data/update_gem.sh +5 -0
  51. metadata +192 -0
@@ -0,0 +1,211 @@
1
+ require 'flash_flow/data/branch'
2
+ require 'flash_flow/data/bitbucket'
3
+ require 'flash_flow/data/github'
4
+
5
+ module FlashFlow
6
+ module Data
7
+
8
+ class Collection
9
+
10
+ attr_accessor :branches
11
+
12
+ def initialize(config=nil)
13
+ @branches = {}
14
+
15
+ if config && config['class'] && config['class']['name']
16
+ collection_class = Object.const_get(config['class']['name'])
17
+ @collection_instance = collection_class.new(config['class'])
18
+ end
19
+ end
20
+
21
+ def self.fetch(config=nil)
22
+ collection = new(config)
23
+ collection.fetch
24
+ collection
25
+ end
26
+
27
+ def self.from_hash(hash, collection_instance=nil)
28
+ collection = new
29
+ collection.branches = branches_from_hash(hash)
30
+ collection.instance_variable_set(:@collection_instance, collection_instance)
31
+ collection
32
+ end
33
+
34
+ def self.branches_from_hash(hash)
35
+ {}.tap do |new_branches|
36
+ hash.each do |_, val|
37
+ branch = val.is_a?(Branch) ? val : Branch.from_hash(val)
38
+ new_branches[branch.ref] = branch
39
+ end
40
+ end
41
+ end
42
+
43
+ def self.key(ref)
44
+ ref
45
+ end
46
+
47
+ def get(ref)
48
+ @branches[key(ref)]
49
+ end
50
+
51
+ def to_hash
52
+ {}.tap do |hash|
53
+ @branches.each do |key, val|
54
+ hash[key] = val.to_hash
55
+ end
56
+ end
57
+ end
58
+ alias :to_h :to_hash
59
+
60
+ def reverse_merge(old)
61
+ merged_branches = @branches.dup
62
+
63
+ merged_branches.each do |_, info|
64
+ info.updated_at = Time.now
65
+ info.created_at ||= Time.now
66
+ end
67
+
68
+ old.branches.each do |full_ref, info|
69
+ if merged_branches.has_key?(full_ref)
70
+ branch = merged_branches[full_ref]
71
+
72
+ branch.created_at = info.created_at
73
+ branch.resolutions = info.resolutions.to_h.merge(branch.resolutions.to_h)
74
+ branch.stories = info.stories.to_a | merged_branches[full_ref].stories.to_a
75
+ branch.merge_order ||= info.merge_order
76
+ if branch.fail?
77
+ branch.conflict_sha ||= info.conflict_sha
78
+ end
79
+ else
80
+ merged_branches[full_ref] = info
81
+ merged_branches[full_ref].status = nil
82
+ end
83
+ end
84
+
85
+ self.class.from_hash(merged_branches, @collection_instance)
86
+ end
87
+
88
+ def to_a
89
+ @branches.values
90
+ end
91
+
92
+ def each
93
+ to_a.each
94
+ end
95
+
96
+ def current_branches
97
+ to_a.select { |branch| branch.current_record }
98
+ end
99
+
100
+ def mergeable
101
+ current_branches.select { |branch| (branch.success? || branch.fail? || branch.unknown?) }
102
+ end
103
+
104
+ def failures
105
+ current_branches.select { |branch| branch.fail? }
106
+ end
107
+
108
+ def successes
109
+ current_branches.select { |branch| branch.success? }
110
+ end
111
+
112
+ def removals
113
+ to_a.select { |branch| branch.removed? }
114
+ end
115
+
116
+ def fetch
117
+ return unless @collection_instance.respond_to?(:fetch)
118
+
119
+ @collection_instance.fetch.each do |b|
120
+ update_or_add(b)
121
+ end
122
+ end
123
+
124
+ def mark_all_as_current
125
+ @branches.each do |_, branch|
126
+ branch.current_record = true
127
+ end
128
+ end
129
+
130
+ def add_to_merge(ref)
131
+ branch = record(ref)
132
+ branch.current_record = true
133
+ @collection_instance.add_to_merge(branch) if @collection_instance.respond_to?(:add_to_merge)
134
+ branch
135
+ end
136
+
137
+ def remove_from_merge(ref)
138
+ branch = record(ref)
139
+ branch.current_record = true
140
+ branch.removed!
141
+ @collection_instance.remove_from_merge(branch) if @collection_instance.respond_to?(:remove_from_merge)
142
+ branch
143
+ end
144
+
145
+ def mark_failure(branch, conflict_sha=nil)
146
+ update_or_add(branch)
147
+ branch.fail!(conflict_sha)
148
+ @collection_instance.mark_failure(branch) if @collection_instance.respond_to?(:mark_failure)
149
+ branch
150
+ end
151
+
152
+ def mark_deleted(branch)
153
+ update_or_add(branch)
154
+ branch.deleted!
155
+ @collection_instance.mark_deleted(branch) if @collection_instance.respond_to?(:mark_deleted)
156
+ branch
157
+ end
158
+
159
+ def mark_success(branch)
160
+ update_or_add(branch)
161
+ branch.success!
162
+ @collection_instance.mark_success(branch) if @collection_instance.respond_to?(:mark_success)
163
+ branch
164
+ end
165
+
166
+ def add_story(ref, story_id)
167
+ branch = get(ref)
168
+ branch.stories ||= []
169
+ branch.stories << story_id
170
+
171
+ @collection_instance.add_story(branch, story_id) if @collection_instance.respond_to?(:add_story)
172
+ branch
173
+ end
174
+
175
+ def code_reviewed?(branch)
176
+ @collection_instance.respond_to?(:code_reviewed?) ? @collection_instance.code_reviewed?(branch) : true
177
+ end
178
+
179
+ def can_ship?(branch)
180
+ @collection_instance.respond_to?(:can_ship?) ? @collection_instance.can_ship?(branch) : true
181
+ end
182
+
183
+ def branch_link(branch)
184
+ @collection_instance.branch_link(branch) if @collection_instance.respond_to?(:branch_link)
185
+ end
186
+
187
+ def set_resolutions(branch, resolutions)
188
+ update_or_add(branch)
189
+ branch.set_resolutions(resolutions)
190
+ @collection_instance.set_resolutions(branch) if @collection_instance.respond_to?(:set_resolutions)
191
+ branch
192
+ end
193
+
194
+ private
195
+
196
+ def key(ref)
197
+ self.class.key(ref)
198
+ end
199
+
200
+ def update_or_add(branch)
201
+ old_branch = @branches[key(branch.ref)]
202
+ @branches[key(branch.ref)] = old_branch.nil? ? branch : old_branch.merge(branch)
203
+ end
204
+
205
+ def record(ref)
206
+ update_or_add(Branch.new(ref))
207
+ end
208
+
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,140 @@
1
+ require 'octokit'
2
+ require 'flash_flow/data/branch'
3
+
4
+ module FlashFlow
5
+ module Data
6
+ class Github
7
+
8
+ attr_accessor :repo, :unmergeable_label
9
+
10
+ def initialize(config={})
11
+ initialize_connection!(config['token'])
12
+ @repo = config['repo']
13
+ @master_branch = config['master_branch'] || master
14
+ @unmergeable_label = config['unmergeable_label'] || 'unmergeable'
15
+ @do_not_merge_label = config['do_not_merge_label'] || 'do not merge'
16
+ @code_reviewed_label = config['code_reviewed_label'] || 'code reviewed'
17
+ @shippable_label = config['shippable_label'] || 'shippable'
18
+ end
19
+
20
+ def initialize_connection!(token)
21
+ if token.nil?
22
+ raise RuntimeError.new("Github token must be set in your flash_flow config file.")
23
+ end
24
+ octokit.configure do |c|
25
+ c.access_token = token
26
+ end
27
+ end
28
+
29
+ def remove_from_merge(branch)
30
+ pr = pr_for(branch)
31
+ if pr && @do_not_merge_label
32
+ add_label(pr.number, @do_not_merge_label)
33
+ end
34
+ end
35
+
36
+ def fetch
37
+ pull_requests.map do |pr|
38
+ Branch.from_hash(
39
+ 'ref' => pr.head.ref,
40
+ 'status' => status_from_labels(pr),
41
+ 'metadata' => metadata(pr),
42
+ 'sha' => pr.head.sha
43
+ )
44
+ end
45
+ end
46
+
47
+ def add_to_merge(branch)
48
+ pr = pr_for(branch)
49
+
50
+ pr ||= create_pr(branch.ref, branch.ref, branch.ref)
51
+ branch.add_metadata(metadata(pr))
52
+
53
+ if pr && @do_not_merge_label
54
+ remove_label(pr.number, @do_not_merge_label)
55
+ end
56
+ end
57
+
58
+ def mark_success(branch)
59
+ remove_label(branch.metadata['pr_number'], @unmergeable_label)
60
+ end
61
+
62
+ def mark_failure(branch)
63
+ add_label(branch.metadata['pr_number'], @unmergeable_label)
64
+ end
65
+
66
+ def code_reviewed?(branch)
67
+ has_label?(branch.metadata['pr_number'], @code_reviewed_label)
68
+ end
69
+
70
+ def can_ship?(branch)
71
+ has_label?(branch.metadata['pr_number'], @shippable_label)
72
+ end
73
+
74
+ def branch_link(branch)
75
+ branch.metadata['pr_url']
76
+ end
77
+
78
+ private
79
+
80
+ def status_from_labels(pull_request)
81
+ case
82
+ when has_label?(pull_request.number, @do_not_merge_label)
83
+ 'removed'
84
+ when has_label?(pull_request.number, @unmergeable_label)
85
+ 'fail'
86
+ else
87
+ nil
88
+ end
89
+ end
90
+
91
+ def pr_for(branch)
92
+ pull_requests.detect { |p| branch.ref == p.head.ref }
93
+ end
94
+
95
+ def create_pr(branch, title, body)
96
+ pr = octokit.create_pull_request(repo, @master_branch, branch, title, body)
97
+ pull_requests << pr
98
+ pr
99
+ end
100
+
101
+ def pull_requests
102
+ @pull_requests ||= octokit.pull_requests(repo).sort_by(&:created_at)
103
+ end
104
+
105
+ def remove_label(pull_request_number, label)
106
+ if has_label?(pull_request_number, label)
107
+ octokit.remove_label(repo, pull_request_number, label)
108
+ end
109
+ end
110
+
111
+ def add_label(pull_request_number, label)
112
+ unless has_label?(pull_request_number, label)
113
+ octokit.add_labels_to_an_issue(repo, pull_request_number, [label])
114
+ end
115
+ end
116
+
117
+ def has_label?(pull_request_number, label_name)
118
+ !!labels(pull_request_number).detect { |label| label == label_name }
119
+ end
120
+
121
+ def labels(pull_request_number)
122
+ @labels ||= {}
123
+ @labels[pull_request_number] ||= octokit.labels_for_issue(repo, pull_request_number).map(&:name)
124
+ end
125
+
126
+ def metadata(pr)
127
+ {
128
+ 'pr_number' => pr.number,
129
+ 'pr_url' => pr.html_url,
130
+ 'user_url' => pr.user.html_url,
131
+ 'repo_url' => pr.head.repo.html_url
132
+ }
133
+ end
134
+
135
+ def octokit
136
+ Octokit
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,44 @@
1
+ require 'json'
2
+ require 'flash_flow/data'
3
+
4
+ module FlashFlow
5
+ module Data
6
+ class Store
7
+ def initialize(filename, git, opts={})
8
+ @filename = filename
9
+ @git = git
10
+ @logger = opts[:logger] || Logger.new('/dev/null')
11
+ end
12
+
13
+ def get
14
+ file_contents = @git.read_file_from_merge_branch(@filename)
15
+ JSON.parse(file_contents)
16
+
17
+ rescue JSON::ParserError, Errno::ENOENT
18
+ @logger.error "Unable to read branch info from file: #{@filename}"
19
+ {}
20
+ end
21
+
22
+ def write(branches, file=nil)
23
+ @git.in_dir do
24
+ file ||= File.open(@filename, 'w')
25
+ file.puts JSON.pretty_generate(sort_branches(branches))
26
+ file.close
27
+ end
28
+
29
+ @git.in_temp_merge_branch do
30
+ @git.add_and_commit(@filename, 'Branch Info', add: {force: true})
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def sort_branches(branches)
37
+ return branches unless branches.is_a?(Hash)
38
+ sorted_branches = {}
39
+ branches.keys.sort.each { |key| sorted_branches[key] = sort_branches(branches[key]) }
40
+ sorted_branches
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,267 @@
1
+ require 'flash_flow/cmd_runner'
2
+ require 'shellwords'
3
+
4
+ module FlashFlow
5
+ class Git
6
+ ATTRIBUTES = [:remote, :merge_branch, :master_branch, :release_branch, :use_rerere]
7
+
8
+ attr_reader *ATTRIBUTES
9
+ attr_reader :working_branch
10
+
11
+ UNMERGED_STATUSES = %w{DD AU UD UA DU AA UU}
12
+
13
+ def initialize(config, logger=nil)
14
+ @cmd_runner = CmdRunner.new(logger: logger)
15
+
16
+ config['release_branch'] ||= config['master_branch']
17
+ config['remote'] ||= config['merge_remote'] # For backwards compatibility
18
+
19
+ ATTRIBUTES.each do |attr|
20
+ unless config.has_key?(attr.to_s)
21
+ raise RuntimeError.new("git configuration missing. Required config parameters: #{ATTRIBUTES}")
22
+ end
23
+
24
+ instance_variable_set("@#{attr}", config[attr.to_s])
25
+ end
26
+
27
+ @working_branch = current_branch
28
+ end
29
+
30
+ def in_dir
31
+ Dir.chdir(@cmd_runner.dir) do
32
+ yield
33
+ end
34
+ end
35
+
36
+ def last_stdout
37
+ @cmd_runner.last_stdout
38
+ end
39
+
40
+ def last_command
41
+ @cmd_runner.last_command
42
+ end
43
+
44
+ def last_success?
45
+ @cmd_runner.last_success?
46
+ end
47
+
48
+ def run(cmd, opts={})
49
+ @cmd_runner.run("git #{cmd}", opts)
50
+ end
51
+
52
+ def working_dir
53
+ @cmd_runner.dir
54
+ end
55
+
56
+ def add_and_commit(files, message, opts={})
57
+ files = [files].flatten
58
+ run("add #{'-f ' if opts[:add] && opts[:add][:force]}#{files.join(' ')}")
59
+ run("commit -m '#{message}'")
60
+ end
61
+
62
+ def merge(branch)
63
+ run("merge #{branch}")
64
+ end
65
+
66
+ def branch_contains?(branch, ref)
67
+ run("branch -a --contains #{ref}", log: CmdRunner::LOG_CMD)
68
+ last_stdout.split("\n").detect { |str| str[2..-1] == branch }
69
+ end
70
+
71
+ def master_branch_contains?(sha)
72
+ branch_contains?("remotes/#{remote}/#{master_branch}", sha)
73
+ end
74
+
75
+ def in_original_merge_branch
76
+ in_branch("#{remote}/#{merge_branch}") { yield }
77
+ end
78
+
79
+ def read_file_from_merge_branch(filename)
80
+ run("show #{remote}/#{merge_branch}:#{filename}", log: CmdRunner::LOG_CMD)
81
+ last_stdout
82
+ end
83
+
84
+ def initialize_rerere(copy_from_dir=nil)
85
+ return unless use_rerere
86
+
87
+ @cmd_runner.run('mkdir .git/rr-cache')
88
+ @cmd_runner.run('cp -R rr-cache/* .git/rr-cache/')
89
+ @cmd_runner.run("cp -R #{File.join(copy_from_dir, '.git/rr-cache/*')} .git/rr-cache/") if copy_from_dir
90
+ end
91
+
92
+ def commit_rerere(current_rereres)
93
+ return unless use_rerere
94
+ @cmd_runner.run('mkdir rr-cache')
95
+ @cmd_runner.run('rm -rf rr-cache/*')
96
+ current_rereres.each do |rerere|
97
+ @cmd_runner.run("cp -R .git/rr-cache/#{rerere} rr-cache/")
98
+ end
99
+
100
+ run('add rr-cache/')
101
+ run("commit -m 'Update rr-cache'")
102
+ end
103
+
104
+ def rerere_resolve!
105
+ return false unless use_rerere
106
+
107
+ if unresolved_conflicts.empty?
108
+ merging_files = staged_and_working_dir_files.select { |s| UNMERGED_STATUSES.include?(s[0..1]) }.map { |s| s[3..-1] }
109
+ conflicts = conflicted_files
110
+
111
+ run("add #{merging_files.join(" ")}")
112
+ run('commit --no-edit')
113
+
114
+ resolutions(conflicts)
115
+ else
116
+ false
117
+ end
118
+ end
119
+
120
+ def unresolved_conflicts
121
+ in_dir do
122
+ conflicted_files.map do |file|
123
+ File.open(file) { |f| f.grep(/>>>>/) }.empty? ? nil : file
124
+ end.compact
125
+ end
126
+ end
127
+
128
+ def resolutions(files)
129
+ {}.tap do |hash|
130
+ files.map do |file|
131
+ hash[file] = resolution_candidates(file)
132
+ end.flatten
133
+ end
134
+ end
135
+
136
+ # git rerere doesn't give you a deterministic way to determine which resolution was used
137
+ def resolution_candidates(file)
138
+ @cmd_runner.run("diff -q --from-file #{file} .git/rr-cache/*/postimage*", log: CmdRunner::LOG_CMD)
139
+ different_files = split_diff_lines(@cmd_runner.last_stdout)
140
+
141
+ @cmd_runner.run('ls -la .git/rr-cache/*/postimage*', log: CmdRunner::LOG_CMD)
142
+ all_files = split_diff_lines(@cmd_runner.last_stdout)
143
+
144
+ all_files - different_files
145
+ end
146
+
147
+ def split_diff_lines(arr)
148
+ arr.split("\n").map { |s| s.split(".git/rr-cache/").last.split("/postimage").first }
149
+ end
150
+
151
+ def staged_and_working_dir_files
152
+ run("status --porcelain")
153
+ last_stdout.split("\n").reject { |line| line[0..1] == '??' }
154
+ end
155
+
156
+ def conflicted_files
157
+ run("diff --name-only --diff-filter=U")
158
+ last_stdout.split("\n")
159
+ end
160
+
161
+ def current_branch
162
+ run("rev-parse --abbrev-ref HEAD")
163
+ last_stdout.strip
164
+ end
165
+
166
+ def most_recent_commit
167
+ run("show -s --format=%cd head")
168
+ end
169
+
170
+ def reset_temp_merge_branch
171
+ in_branch(master_branch) do
172
+ run("fetch #{remote}")
173
+ run("branch -D #{temp_merge_branch}")
174
+ run("checkout -b #{temp_merge_branch}")
175
+ run("reset --hard #{remote}/#{master_branch}")
176
+ run("clean -x -f -d")
177
+ end
178
+ end
179
+
180
+ def push(branch, force=false)
181
+ run("push #{'-f' if force} #{remote} #{branch}:#{branch}")
182
+ end
183
+
184
+ def copy_temp_to_branch(branch, squash_message = nil)
185
+ run("checkout #{temp_merge_branch}")
186
+ run("merge --strategy=ours --no-edit #{branch}")
187
+ run("checkout #{branch}")
188
+ run("merge #{temp_merge_branch}")
189
+
190
+ squash_commits(branch, squash_message) if squash_message
191
+ end
192
+
193
+ def delete_temp_merge_branch
194
+ in_branch(master_branch) do
195
+ run("branch -d #{temp_merge_branch}")
196
+ end
197
+ end
198
+
199
+ def in_temp_merge_branch(&block)
200
+ in_branch(temp_merge_branch, &block)
201
+ end
202
+
203
+ def in_merge_branch(&block)
204
+ in_branch(merge_branch, &block)
205
+ end
206
+
207
+ def in_branch(branch)
208
+ begin
209
+ starting_branch = current_branch
210
+ run("checkout #{branch}")
211
+
212
+ yield
213
+ ensure
214
+ run("checkout #{starting_branch}")
215
+ end
216
+ end
217
+
218
+ def temp_merge_branch
219
+ "flash_flow-#{merge_branch}"
220
+ end
221
+
222
+ def get_sha(branch, opts={})
223
+ if opts[:short]
224
+ run("rev-parse --short #{branch}")
225
+ else
226
+ run("rev-parse #{branch}")
227
+ end
228
+ last_stdout.strip if last_success?
229
+ end
230
+
231
+ def branch_exists?(branch)
232
+ run("rev-parse --verify #{branch}")
233
+ last_success?
234
+ end
235
+
236
+ def ahead_of_master?(branch)
237
+ branch_exists?(branch) && !master_branch_contains?(get_sha(branch))
238
+ end
239
+
240
+ def version
241
+ run('--version')
242
+ semver_regex = Regexp.new('.*(\d+\.\d+\.\d+).*')
243
+ running_version = last_stdout.strip
244
+ if semver = semver_regex.match(running_version)
245
+ semver[1]
246
+ end
247
+ end
248
+
249
+ private
250
+
251
+ def squash_commits(branch, commit_message)
252
+ unless branch_exists?("#{remote}/#{branch}")
253
+ run("push #{remote} #{master_branch}:#{branch}")
254
+ end
255
+
256
+ # Get all the files that differ between existing acceptance and new acceptance
257
+ run("diff --name-only #{remote}/#{branch} #{branch}")
258
+ files = last_stdout.split("\n")
259
+ run("reset #{remote}/#{branch}")
260
+
261
+ run("add -f #{files.map { |f| "\"#{Shellwords.escape(f)}\"" }.join(" ")}")
262
+
263
+ run("commit -m '#{commit_message}'")
264
+ end
265
+
266
+ end
267
+ end