Module: Fbe

Defined in:
lib/fbe.rb,
lib/fbe/unmask_repos.rb

Overview

Unmask.

Author

Yegor Bugayenko ([email protected])

Copyright

Copyright © 2024 Zerocracy

License

MIT

Defined Under Namespace

Modules: Middleware Classes: Award, Conclude, FakeOctokit, Graph, Iterate

Constant Summary collapse

VERSION =

Current version of the gem (changed by .rultor.yml on every release)

'0.0.0'

Class Method Summary collapse

Class Method Details

.bylaws(anger: 2, love: 2, paranoia: 2) ⇒ Object

A generator of policies/bylaws.

Author

Yegor Bugayenko ([email protected])

Copyright

Copyright © 2024 Yegor Bugayenko

License

MIT



33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/fbe/bylaws.rb', line 33

def Fbe.bylaws(anger: 2, love: 2, paranoia: 2)
  raise 'The "anger" must be in the [0..4] interval' unless !anger.negative? && anger < 5
  raise 'The "lover" must be in the [0..4] interval' unless !love.negative? && love < 5
  raise 'The "paranoia" must be in the [1..4] interval' unless paranoia.positive? && paranoia < 5
  home = File.join(__dir__, '../../assets/bylaws')
  raise "The directory with templates is absent '#{home}'" unless File.exist?(home)
  Dir[File.join(home, '*.liquid')].to_h do |f|
    formula = Liquid::Template.parse(File.read(f)).render(
      'anger' => anger, 'love' => love, 'paranoia' => paranoia
    )
    [File.basename(f).gsub(/\.liquid$/, ''), formula]
  end
end

.conclude(fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global) ⇒ Object

Create a conclude code block.

Parameters:

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase

  • judge (String) (defaults to: $judge)

    The name of the judge, from the judges tool

  • global (Hash) (defaults to: $global)

    The hash for global caching

  • options (Judges::Options) (defaults to: $options)

    The options coming from the judges tool

  • loog (Loog) (defaults to: $loog)

    The logging facility



37
38
39
40
# File 'lib/fbe/conclude.rb', line 37

def Fbe.conclude(fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global, &)
  c = Fbe::Conclude.new(fb:, judge:, loog:, options:, global:)
  c.instance_eval(&)
end

.copy(source, target, except: []) ⇒ Object

Make a copy of a fact, moving all properties to a new fact.

Parameters:

  • source (Factbase::Fact)

    The source

  • target (Factbase::Fact)

    The targer

  • except (Array<String>) (defaults to: [])

    List of properties to NOT copy



33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/fbe/copy.rb', line 33

def Fbe.copy(source, target, except: [])
  raise 'The source is nil' if source.nil?
  raise 'The target is nil' if target.nil?
  raise 'The except is nil' if except.nil?
  source.all_properties.each do |k|
    next unless target[k].nil?
    next if except.include?(k)
    source[k].each do |v|
      target.send(:"#{k}=", v)
    end
  end
end

.fb(fb: $fb, global: $global, options: $options, loog: $loog) ⇒ Object

Returns an instance of Factbase (cached).

Parameters:

  • fb (Factbase) (defaults to: $fb)

    The global factbase provided by the judges tool

  • global (Hash) (defaults to: $global)

    The hash for global caching

  • options (Judges::Options) (defaults to: $options)

    The options coming from the judges tool

  • loog (Loog) (defaults to: $loog)

    The logging facility



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/fbe/fb.rb', line 39

def Fbe.fb(fb: $fb, global: $global, options: $options, loog: $loog)
  global[:fb] ||=
    begin
      rules = Dir.glob(File.join('rules', '*.fe')).map { |f| File.read(f) }
      fbe = Factbase::Rules.new(
        fb,
        "(and \n#{rules.join("\n")}\n)",
        uid: '_id'
      )
      fbe =
        Factbase::Pre.new(fbe) do |f, fbt|
          max = fbt.query('(eq _id (max _id))').each.to_a.first
          f._id = (max.nil? ? 0 : max._id) + 1
          f._time = Time.now
          f._version = "#{Factbase::VERSION}/#{Judges::VERSION}/#{options.action_version}"
          f._job = options.job_id unless options.job_id.nil?
        end
      Factbase::Looged.new(fbe, loog)
    end
end

.github_graph(options: $options, global: $global, loog: $loog) ⇒ Object

Interface to GitHub GraphQL API.

Parameters:

  • options (Judges::Options) (defaults to: $options)

    The options available globally

  • global (Hash) (defaults to: $global)

    Hash of global options

  • loog (Loog) (defaults to: $loog)

    Logging facility



34
35
36
37
38
39
40
41
42
# File 'lib/fbe/github_graph.rb', line 34

def Fbe.github_graph(options: $options, global: $global, loog: $loog)
  global[:github_graph] ||=
    if options.testing.nil?
      Fbe::Graph.new(token: options.github_token || ENV.fetch('GITHUB_TOKEN', nil))
    else
      loog.debug('The connection to GitHub GraphQL API is mocked')
      Fbe::Graph::Fake.new
    end
end

.if_absent(fb: Fbe.fb) {|f| ... } ⇒ Object

Injects a fact if it’s absent in the factbase, otherwise (it is already there) returns NIL.

Yields:

  • (f)


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
# File 'lib/fbe/if_absent.rb', line 32

def Fbe.if_absent(fb: Fbe.fb)
  attrs = {}
  f =
    others(map: attrs) do |*args|
      k = args[0]
      if k.end_with?('=')
        @map[k[0..-2].to_sym] = args[1]
      else
        @map[k.to_sym]
      end
    end
  yield f
  q = attrs.except('_id', '_time', '_version').map do |k, v|
    vv = v.to_s
    if v.is_a?(String)
      vv = "'#{vv.gsub('"', '\\\\"').gsub("'", "\\\\'")}'"
    elsif v.is_a?(Time)
      vv = v.utc.iso8601
    end
    "(eq #{k} #{vv})"
  end.join(' ')
  q = "(and #{q})"
  return nil unless fb.query(q).each.to_a.empty?
  n = fb.insert
  attrs.each { |k, v| n.send("#{k}=", v) }
  n
end

.issue(fact, options: $options, global: $global, loog: $loog) ⇒ Object

Converts an ID of GitHub issue into a nicely formatting string.

Parameters:

  • fact (Factbase::Fact)

    The fact, where to get the ID of GitHub issue

  • options (Judges::Options) (defaults to: $options)

    The options coming from the judges tool

  • global (Hash) (defaults to: $global)

    The hash for global caching

  • loog (Loog) (defaults to: $loog)

    The logging facility



33
34
35
36
37
38
39
40
41
# File 'lib/fbe/issue.rb', line 33

def Fbe.issue(fact, options: $options, global: $global, loog: $loog)
  rid = fact['repository']
  raise "There is no 'repository' property" if rid.nil?
  rid = rid.first.to_i
  issue = fact['issue']
  raise "There is no 'issue' property" if issue.nil?
  issue = issue.first.to_i
  "#{Fbe.octo(global:, options:, loog:).repo_name_by_id(rid)}##{issue}"
end

.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global) ⇒ Object

Create a conclude code block.



31
32
33
34
# File 'lib/fbe/iterate.rb', line 31

def Fbe.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global, &)
  c = Fbe::Iterate.new(fb:, loog:, options:, global:)
  c.instance_eval(&)
end

.just_one(fb: Fbe.fb) {|f| ... } ⇒ Object

Injects a fact if it’s absent in the factbase, otherwise (it is already there) returns the existing one.

Yields:

  • (f)


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
# File 'lib/fbe/just_one.rb', line 32

def Fbe.just_one(fb: Fbe.fb)
  attrs = {}
  f =
    others(map: attrs) do |*args|
      k = args[0]
      if k.end_with?('=')
        @map[k[0..-2].to_sym] = args[1]
      else
        @map[k.to_sym]
      end
    end
  yield f
  q = attrs.except('_id', '_time', '_version').map do |k, v|
    vv = v.to_s
    if v.is_a?(String)
      vv = "'#{vv.gsub('"', '\\\\"').gsub("'", "\\\\'")}'"
    elsif v.is_a?(Time)
      vv = v.utc.iso8601
    end
    "(eq #{k} #{vv})"
  end.join(' ')
  q = "(and #{q})"
  before = fb.query(q).each.to_a.first
  return before unless before.nil?
  n = fb.insert
  attrs.each { |k, v| n.send("#{k}=", v) }
  n
end

.mask_to_regex(mask) ⇒ Object



32
33
34
35
36
# File 'lib/fbe/unmask_repos.rb', line 32

def self.mask_to_regex(mask)
  org, repo = mask.split('/')
  raise "Org '#{org}' can't have an asterisk" if org.include?('*')
  Regexp.compile("#{org}/#{repo.gsub('*', '.*')}")
end

.octo(options: $options, global: $global, loog: $loog) ⇒ Object

Interface to GitHub API.

It is supposed to be used instead of Octokit client, because it is pre-configured and enables additional fearues, such as retrying, logging, and caching.

Parameters:

  • options (Judges::Options) (defaults to: $options)

    The options available globally

  • global (Hash) (defaults to: $global)

    Hash of global options

  • loog (Loog) (defaults to: $loog)

    Logging facility



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
# File 'lib/fbe/octo.rb', line 46

def Fbe.octo(options: $options, global: $global, loog: $loog)
  raise 'The $global is not set' if global.nil?
  global[:octo] ||=
    begin
      if options.testing.nil?
        o = Octokit::Client.new
        token = options.github_token
        if token.nil?
          loog.debug("The 'github_token' option is not provided")
          token = ENV.fetch('GITHUB_TOKEN', nil)
          if token.nil?
            loog.debug("The 'GITHUB_TOKEN' environment variable is not set")
          else
            loog.debug("The 'GITHUB_TOKEN' environment was provided")
          end
        else
          loog.debug("The 'github_token' option was provided")
        end
        if token.nil?
          loog.warn('Accessing GitHub API without a token!')
        elsif token.empty?
          loog.warn('The GitHub API token is an empty string, won\'t use it')
        else
          o = Octokit::Client.new(access_token: token)
          loog.info("Accessing GitHub API with a token (#{token.length} chars, ending by #{token[-4..]})")
        end
        o.auto_paginate = true
        o.per_page = 100
        o.connection_options = {
          request: {
            open_timeout: 15,
            timeout: 15
          }
        }
        stack =
          Faraday::RackBuilder.new do |builder|
            builder.use(
              Faraday::Retry::Middleware,
              exceptions: Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS + [
                Octokit::TooManyRequests, Octokit::ServiceUnavailable
              ],
              max: 4,
              interval: ENV['RACK_ENV'] == 'test' ? 0.01 : 4,
              methods: [:get],
              backoff_factor: 2
            )
            builder.use(Fbe::Middleware::Quota, loog:, pause: options.github_api_pause || 60)
            builder.use(Faraday::HttpCache, serializer: Marshal, shared_cache: false, logger: Loog::NULL)
            builder.use(Octokit::Response::RaiseError)
            builder.use(
              Faraday::Response::Logger,
              loog,
              {
                formatter: Fbe::Middleware::LoggingFormatter,
                log_only_errors: true,
                headers: true,
                bodies: true,
                errors: false
              }
            )
            builder.adapter(Faraday.default_adapter)
          end
        o.middleware = stack
        o = Verbose.new(o, log: loog)
      else
        loog.debug('The connection to GitHub API is mocked')
        o = Fbe::FakeOctokit.new
      end
      decoor(o, loog:) do
        def off_quota
          left = @origin.rate_limit.remaining
          if left < 5
            @loog.info("To much GitHub API quota consumed already (remaining=#{left}), stopping")
            true
          else
            false
          end
        end

        def user_name_by_id(id)
          json = @origin.user(id)
          name = json[:login]
          @loog.debug("GitHub user ##{id} has a name: @#{name}")
          name
        end

        def repo_id_by_name(name)
          json = @origin.repository(name)
          id = json[:id]
          @loog.debug("GitHub repository #{name} has an ID: ##{id}")
          id
        end

        def repo_name_by_id(id)
          json = @origin.repository(id)
          name = json[:full_name]
          @loog.debug("GitHub repository ##{id} has a name: #{name}")
          name
        end
      end
    end
end

.overwrite(fact, property, value, fb: Fbe.fb) ⇒ Object

Overwrite a property in the fact.

If the property doesn’t exist in the fact, it will be added. If it does exist, it will be removed (the entire fact will be destroyed, new fact created, and property set).

It is important that the fact has _id property. If it doesn’t, an exception will be raised.

Parameters:

  • fact (Factbase::Fact)

    The fact to modify

  • property (String)

    The name of the property to set

  • value (Any)

    The value to set



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/fbe/overwrite.rb', line 40

def Fbe.overwrite(fact, property, value, fb: Fbe.fb)
  raise 'The fact is nil' if fact.nil?
  raise "The property is not a String but #{property.class} (#{property})" unless property.is_a?(String)
  return if !fact[property].nil? && fact[property].size == 1 && fact[property].first == value
  before = {}
  fact.all_properties.each do |prop|
    before[prop.to_s] = fact[prop]
  end
  id = fact['_id']&.first
  raise 'There is no _id in the fact, cannot use Fbe.overwrite' if id.nil?
  raise "No facts by _id = #{id}" if fb.query("(eq _id #{id})").delete!.zero?
  n = fb.insert
  before[property.to_s] = [value]
  before.each do |k, vv|
    next unless n[k].nil?
    vv.each do |v|
      n.send("#{k}=", v)
    end
  end
end

.pmp(fb: Fbe.fb, global: $global, options: $options, loog: $loog) ⇒ Object

Get configuration parameter from the “PMP” fact.

Parameters:

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase

  • global (Hash) (defaults to: $global)

    The hash for global caching

  • options (Judges::Options) (defaults to: $options)

    The options coming from the judges tool

  • loog (Loog) (defaults to: $loog)

    The logging facility



35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/fbe/pmp.rb', line 35

def Fbe.pmp(fb: Fbe.fb, global: $global, options: $options, loog: $loog)
  others do |*args1|
    area = args1.first
    others do |*args2|
      param = args2.first
      f = Fbe.fb(global:, fb:, options:, loog:).query("(and (eq what 'pmp') (eq area '#{area}'))").each.to_a.first
      raise "Unknown area '#{area}'" if f.nil?
      r = f[param]
      raise "Unknown property '#{param}' in the '#{area}' area" if r.nil?
      r.first
    end
  end
end

.regularly(area, p_every_days, p_since_days = nil, fb: Fbe.fb, judge: $judge, loog: $loog) {|f| ... } ⇒ Object

Run the block provided every X days.

Parameters:

  • area (String)

    The name of the PMP area

  • p_every_days (Integer)

    How frequently to run, every X days

  • p_since_days (Integer) (defaults to: nil)

    Since when to collect stats, X days

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase

  • judge (String) (defaults to: $judge)

    The name of the judge, from the judges tool

  • loog (Loog) (defaults to: $loog)

    The logging facility

Yields:

  • (f)


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/fbe/regularly.rb', line 36

def Fbe.regularly(area, p_every_days, p_since_days = nil, fb: Fbe.fb, judge: $judge, loog: $loog, &)
  pmp = fb.query("(and (eq what 'pmp') (eq area '#{area}') (exists #{p_every_days}))").each.to_a.first
  interval = pmp.nil? ? 7 : pmp[p_every_days].first
  unless fb.query(
    "(and
      (eq what '#{judge}')
      (gt when (minus (to_time (env 'TODAY' '#{Time.now.utc.iso8601}')) '#{interval} days')))"
  ).each.to_a.empty?
    loog.debug("#{$judge} statistics have recently been collected, skipping now")
    return
  end
  f = fb.insert
  f.what = judge
  f.when = Time.now
  unless p_since_days.nil?
    days = pmp.nil? ? 28 : pmp[p_since_days].first
    since = Time.now - (days * 24 * 60 * 60)
    f.since = since
  end
  yield f
end

.repeatedly(area, p_every_hours, fb: Fbe.fb, judge: $judge, loog: $loog) {|fb.query("(and (eq what '#{judge}'))").each.to_a.first| ... } ⇒ Object

Run the block provided every X hours.

Parameters:

  • area (String)

    The name of the PMP area

  • p_every_hours (Integer)

    How frequently to run, every X hours

  • fb (Factbase) (defaults to: Fbe.fb)

    The factbase

  • judge (String) (defaults to: $judge)

    The name of the judge, from the judges tool

  • loog (Loog) (defaults to: $loog)

    The logging facility

Yields:

  • (fb.query("(and (eq what '#{judge}'))").each.to_a.first)


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/fbe/repeatedly.rb', line 36

def Fbe.repeatedly(area, p_every_hours, fb: Fbe.fb, judge: $judge, loog: $loog, &)
  pmp = fb.query("(and (eq what 'pmp') (eq area '#{area}') (exists #{p_every_hours}))").each.to_a.first
  hours = pmp.nil? ? 24 : pmp[p_every_hours].first
  unless fb.query(
    "(and
      (eq what '#{judge}')
      (gt when (minus (to_time (env 'TODAY' '#{Time.now.utc.iso8601}')) '#{hours} hours')))"
  ).each.to_a.empty?
    loog.debug("#{$judge} have recently been executed, skipping now")
    return
  end
  f = fb.query("(and (eq what '#{judge}'))").each.to_a.first
  if f.nil?
    f = fb.insert
    f.what = judge
  end
  Fbe.overwrite(f, 'when', Time.now)
  yield fb.query("(and (eq what '#{judge}'))").each.to_a.first
end

.sec(fact, prop = :seconds) ⇒ Object

Converts number of seconds into text.

Parameters:

  • fact (Factbase::Fact)

    The fact, where to get the number of seconds

  • prop (String) (defaults to: :seconds)

    The property in the fact, with the seconds



31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/fbe/sec.rb', line 31

def Fbe.sec(fact, prop = :seconds)
  s = fact[prop.to_s]
  raise "There is no #{prop} property" if s.nil?
  s = s.first.to_i
  if s < 60
    format('%d seconds', s)
  elsif s < 60 * 60
    format('%d minutes', s / 60)
  elsif s < 60 * 60 * 24
    format('%d hours', s / (60 * 60))
  else
    format('%d days', s / (60 * 60 * 24))
  end
end

.unmask_repos(options: $options, global: $global, loog: $loog) ⇒ Object



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/fbe/unmask_repos.rb', line 38

def self.unmask_repos(options: $options, global: $global, loog: $loog)
  repos = []
  octo = Fbe.octo(loog:, global:, options:)
  masks = (options.repositories || '').split(',')
  masks.reject { |m| m.start_with?('-') }.each do |mask|
    unless mask.include?('*')
      repos << mask
      next
    end
    re = Fbe.mask_to_regex(mask)
    octo.repositories(mask.split('/')[0]).each do |r|
      repos << r[:full_name] if re.match?(r[:full_name])
    end
  end
  masks.select { |m| m.start_with?('-') }.each do |mask|
    re = Fbe.mask_to_regex(mask[1..])
    repos.reject! { |r| re.match?(r) }
  end
  repos.reject! { |repo| octo.repository(repo)[:archived] }
  raise "No repos found matching: #{options.repositories}" if repos.empty?
  loog.debug("Scanning #{repos.size} repositories: #{repos.join(', ')}...")
  repos
end

.who(fact, prop = :who, options: $options, global: $global, loog: $loog) ⇒ Object

Converts an ID of GitHub user into a nicely formatting string with his name.

Parameters:

  • fact (Factbase::Fact)

    The fact, where to get the ID of GitHub user

  • prop (String) (defaults to: :who)

    The property in the fact, with the ID

  • options (Judges::Options) (defaults to: $options)

    The options coming from the judges tool

  • global (Hash) (defaults to: $global)

    The hash for global caching

  • loog (Loog) (defaults to: $loog)

    The logging facility



34
35
36
37
38
39
# File 'lib/fbe/who.rb', line 34

def Fbe.who(fact, prop = :who, options: $options, global: $global, loog: $loog)
  id = fact[prop.to_s]
  raise "There is no #{prop} property" if id.nil?
  id = id.first.to_i
  "@#{Fbe.octo(options:, global:, loog:).user_name_by_id(id)}"
end