#!/usr/bin/env ruby # Copyright (c) 2023 Yegor Bugayenko # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the 'Software'), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. STDOUT.sync = true require 'slop' require 'loog' require 'octokit' require 'nokogiri' require 'backtrace' require 'fileutils' require 'obk' require_relative '../lib/fief/version' require_relative '../lib/fief/repos' loog = Loog::REGULAR def config(path) f = File.expand_path(path) args = [] args += File.readlines(f).map(&:strip).reject { |a| a.empty? } if File.exist?(f) args end args = config('~/.fief') + config('.fief') + ARGV opts = Slop.parse(args, strict: true, help: true) do |o| o.banner = "Usage (#{Fief::VERSION}): fief [options]" o.bool '-h', '--help', 'Show these instructions' o.bool '--version', 'Show current version' o.bool '--verbose', 'Print as much log messages as possible' o.bool '--dry', 'Make no real round trips to GitHub' o.bool '--reuse', 'Don\'t fetch from GitHub, reuse the existing XML file' o.integer '--delay', 'Delay between HTTP calls to GitHub API, in milliseconds', default: 1000 o.string '--to', 'Directory where to save all files to', default: './fief' o.string '--token', 'GitHub authentication token' o.array '--metrics', 'Names of metrics to use (all by default)' o.array '--include', 'Mask of GitHub repo to include, e.g. yegor256/*' o.array '--exclude', 'Mask of GitHub repo to exclude' end if opts.help? puts opts exit end if opts.verbose? loog = Loog::VERBOSE end if opts.version? loog.info(Fief::VERSION) exit end Encoding.default_external = Encoding::UTF_8 Encoding.default_internal = Encoding::UTF_8 def build_xml(opts, loog) start = Time.now if opts.token? api = Octokit::Client.new(:access_token => opts[:token]) else api = Octokit::Client.new loog.warn("Connecting to GitHub without a token, this may lead to errors, use --token") end api.auto_paginate = true api = Obk.new(api, pause: opts[:delay]) repos = [ 'yegor256/fief' ] if !opts[:dry] repos = Fief::Repos.new(opts, api, loog).all end data = [] legend = {} repos.each do |repo| Dir[File.join(__dir__, '../lib/fief/metrics/*.rb')].each do |f| name = File::basename(f).split('.')[0] if !opts[:metrics].empty? && !opts[:metrics].include?(name) loog.info("Ignoring #{user}/#{name} due to --metrics") next end type = "Fief::#{name.capitalize}" loog.info("Reading '#{name}' for #{repo}...") require_relative f m = type.split('::').reduce(Module, :const_get).new(api, repo, opts) if opts.dry? measures = [{ title: 'Pulls+', value: 42, legend: 'Open pulls' }, { title: 'Issues', value: 42 }] else measures = m.take(loog) end measures.each do |m| legend[m[:title]] = m[:legend] end if data.find { |d| d[:name] == repo }.nil? data << {name: repo, metrics: []} end data.find { |d| d[:name] == repo }[:metrics] += measures loog.info("Read from '#{name}': #{measures.map { |m| "#{m[:title]}=#{m[:value]}" }.join(', ')}") end end p data builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml| xml.fief(time: Time.now, sec: Time.now - start) do xml.titles do data.map { |r| r[:metrics].map { |ms| ms[:title] } }.flatten.uniq.each do |t| xml.title do xml.text t end end end xml.legend do legend.each do |t, text| xml.column(title: t) do xml.text text end end end xml.repositories do data.each do |r| xml.repository(id: r[:name]) do xml.metrics do r[:metrics].each do |ms| xml.m(id: ms[:title]) do xml.parent.set_attribute('alert', 'yes') if ms[:alert] xml.text ms[:value] end end end end end end end end xml = builder.to_xml loog.debug(xml) xml end begin raise 'At least one --include is required' if opts[:include].empty? home = File.absolute_path(opts[:to]) loog.debug("All files generated will be saved to #{home}") if File.exist?(home) loog.debug("Directory #{home} exists") else FileUtils.mkdir_p(home) loog.debug("Directory #{home} created") end index = File.join(home, 'index.xml') if opts[:reuse] xml = File.read(index) else xml = build_xml(opts, loog) File.write(index, xml) loog.debug("XML saved to #{index} (#{File.size(index)} bytes)") end xslt = Nokogiri::XSLT(File.read(File.join(__dir__, '../assets/index.xsl'))) html = xslt.transform(Nokogiri::XML(xml), 'version' => "'#{Fief::VERSION}'") loog.debug(html) front = File.join(home, 'index.html') File.write(front, html.to_html(indent: 0).gsub("\n", '')) loog.debug("HTML saved to #{front} (#{File.size(front)} bytes)") rescue StandardError => e loog.error(Backtrace.new(e)) exit -1 end