Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pr-pull: allow casks to be pulled #12692

Merged
merged 4 commits into from
Apr 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
130 changes: 82 additions & 48 deletions Library/Homebrew/dev-cmd/pr-pull.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,51 +108,63 @@ def signoff!(path, pr: nil, dry_run: false)
end
end

def determine_bump_subject(old_contents, new_contents, formula_path, reason: nil)
formula_path = Pathname(formula_path)
formula_name = formula_path.basename.to_s.chomp(".rb")
def get_package(tap, subject_name, subject_path, content)
if subject_path.dirname == tap.cask_dir
cask = begin
Cask::CaskLoader.load(content.dup)
rescue Cask::CaskUnavailableError
nil
end
return cask
end

new_formula = begin
Formulary.from_contents(formula_name, formula_path, new_contents, :stable)
begin
Formulary.from_contents(subject_name, subject_path, content, :stable)
rescue FormulaUnavailableError
nil
end
end

return "#{formula_name}: delete #{reason}".strip if new_formula.blank?
def determine_bump_subject(old_contents, new_contents, subject_path, reason: nil)
subject_path = Pathname(subject_path)
tap = Tap.from_path(subject_path)
subject_name = subject_path.basename.to_s.chomp(".rb")
is_cask = subject_path.dirname == tap.cask_dir
name = is_cask ? "cask" : "formula"

old_formula = begin
Formulary.from_contents(formula_name, formula_path, old_contents, :stable)
rescue FormulaUnavailableError
nil
end
new_package = get_package(tap, subject_name, subject_path, new_contents)

return "#{subject_name}: delete #{reason}".strip if new_package.blank?

return "#{formula_name} #{new_formula.stable.version} (new formula)" if old_formula.blank?
old_package = get_package(tap, subject_name, subject_path, old_contents)

if old_formula.stable.version != new_formula.stable.version
"#{formula_name} #{new_formula.stable.version}"
elsif old_formula.revision != new_formula.revision
"#{formula_name}: revision #{reason}".strip
if old_package.blank?
"#{subject_name} #{new_package.version} (new #{name})"
elsif old_package.version != new_package.version
"#{subject_name} #{new_package.version}"
elsif !is_cask && old_package.revision != new_package.revision
"#{subject_name}: revision #{reason}".strip
else
"#{formula_name}: #{reason || "rebuild"}".strip
"#{subject_name}: #{reason || "rebuild"}".strip
end
end

# Cherry picks a single commit that modifies a single file.
# Potentially rewords this commit using {determine_bump_subject}.
def reword_formula_commit(commit, file, reason: "", verbose: false, resolve: false, path: ".")
formula_file = Pathname.new(path) / file
formula_name = formula_file.basename.to_s.chomp(".rb")
def reword_package_commit(commit, file, reason: "", verbose: false, resolve: false, path: ".")
package_file = Pathname.new(path) / file
package_name = package_file.basename.to_s.chomp(".rb")

odebug "Cherry-picking #{formula_file}: #{commit}"
odebug "Cherry-picking #{package_file}: #{commit}"
Utils::Git.cherry_pick!(path, commit, verbose: verbose, resolve: resolve)

old_formula = Utils::Git.file_at_commit(path, file, "HEAD^")
new_formula = Utils::Git.file_at_commit(path, file, "HEAD")
old_package = Utils::Git.file_at_commit(path, file, "HEAD^")
new_package = Utils::Git.file_at_commit(path, file, "HEAD")

bump_subject = determine_bump_subject(old_formula, new_formula, formula_file, reason: reason).strip
bump_subject = determine_bump_subject(old_package, new_package, package_file, reason: reason).strip
subject, body, trailers = separate_commit_message(path.git_commit_message)

if subject != bump_subject && !subject.start_with?("#{formula_name}:")
if subject != bump_subject && !subject.start_with?("#{package_name}:")
safe_system("git", "-C", path, "commit", "--amend", "-q",
"-m", bump_subject, "-m", subject, "-m", body, "-m", trailers)
ohai bump_subject
Expand All @@ -164,7 +176,7 @@ def reword_formula_commit(commit, file, reason: "", verbose: false, resolve: fal
# Cherry picks multiple commits that each modify a single file.
# Words the commit according to {determine_bump_subject} with the body
# corresponding to all the original commit messages combined.
def squash_formula_commits(commits, file, reason: "", verbose: false, resolve: false, path: ".")
def squash_package_commits(commits, file, reason: "", verbose: false, resolve: false, path: ".")
odebug "Squashing #{file}: #{commits.join " "}"

# Format commit messages into something similar to `git fmt-merge-message`.
Expand Down Expand Up @@ -197,10 +209,10 @@ def squash_formula_commits(commits, file, reason: "", verbose: false, resolve: f
Utils::Git.cherry_pick!(path, "--no-commit", *commits, verbose: verbose, resolve: resolve)

# Determine the bump subject by comparing the original state of the tree to its current state.
formula_file = Pathname.new(path) / file
old_formula = Utils::Git.file_at_commit(path, file, "#{commits.first}^")
new_formula = File.read(formula_file)
bump_subject = determine_bump_subject(old_formula, new_formula, formula_file, reason: reason)
package_file = Pathname.new(path) / file
old_package = Utils::Git.file_at_commit(path, file, "#{commits.first}^")
new_package = package_file.read
bump_subject = determine_bump_subject(old_package, new_package, package_file, reason: reason)

# Commit with the new subject, body, and trailers.
safe_system("git", "-C", path, "commit", "--quiet",
Expand All @@ -215,18 +227,22 @@ def autosquash!(original_commit, tap:, reason: "", verbose: false, resolve: fals
commits = Utils.safe_popen_read("git", "-C", tap.path, "rev-list",
"--reverse", "#{original_commit}..HEAD").lines.map(&:strip)

# Generate a bidirectional mapping of commits <=> formula files.
# Generate a bidirectional mapping of commits <=> formula/cask files.
files_to_commits = {}
commits_to_files = commits.to_h do |commit|
files = Utils.safe_popen_read("git", "-C", tap.path, "diff-tree", "--diff-filter=AMD",
"-r", "--name-only", "#{commit}^", commit).lines.map(&:strip)
files.each do |file|
files_to_commits[file] ||= []
files_to_commits[file] << commit
next if (tap.path/file).dirname == tap.formula_dir && File.extname(file) == ".rb"
tap_file = tap.path/file
if (tap_file.dirname == tap.formula_dir || tap_file.dirname == tap.cask_dir) &&
File.extname(file) == ".rb"
next
end

odie <<~EOS
Autosquash can't squash commits that modify non-formula files.
Autosquash can only squash commits that modify formula or cask files.
File: #{file}
Commit: #{commit}
EOS
Expand All @@ -246,13 +262,13 @@ def autosquash!(original_commit, tap:, reason: "", verbose: false, resolve: fals
files = commits_to_files[commit]
if files.length == 1 && files_to_commits[files.first].length == 1
# If there's a 1:1 mapping of commits to files, just cherry pick and (maybe) reword.
reword_formula_commit(commit, files.first, path: tap.path, reason: reason, verbose: verbose, resolve: resolve)
reword_package_commit(commit, files.first, path: tap.path, reason: reason, verbose: verbose, resolve: resolve)
processed_commits << commit
elsif files.length == 1 && files_to_commits[files.first].length > 1
# If multiple commits modify a single file, squash them down into a single commit.
file = files.first
commits = files_to_commits[file]
squash_formula_commits(commits, file, path: tap.path, reason: reason, verbose: verbose, resolve: resolve)
squash_package_commits(commits, file, path: tap.path, reason: reason, verbose: verbose, resolve: resolve)
processed_commits += commits
else
# We can't split commits (yet) so just raise an error.
Expand Down Expand Up @@ -293,27 +309,45 @@ def formulae_need_bottles?(tap, original_commit, user, repo, pr, args:)

return false if labels.include?("CI-syntax-only") || labels.include?("CI-no-bottles")

changed_formulae(tap, original_commit).any? do |f|
!f.bottle_unneeded? && !f.bottle_disabled?
changed_packages(tap, original_commit).any? do |f|
!f.instance_of?(Cask::Cask) && !f.bottle_unneeded? && !f.bottle_disabled?
SMillerDev marked this conversation as resolved.
Show resolved Hide resolved
end
end

def changed_formulae(tap, original_commit)
if Homebrew::EnvConfig.disable_load_formula?
opoo "Can't check if updated bottles are necessary as HOMEBREW_DISABLE_LOAD_FORMULA is set!"
return
end
def changed_packages(tap, original_commit)
formulae = Utils.popen_read("git", "-C", tap.path, "diff-tree",
"-r", "--name-only", "--diff-filter=AM",
original_commit, "HEAD", "--", tap.formula_dir)
.lines
.map do |line|
next unless line.end_with? ".rb\n"

Utils.popen_read("git", "-C", tap.path, "diff-tree",
"-r", "--name-only", "--diff-filter=AM",
original_commit, "HEAD", "--", tap.formula_dir)
.lines
.map do |line|
name = "#{tap.name}/#{File.basename(line.chomp, ".rb")}"
if Homebrew::EnvConfig.disable_load_formula?
opoo "Can't check if updated bottles are necessary as HOMEBREW_DISABLE_LOAD_FORMULA is set!"
break
end
begin
Formulary.resolve(name)
rescue FormulaUnavailableError
nil
end
end.compact
casks = Utils.popen_read("git", "-C", tap.path, "diff-tree",
"-r", "--name-only", "--diff-filter=AM",
original_commit, "HEAD", "--", tap.cask_dir)
.lines
.map do |line|
next unless line.end_with? ".rb\n"

name = "#{tap.name}/#{File.basename(line.chomp, ".rb")}"
Formula[name]
begin
Cask::CaskLoader.load(name)
rescue Cask::CaskUnavailableError
nil
end
end.compact
formulae + casks
end

def download_artifact(url, dir, pr)
Expand Down
85 changes: 83 additions & 2 deletions Library/Homebrew/test/dev-cmd/pr-pull_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,38 @@ class Foo < Formula
end
EOS
end
let(:cask_rebuild) do
<<~EOS
cask "food" do
desc "Helpful description"
version "1.0"
url "https://brew.sh/food-\#{version}.tgz"
end
EOS
end
let(:cask_version) do
<<~EOS
cask "food" do
version "2.0"
url "https://brew.sh/food-\#{version}.tgz"
end
EOS
end
let(:cask) do
<<~EOS
cask "food" do
version "1.0"
url "https://brew.sh/food-\#{version}.tgz"
end
EOS
end
let(:tap) { Tap.fetch("Homebrew", "foo") }
let(:formula_file) { tap.path/"Formula/foo.rb" }
let(:cask_file) { tap.cask_dir/"food.rb" }
let(:path) { (Tap::TAP_DIRECTORY/"homebrew/homebrew-foo").extend(GitRepositoryExtension) }

describe "#autosquash!" do
it "squashes a formula correctly" do
it "squashes a formula or cask correctly" do
secondary_author = "Someone Else <me@example.com>"
(tap.path/"Formula").mkpath
formula_file.write(formula)
Expand All @@ -61,11 +88,26 @@ class Foo < Formula
expect(tap.path.git_commit_message).to include("foo 2.0")
expect(tap.path.git_commit_message).to include("Co-authored-by: #{secondary_author}")
end

(path/"Casks").mkpath
cask_file.write(cask)
cd path do
safe_system Utils::Git.git, "add", cask_file
safe_system Utils::Git.git, "commit", "-m", "food 1.0 (new cask)"
original_hash = `git rev-parse HEAD`.chomp
File.write(cask_file, cask_rebuild)
safe_system Utils::Git.git, "commit", cask_file, "-m", "rebuild"
File.write(cask_file, cask_version)
safe_system Utils::Git.git, "commit", cask_file, "-m", "version", "--author=#{secondary_author}"
described_class.autosquash!(original_hash, tap: tap)
expect(path.git_commit_message).to include("food 2.0")
expect(path.git_commit_message).to include("Co-authored-by: #{secondary_author}")
end
end
end

describe "#signoff!" do
it "signs off a formula" do
it "signs off a formula or cask" do
(tap.path/"Formula").mkpath
formula_file.write(formula)
cd tap.path do
Expand All @@ -75,6 +117,33 @@ class Foo < Formula
end
described_class.signoff!(tap.path)
expect(tap.path.git_commit_message).to include("Signed-off-by:")

(path/"Casks").mkpath
cask_file.write(cask)
cd path do
safe_system Utils::Git.git, "add", cask_file
safe_system Utils::Git.git, "commit", "-m", "food 1.0 (new cask)"
end
described_class.signoff!(tap.path)
expect(tap.path.git_commit_message).to include("Signed-off-by:")
end
end

describe "#get_package" do
it "returns a formula" do
expect(described_class.get_package(tap, "foo", formula_file, formula)).to be_a(Formula)
end

it "returns nil for an unknown formula" do
expect(described_class.get_package(tap, "foo", formula_file, "")).to be_nil
end

it "returns a cask" do
expect(described_class.get_package(tap, "foo", cask_file, cask)).to be_a(Cask::Cask)
end

it "returns nil for an unknown cask" do
expect(described_class.get_package(tap, "foo", cask_file, "")).to be_nil
end
end

Expand All @@ -83,10 +152,18 @@ class Foo < Formula
expect(described_class.determine_bump_subject("", formula, formula_file)).to eq("foo 1.0 (new formula)")
end

it "correctly bumps a new cask" do
expect(described_class.determine_bump_subject("", cask, cask_file)).to eq("food 1.0 (new cask)")
end

it "correctly bumps a formula version" do
expect(described_class.determine_bump_subject(formula, formula_version, formula_file)).to eq("foo 2.0")
end

it "correctly bumps a cask version" do
expect(described_class.determine_bump_subject(cask, cask_version, cask_file)).to eq("food 2.0")
end

it "correctly bumps a formula revision with reason" do
expect(described_class.determine_bump_subject(
formula, formula_revision, formula_file, reason: "for fun"
Expand All @@ -100,6 +177,10 @@ class Foo < Formula
it "correctly bumps a formula deletion" do
expect(described_class.determine_bump_subject(formula, "", formula_file)).to eq("foo: delete")
end

it "correctly bumps a cask deletion" do
expect(described_class.determine_bump_subject(cask, "", cask_file)).to eq("food: delete")
end
end
end
end