Skip to content

Commit 21b5d67

Browse files
committed
formula_auditor: audit revision and compatibility_version
Add a new audit method to check if the `revision` and `compatibility_version` are consistent with each other. This ensures that when a formula's `compatibility_version` is increased, the `revision` of dependent formulas was also increased. Inversely, when a formula's `revision` is increased in the same PR as one of its dependencies, the `compatibility_version` of dependent formulae must be increased by 1.
1 parent d3a6118 commit 21b5d67

File tree

2 files changed

+429
-133
lines changed

2 files changed

+429
-133
lines changed

Library/Homebrew/formula_auditor.rb

Lines changed: 148 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require "resource_auditor"
88
require "utils/shared_audits"
99
require "utils/output"
10+
require "utils/git"
1011

1112
module Homebrew
1213
# Auditor for checking common violations in {Formula}e.
@@ -39,8 +40,7 @@ def initialize(formula, options = {})
3940
@spdx_license_data = options[:spdx_license_data]
4041
@spdx_exception_data = options[:spdx_exception_data]
4142
@tap_audit = options[:tap_audit]
42-
@previous_committed = {}
43-
@newest_committed = {}
43+
@committed_version_info_cache = {}
4444
end
4545

4646
def audit_style
@@ -877,8 +877,10 @@ def audit_revision
877877
new_formula_problem("New formulae should not define a revision.") if @new_formula && !formula.revision.zero?
878878

879879
return unless @git
880-
return unless formula.tap # skip formula not from core or any taps
881-
return unless formula.tap.git? # git log is required
880+
881+
tap = formula.tap
882+
return if tap.nil?
883+
return unless tap.git?
882884
return if formula.stable.blank?
883885

884886
current_version = formula.stable.version
@@ -900,6 +902,81 @@ def audit_revision
900902
current_revision > (newest_committed[:revision] + 1)
901903
problem "`revision` should only increment by 1"
902904
end
905+
906+
revision_increment = if (previous_revision = previous_committed[:revision])
907+
current_revision - previous_revision
908+
else
909+
current_revision
910+
end
911+
return if revision_increment != 1
912+
913+
dependency_names = formula.recursive_dependencies.map(&:name)
914+
return if dependency_names.empty?
915+
916+
changed_dependency_paths = changed_formulae_paths(tap, only_names: dependency_names)
917+
return if changed_dependency_paths.empty?
918+
919+
missing_compatibility_bumps = changed_dependency_paths.filter_map do |path|
920+
changed_formula = Formulary.factory(path)
921+
# Each changed dependency must raise its compatibility_version by exactly one.
922+
previous_committed_dependency, = committed_version_info(formula: changed_formula)
923+
previous_compatibility_version = previous_committed_dependency[:compatibility_version] || 0
924+
current_compatibility_version = changed_formula.compatibility_version || 0
925+
next if current_compatibility_version == previous_compatibility_version + 1
926+
927+
"#{changed_formula.name} (#{previous_compatibility_version} to #{current_compatibility_version})"
928+
end
929+
return if missing_compatibility_bumps.empty?
930+
931+
problem "`revision` increased but recursive dependencies must increase `compatibility_version` by 1: " \
932+
"#{missing_compatibility_bumps.join(", ")}."
933+
end
934+
935+
def audit_compatibility_version
936+
return unless @git
937+
938+
tap = formula.tap
939+
return if tap.nil?
940+
return unless tap.git?
941+
942+
previous_committed, = committed_version_info
943+
return if previous_committed.empty?
944+
945+
previous_compatibility_version = previous_committed[:compatibility_version] || 0
946+
current_compatibility_version = formula.compatibility_version || previous_compatibility_version
947+
948+
if current_compatibility_version < previous_compatibility_version
949+
problem "`compatibility_version` should not decrease " \
950+
"from #{previous_compatibility_version} to #{current_compatibility_version}"
951+
return
952+
elsif current_compatibility_version > (previous_compatibility_version + 1)
953+
problem "`compatibility_version` should only increment by 1"
954+
return
955+
end
956+
957+
compatibility_increment = current_compatibility_version - previous_compatibility_version
958+
return if compatibility_increment.zero?
959+
960+
dependent_revision_bumps = changed_formulae_paths(tap).filter_map do |path|
961+
changed_formula = Formulary.factory(path)
962+
next if changed_formula.name == formula.name
963+
964+
dependencies = changed_formula.recursive_dependencies.map(&:name)
965+
# Only formulae that depend (recursively) on the audited formula can justify the bump.
966+
next unless dependencies.include?(formula.name)
967+
968+
previous_committed_dependent, = committed_version_info(formula: changed_formula)
969+
previous_revision = previous_committed_dependent[:revision] || 0
970+
current_revision = changed_formula.revision
971+
next if current_revision != previous_revision + 1
972+
973+
changed_formula.name
974+
end
975+
return if dependent_revision_bumps.any?
976+
977+
problem "`compatibility_version` increased from #{previous_compatibility_version} to " \
978+
"#{current_compatibility_version} but no recursive dependent formulae increased " \
979+
"`revision` by 1 in this PR."
903980
end
904981

905982
def audit_version_scheme
@@ -1064,12 +1141,57 @@ def linux_only_gcc_dep?(formula)
10641141
true
10651142
end
10661143

1067-
def committed_version_info
1068-
return [] unless @git
1069-
return [] unless formula.tap # skip formula not from core or any taps
1070-
return [] unless formula.tap.git? # git log is required
1071-
return [] if formula.stable.blank?
1072-
return [@previous_committed, @newest_committed] if @previous_committed.present? || @newest_committed.present?
1144+
sig { params(tap: Tap, only_names: T.nilable(T::Array[String])).returns(T::Array[Pathname]) }
1145+
def changed_formulae_paths(tap, only_names: nil)
1146+
return [] unless tap.git?
1147+
1148+
base_ref = "origin/HEAD"
1149+
changed_paths = Utils.safe_popen_read(Utils::Git.git, "-C", tap.path, "diff", "--name-only", base_ref)
1150+
.lines
1151+
.filter_map do |line|
1152+
relative_path = line.chomp
1153+
next unless relative_path.end_with?(".rb")
1154+
1155+
absolute_path = tap.path/relative_path
1156+
next unless absolute_path.exist?
1157+
next unless absolute_path.to_s.start_with?(tap.formula_dir.to_s)
1158+
1159+
absolute_path
1160+
end
1161+
1162+
return changed_paths if only_names.blank?
1163+
1164+
expected_paths = only_names.filter_map do |name|
1165+
relative_name = name.to_s.delete_prefix("#{tap.name}/")
1166+
relative_path = Pathname(relative_name)
1167+
relative_path = relative_path.sub_ext(".rb") if relative_path.extname.empty?
1168+
absolute_path = tap.formula_dir/relative_path
1169+
absolute_path.expand_path if absolute_path.exist?
1170+
rescue
1171+
nil
1172+
end.map(&:to_s)
1173+
1174+
return [] if expected_paths.empty?
1175+
1176+
changed_paths.select { |path| expected_paths.include?(path.expand_path.to_s) }
1177+
end
1178+
1179+
def committed_version_info(formula: @formula)
1180+
empty_result = [{}, {}]
1181+
return empty_result unless @git
1182+
return empty_result unless formula.tap # skip formula not from core or any taps
1183+
return empty_result unless formula.tap.git? # git log is required
1184+
return empty_result if formula.stable.blank?
1185+
1186+
cache_key = if formula.respond_to?(:full_name)
1187+
formula.full_name
1188+
else
1189+
formula.name
1190+
end
1191+
return @committed_version_info_cache[cache_key] if @committed_version_info_cache.key?(cache_key)
1192+
1193+
previous_committed = {}
1194+
newest_committed = {}
10731195

10741196
current_version = formula.stable.version
10751197
current_revision = formula.revision
@@ -1081,28 +1203,30 @@ def committed_version_info
10811203
stable = f.stable
10821204
next if stable.blank?
10831205

1084-
@previous_committed[:version] = stable.version
1085-
@previous_committed[:checksum] = stable.checksum
1086-
@previous_committed[:version_scheme] = f.version_scheme
1087-
@previous_committed[:revision] = f.revision
1088-
1089-
@newest_committed[:version] ||= @previous_committed[:version]
1090-
@newest_committed[:checksum] ||= @previous_committed[:checksum]
1091-
@newest_committed[:revision] ||= @previous_committed[:revision]
1092-
@newest_committed[:url] ||= stable.url
1206+
previous_committed[:version] = stable.version
1207+
previous_committed[:checksum] = stable.checksum
1208+
previous_committed[:version_scheme] = f.version_scheme
1209+
previous_committed[:revision] = f.revision
1210+
previous_committed[:compatibility_version] = f.compatibility_version
1211+
1212+
newest_committed[:version] ||= previous_committed[:version]
1213+
newest_committed[:checksum] ||= previous_committed[:checksum]
1214+
newest_committed[:revision] ||= previous_committed[:revision]
1215+
newest_committed[:url] ||= stable.url
1216+
newest_committed[:compatibility_version] ||= previous_committed[:compatibility_version]
10931217
end
10941218
rescue MacOSVersion::Error
10951219
break
10961220
end
10971221

1098-
break if @previous_committed[:version] && current_version != @previous_committed[:version]
1099-
break if @previous_committed[:revision] && current_revision != @previous_committed[:revision]
1222+
break if previous_committed[:version] && current_version != previous_committed[:version]
1223+
break if previous_committed[:revision] && current_revision != previous_committed[:revision]
11001224
end
11011225

1102-
@previous_committed.compact!
1103-
@newest_committed.compact!
1226+
previous_committed.compact!
1227+
newest_committed.compact!
11041228

1105-
[@previous_committed, @newest_committed]
1229+
@committed_version_info_cache[cache_key] = [previous_committed, newest_committed]
11061230
end
11071231
end
11081232
end

0 commit comments

Comments
 (0)