require 'highline' require 'harvested' require 'github_api' require 'ruby-progressbar' module FireWatch class Runner ISSUE_REGEX = /.?#(\d+)/ def self.invoke cli = HighLine.new # Github Client github_login = cli.ask('Github Login: ') github_password = cli.ask('Github Password: ') { |q| q.echo = 'x' } @github_client = Github.new(auto_pagination: true) do |config| config.basic_auth = "#{github_login}:#{github_password}" if cli.agree("Do you use Two-Factor authentication (non-sms)?") config.connection_options = { headers: {"X-GitHub-OTP" => cli.ask('Two-Factor Code')} } end end puts "Fetching Repos from Github..." repos = @github_client.repos.list(org: 'wildland').sort_by{|r| r.name} selected_repo = nil cli.choose do |menu| menu.prompt = "Select Github Repo:" repos.each do |r| menu.choice(r.name) do selected_repo = r end end end puts "Fetching Milestones from Github..." milestones = @github_client.issues.milestones.list(user: 'wildland',repo: selected_repo.name, state: 'all').sort_by{|m| m.title} selected_milestones = [] done = false loop do cli.choose do |menu| menu.prompt = "Add/Remove Milestones (#{selected_milestones.map(&:title).join(', ')}): " menu.choice("Done Adding Milestones") { done = true } if selected_milestones.count > 0 milestones.each do |m| menu.choice(m.title) do if selected_milestones.include? m selected_milestones.delete(m) else selected_milestones.push(m) end end end end break if done end # Harvest Client harvest_username = cli.ask('Harvest Username: ') harvest_password = cli.ask('Harvest Password: ') { |q| q.echo = 'x' } @harvest_client = Harvest.client(username: harvest_username, password: harvest_password, subdomain: 'wildland') puts "Fetching Clients and Projects from Harvest..." clients = @harvest_client.clients.all projects = @harvest_client.projects.all selected_projects = [] done = false show_all = false loop do cli.choose do |menu| menu.prompt = "Add/Remove Project (#{selected_projects.map(&:name).join(', ')}): " menu.choice("Done Adding Projects") { done = true } if selected_projects.count > 0 clients.sort_by(&:name).each do |client| projects.select{|p| p.client_id == client.id}.sort_by(&:name).each do |p| next unless show_all || p.active menu.choice("#{client.name} - #{p.name}") do if selected_projects.include? p selected_projects.delete(p) else selected_projects.push(p) end end end end if show_all menu.choice("Only Show Active Projects") { show_all = false } else menu.choice("Include Inactive Projects") { show_all = true } end end break if done end puts "Fetching Users from Harvest..." harvest_users = @harvest_client.users.all time_entries = [] selected_projects.each do |project| if project.starts_on.nil? puts "No start date for #{project.name}" next end time_entries += @harvest_client.reports.time_by_project( project, Time.parse(project.starts_on), project.ends_on.nil? ? Time.now : Time.parse(project.ends_on) ) end #Process Data csv_file_name = cli.ask('File name for report?').concat('.csv') puts "Generating Report with " + selected_milestones.map(&:title).join(', ') + " vs " + selected_projects.map(&:name).join(', ') puts "Saving to #{csv_file_name}" progressbar = ProgressBar.create(format: "%a %b\u{15E7}%i %p%% %t", progress_mark: ' ', remainder_mark: "\u{FF65}", starting_at: 10) CSV.open(csv_file_name, "wb", force_quotes: true) do |csv| csv << [ "Repo", "Milestone", "Issue Number", "Issue Title", "Project", "Who", "Scope Time", "Actual Time", "Status", "Merged", "Labels" ] selected_milestones.each do |selected_milestone| issues, pull_request_issues = @github_client.issues.list(user: 'wildland', repo: selected_repo.name, milestone: selected_milestone.number, state: 'all').partition{|i| i.pull_request == nil} pull_requests = pull_request_issues.map do |i| @github_client.pull_requests.get(user: 'wildland', repo: selected_repo.name, number: i.number).body end issues.each do |issue| relevant_time_entries, time_entries = time_entries.partition do |entry| entry.notes.nil? ? false : entry.notes.scan(ISSUE_REGEX).flatten.any?{|m| m.eql? issue.number.to_s } end if relevant_time_entries.empty? csv << [ selected_repo.name, selected_milestone.title, issue.number, issue.title, "", "", !issue.labels.nil? && !issue.labels.find{|l| l.name.include?"size:"}.nil? ? issue.labels.find{|l| l.name.include?"size:"}.name : "", "", issue.state, "", issue.labels.nil? ? "" : issue.labels.compact.map{|l| l.name}.join(', ') ] else relevant_time_entries.chunk{|i| i.user_id }.each do |user_id, user_time_entries| user = harvest_users.find{|u| u.id == user_id} csv << [ selected_repo.name, selected_milestone.title, issue.number, issue.title, projects.select{|p| user_time_entries.map(&:project_id).uniq.include? p.id}.map(&:name).join(', '), "#{user.first_name} #{user.last_name}", # Who !issue.labels.nil? && !issue.labels.find{|l| l.name.include?"size:"}.nil? ? issue.labels.find{|l| l.name.include?"size:"}.name : "", user_time_entries.inject(0){|sum, e| sum + (e.hours / e.notes.scan(ISSUE_REGEX).flatten.count) }, issue.state, "", issue.labels.nil? ? "" : issue.labels.map{|l| l.name}.join(', ') ] end end progressbar.increment end pull_requests.each do |pr| relevant_time_entries, time_entries = time_entries.partition do |entry| entry.notes.nil? ? false : entry.notes.scan(ISSUE_REGEX).flatten.any?{|m| m.eql? issue.number.to_s } end if relevant_time_entries.empty? csv << [ selected_repo.name, selected_milestone.title, pr.number, pr.title, "", "", !pr.labels.nil? && !pr.labels.find{|l| l.name.include?"size:"}.nil? ? pr.labels.find{|l| l.name.include?"size:"}.name : "", "", pr.state, pr.merged ? "Y" : "N", pr.labels.nil? ? "" : pr.labels.map{|l| l.name}.join(', ') ] else relevant_time_entries.chunk{|i| i.user_id }.each do |user_id, user_time_entries| user = harvest_users.find{|u| u.id == user_id} csv << [ selected_repo.name, selected_milestone.title, pr.number, pr.title, projects.select{|p| user_time_entries.map(&:project_id).uniq.include? p.id}.map(&:name).join(', '), "#{user.first_name} #{user.last_name}", # Who !pr.labels.nil? && !pr.labels.find{|l| l.name.include?"size:"}.nil? ? pr.labels.find{|l| l.name.include?"size:"}.name : "", user_time_entries.inject(0){|sum, e| sum + (e.hours / e.notes.scan(ISSUE_REGEX).flatten.count) }, pr.state, pr.merged ? "Y" : "N", pr.labels.nil? ? "" : issue.labels.map{|l| l.name}.join(', ') ] end end progressbar.increment end end end progressbar.finish if time_entries.count > 0 && cli.agree("#{time_entries.count} Time entries not in this milestone. Create a list of them?") time_entries_file_name = cli.ask('File name for report?').concat('.csv') CSV.open(time_entries_file_name, "wb", force_quotes: true) do |csv| csv << [ "Created At", "Who", "Hours", "Notes" ] time_entries.each do |entry| csv << [ entry.created_at, "#{harvest_users.find{|u| u.id == entry.user_id}.first_name} #{harvest_users.find{|u| u.id == entry.user_id}.last_name}", entry.hours, entry.notes ] end end end rescue Github::Error::GithubError => e puts 'Github API Error' puts e.to_s rescue Harvest::AuthenticationFailed puts 'Unable to authenticate to Harvest. Check username/password.' rescue Harvest::HTTPError => e puts 'Harvest API Error' puts e.to_s end end end