#!/usr/bin/env ruby -wU #******************************************************************************* # # INTENT # # This script codifies the "moment" that someone starts "working" on an issue. # The goal is leveraging git to capture both the opening and closing moments # From those moments we can compute the elapsed time for resolving an issue. # # The script is "close" to where the developers are working, and is hopefully # easier to incorporate with developer workflow. Without using this script, we # have less precise "starting moment", if at all. # # The result is that we can now report two time-based attributes for issues: # # * Elapsed time: How long an issue took to complete # * Estimated effort: How much time we think it will take to complete; This is # not elapsed time but instead an amount of focused time. # # The conjecture is given those two time-based attributes we can begin # improving the interpretation of our Feature estimates. # # Features will likely be comprised of a collection of issues; The estimate for # a feature is much less precise and I believe is equal parts "Estimated Effort" # and "Estimated Complexity". # #******************************************************************************* #******************************************************************************* # # CONFIGURATION OPTIONS # #******************************************************************************* CONFIG_KEYS = [:REMOTE, :FROM_BRANCH, :REPOSITORY_PATH, :ISSUE_TITLE, :STARTED_ISSUES_FILE].freeze REPOSITORY_PATH = ENV.fetch('REPOSITORY_PATH') { File.expand_path(File.join(File.dirname(__FILE__), '../')) } REMOTE = ENV.fetch('REMOTE', 'origin') FROM_BRANCH = ENV.fetch('FROM_BRANCH', 'master') STARTED_ISSUES_FILE = ENV.fetch('STARTED_ISSUES_FILE', '.started-issues') #******************************************************************************* # # HELP OPTIONS # #******************************************************************************* if ARGV.grep(/-h/i).size == 1 $stdout.puts "" $stdout.puts "$ ./#{File.basename(__FILE__)} 123" $stdout.puts "" $stdout.puts "This script will create an issue branch and update the remote repository." $stdout.puts "" $stdout.puts "* Create a new branch for the given issue number" $stdout.puts "* Touch and append the issue number to a tracking file" $stdout.puts "* Write a rudimentary commit message" $stdout.puts "" $stdout.puts "Note: There are steps to insure you have a clean working directory." $stdout.puts "Note: If you have spaces in your configuration all bets are off!" $stdout.puts "" $stdout.puts "Current Configuration:" CONFIG_KEYS.each do |key| $stdout.puts "\t#{key}='#{Object.const_get(key)}'" end $stdout.puts "" $stdout.puts "You can override the configuration option by adding the corresponding" $stdout.puts "ENV variable." $stdout.puts "" $stdout.puts "Example:" $stdout.puts "$ REMOTE=origin ./scripts/#{File.basename(__FILE__)}" exit(0) end #******************************************************************************* # # GUARD # #******************************************************************************* # Guard that I have an issue number issue_source = nil ISSUE_NUMBER = ARGV.shift if ISSUE_NUMBER =~ /^\w+-\d+$/ issue_source = :jira elsif ISSUE_NUMBER =~ /^\d+$/ issue_source = :github else $stderr.puts "Expected first parameter to be a JIRA or GitHub issue number.\n\n" $stderr.puts "See help for details on specifying an issue number.\n\n" $stderr.puts "$ ./#{File.basename(__FILE__)} -h" exit!(1) end unless system("which jira") $stderr.puts "Expected to find `jira' in your path." $stderr.puts "Run the following:" $stderr.puts "\tbrew tap ndlib/dlt\n\tbrew install go-jira\n\n" exit!(9) end unless File.exist?(File.join(ENV['HOME'], '.jira.d/config.yml')) $stderr.puts "Expected ~/.jira.d/config.yml to exist" $stderr.puts "Run the following:" $stderr.puts "\tmkdir ~/.jira.d\n\techo 'endpoint: https://jira.library.nd.edu' > ~/.jira.d/config.yml" exit!(10) end # Capture the issue_title ISSUE_TITLE = ENV.fetch('ISSUE_TITLE', nil) if ISSUE_TITLE.nil? case issue_source when :github issue_title = begin remote_url = `cd #{REPOSITORY_PATH} && git config --get remote.#{REMOTE}.url`.strip match = remote_url.match(/(\w+)\/(\w+)(?:\.git)?\Z/) if match require 'open-uri' require 'json' owner, repository = match.captures document = open("https://api.github.com/repos/#{owner}/#{repository}/issues/#{ISSUE_NUMBER}.json").read json = JSON.parse(document) json.fetch('title').gsub(/\W+/, '-') else 'issue-on-github' end rescue 'issue-on-github' end when :jira issue_title = begin require 'yaml' # TODO(dbrower): sometimes this will block waiting for the user to # enter a password. Is better way to handle the situation? document = `jira view #{ISSUE_NUMBER}` # sometimes there is garbage before the "issue:" line. document.sub!(/\A[^i]*/, '') issue = YAML.load(document) title = issue["summary"] if title == "" $stdout.puts "Problem finding issue #{ISSUE_NUMBER}" exit!(5) end title rescue $stdout.puts "Problem finding issue #{ISSUE_NUMBER}" exit!(6) end end else issue_title = ISSUE_TITLE end # Ensure that we don't have ridiculously long branch names issue_title = issue_title[0..30] # Guard that directories exist [:REPOSITORY_PATH].each do |key| repository_path = Object.const_get(key) unless File.directory?(repository_path) $stderr.puts "Expected directory for #{key} @ #{repository_path} to exist.\n\n" $stderr.puts "See help for details on specifying #{key}.\n\n" $stderr.puts "$ ./#{File.basename(__FILE__)} -h" exit!(2) end end # Guard that we have a clean working directory if `cd #{REPOSITORY_PATH} && git status --porcelain`.strip.size > 0 $stderr.puts "Repository @ #{REPOSITORY_PATH} did not have a clean working directory" exit!(3) end #******************************************************************************* # # DO STUFF # #******************************************************************************* `cd #{REPOSITORY_PATH} && git checkout #{FROM_BRANCH}` `cd #{REPOSITORY_PATH} && git pull --rebase` TO_BRANCH = "#{ISSUE_NUMBER}-#{issue_title.gsub(/\W+/, '-')}" if `cd #{REPOSITORY_PATH} && git branch -l | grep '#{TO_BRANCH}$'`.strip.size > 0 $stderr.puts "ERROR: Branch #{TO_BRANCH} already exists" exit!(4) end `cd #{REPOSITORY_PATH} && git checkout -b #{TO_BRANCH} && echo "#{ISSUE_NUMBER}" >> #{STARTED_ISSUES_FILE} && git add #{STARTED_ISSUES_FILE}` path_to_commit_message = File.expand_path(File.join(REPOSITORY_PATH, '../COMMIT.msg')) begin File.open(path_to_commit_message, 'w+') do |file| file.puts "Claiming issue #{ISSUE_NUMBER}" file.puts "" if issue_source == :github file.puts "relates to ##{ISSUE_NUMBER}" file.puts "" end message = "$ ./script/#{File.basename(__FILE__)} #{ISSUE_NUMBER}" CONFIG_KEYS.each_with_object(message) do |key, mem| if ENV.key?(key.to_s) mem = "#{key}=\"#{ENV[key.to_s].to_s}\" #{mem}" end mem end file.puts message file.puts "" file.puts "[skip ci]" end $stdout.puts `cd #{REPOSITORY_PATH} && git commit -F #{path_to_commit_message}` ensure File.unlink(path_to_commit_message) rescue true end # If the commit was successful, assign the issue to us and move into "In Progress" if issue_source == :jira system("jira take #{ISSUE_NUMBER}") exit!(5) unless $?.success? system("jira --noedit transition \"In Progress\" #{ISSUE_NUMBER}") exit!(5) unless $?.success? end