daq_flow 1.0.4

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