#!/usr/bin/env ruby # frozen_string_literal: true # vim: ft=ruby require "bundler/setup" require "dry/cli" require "json" require "tty/progressbar" require "csv" require "active_support/inflector" require "yaml" require_relative "../base" module Githuh module CLI module Commands module Issue class Export < Base FORMATS = { json: "json", csv: "csv", }.freeze DEFAULT_FORMAT = :csv DEFAULT_OUTPUT_FORMAT = "..issues." attr_accessor :filename, :file, :output, :repo, :issues, :format, :record_count, :mapping desc "Export Repo issues into a CSV or JSON format\n" \ " Default output file is " + DEFAULT_OUTPUT_FORMAT.bold.yellow argument :repo, type: :string, required: true, desc: 'Name of the repo, eg "rails/rails"' option :file, required: false, desc: "Output file, overrides " + DEFAULT_OUTPUT_FORMAT option :format, values: FORMATS.keys.map(&:to_s), default: DEFAULT_FORMAT.to_s, required: false, desc: "Output format" option :mapping, type: :string, require: false, desc: "YAML file with label to estimates mapping" def call(repo: nil, file: nil, format: nil, mapping: nil, **opts) super(**opts) self.record_count = 0 self.repo = repo raise ArgumentError, "argument is required" unless repo raise ArgumentError, "argument is not a repository, expected eg 'rails/rails'" unless repo =~ %r{/} self.mapping = {} if mapping && ::File.exist?(mapping) self.mapping = ::YAML.safe_load(::File.read(mapping))['label-to-estimates'] || {} end Export.const_set(:LabelEstimates, self.mapping) self.issues = [] self.output = StringIO.new self.format = (format || DEFAULT_FORMAT).to_sym self.filename = file || file_name(repo) self.file = File.open(filename, "w") print_summary raise ArgumentError, "Format is not provided" unless FORMATS.key?(format&.to_sym) # —————————— actually get all issues ——————————————— self.file.write send("render_as_#{format}", fetch_issues) # ———————————————————————————————————————————————————————— print_conclusion ensure file.close if file.respond_to?(:close) && !file.closed? end def fetch_issues client.auto_paginate = true self.issues = filter_issues(client.issues(repo, query: default_options)).tap do |issue_list| self.record_count = issue_list.size bar("Issues")&.advance end end def filter_issues(issues_list) issues_list.reject do |issue| issue.html_url =~ /pull/ end end def bar_size record_count + 1 end def default_options { state: "open" } end def self.issue_labels(issue) issue.labels.map(&:name) end def self.find_user(client, username) @user_cache ||= {} @user_cache[username] ||= client.user(username).name end CSV_MAP = { "Labels" => ->(_client, issue) { issue_labels(issue).reject { |l| LabelEstimates.key?(l) }.join(",").downcase }, "Type" => ->(*) { "feature" }, "Estimate" => ->(_client, issue) do el = issue_labels(issue).find { |l| LabelEstimates.key?(l) } el ? LabelEstimates[el] : nil end, "Current State" => ->(*) { "unstarted" }, "Requested By" => ->(client, issue) do find_user(client, issue.user.login) end, "Owned By" => ->(client, issue) do find_user(client, issue.user.login) end, "Description" => ->(_client, issue) { issue.body }, "Created at" => ->(_client, issue) { issue.created_at }, }.freeze CSV_HEADER = %w(Id Title Labels Type Estimate) + ["Current State", "Created at", "Accepted at", "Deadline", "Requested By", "Owned By", "Description", "Comment", "Comment", "Comment", "Comment"].freeze # Id,Title,Labels,Type,Estimate,Current State,Created at,Accepted at,Deadline,Requested By,Owned By,Description,Comment,Comment # 100, existing started story,"label one,label two",feature,1,started,"Nov 22, 2007",,,user1,user2,this will update story 100,, # ,new story,label one,feature,-1,unscheduled,,,,user1,,this will create a new story in the icebox,comment1,comment2 def render_as_csv(issue_list) # puts "rendering issues as CVS:" # pp issue_list ::CSV.generate do |csv| csv << CSV_HEADER issue_list.each do |issue| row = [] CSV_HEADER.each do |column| method = column.downcase.underscore.to_sym value = if CSV_MAP[column] CSV_MAP[column][client, issue] else begin issue.to_h[method] rescue StandardError nil end end value = value.strip if value.is_a?(String) row << value end csv << row bar&.advance end bar.finish end end def render_as_json(issue_list) JSON.pretty_generate(issue_list.map(&:to_h)) end private def print_conclusion puts puts TTY::Box.info("Success: written a total of #{record_count} records to #{filename}", **{ width: ui_width, padding: 1 }) puts end def print_summary puts puts TTY::Box.info("Format : #{self.format}\n" \ "File : #{filename}\n" \ "Repo : #{repo}\n", **{ width: ui_width, padding: 1 }) puts end def file_name(repo) "#{repo.gsub(%r{/}, '.')}.issues.#{FORMATS[self.format.to_sym]}" end end end register "issue", aliases: ["r"] do |prefix| prefix.register "export", Issue::Export end end end end