Class: Utils::Analytics::Releases Private

Inherits:
Object
  • Object
show all
Defined in:
utils/analytics/releases.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.

Release analytics derived from stable Git tags and their commit ranges.

Instance Method Summary collapse

Constructor Details

#initialize(path) ⇒ 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:



11
12
13
# File 'utils/analytics/releases.rb', line 11

def initialize(path)
  @path = path
end

Instance Method Details

#git(*args) ⇒ String

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:



140
141
142
# File 'utils/analytics/releases.rb', line 140

def git(*args)
  Utils.safe_popen_read("git", "-C", @path.to_s, *args).chomp
end

#report ⇒ Hash{String => T.anything}

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:



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

def report
  if git("rev-parse", "--is-shallow-repository") == "true"
    raise "Release analytics require complete brew Git history"
  end

  tags = git("for-each-ref", "--merged=HEAD",
             "--format=%(refname:strip=2)%09%(objectname)%09%(*objectname)%09" \
             "%(committerdate:unix)%09%(*committerdate:unix)", "refs/tags").lines.filter_map do |line|
    name, revision, annotated_revision, timestamp, annotated_timestamp = line.chomp.split("\t", -1)
    next if name.nil? || revision.nil? || timestamp.nil?
    next unless name.match?(/\Av?\d+(?:\.\d+){1,2}\z/)

    [name, annotated_revision.presence || revision, annotated_timestamp.presence || timestamp]
  end
  signature = Digest::SHA256.hexdigest([
    Digest::SHA256.file(__FILE__).hexdigest, git("ls-tree", "HEAD", "--", ".mailmap")
  ].join("\n"))
  cache = HOMEBREW_CACHE/"analytics/git/releases.json"
  data = cache.exist? ? JSON.parse(cache.read) : {}
  data = {} if data["signature"] != signature
  if data["tags"] != tags
    first_parents = git("rev-list", "--parents", "HEAD").lines.to_h do |line|
      revision, parent = line.split
      [revision, parent || ""]
    end
    versions = tags.to_h { |name, revision, _| [revision, name] }
    saved = data.fetch("releases", []).to_h { |row| [row.fetch("version"), row] }
    releases = tags.map do |name, revision, timestamp|
      previous = first_parents.fetch(revision, "")
      previous = first_parents.fetch(previous, "") until previous.empty? || versions.key?(previous)
      row = saved[name]
      next row if row && row["revision"] == revision && row["previous_revision"] == previous.presence

      {
        "version"           => name,
        "revision"          => revision,
        "previous_version"  => versions[previous],
        "previous_revision" => previous.presence,
        "date"              => Time.at(timestamp.to_i).utc.strftime("%F"),
      }.merge(statistics(previous.empty? ? revision : "#{previous}..#{revision}"))
    end
    data["releases"] = releases.sort_by do |row|
      [row.fetch("date"), Version.new(row.fetch("version"))]
    end.reverse
    seen = Set.new
    data.fetch("releases").reverse_each do |row|
      identities = row.fetch("authors")
      row["new_contributors"] = (identities - seen.to_a).size
      seen.merge(identities)
    end
    boundaries = data.fetch("releases").select { |row| row.fetch("version").end_with?(".0") }
    boundaries.sort_by! { |row| Version.new(row.fetch("version")) }
    boundaries.reverse!
    data["aggregates"] = boundaries.filter_map do |row|
      version = Version.new(row.fetch("version"))
      kind = version.minor.to_i.zero? ? "major" : "minor"
      previous = boundaries.find do |candidate|
        value = Version.new(candidate.fetch("version"))
        value < version && (kind == "minor" || value.minor.to_i.zero?)
      end
      next unless previous

      saved = data.fetch("aggregates", []).find do |candidate|
        candidate["revision"] == row["revision"] && candidate["previous_revision"] == previous["revision"]
      end
      next saved if saved

      aggregate = row.slice("version", "revision", "date").merge(
        "kind" => kind, "previous_version" => previous.fetch("version"),
        "previous_revision" => previous.fetch("revision")
      ).merge(statistics("#{previous.fetch("revision")}..#{row.fetch("revision")}"))
      earlier = git("log", "--no-merges", "--format=%aE", previous.fetch("revision"))
                .b.lines(chomp: true).map { |email| email.unpack1("H*") }
      aggregate["new_contributors"] = (aggregate.fetch("authors") - earlier).size
      aggregate
    end
    data["totals"] = statistics(*tags.map { |_, revision, _| revision }).except("authors")
    data["totals"]["releases"] = tags.size
    data["totals"]["new_contributors"] = data.fetch("totals").fetch("contributors")
    data["tags"] = tags
    data["signature"] = signature
    cache.dirname.mkpath
    cache.atomic_write(JSON.generate(data))
  end
  releases = data.fetch("releases").map { |row| row.except("authors") }
  years = releases.map { |row| row.fetch("date")[0, 4] }.tally
  {
    "generated_at" => Time.now.utc.iso8601,
    "source"       => "https://github.com/Homebrew/brew",
    "revision"     => git("rev-parse", "HEAD"),
    "years"        => [{ "period" => "current", "releases" => releases.size },
                       *(2009..Time.now.utc.year).to_a.reverse.map do |year|
                         { "period" => year.to_s, "releases" => years.fetch(year.to_s, 0) }
                       end],
    "releases"     => releases,
    "aggregates"   => data.fetch("aggregates").map { |row| row.except("authors") },
    "totals"       => data.fetch("totals"),

  }
end

#statistics(*revisions) ⇒ Hash{String => T.anything}

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:



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'utils/analytics/releases.rb', line 118

def statistics(*revisions)
  authors = Set.new
  pull_requests = Set.new
  commits = 0
  unless revisions.empty?
    git("log", "--format=%P%x09%aE%x09%s", *revisions).b.each_line do |line|
      parents, email, subject = line.chomp.split("\t", 3)
      next if parents.nil? || email.nil? || subject.nil?

      if parents.split.size < 2
        commits += 1
        authors << email
      end
      number = subject[/\AMerge pull request #(\d+)\b/, 1] || subject[/\(#(\d+)\)\z/, 1]
      pull_requests << number if number
    end
  end
  { "commits" => commits, "pull_requests" => pull_requests.size, "contributors" => authors.size,
    "authors" => authors.map { |email| email.unpack1("H*") } }
end