Skip to content
Snippets Groups Projects
jira 38 KiB
Newer Older
  • Learn to ignore specific revisions
  • #!/usr/bin/env ruby
    
    require 'rubygems'
    require 'json'
    require 'pp'
    
    require 'set'
    
    # Try the following commands:
    #
    #   jira sprint S133  [lists the tasks in S133]
    #   jira plan S134    [shows the tasks planned for S134 in a form suitable for planning]
    #
    
    # Uses the Rest API: http://docs.atlassian.com/jira/REST/4.2.1/
    #
    # ----
    # 
    # This script requires the json gem:
    #
    #    http://flori.github.com/json/
    #
    # Which can be installed:
    #
    #   gem install json
    #
    
    #
    # == Preferences
    #
    
    # The url for JIRA
    $jira_url = 'https://jira-bsse.ethz.ch'
    
    # The url portion for the API
    
    #$jira_api_url = "#{$jira_url}/rest/api/2.0.alpha1"
    $jira_api_url = "#{$jira_url}/rest/api/2"
    
    
    # Prefs path
    $jira_prefs_path = File.expand_path('~/.jira')
    
    # Cookie location
    $jira_cookie_path = File.join($jira_prefs_path, 'cookie.txt')
    
    
    
    # 
    # == Issue Object
    #
    class Issue
      
      def initialize(data)
        @issue = data
      end
      
      def key
        return @issue["key"]
      end
      
      def fields
        return @issue["fields"]
      end
      
      def implements
        implements = nil
        links = self.fields["issuelinks"]
        unless links.nil?
          links.each { | link | implements =  Issue.new(link["outwardIssue"]) if "implements" == link["type"]["outward"] && !link["outwardIssue"].nil? }
        end
    
        return implements
      end
      
      def implemented_by
        implementedby = []
        links = self.fields["issuelinks"]
        unless links.nil?
          links.each { | link | implementedby << Issue.new(link["inwardIssue"]) if "implements" == link["type"]["outward"] && !link["inwardIssue"].nil?}
        end
        return implementedby
      end
      
      def time
        return 0 if self.fields["timetracking"].nil?
        return self.fields["timetracking"]["remainingEstimateSeconds"] ? self.fields["timetracking"]["remainingEstimateSeconds"] : 0
      end
      
      def status
        return self.fields["status"] ? self.fields["status"]["name"] : nil
      end
      
      def tester
        return self.fields["customfield_10250"] ? self.fields["customfield_10250"]["name"] : nil
      end
      
      def summary
        return self.fields["summary"]
      end
      
      def fix_version
        fix_versions = self.fields["fixVersions"]
        return "Unscheduled" if fix_versions.nil?
    
        return "" if fix_versions[0].nil?
    
      
      def transitions
        @issue["transitions"]
      end
      
      def next_sprint
        self.fields["customfield_10550"]
      end
      
      def resolved_or_closed?
        status = self.status
        (status == "Resolved" || status == "Closed")
      end
    
    #
    # A module that implements some helpful operations
    #
    module JiraHelpers
    
      def JiraHelpers.search(query, limit=nil)
    
        search_cmd = "curl -s --get --cookie #{$jira_cookie_path} '#{$jira_api_url}/search' --data-urlencode 'os_authType=cookie' --data-urlencode 'jql=#{query}' --data-urlencode 'fields=*all,-comment'"
    
        search_cmd = search_cmd + " --data-urlencode 'maxResults=#{limit}'" unless limit.nil?
        return `#{search_cmd}`
    
      def JiraHelpers.issue(issue_number, limit=nil)
    
        issue_cmd = "curl -s --get --cookie #{$jira_cookie_path} '#{$jira_api_url}/issue/#{issue_number}' --data-urlencode 'os_authType=cookie' --data-urlencode 'expand=transitions'"
    
        data = `#{issue_cmd}`
        issue_data = JSON.load(data)
        return Issue.new(issue_data)    
      end  
      
    
      def JiraHelpers.rank(issue, after, before=nil)
        rank_data = {"issueKeys" => [issue], "customFieldId" => 10050 }
        rank_data["rankAfterKey"] = after
        rank_data["rankBeforeKey"] = before unless before.nil?    
    
        rank_cmd = "curl -s --cookie #{$jira_cookie_path} -H 'Content-Type: application/json' -X PUT '#{$jira_url}/rest/greenhopper/1.0/rank' -d '#{JSON.generate(rank_data)}'"
    
      end 
      
      def JiraHelpers.create_sp_issue(summary, description, fixversion)
        # May need to escape the summary
        issue_data = {
          "fields" => { 
            "project" => {"key" => "SP" } , 
            "summary" => summary, 
            "description" => description, 
            "fixVersions" => [ {"name" => fixversion } ],
            "issuetype" => {"name" => "Task"} 
          } 
        }
        issue_cmd = "curl -s --cookie #{$jira_cookie_path} -H 'Content-Type: application/json' '#{$jira_api_url}/issue/' -d '#{JSON.generate(issue_data)}'"
        ans = `#{issue_cmd}`
        return JSON.load(ans)
      end
      
      def JiraHelpers.link_sp_to_bis(sp_key, bis_key)
        link_data = {
          "type" => { "name" => "Hierarchy" },
          "inwardIssue" => { "key" => sp_key },
          "outwardIssue" => { "key" => bis_key }
        }
        link_cmd = "curl -s --cookie #{$jira_cookie_path} -H 'Content-Type: application/json' '#{$jira_api_url}/issueLink' -d '#{JSON.generate(link_data)}'"
        return `#{link_cmd}`
    
      end
      
      def JiraHelpers.remove_next_sprint(bis_key)
        update_data = {
          "fields" => {
            "customfield_10550" => nil
          }
        }
        update_cmd = "curl -s --cookie #{$jira_cookie_path} -H 'Content-Type: application/json' '#{$jira_api_url}/issue/#{bis_key}' -X PUT -d '#{JSON.generate(update_data)}'"
        return `#{update_cmd}`
      end
      
      def JiraHelpers.set_next_sprint(bis_key)
        update_data = {
          "fields" => {
            "customfield_10550" => [
                {"value"=>"Yes", "id"=>"10240", "self"=>"https://jira-bsse.ethz.ch/rest/api/2/customFieldOption/10240"}
              ]
          }
        }
        update_cmd = "curl -s --cookie #{$jira_cookie_path} -H 'Content-Type: application/json' '#{$jira_api_url}/issue/#{bis_key}' -X PUT -d '#{JSON.generate(update_data)}'"
        return `#{update_cmd}`
      end
      
      def JiraHelpers.set_fix_version(bis_key, fixversion)
        update_data = { "fields" => { "fixVersions" => [ {"name" => fixversion } ] } }
        update_cmd = "curl -s --cookie #{$jira_cookie_path} -H 'Content-Type: application/json' '#{$jira_api_url}/issue/#{bis_key}' -X PUT -d '#{JSON.generate(update_data)}'"
        return `#{update_cmd}`
      end
      
      def JiraHelpers.transition(bis_key, trans_id)
        transition_data = {
          "transition" => { "id" => "#{trans_id}"}
        }
        transition_cmd = "curl -s --cookie #{$jira_cookie_path} -H 'Content-Type: application/json' '#{$jira_api_url}/issue/#{bis_key}/transitions' -d '#{JSON.generate(transition_data)}'"
        return `#{transition_cmd}`
      end
    
    
      # Search and return the full data for the found objects
      def JiraHelpers.search_full(query, silent)
        print "Retrieving issues" unless silent
        ans = JiraHelpers.search(query)
        data = JSON.load(ans)
    
        full_issues_data = data["issues"]
        full_issues = full_issues_data.collect { | issue_data | Issue.new(issue_data) }
    
      # Get the full data for entries that implement {issues}.
      def JiraHelpers.retrieve_implementors(issues, silent)
        print "Retrieving implementing issues" unless silent
        implementors = []
        issues.each do | issue |
    
          print "." unless silent
          implementors.concat issue.implemented_by
    
        print "\n" unless silent   
        return implementors
    
      end
      
      def JiraHelpers.session_valid_raw
        # Just get the response code, don't care about the rest
        ans = `curl -s -w %{http_code} -o /dev/null --head --get --cookie #{$jira_cookie_path} --data-urlencode 'os_authType=cookie' '#{$jira_url}/rest/auth/1/session'`
        return ans
    
        # Just get the response code, don't care about the rest
    
        ans = JiraHelpers.session_valid_raw
    
        return false if $?.to_i != 0
        return ans == "200"
    
    #
    # A module that implements some helpful operations
    #
    module InputHelpers
      def InputHelpers.args(first_arg)
        cmd = ARGV[first_arg .. -1].inject("") { | all, each | all + " " + each }
        cmd.strip!
        return cmd
      end
      
      def InputHelpers.sprint_name(sprint_name_or_number)
        sprintNumber = "S" if sprint_name_or_number.nil?
        sprintNumber = sprint_name_or_number.match("^[S|s].*") ? sprint_name_or_number : "S" + sprint_name_or_number
        return sprintNumber
      end
        
    end
      
    
    #
    # == Commands
    #
    
    #
    # The abstract superclass of commands
    # 
    class JiraCommand
      
      attr_accessor :silent
      
      def initialize
        @silent = true
      end
      
      # Return a description of the command to run
      def description
        return nil
      end
      
      # Run the command and return the result
      def run
        return nil
      end
    
      # Return true if the result should be printed. 
      #
      # Default: print if the result is not empty.
      def should_print_result(result)
        return !result.empty? 
      end
    
    
      # helper method to print time in a formatted way
      def ptime(time)
        time == 0 ? "" : "%.1fh" % (time / 3600.0)
      end
    
    
    end
    
    #
    # The help command
    #
    class Help < JiraCommand
      def description
        return "help"
      end
      
      def run
        # show help
    
    jakubs's avatar
    jakubs committed
    return <<-eos
    NAME
        jira
    
    SYNOPSIS
        jira commmand [arguments]
    
    DESCRIPTION
        A simple api for jira. Login as jira user and get some info. The available commands are:    
    
    
    	login		asks for login and password to jira and stores a session cookie
    
    jakubs's avatar
    jakubs committed
    
    	sprint S133	get the information about the given sprint
    
    	dump url	print given url in pure json
    
    
    	plan 133	shows planned BIS issues
    
    	cust 10		shows an overview of the N highest-priority issues from customer projects (OBP, SOB)
    	
    	search 'project=SP AND fixVersion = S139 ORDER BY \"Global Rank\"' 
    				runs a jira query and shows the results
    	
    	rank 133 	ranks all issues in S133 in accordance with the ranks of the BIS issues	
    	rank SP-121 SP-122
    				ranks SP-121 after SP-122
    				
    	kanban 133 	returns a Kanban list of the issues in S133
    	
    	create 133 	create issues for all BIS issues designated as next sprint	
    	create 133 BIS-111
    				create an issue in S133 for BIS-111
    
    	start 133	mark all BIS issues in S133 as "Active in sprint" and clear the "Next Sprint" flag
    	
    	finish 133	Show whether the parent issues to those in S133 are resolved or not				
    	finish 133 exec	mark all BIS issues in S133 that have no other open issues as resolved. 
    				Set "Next Sprint" for those with open issues.
    				
    	move 133 	Set the fix version to 133 for all issues that are connected to the BIS issues
    	move 133 SP-452	Set the fix version of SP-452 to 133
    
    jakubs's avatar
    jakubs committed
      eos
    
        print "Enter jira login (e.g. alincoln): "
        @jira_user = $stdin.gets().strip
    
      def description
        return "login"
      end
      
      def run
        # The url portion for logging in
    
        # may also want to try curl -c cookie_jar -H "Content-Type: application/json" -d '{"username" : "admin", "password" : "admin"}' #{$jira_url}/jira/rest/auth/latest/session
    
        jira_login = 'secure/Dashboard.jspa?os_authType=basic'
        Dir.mkdir($jira_prefs_path) unless File.exists?($jira_prefs_path)
    
        return `curl --head -s -u #{@jira_user} --cookie-jar #{$jira_cookie_path} '#{$jira_url}/#{jira_login}'`
    
    #
    # Check if the session is valid
    #
    class SessionValid < JiraCommand
     
      def initialize
        super
      end
     
      def description
        return "session"
      end
      
      def run
        return JiraHelpers.session_valid_raw
      end
    end
    
    
    #
    # Lists the issues in the sprint
    #
    class LoggedInCommand < JiraCommand
      def run
        Login.new.run unless File.exists?($jira_cookie_path)
        Login.new.run unless JiraHelpers.session_valid?
        return self.run_logged_in
      end
    
      # For subclasses to implement
      def run_logged_in
        return ""
      end
    end
    
    #
    # Shows the JSON for an issue -- useful for debugging
    #
    class DumpIssue < LoggedInCommand
      def initialize
        super
        @issueUrl = ARGV[1]
      end
      
      def description
        return "dump #{@issueUrl}"
      end
      
      
      def run_logged_in
        if @issueUrl.nil?
          ans =  JiraHelpers.search("project=SP AND fixVersion = S133 ORDER BY \"Global Rank\" ASC")
          @issueUrl = JSON.load(ans)["issues"][0]["self"]
        end
    
    
        ans = `curl -s --get --cookie #{$jira_cookie_path} '#{@issueUrl}' --data-urlencode 'expand=transitions'`
    
    #
    # Prioritize an issue
    #
    
    # The REST API can be browsed using the REST endpoint browser. The particular endpoint you are looking for is a PUT request to /rest/greenhopper/1.0/rank. The request body looks # like: 
    
    # {"issueKeys":["ANERDS-102"],"rankBeforeKey":"ANERDS-94","rankAfterKey":"ANERDS-7","customFieldId":10050}
    # The issueKeys are the items to rank (there can be several), the rankBeforeKey is the issue to rank the items before and the rankAfterKey is the issue to rank these items after. # Because there may be multuple Global Rank fields in your JIRA the customFieldId is used to specify which one is used for this ranking.
    
        # "customfield_10050": {
        #   "required": false,
        #   "schema": {
        #     "type": "array",
        #     "items": "string",
        #     "custom": "com.pyxis.greenhopper.jira:gh-global-rank",
        #     "customId": 10050
        #   },
        #   "name": "Global Rank",
        #   "operations": ["set"]
        # },
    class RankIssue < LoggedInCommand
      def initialize
        super
        @issue = ARGV[1]
        @after = ARGV[2]
        @before = ARGV[3]
      end
      
      def description
        return "rank #{@issue} after #{@after} before #{@before}"
      end
      
      
      def run_logged_in
        ans = JiraHelpers.rank(@issue, @after, @before)
        return ans
      end
      
    end
    
    
    class RankSprint < LoggedInCommand
      def initialize
        super
        @sprintNumber = InputHelpers.sprint_name(ARGV[1])
      end
      
      def description
        return "rank #{@sprintNumber}"
      end
      
      
      def run_logged_in_old
        ans = JiraHelpers.rank(@issue, @after, @before)
        return ans
      end
    
      def run_logged_in
        sp_issues = retrieve_sprint_issues()
        bis_issues = retrieve_bis_issues(sp_issues)
        
        self.rank_issues(sp_issues, bis_issues)
        self.print_issues
        # Nothing to show
        return "#{sp_issues.length} issues"
      end
      
      def retrieve_sprint_issues()
        query = "project=SP AND fixVersion = #{@sprintNumber} ORDER BY \"Global Rank\" ASC" 
        sp_issues = JiraHelpers.search_full(query, @silent)
        init_sp_issue_dict(sp_issues)
        return sp_issues
      end
      
      def retrieve_bis_issues(sp_issues)
        implemented_issues = []
        sp_issues.each do | issue |
          implements_key = issue.implements.key unless issue.implements.nil?
          implemented_issues << issue.implements unless issue.implements.nil?
        end
        bis_issue_list = implemented_issues.collect{|issue| issue.key }.join(",")
        bis_query= "project = BIS AND key in (#{bis_issue_list}) ORDER BY \"Global Rank\" ASC"
        bis_issues = JiraHelpers.search_full(bis_query, @silent)
        return bis_issues
      end    
      
      def init_sp_issue_dict(sp_issues)
        @sp_issue_dict = {}
        sp_issues.each do | issue |
          key = issue.key
          @sp_issue_dict[key] = issue
        end
      end
      
      def rank_issues(sp_issues, bis_issues)
        not_in_bis = []
        bis_ranking = []
        seen_sp_issues = [].to_set
        bis_issues.each do | bis_issue |
          bis_issue.implemented_by.each do | sp |
            unless @sp_issue_dict[sp.key].nil?
              seen_sp_issues << sp.key
              bis_ranking << @sp_issue_dict[sp.key]
            end
          end
        end
        
        sp_issues.each do | sp_issue |
          not_in_bis << sp_issue unless seen_sp_issues.include?(sp_issue.key)
        end
        
        # not in bis comes first, in arbitrary order, then the remaining issues
        ranked_issues = not_in_bis + bis_ranking
        ranked_issues.each_cons(2) do | first, second |
          JiraHelpers.rank(second.key, first.key)
        end
        
      end
      
      def print_issues
        query = "project=SP AND fixVersion = #{@sprintNumber} ORDER BY \"Global Rank\" ASC" 
        full_issues = JiraHelpers.search_full(query, @silent)
        header = "%8s\t%12s\t%s" % ["Key", "Implements", "Summary"]
        puts header
        full_issues.each do | issue |
          key = issue.key
          implements_key = issue.implements.key unless issue.implements.nil?
          summary = issue.summary
          row = "%8s\t%12s\t%s" % [key, implements_key, summary]
          puts row
        end
        print " ", ("-" * 27), "\n"
      end  
      
    end
    
    
    #
    # Shows the JSON for a search results -- useful for debugging
    #
    class DumpSearch < LoggedInCommand
      def initialize
        super
        @query = ARGV[1]
        @count = ARGV[2]
      end
      
      def description
        return "search #{@query} #{@count}"
      end
      
      
      def run_logged_in
        @query = "project=SP AND fixVersion = S139 ORDER BY \"Global Rank\" ASC" if @query.nil?
        @count = 1 if @count.nil?
    
        ans = JiraHelpers.search(@query, @count)
        data = JSON.load(ans)
        return JSON.pretty_generate(data)
      end
      
    end
    
    
    #
    # Lists the issues in the sprint
    #
    class ListSprint < LoggedInCommand
      def initialize
        super
    
        @sprintNumber = InputHelpers.sprint_name(ARGV[1])
    
      end
      
      def description
        return "sprint #{@sprintNumber}"
      end
      
      
      def run_logged_in
        query = "project=SP AND fixVersion = #{@sprintNumber} ORDER BY \"Global Rank\" ASC" 
        full_issues = JiraHelpers.search_full(query, @silent)
        self.print_issues_table(full_issues)
        # Nothing to show
        return "#{full_issues.length} issues"
      end
      
      def print_issues_table(full_issues)
        header = "%8s\t%12s\t%6s\t%12s\t%8s\t%s" % ["Key", "Implements", "Time", "Status", "Tester", "Summary"]
        puts header
        time_remaining = 0.0
        full_issues.each do | issue |
    
          key = issue.key
          implements_key = issue.implements.key unless issue.implements.nil?
          time = issue.time
          status = issue.status
          tester = issue.tester
          summary = issue.summary
    
          row = "%8s\t%12s\t%6s\t%12s\t%8s\t%s" % [key, implements_key, ptime(time), status, tester, summary]
    
          puts row
          
          # Tasks that are resolved can be considered to have 0 time remaining
    
          time_remaining = time_remaining + time unless (status == "Resolved" || status == "Closed")
    
        puts "   Time Remaining : %.1fh" % (time_remaining / 3600.0)
    
    #
    # Lists the issues in the kanban cycle
    #
    class ListKanban < LoggedInCommand
      def initialize
        super
        @sprintNumber = InputHelpers.sprint_name(ARGV[1])
      end
      
      def description
        return "kanban #{@sprintNumber}"
      end
      
      
      def run_logged_in
        query = "project=BIS AND status not in (Resolved, Closed) OR (status = Resolved AND fixVersion = #{@sprintNumber}) ORDER BY \"Global Rank\" ASC" 
        full_issues = JiraHelpers.search_full(query, @silent)
        self.print_issues_table(full_issues)
        # Nothing to show
        return "#{full_issues.length} issues"
      end
      
      def print_issues_table(full_issues)
        header = "%8s  %12s\t%12s\t%8s\t%s" % ["Key", "Implements", "Status", "Tester", "Summary"]
        puts header
        full_issues.each do | issue |
          key = issue.key
          implements_key = issue.implements.key unless issue.implements.nil?
          status = issue.status
          tester = issue.tester
          summary = issue.summary
          row = "%8s  %12s\t%12s\t%8s\t%s" % [key, implements_key, status, tester, summary]
          puts row
        end
        print " ", ("-" * 27), "\n"
      end
    end
    
    
    
    #
    # List the issues slated for a sprint in a form that is helpful for planning
    #
    class PlanSprint < LoggedInCommand
      def initialize
        super
    
        @sprintNumber = InputHelpers.sprint_name(ARGV[1])
        @total = 0
    
      end
      
      def description
        return "plan #{@sprintNumber}"
      end
      
      
      def run_logged_in
        sp_query = "project=SP AND fixVersion = #{@sprintNumber} ORDER BY \"Global Rank\" ASC" 
        sp_issues = JiraHelpers.search_full(sp_query, @silent)
        init_sp_issue_dict(sp_issues)
        
        bis_query= "project = BIS AND status not in (Resolved, Closed) AND \"Next Sprint\" = YES ORDER BY \"Global Rank\" ASC"
    
        bis_issues = JiraHelpers.search_full(bis_query, @silent)
        enrich_sp_issue_dict(JiraHelpers.retrieve_implementors(bis_issues, @silent))
    
        
        ccs_query= "project = CCS AND status not in (Resolved, Closed) AND \"Next Sprint\" = YES ORDER BY \"Global Rank\" ASC"
        ccs_issues = JiraHelpers.search_full(ccs_query, @silent)
    
        enrich_sp_issue_dict(JiraHelpers.retrieve_implementors(ccs_issues, @silent))    
    
        
        swe_query= "project = SWE AND status not in (Resolved, Closed) AND \"Next Sprint\" = YES ORDER BY \"Global Rank\" ASC"
        swe_issues = JiraHelpers.search_full(swe_query, @silent)
    
        enrich_sp_issue_dict(JiraHelpers.retrieve_implementors(swe_issues, @silent))
    
        print_issues_table("BIS", bis_issues)
        print_issues_table("CCS", ccs_issues)    
        print_issues_table("SWE", swe_issues)    
    
        print_unseen_sp_issues_table(sp_issues)
    
        
        puts ("=" * 12)
        puts "Total %8.1fh" % [@total / 3600.0]
        puts ("-" * 12)
    
        issue_count = bis_issues.length + ccs_issues.length + swe_issues.length
        return "#{issue_count} issues"
    
      end
      
      def init_sp_issue_dict(sp_issues)
        @sp_issue_dict = {}
        sp_issues.each do | issue |
    
        @seen_sp_issues = [].to_set
      end
      
    
      #
      # Take those issues that are not yet resolved / closed and add them to the sp issues dict
      #
      def enrich_sp_issue_dict(implementors)
        implementors.each do | issue |
          sp = issue.key
          next if @sp_issue_dict[sp]
          
          status = issue.status
    
          @sp_issue_dict[sp] = issue unless issue.resolved_or_closed?
    
      def print_unseen_sp_issues_table(full_issues)
        puts ("=" * 12)
        puts "SP Missed"
        puts ("-" * 12)
        header = "%8s\t%12s\t%12s\t%6s\t%s" % ["Subtotal", "Key", "SP", "Time", "Summary"]
        puts header
        subtotal = 0.0
        full_issues.each do | issue |
    
          next if @seen_sp_issues.include?(sp)
          
    
          key = "----"
          key = issue.implements.key unless issue.implements.nil?
          time = issue.time
          summary = issue.summary
    
          # Tasks that are resolved can be considered to have 0 time remaining
    
          subtotal = subtotal + time unless status == "Resolved"
    
          row = "%8.1fh\t%12s\t%12s\t%6s\t%s" % [subtotal / 3600.0, key, sp, ptime(time), summary]
    
      end
      
      def print_issues_table(title, full_issues)
        puts ("=" * 12)
        puts title
        puts ("-" * 12)
        header = "%8s\t%12s\t%12s\t%6s\t%s" % ["Subtotal", "Key", "SP", "Time", "Summary"]
        puts header
        subtotal = 0.0
        full_issues.each do | issue |
    
          key = issue.key
          summary = issue.summary
          parent = issue.fields["parent"]
          parent = parent["key"] unless parent.nil?
          summary = "#{parent} / #{summary}" unless parent.nil?
    
          issue.implemented_by.each do | sp_issue |
            sp = sp_issue.key
    
            # We are only interested in links to issues in the specified sprint
    
            implementedby << sp if @sp_issue_dict[sp]
    
            row = "%8s\t%12s\t%12s\t%6s\t%s" % ["----", key, "----", "", summary]
    
            puts row
            next
          end
          
          implementedby.each_with_index do | sp, index |
            # print one row for each implemented by
            spissue = @sp_issue_dict[sp]
            next if spissue.nil?
    
            next if @seen_sp_issues.include?(sp)
            
            spfields = spissue.fields
            time = spissue.time
    
    
            # Tasks that are resolved can be considered to have 0 time remaining
    
            fix_version = spissue.fix_version
            issue_in_different_sprint = fix_version != @sprintNumber
            status = spissue.status       
            subtotal = subtotal + time unless (status == "Resolved" || status == "Closed" || issue_in_different_sprint)
    
              issue_summary = summary
              issue_summary =  "[#{fix_version}] #{issue_summary}" if issue_in_different_sprint
    
              row = "%8.1fh\t%12s\t%12s\t%6s\t%s" % [subtotal / 3600.0, key, sp, ptime(time), issue_summary]
    
              issue_summary = "\""
              issue_summary =  "[#{fix_version}] #{issue_summary}" if issue_in_different_sprint
    
              row = "%8.1fh\t%12s\t%12s\t%6s\t%s" % [subtotal / 3600.0, "\"", sp, ptime(time), issue_summary]
    
            @seen_sp_issues.add(sp)
          end
        end
    
    #
    # Create sprint issues for the issues in the sprint
    #
    class CreateSprint < LoggedInCommand
      def initialize
         super
         @sprintNumber = ARGV[1]
         @sprintNumber = "S" if @sprintNumber.nil?
         @sprintNumber = "S" + @sprintNumber unless @sprintNumber.match("^[S|s].*")
         @parent_issue = ARGV[2]
       end
    
      def description
        return "create #{@sprintNumber}"
      end
    
    
      def run_logged_in
        header = "%12s\t%12s\t%s" % ["Key", "SP", "Summary"]
        puts header
            
        # if no issue is specified, create all issues that relate to bis
        return create_bis_issues if @parent_issue.nil?
        
        # create the issue explicitly requested
        return create_explicit_issue
      end
      
      def create_explicit_issue
        issue = JiraHelpers.issue(@parent_issue)
        create_issues(@parent_issue, [issue])
        return "1 issue"
      end
       
      def create_bis_issues
        sp_query = "project=SP AND fixVersion = #{@sprintNumber} ORDER BY \"Global Rank\" ASC" 
        sp_issues = JiraHelpers.search_full(sp_query, @silent)
        init_sp_issue_dict(sp_issues)
    
        bis_query= "project = BIS AND status not in (Resolved, Closed) AND \"Next Sprint\" = YES ORDER BY \"Global Rank\" ASC"
        bis_issues = JiraHelpers.search_full(bis_query, @silent)
        enrich_sp_issue_dict(JiraHelpers.retrieve_implementors(bis_issues, @silent))
    
        ccs_query= "project = CCS AND status not in (Resolved, Closed) AND \"Next Sprint\" = YES ORDER BY \"Global Rank\" ASC"
        ccs_issues = JiraHelpers.search_full(ccs_query, @silent)
        enrich_sp_issue_dict(JiraHelpers.retrieve_implementors(ccs_issues, @silent))    
    
        swe_query= "project = SWE AND status not in (Resolved, Closed) AND \"Next Sprint\" = YES ORDER BY \"Global Rank\" ASC"
        swe_issues = JiraHelpers.search_full(swe_query, @silent)
        enrich_sp_issue_dict(JiraHelpers.retrieve_implementors(swe_issues, @silent))
    
        create_necessary_issues("BIS", bis_issues)
        #     print_issues_table("CCS", ccs_issues)
        #     print_issues_table("SWE", swe_issues)
    
        # Nothing to show
        issue_count = bis_issues.length + ccs_issues.length + swe_issues.length
        return "#{issue_count} issues"
      end
    
      def init_sp_issue_dict(sp_issues)
         @sp_issue_dict = {}
         sp_issues.each do | issue |
           key = issue.key
           @sp_issue_dict[key] = issue
         end
         @seen_sp_issues = [].to_set
      end
    
      #
      # Take those issues that are not yet resolved / closed and add them to the sp issues dict
      #
      def enrich_sp_issue_dict(implementors)
         implementors.each do | issue |
           sp = issue.key
           next if @sp_issue_dict[sp]
    
           status = issue.status
           @sp_issue_dict[sp] = issue unless (status == "Resolved" || status == "Closed")
         end 
      end  
    
      def create_necessary_issues(title, full_issues)
        issues_to_create = []
        full_issues.each do | issue |
          is_umbrella = !issue.fields["sub-tasks"].nil?
          next if is_umbrella
          implementedby = []
          issue.implemented_by.each do | sp_issue |
            sp = sp_issue.key
            # We are only interested in links to issues in the specified sprint
            implementedby << sp if @sp_issue_dict[sp]
          end
    
          issues_to_create << issue if implementedby.length < 1
        end
        create_issues(title, issues_to_create)    
      end
      
      def create_issues(title, issues_to_create)
        puts ("=" * 12)
        puts title
        puts ("-" * 12)
        issues_to_create.each do  | issue |
          summary = "#{issue.key} : #{issue.summary}"
          desc = "See #{issue.key}."
          sp = JiraHelpers.create_sp_issue(summary, desc, @sprintNumber)
          sp_key = sp["key"]
          JiraHelpers.link_sp_to_bis(sp_key, issue.key)
          row = "%12s\t%12s\t%s" % [issue.key, sp_key, summary]
          puts row
        end
      end
    end
    
    
    #
    # List the issues slated from customer projects
    #
    class CustIssues < LoggedInCommand
      def initialize
        super
      end
      
      def description
        return "cust"
      end
      
      
      def run_logged_in
        obp_query = "project=OBP AND status not in (Resolved, Closed) ORDER BY \"Global Rank\" ASC"
        obp_issues = JiraHelpers.search_full(obp_query, @silent)
        
        sob_query = "project=SOB AND status not in (Resolved, Closed) ORDER BY \"Global Rank\" ASC"
        sob_issues = JiraHelpers.search_full(sob_query, @silent)
        
        all_issues = [obp_issues, sob_issues].flatten
        implementors = JiraHelpers.retrieve_implementors(all_issues, @silent)
        init_implementors_dict(implementors)
        
        print_issues_table("OBP", obp_issues)
        print_issues_table("SOB", sob_issues)
        
        # Nothing to show
        return "#{all_issues.length} issues"
      end
      
      def init_implementors_dict(implementors)
        @implementors_dict = {}
        implementors.each do | issue |
    
          key = issue.key
    
          @implementors_dict[key] = issue
        end
      end
      
      def print_issues_table(title, full_issues)
        puts ("=" * 12)
        puts title
        puts ("-" * 12)
        header = "%12s\t%12s\t%s" % ["Key", "BIS", "Summary"]
        puts header
        full_issues.each do | issue |