-
-
Notifications
You must be signed in to change notification settings - Fork 9.3k
/
bump-formula-pr.rb
538 lines (477 loc) 路 22.4 KB
/
bump-formula-pr.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
# typed: true
# frozen_string_literal: true
require "abstract_command"
require "formula"
require "cli/parser"
require "utils/pypi"
require "utils/tar"
module Homebrew
module DevCmd
class BumpFormulaPr < AbstractCommand
cmd_args do
description <<~EOS
Create a pull request to update <formula> with a new URL or a new tag.
If a <URL> is specified, the <SHA-256> checksum of the new download should also
be specified. A best effort to determine the <SHA-256> will be made if not supplied
by the user.
If a <tag> is specified, the Git commit <revision> corresponding to that tag
should also be specified. A best effort to determine the <revision> will be made
if the value is not supplied by the user.
If a <version> is specified, a best effort to determine the <URL> and <SHA-256> or
the <tag> and <revision> will be made if both values are not supplied by the user.
*Note:* this command cannot be used to transition a formula from a
URL-and-SHA-256 style specification into a tag-and-revision style specification,
nor vice versa. It must use whichever style specification the formula already uses.
EOS
switch "-n", "--dry-run",
description: "Print what would be done rather than doing it."
switch "--write-only",
description: "Make the expected file modifications without taking any Git actions."
switch "--commit",
depends_on: "--write-only",
description: "When passed with `--write-only`, generate a new commit after writing changes " \
"to the formula file."
switch "--no-audit",
description: "Don't run `brew audit` before opening the PR."
switch "--strict",
description: "Run `brew audit --strict` before opening the PR."
switch "--online",
description: "Run `brew audit --online` before opening the PR."
switch "--no-browse",
description: "Print the pull request URL instead of opening in a browser."
switch "--no-fork",
description: "Don't try to fork the repository."
comma_array "--mirror",
description: "Use the specified <URL> as a mirror URL. If <URL> is a comma-separated list " \
"of URLs, multiple mirrors will be added."
flag "--fork-org=",
description: "Use the specified GitHub organization for forking."
flag "--version=",
description: "Use the specified <version> to override the value parsed from the URL or tag. Note " \
"that `--version=0` can be used to delete an existing version override from a " \
"formula if it has become redundant."
flag "--message=",
description: "Prepend <message> to the default pull request message."
flag "--url=",
description: "Specify the <URL> for the new download. If a <URL> is specified, the <SHA-256> " \
"checksum of the new download should also be specified."
flag "--sha256=",
depends_on: "--url=",
description: "Specify the <SHA-256> checksum of the new download."
flag "--tag=",
description: "Specify the new git commit <tag> for the formula."
flag "--revision=",
description: "Specify the new commit <revision> corresponding to the specified git <tag> " \
"or specified <version>."
switch "-f", "--force",
description: "Remove all mirrors if `--mirror` was not specified."
switch "--install-dependencies",
description: "Install missing dependencies required to update resources."
flag "--python-package-name=",
description: "Use the specified <package-name> when finding Python resources for <formula>. " \
"If no package name is specified, it will be inferred from the formula's stable URL."
comma_array "--python-extra-packages=",
description: "Include these additional Python packages when finding resources."
comma_array "--python-exclude-packages=",
description: "Exclude these Python packages when finding resources."
conflicts "--dry-run", "--write-only"
conflicts "--no-audit", "--strict"
conflicts "--no-audit", "--online"
conflicts "--url", "--tag"
named_args :formula, max: 1, without_api: true
end
sig { override.void }
def run
if args.revision.present? && args.tag.nil? && args.version.nil?
raise UsageError, "`--revision` must be passed with either `--tag` or `--version`!"
end
# As this command is simplifying user-run commands then let's just use a
# user path, too.
ENV["PATH"] = PATH.new(ORIGINAL_PATHS).to_s
# Use the user's browser, too.
ENV["BROWSER"] = Homebrew::EnvConfig.browser
formula = args.named.to_formulae.first
new_url = args.url
raise FormulaUnspecifiedError if formula.blank?
odie "This formula is disabled!" if formula.disabled?
odie "This formula is deprecated and does not build!" if formula.deprecation_reason == :does_not_build
odie "This formula is not in a tap!" if formula.tap.blank?
odie "This formula's tap is not a Git repository!" unless formula.tap.git?
odie <<~EOS unless formula.tap.allow_bump?(formula.name)
Whoops, the #{formula.name} formula has its version update
pull requests automatically opened by BrewTestBot!
We'd still love your contributions, though, so try another one
that's not in the autobump list:
#{Formatter.url("#{formula.tap.remote}/blob/master/.github/autobump.txt")}
EOS
formula_spec = formula.stable
odie "#{formula}: no stable specification found!" if formula_spec.blank?
# This will be run by `brew audit` later so run it first to not start
# spamming during normal output.
Homebrew.install_bundler_gems!(groups: ["audit", "style"]) unless args.no_audit?
tap_remote_repo = formula.tap.full_name || formula.tap.remote_repo
remote = "origin"
remote_branch = formula.tap.git_repo.origin_branch_name
previous_branch = "-"
check_open_pull_requests(formula, tap_remote_repo)
new_version = args.version
check_new_version(formula, tap_remote_repo, version: new_version) if new_version.present?
opoo "This formula has patches that may be resolved upstream." if formula.patchlist.present?
if formula.resources.any? { |resource| !resource.name.start_with?("homebrew-") }
opoo "This formula has resources that may need to be updated."
end
old_mirrors = formula_spec.mirrors
new_mirrors ||= args.mirror
new_mirror ||= determine_mirror(new_url)
new_mirrors ||= [new_mirror] if new_mirror.present?
check_for_mirrors(formula, old_mirrors, new_mirrors) if new_url.present?
old_hash = formula_spec.checksum&.hexdigest
new_hash = args.sha256
new_tag = args.tag
new_revision = args.revision
old_url = formula_spec.url
old_tag = formula_spec.specs[:tag]
old_formula_version = formula_version(formula)
old_version = old_formula_version.to_s
forced_version = new_version.present?
new_url_hash = if new_url.present? && new_hash.present?
check_new_version(formula, tap_remote_repo, url: new_url) if new_version.blank?
true
elsif new_tag.present? && new_revision.present?
check_new_version(formula, tap_remote_repo, url: old_url, tag: new_tag) if new_version.blank?
false
elsif old_hash.blank?
if new_tag.blank? && new_version.blank? && new_revision.blank?
raise UsageError, "#{formula}: no `--tag` or `--version` argument specified!"
end
if old_tag.present?
new_tag ||= old_tag.gsub(old_version, new_version)
if new_tag == old_tag
odie <<~EOS
You need to bump this formula manually since the new tag
and old tag are both #{new_tag}.
EOS
end
check_new_version(formula, tap_remote_repo, url: old_url, tag: new_tag) if new_version.blank?
resource_path, forced_version = fetch_resource_and_forced_version(formula, new_version, old_url,
tag: new_tag)
new_revision = Utils.popen_read("git", "-C", resource_path.to_s, "rev-parse", "-q", "--verify", "HEAD")
new_revision = new_revision.strip
elsif new_revision.blank?
odie "#{formula}: the current URL requires specifying a `--revision=` argument."
end
false
elsif new_url.blank? && new_version.blank?
raise UsageError, "#{formula}: no `--url` or `--version` argument specified!"
else
new_url ||= PyPI.update_pypi_url(old_url, T.must(new_version))
if new_url.blank?
new_url = update_url(old_url, old_version, T.must(new_version))
if new_mirrors.blank? && old_mirrors.present?
new_mirrors = old_mirrors.map do |old_mirror|
update_url(old_mirror, old_version, T.must(new_version))
end
end
end
if new_url == old_url
odie <<~EOS
You need to bump this formula manually since the new URL
and old URL are both:
#{new_url}
EOS
end
check_new_version(formula, tap_remote_repo, url: new_url) if new_version.blank?
resource_path, forced_version = fetch_resource_and_forced_version(formula, new_version, new_url)
Utils::Tar.validate_file(resource_path)
new_hash = resource_path.sha256
end
replacement_pairs = []
if formula.revision.nonzero?
replacement_pairs << [
/^ revision \d+\n(\n( head "))?/m,
"\\2",
]
end
replacement_pairs += formula_spec.mirrors.map do |mirror|
[
/ +mirror "#{Regexp.escape(mirror)}"\n/m,
"",
]
end
replacement_pairs += if new_url_hash.present?
[
[
/#{Regexp.escape(formula_spec.url)}/,
new_url,
],
[
old_hash,
new_hash,
],
]
elsif new_tag.present?
[
[
/tag:(\s+")#{formula_spec.specs[:tag]}(?=")/,
"tag:\\1#{new_tag}\\2",
],
[
formula_spec.specs[:revision],
new_revision,
],
]
elsif new_url.present?
[
[
/#{Regexp.escape(formula_spec.url)}/,
new_url,
],
[
formula_spec.specs[:revision],
new_revision,
],
]
else
[
[
formula_spec.specs[:revision],
new_revision,
],
]
end
old_contents = formula.path.read
if new_mirrors.present?
replacement_pairs << [
/^( +)(url "#{Regexp.escape(T.must(new_url))}"[^\n]*?\n)/m,
"\\1\\2\\1mirror \"#{new_mirrors.join("\"\n\\1mirror \"")}\"\n",
]
end
if forced_version && new_version != "0"
replacement_pairs << if old_contents.include?("version \"#{old_formula_version}\"")
[
"version \"#{old_formula_version}\"",
"version \"#{new_version}\"",
]
elsif new_mirrors.present?
[
/^( +)(mirror "#{Regexp.escape(new_mirrors.last)}"\n)/m,
"\\1\\2\\1version \"#{new_version}\"\n",
]
elsif new_url.present?
[
/^( +)(url "#{Regexp.escape(new_url)}"[^\n]*?\n)/m,
"\\1\\2\\1version \"#{new_version}\"\n",
]
elsif new_revision.present?
[
/^( {2})( +)(:revision => "#{new_revision}"\n)/m,
"\\1\\2\\3\\1version \"#{new_version}\"\n",
]
end
elsif forced_version && new_version == "0"
replacement_pairs << [
/^ version "[\w.\-+]+"\n/m,
"",
]
end
new_contents = Utils::Inreplace.inreplace_pairs(formula.path,
replacement_pairs.uniq.compact,
read_only_run: args.dry_run?,
silent: args.quiet?)
new_formula_version = formula_version(formula, new_contents)
if new_formula_version < old_formula_version
formula.path.atomic_write(old_contents) unless args.dry_run?
odie <<~EOS
You need to bump this formula manually since changing the version
from #{old_formula_version} to #{new_formula_version} would be a downgrade.
EOS
elsif new_formula_version == old_formula_version
formula.path.atomic_write(old_contents) unless args.dry_run?
odie <<~EOS
You need to bump this formula manually since the new version
and old version are both #{new_formula_version}.
EOS
end
alias_rename = alias_update_pair(formula, new_formula_version)
if alias_rename.present?
ohai "Renaming alias #{alias_rename.first} to #{alias_rename.last}"
alias_rename.map! { |a| formula.tap.alias_dir/a }
end
unless args.dry_run?
resources_checked = PyPI.update_python_resources! formula,
version: new_formula_version.to_s,
package_name: args.python_package_name,
extra_packages: args.python_extra_packages,
exclude_packages: args.python_exclude_packages,
install_dependencies: args.install_dependencies?,
silent: args.quiet?,
ignore_non_pypi_packages: true
end
run_audit(formula, alias_rename, old_contents)
pr_message = "Created with `brew bump-formula-pr`."
if resources_checked.nil? && formula.resources.any? { |resource| !resource.name.start_with?("homebrew-") }
pr_message += <<~EOS
- [ ] `resource` blocks have been checked for updates.
EOS
end
if new_url =~ %r{^https://github\.com/([\w-]+)/([\w-]+)/archive/refs/tags/(v?[.0-9]+)\.tar\.}
owner = Regexp.last_match(1)
repo = Regexp.last_match(2)
tag = Regexp.last_match(3)
github_release_data = begin
GitHub::API.open_rest("#{GitHub::API_URL}/repos/#{owner}/#{repo}/releases/tags/#{tag}")
rescue GitHub::API::HTTPNotFoundError
# If this is a 404: we can't do anything.
nil
end
if github_release_data.present?
pre = "pre" if github_release_data["prerelease"].present?
pr_message += <<~XML
<details>
<summary>#{pre}release notes</summary>
<pre>#{github_release_data["body"]}</pre>
</details>
XML
end
end
pr_info = {
sourcefile_path: formula.path,
old_contents:,
additional_files: alias_rename,
remote:,
remote_branch:,
branch_name: "bump-#{formula.name}-#{new_formula_version}",
commit_message: "#{formula.name} #{new_formula_version}",
previous_branch:,
tap: formula.tap,
tap_remote_repo:,
pr_message:,
}
GitHub.create_bump_pr(pr_info, args:)
end
private
def determine_mirror(url)
case url
when %r{.*ftp\.gnu\.org/gnu.*}
url.sub "ftp.gnu.org/gnu", "ftpmirror.gnu.org"
when %r{.*download\.savannah\.gnu\.org/*}
url.sub "download.savannah.gnu.org", "download-mirror.savannah.gnu.org"
when %r{.*www\.apache\.org/dyn/closer\.lua\?path=.*}
url.sub "www.apache.org/dyn/closer.lua?path=", "archive.apache.org/dist/"
when %r{.*mirrors\.ocf\.berkeley\.edu/debian.*}
url.sub "mirrors.ocf.berkeley.edu/debian", "mirrorservice.org/sites/ftp.debian.org/debian"
end
end
def check_for_mirrors(formula, old_mirrors, new_mirrors)
return if new_mirrors.present? || old_mirrors.empty?
if args.force?
opoo "#{formula}: Removing all mirrors because a `--mirror=` argument was not specified."
else
odie <<~EOS
#{formula}: a `--mirror=` argument for updating the mirror URL(s) was not specified.
Use `--force` to remove all mirrors.
EOS
end
end
sig { params(old_url: String, old_version: String, new_version: String).returns(String) }
def update_url(old_url, old_version, new_version)
new_url = old_url.gsub(old_version, new_version)
return new_url if (old_version_parts = old_version.split(".")).length < 2
return new_url if (new_version_parts = new_version.split(".")).length != old_version_parts.length
partial_old_version = T.must(old_version_parts[0..-2]).join(".")
partial_new_version = T.must(new_version_parts[0..-2]).join(".")
new_url.gsub(%r{/(v?)#{Regexp.escape(partial_old_version)}/}, "/\\1#{partial_new_version}/")
end
def fetch_resource_and_forced_version(formula, new_version, url, **specs)
resource = Resource.new
resource.url(url, **specs)
resource.owner = Resource.new(formula.name)
forced_version = new_version && new_version != resource.version.to_s
resource.version(new_version) if forced_version
odie "Couldn't identify version, specify it using `--version=`." if resource.version.blank?
[resource.fetch, forced_version]
end
def formula_version(formula, contents = nil)
spec = :stable
name = formula.name
path = formula.path
if contents.present?
Formulary.from_contents(name, path, contents, spec).version
else
Formulary::FormulaLoader.new(name, path).get_formula(spec).version
end
end
def check_open_pull_requests(formula, tap_remote_repo)
GitHub.check_for_duplicate_pull_requests(formula.name, tap_remote_repo,
state: "open",
file: formula.path.relative_path_from(formula.tap.path).to_s,
quiet: args.quiet?)
end
def check_new_version(formula, tap_remote_repo, version: nil, url: nil, tag: nil)
if version.nil?
specs = {}
specs[:tag] = tag if tag.present?
version = Version.detect(url, **specs).to_s
return if version.blank?
end
check_throttle(formula, version)
check_closed_pull_requests(formula, tap_remote_repo, version:)
end
def check_throttle(formula, new_version)
throttled_rate = formula.livecheck.throttle
throttled_rate ||= formula.tap.audit_exceptions.dig(:throttled_formulae, formula.name)
return if throttled_rate.blank?
formula_suffix = Version.new(new_version).patch.to_i
return if formula_suffix.modulo(throttled_rate).zero?
odie "#{formula} should only be updated every #{throttled_rate} releases on multiples of #{throttled_rate}"
end
def check_closed_pull_requests(formula, tap_remote_repo, version:)
# if we haven't already found open requests, try for an exact match across closed requests
GitHub.check_for_duplicate_pull_requests(formula.name, tap_remote_repo,
version:,
state: "closed",
file: formula.path.relative_path_from(formula.tap.path).to_s,
quiet: args.quiet?)
end
def alias_update_pair(formula, new_formula_version)
versioned_alias = formula.aliases.grep(/^.*@\d+(\.\d+)?$/).first
return if versioned_alias.nil?
name, old_alias_version = versioned_alias.split("@")
new_alias_regex = (old_alias_version.split(".").length == 1) ? /^\d+/ : /^\d+\.\d+/
new_alias_version, = *new_formula_version.to_s.match(new_alias_regex)
return if Version.new(new_alias_version) <= Version.new(old_alias_version)
[versioned_alias, "#{name}@#{new_alias_version}"]
end
def run_audit(formula, alias_rename, old_contents)
audit_args = ["--formula"]
audit_args << "--strict" if args.strict?
audit_args << "--online" if args.online?
if args.dry_run?
if args.no_audit?
ohai "Skipping `brew audit`"
elsif audit_args.present?
ohai "brew audit #{audit_args.join(" ")} #{formula.path.basename}"
else
ohai "brew audit #{formula.path.basename}"
end
return
end
FileUtils.mv alias_rename.first, alias_rename.last if alias_rename.present?
failed_audit = false
if args.no_audit?
ohai "Skipping `brew audit`"
elsif audit_args.present?
system HOMEBREW_BREW_FILE, "audit", *audit_args, formula.full_name
failed_audit = !$CHILD_STATUS.success?
else
system HOMEBREW_BREW_FILE, "audit", formula.full_name
failed_audit = !$CHILD_STATUS.success?
end
return unless failed_audit
formula.path.atomic_write(old_contents)
FileUtils.mv alias_rename.last, alias_rename.first if alias_rename.present?
odie "`brew audit` failed!"
end
end
end
end