Class: Utils::Analytics::FormulaFeeds Private

Inherits:
Object
  • Object
show all
Defined in:
utils/analytics/formula_feeds.rb

Overview

This class is part of a private API. This class may only be used in the Homebrew/brew repository. Third parties should avoid using this class if possible, as it may be removed or changed without warning.

Collects bounded formula feeds from first-parent Git history.

Defined Under Namespace

Classes: Change

Constant Summary collapse

Entry =

This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.

T.type_alias { T::Hash[String, T.any(String, T::Array[String])] }
Builds =

This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.

T.type_alias { T::Hash[String, FormulaHistory::Build] }
LIMITS =

This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.

T.let({ "new" => 50, "updated" => 500 }.freeze, T::Hash[String, Integer])
BATCH_SIZE =

This constant is part of a private API. This constant may only be used in the Homebrew/brew repository. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.

100

Instance Method Summary collapse

Constructor Details

#initialize(repository, revision: "HEAD", limits: LIMITS) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:



26
27
28
29
30
31
# File 'utils/analytics/formula_feeds.rb', line 26

def initialize(repository, revision: "HEAD", limits: LIMITS)
  @history = T.let(FormulaHistory.new(repository), FormulaHistory)
  @revision = T.let(@history.git("rev-parse", "--verify", "#{revision}^{commit}"), String)
  @limits = limits
  @builds = T.let({}, T::Hash[[String, String], Builds])
end

Instance Method Details

#builds(path, blob, sources) ⇒ Builds

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



81
82
83
84
85
86
87
# File 'utils/analytics/formula_feeds.rb', line 81

def builds(path, blob, sources)
  @builds[[path, blob]] ||= @history.builds(path, sources.fetch(blob)).tap do |builds|
    if builds.values.any?(&:error)
      raise "Cannot read formula metadata for #{path} at #{blob}"
    end
  end
end

#changes(offset) ⇒ Array<Change>?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



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
# File 'utils/analytics/formula_feeds.rb', line 90

def changes(offset)
  output = @history.git("log", @revision, "--first-parent", "--raw", "--root", "--no-abbrev",
                        "--diff-merges=first-parent", "--find-renames=50%",
                        "--max-count=#{BATCH_SIZE}", "--skip=#{offset}",
                        "--format=%x00commit%x00%H%x00%ct", "-z")
  return if output.empty?

  tokens = output.split("\0")
  result = T.let([], T::Array[Change])
  deleted = T.let({}, T::Hash[[String, String], [String, String]])
  commit = T.let("", String)
  timestamp = 0
  while (token = tokens.shift)
    if token == "commit"
      commit = tokens.shift || raise("Missing commit")
      timestamp = Integer(tokens.shift || raise("Missing commit date"), 10)
    elsif token.lstrip.start_with?(":")
       = token.lstrip.delete_prefix(":").split
      old_mode = .fetch(0)
      mode = .fetch(1)
      before = .fetch(2)
      after = .fetch(3)
      status = .fetch(4)
      old_path = tokens.shift || raise("Missing formula path")
      path = status.start_with?("R", "C") ? (tokens.shift || raise("Missing renamed formula path")) : old_path
      if status == "D" && %w[100644 100755].include?(old_mode) && @history.formula_path?(old_path)
        deleted[[commit, File.basename(old_path)]] = [old_path, before]
      end
      next if %w[100644 100755].exclude?(mode) || !@history.formula_path?(path)

      added = %w[100644 100755].exclude?(old_mode) || !@history.formula_path?(old_path)
      result << Change.new(commit:, timestamp:, path:, old_path:, before: added ? nil : before, after:, added:)
    end
  end
  result.each do |change|
    next unless change.added
    next unless (previous = deleted[[change.commit, File.basename(change.path)]])

    change.old_path, change.before = previous
    change.added = false
  end
  result
end

#generate ⇒ Hash{String => String, Array<Entry>}

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



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
# File 'utils/analytics/formula_feeds.rb', line 34

def generate
  if @history.git("rev-parse", "--is-shallow-repository") == "true"
    raise "Formula feeds require complete Git history; unshallow the core checkout first"
  end

  feeds = @limits.to_h { |name, _limit| [name, T.let([], T::Array[Entry])] }
  offset = 0
  until feeds.all? { |name, entries| entries.length >= @limits.fetch(name) }
    changes = changes(offset)
    break if changes.nil?

    selected = changes.select do |change|
      feed = change.added ? "new" : "updated"
      feeds.fetch(feed).length < @limits.fetch(feed)
    end
    blobs = selected.flat_map { |change| [change.before, change.after] }.compact.uniq
    sources = blobs.empty? ? {} : @history.sources(blobs)
    selected.each do |change|
      feed = change.added ? "new" : "updated"
      next if feeds.fetch(feed).length >= @limits.fetch(feed)

      after = builds(change.path, change.after, sources)
      before_blob = change.before
      before = before_blob ? builds(change.old_path, before_blob, sources) : nil
      next if before && after.none? do |platform, build|
        old = before.fetch(platform)
        old.version && build.version && (old.version != build.version || build.revision > old.revision)
      end

      feeds.fetch(feed) << {
        "id"       => "#{change.commit}:#{change.path}",
        "name"     => File.basename(change.path, ".rb"),
        "date"     => Time.at(change.timestamp).utc.iso8601,
        "commit"   => change.commit,
        "versions" => after.values.filter_map do |build|
          next unless (version = build.version)

          build.revision.positive? ? "#{version}_#{build.revision}" : version
        end.uniq,
      }
    end
    offset += BATCH_SIZE
  end
  { "revision" => @revision, **feeds }
end