#!/usr/bin/env ruby
# IMPORTANT:
#   This file has been generated by ruby_collate; avoid changing this file;
#   instead, change the source files and use ruby_collate to re-gen this file.


module GitIssue
  class GitError < StandardError; end
  class IssueError < StandardError; end
end
#end of errors.rb

require 'fileutils'
require 'ostruct'
module GitIssue
  class Helper
    def self.first_non_blank(ary)
      ary.delete("")
      ary.delete(nil)
      ary.first
    end
    def self.is_full_sha?(str)
      !!(str =~ /^[a-fA-F0-9]{40}$/)
    end
    def self.is_partial_sha?(str)
      !!(str =~ /^[a-fA-F0-9]{1,40}$/)
    end
    def self.is_tag_spec(tag_str)
      result = catch :failed do # will be nil if thrown, else true
        throw :failed if tag_str.nil?
        split = tag_str.split(":")
        throw :failed if split.length > 2
        throw :failed if split[0] !~ /^\w+$/
        throw :failed if split[1] and split[1] =~ /\s/

        true # only get here if it's legit.
      end
      !!result
    end

    def self.tags_match(tag_ary, obj_tags)
      tag_ary.all? do |tag_q|
        if tag_q.value then
          regx = "^#{tag_q.value.gsub("?", ".{1}").gsub("*", ".*?")}$"
          obj_tags.any?{|_k, o_t| o_t.key == tag_q.key and
                              o_t.value =~ /#{regx}/}
        else
          obj_tags.any?{|_k, o_t| o_t.key == tag_q.key}
        end
      end
    end

    def self.terminal_size
      rows, cols = [39, 79]
      if RUBY_VERSION =~ /2\.\d.\d/ then
        begin
          window = [0,0,0,0].pack('SSSS')

          fd = IO.sysopen("/dev/tty", "w")
          terminal = IO.new(fd, "w")

          terminal.ioctl(1074295912, window)

          rows, cols, _width, _height = window.unpack('SSSS')
        rescue
        end
      elsif ENV['LINES'] and ENV['COLUMNS']
        rows = ENV['LINES']
        cols = ENV['COLUMNS']
      end
      OpenStruct.new(:rows =>rows, :cols => cols)
    end

    # grabbed from the 1.9.3 stdlb ShellWords module,
    # for compatibility with 1.8.7
    def self.shellescape(str)
      # An empty argument will be skipped, so return empty quotes.
      return "''" if str.empty?

      str = str.dup

      # Treat multibyte characters as is.  It is caller's responsibility
      # to encode the string in the right encoding for the shell
      # environment.
      str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\\\\\1")

      # A LF cannot be escaped with a backslash because a backslash + LF
      # combo is regarded as line continuation and simply ignored.
      str.gsub!(/\n/, "'\n'")

      return str
    end

    def self.suppress_output
      if ENV['GIT_ISSUE_DEBUG'] then
        yield
      else
        begin
          original_stderr = $stderr.clone
          original_stdout = $stdout.clone
          $stderr.reopen(File.new('/dev/null', 'w'))
          $stdout.reopen(File.new('/dev/null', 'w'))
          retval = yield
        rescue Exception => e
          $stdout.reopen(original_stdout)
          $stderr.reopen(original_stderr)
          raise e
        ensure
          $stdout.reopen(original_stdout)
          $stderr.reopen(original_stderr)
        end
        retval
      end
    end
  end
end
#end of helper.rb

module GitIssue
  class GitWorker
    def self.run(command, opts = :no_opts)
      if opts == :no_opts then
        opts = {:raise => true}
      end
      warn "running #{command.inspect}" if ENV['GIT_ISSUE_DEBUG']
      res = `#{command}`.chomp
      warn "got #{$?} : #{res.inspect}" if ENV['GIT_ISSUE_DEBUG']
      raise GitError.new(command) if ($? != 0 and opts[:raise])
      res
    end

    def self.current_sha(branch = "HEAD")
      run("git rev-parse #{branch}")
    end
    def self.current_branch
      res = nil
      begin
        res = run("git symbolic-ref HEAD --short")
      rescue GitError
      end
      res
    end

    def self.stash_all
      run("git stash -u")
    end
    def self.unstash
      run("git stash pop")
    end
    def self.switch_to(branch_name)
      run("git checkout #{branch_name}")
    end
    def self.branch_exists?(branch_name)
      run("git branch --list #{branch_name}") != ""
    end
    def self.remote_branches(branch_name)
      run("git branch -r --list */#{branch_name}").split("\n")
    end
    def self.create_remote_tracking_branch(remote, branch_name)
      run("git branch #{branch_name} #{remote}")
    end
    def self.create_orphan_branch(branch_name)
      orig_dir = Dir.pwd
      orig_branch = current_branch
      Dir.chdir(path_to_top_level)
      begin
        run("git checkout --orphan #{branch_name}")
        run("git rm -r -f --ignore-unmatch .")
        commit("initial commit of #{branch_name}", "--allow-empty")
      ensure
        switch_branch(orig_branch)
        Dir.chdir(orig_dir)
      end
    end
    def self.switch_branch(branch_name)
      run("git checkout #{branch_name}")
    end
    def self.path_to_top_level
      result = run("git rev-parse --show-cdup")
      result = "." if result == ""
      result
    end
    def self.remove_folder(path)
      run("git rm -r #{path}")
    end
    def self.config(key, default = nil)
      res = run("git config --get #{key}", :raise => false)
      (res == "") ? default : res
    end
    def self.list_format
      config("issue.list-format", "withtags")
    end
    def self.issues_branch
      config("issue.branch", "issues")
    end
    def self.issues_folder
      config("issue.folder", ".issues")
    end
    def self.default_tag
      config("issue.default-tag", "kind").to_sym
    end
    def self.user_name
      config("user.name", "anon")
    end
    def self.user_email
      config("user.email", "anon@anon.org")
    end
    def self.editor
      config "core.editor"
    end
    def self.pager
      config "core.pager"
    end
    def self.get_obj(sha)
      run("git cat-file -p #{sha}")
    end
    def self.user_str
      res = config "issue.user"
      if res == nil then
        res = "#{user_email} (#{user_name})"
      end
    end
    def self.hash_str(data)
      run("echo \"#{Helper.shellescape(data)}\" | git hash-object --stdin")
    end
    def self.hash_file(filename)
      run("git hash-object #{filename}")
    end
    def self.pending_changes?
      run("git status -s") != ""
    end
    def self.add(folder)
      run("git add #{folder}")
    end
    def self.commit(commit_message, options = "")
      run("git commit -m #{Helper.shellescape(commit_message)} #{options}")
    end

    def self.list_issues
      if !branch_exists?(issues_branch) then
        work_on_issues_branch do
          #set up the issues branch if this is the first command they run
        end
      end
      run("git ls-tree --full-tree -r #{issues_branch} -- #{issues_folder}").strip.split("\n")
    end

    def self.validate_repo_state
      if current_branch == nil
        raise IssueError.new "You are in detached-HEAD mode.  Please check out a branch and try again."
      end
      if !branch_exists?(current_branch) then
        raise IssueError.new "This repository needs at least an initial commit.  Please commit something and try again."
      end
      merge_issues_with_upstream
    end

    def self.merge_issues_with_upstream
      ib = issues_branch
      rb = config("branch.#{ib}.remote", :stop!)
      if rb != :stop! then

        rb += "/#{ib}"
        if current_sha(ib) != current_sha(rb) then
          if run("git rev-list --count #{ib}..#{rb}").to_i > 0 then
            work_on_issues_branch! do
              begin
                run "git merge --no-edit #{rb}"
                puts "updated issues branch"
              rescue GitError
                run "git merge --abort", :raise => false
                raise IssueError.new "\
  Attempted to merge issues with upstream (#{rb}), and failed.  Please manually
  merge #{ib} and #{rb}. Try:

    \"git checkout #{ib}\"
    \"git merge #{rb}\"

  "
              end
            end #work_on_issues_branch
          else #we are at least caught up w/ origin
            commits_ahead = run("git rev-list --count #{ib}..#{rb}").to_i
            if commits_ahead > 0 then
              puts "your git issues branch is #{commits_ahead} ahead of #{rb}"
            end
          end
        end
      end

    end
    def self.work_on_branch(target_branch, options = {}, &block)
      orig_dir = Dir.pwd
      repo_path = File.expand_path(path_to_top_level)

      begin
        if target_branch == current_branch then
          need_to_stash = (pending_changes?)
          stash_all if need_to_stash
          begin
            Dir.chdir(repo_path)
            block.call(repo_path)
          ensure
            unstash if need_to_stash
          end
        else
          create = false
          # first, do we have the branch?
          if not branch_exists?(target_branch) then
            remotes = remote_branches(target_branch)
            if remotes.one? then
              create_remote_tracking_branch(remotes[0], target_branch)
            elsif remotes.none? then
              if options[:create_orphan] then
                create = true
              else
                raise IssueError.new("#{target_branch} does not exist")
              end
            else # more than one
              raise IssueError.new("multiple remotes for #{target_branch}.  Please check one out and try again.")
            end
          end

          Dir.mktmpdir do |base_path|
            u_name = user_name
            u_email = user_email
            Helper.suppress_output do
              clone_dir = "#{base_path}/tmpclone"
              if create then
                run("git clone #{repo_path} #{clone_dir}")
              else
                run("git clone --single-branch -b #{target_branch} #{repo_path} #{clone_dir}")
              end
              Dir.chdir(clone_dir)
              run("git config user.name #{u_name}")
              run("git config user.email #{u_email}")
              if create then
                create_orphan_branch(target_branch)
                switch_branch(target_branch)
              end
              block.call(clone_dir)
              run("git push origin #{target_branch}")
            end
          end
        end
      ensure
        Dir.chdir(orig_dir)
      end
    end


    def self.work_on_branch!(target_branch, options = {}, &block)
      orig_branch = current_branch
      orig_dir = Dir.pwd
      Dir.chdir(path_to_top_level)
      begin
        need_to_stash = (pending_changes?)
        stash_all if need_to_stash
        if target_branch == orig_branch then
          block.call(Dir.pwd)
        else
          # first, get to the right branch
          begin
            switch_branch(target_branch)
          rescue GitError
            if options[:create_orphan] then
              create_orphan_branch(target_branch)
            else
              raise IssueError.new("#{target_branch} does not exist")
            end
          end
          # now, do the work and get back to the original branch
          begin
            block.call(Dir.pwd)
          ensure
            switch_branch(orig_branch)
          end
        end
      ensure
        unstash if need_to_stash
        Dir.chdir(orig_dir)
      end
    end


    def self.work_on_issues_branch &block
      work_on_branch(issues_branch, {:create_orphan => true}) do |base_path|
        issues_folder_path = "#{base_path}/#{issues_folder}"
        FileUtils.mkdir_p(issues_folder_path)
        block.call(issues_folder_path)
      end
    end

    def self.work_on_issues_branch! &block
      work_on_branch!(issues_branch, {:create_orphan => true}) do |base_path|
        issues_folder_path = "#{base_path}/#{issues_folder}"
        FileUtils.mkdir_p(issues_folder_path)
        block.call(issues_folder_path)
      end
    end
  end
end
#end of git_worker.rb

require 'tempfile'
require 'ostruct'
require 'optparse'

require 'yaml'
module GitIssue
  class Tag
    attr_reader :key, :value
    def initialize(hash)
      @key = atomize(hash[:key])
      @value = hash[:value]
    end

    def atomize(sym_or_str)
      result = sym_or_str
      if sym_or_str.is_a? String then
        result = sym_or_str.to_sym
      end
      result
    end
    def detail_string
      "#{key}:#{value}"
    end

    def yaml
      {:key => key,
       :value => value}.to_yaml
    end

    def write(folder)
      tags_folder = "#{folder}/tags"
      FileUtils.mkdir_p(tags_folder)
      File.open("#{tags_folder}/#{key}.tag", 'w') do |f|
        f.write(self.yaml)
      end
    end

    def self.create(issue, tags, opts = {:events => true})
      res = []
      GitWorker.work_on_issues_branch do |issues_folder|
        issue_folder = "#{issues_folder}/#{issue.issue_id}"
        FileUtils.mkdir_p(issue_folder)
        tags.each do |k, v|
          tag_obj = self.new({:key => k, :value => v})
          if v.class == self then
            tag_obj = v
          end
          tag_obj.write(issue_folder)
          res << tag_obj
          if opts[:events] then
            event_obj = Event.new({:event_type =>:set_tag,
                                   :title => k.to_s,
                                   :text => "#{k.inspect} => #{tag_obj.value.inspect}"})
            event_obj.write(issue_folder)
          end
        end
        GitWorker.add(issue_folder)
        GitWorker.commit("add tags #{tags.keys.inspect}")
      end
      res
    end

    def self.delete(issue, tags)
      keys = tags.keys
      GitWorker.work_on_issues_branch do |issues_folder|
        tags_folder = "#{issues_folder}/#{issue.issue_id}/tags"
        tags.each do |k, t|
          filename = "#{tags_folder}/#{k}.tag"
          if File.exists?(filename) then
            File.delete(filename)
          else
            raise IssueError.new "issue #{issue.short_id} does not have tag #{k}"
          end
        end
        GitWorker.add(issues_folder)
        GitWorker.commit("#{issue.short_id}: remove tags: #{keys}")
      end
    end

    def self.tag_string(tags)
      tags.map{|t| "#{t.key}:#{t.value}"}.join("  ")
    end

    def self.all
      result = []
      all = GitWorker.list_issues
      all.each do |i|
        m = i.match(/^100644\s+blob\s+([a-f0-9]{40})\s+.*?\/([a-f0-9]{40})\/tags\/.*?\.tag$/)
        if m then
          text = GitWorker.get_obj(m[1  ])
          obj = YAML::load(text)
          result << self.new(obj)
        end
      end
      result
    end

    def self.parse(tag_str)
      result = {}
      raw_tags = "#{tag_str}".split(",")
      raw_tags.each do |raw|
        if Helper.is_tag_spec(raw) then
          split = raw.split(":")
          result[split[0]] = self.new({:key =>split[0],:value => split[1]})
        else
          raise IssueError "#{raw} does not appear to be a valid tag. Try '<tagname>:<value>' or just '<tagname>'"
        end
      end
      result
    end
  end
end
#end of tag.rb

require 'yaml'
module GitIssue
  class Event
    attr_reader :creator,
                :title,
                :text,
                :created_at,
                :event_type,
                :event_id

    def initialize(hash)
      @creator = (hash[:creator] or GitWorker.user_str)
      @title = (hash[:title])
      @text = (hash[:text])
      @created_at = (hash[:created_at] or Time.now.to_f)
      @event_type = (hash[:event_type])
      @event_id = (hash[:event_id] or GitWorker.hash_str("#{@created_at} #{@creator} #{@event_type} #{@title} #{@text}"))
    end

    def short_id
      event_id[0..7]
    end

    def yaml
      {
        :creator => creator,
        :title => title,
        :text => text,
        :created_at => created_at,
        :event_type => event_type,
        :event_id => event_id
      }.to_yaml
    end

    def write(folder)
      events_folder = "#{folder}/events"
      FileUtils.mkdir_p(events_folder)
      File.open("#{events_folder}/#{event_id}.event", 'w') do |f|
        f.write(self.yaml)
      end
    end

    def detail_string
      create_time = Time.at(self.created_at)
      summary_str = "#{creator} #{type_string}"
      desc = ("#{self.text}" == "") ? "" : "\n#{self.text}"
      "#{create_time}: #{summary_str} - #{self.title}#{desc}"
    end

    def markdown_string

      case event_type
      when :create
        ""
      when :set_tag
        if Issue.standard_tags.any?{|t| t.to_s == title} then
          ""
        else
          "\
#{"set #{text}  "}
_#{creator} at #{Time.at(created_at)}_"
        end
      when :comment
"\
new comment:

> \#\#\#\# #{title}
> #{text}

_#{creator} at #{Time.at(created_at)}_
"     else
        ""
      end
    end

    def self.create(issue, new_event_args)
      event_obj = nil
      GitWorker.work_on_issues_branch do |issues_folder|
        issue_folder = "#{issues_folder}/#{issue.issue_id}"
        FileUtils.mkdir_p(issue_folder)
        event_obj = self.send(:new, new_event_args)
        event_obj.write(issue_folder)
        GitWorker.add(issue_folder)
        GitWorker.commit("#{issue.short_id} add event: #{event_obj.title}")
      end
      event_obj
    end

    def self.delete(event_id)
      files = GitWorker.list_issues
      files.delete_if{|f| !f.include?(event_id)}
      case files.length
      when 0
        raise IssueError.new "no matching id for #{event_id}"
      when 1
        m = files[0].match(/^100644\s+blob\s+([a-f0-9]{40})\s+(.*?\/#{id}\.event)$/)
        # remove the base issue folder from the path
        path = m[1].split("/")[1..-1].join("/")
        GitWorker.work_on_issues_branch do |issues_folder|
          File.delete("#{issues_folder}/#{path}")
          GitWorker.add(issues_folder)
          GitWorker.commit("deleted comment #{event_id[0..7]}")
        end
      else
        raise IssueError.new "non-distinct id. Please specify more characters."
      end
    end

    def type_string
      case self.event_type
      when nil
        ""
      when :comment
        "commented"
      when :set_tag
        "set tag"
      when :delete_tag
        "removed tag"
      when :close
        "closed"
      when :create
        "created"
      end
    end


  end
end
#end of event.rb

require 'yaml'
module GitIssue
  class Issue
    attr_writer :tags, :events
    attr_reader :issue_id
    def initialize(issue_id)
      @issue_id = issue_id
    end

    def tags; @tags ||= {}; end
    def events; @events ||= [] end

    def title; tags[:title].value or ""; end
    def status; tags[:status].value or ""; end
    def creator; tags[:creator].value; end
    def created_at; tags[:created_at].value end
    def description; tags[:description].value or "" end


    def short_id; issue_id[0..7]; end
    def custom_tags
      res = tags.dup
      Issue.standard_tags.each do |f|
        res.delete(f)
      end
      res
    end

    def update(hash)
      GitWorker.work_on_issues_branch do
        hash.each do |k,v|
          if (self.tags[k] and self.tags[k].value == v) then
            hash.delete(k)
          end
        end
        Tag.create(self, hash)
      end
    end

    def delete
      GitWorker.work_on_issues_branch do |issues_folder|
        issue_folder = "#{issues_folder}/#{self.issue_id}"
        GitWorker.remove_folder(issue_folder)
        GitWorker.commit("deleted #{short_id}: #{title}")
      end
    end

    def as_table_row(formatter)
      case formatter
      when "withtags"
        ctags = custom_tags
        pad_status = status.ljust(6).capitalize
        tag_str = ctags.none? ? "" : "\n  Tags: #{ctags.map{|k,v| v.detail_string}.join("  ")}"
        "#{pad_status}  #{short_id}  #{title}#{tag_str}"
      when "oneline"
        short_status = "#{status[0..0]}".capitalize
        def_tag = tags[GitWorker.default_tag]
        kind = (def_tag.nil? ? "  " : def_tag.value)
        "#{short_status} #{short_id} #{kind}: #{title}"
      when "detail"
        detail_string
      end
    end


    def self.table_header(formatter)
      case formatter
      when "withtags"
"Status  ID        Title
-------------------------------------------------------------------------------"
#Open    deadbeef  The real problem with this app is that the cows are alive.
#    Tags: kind:bug  assigned_to:chris
#Closed  cafefeed  We need a way to get the devs to eat non-pizza food.
#    Tags: kind:feature  priority:low
#
      when "oneline"
"S ID       Title
-------------------------------------------------------------------------------"
      end
    end

    def self.all
      raw_issues = GitWorker.list_issues
      ids_from_list(raw_issues).map{|i| create_from_list(i, raw_issues)}
    end

    def self.find(partial_id)
      raw_issues = GitWorker.list_issues
      ids = ids_from_list(raw_issues)
      ids.delete_if{|i| !i.start_with?(partial_id)}
      ids.map{|i| create_from_list(i, raw_issues)}
    end

    def self.ids_from_list(list)
      list.map{|i| i.match(/\/([a-f0-9]{40})\//)[1]}.uniq
    end

    def self.create_new(title, description, tags)
      time = Time.now.to_f
      creator = GitWorker.user_str
      issue_id = GitWorker.hash_str("#{time} #{creator} #{title} #{description}")
      issue = self.new(issue_id)
      tag_hash = {}
      tags.each do |k,v|
        tag_hash[k] = v
      end
      GitWorker.work_on_issues_branch do
        Tag.create(issue, {:created_at=> time,
                           :creator => creator,
                           :assigned_to => creator,
                           :title => title,
                           :status => "open",
                           :description => description}, :events => false)
        if tag_hash.any? then
          Tag.create(issue, tag_hash)
        end
        Event.create(issue, {:event_type => :create})
      end
      issue
    end

    def self.create_from_list(issue_id, list)
      list = list.dup
      new_obj = self.new(issue_id)
      list.delete_if{|i| !i.include?(issue_id)}

      parts = list.map do |i|
        m = i.match(/^100644\s+blob\s+([a-f0-9]{40})\s+.*?\/#{issue_id}\/(.*)$/)
        {:sha => m[1], :path => m[2]}
      end
      parts.each do |i|
        piece = YAML::load(GitWorker.get_obj(i[:sha]))

        split = i[:path].split("/")
        if split[0] == "events" then
          new_obj.events << Event.new(piece)
        elsif split[0] == "tags" then
          piece[:key] = piece[:key].to_sym
          new_obj.tags[piece[:key]] = Tag.new(piece)
        end
      end
      new_obj
    end

    def detail_string
      history = events.sort_by{|e| e.created_at}
      tag_string = custom_tags.map{|k,t| t.detail_string}.join("  ")
"\
#{issue_id}
Title:      #{tags[:title].value}
Created by: #{tags[:creator].value} at #{Time.at(tags[:created_at].value)}
Tags:       #{tag_string}

#{tags[:description].value}

History:
#{history.map{|h| h.detail_string}.join("\n\n")}
"
    end

    def self.standard_tags
      [:title, :description, :status, :creator, :created_at]
    end
  end
end
#end of issue.rb


module GitIssue
  class BaseCommand
    def self.help_message
      class_variable_get(:@@help_message)
    end

    def get_message_from_editor(initial_editor_text, clean=true)
      result = ""
      editor_status = :not_execute
      Dir.mktmpdir do |d|
        filename = "#{d}/new_issue.txt"
        File.open(filename, 'w') do |f|
          f.write(initial_editor_text)
        end
        system "#{get_editor} #{filename}"  #use system rather than backticks
        editor_status = $?
        result = File.read(filename)
      end
      if editor_status != 0 then
        raise IssueError.new "Editor error, exit code #{input.editor_status.inspect}"
      end
      if clean then
        result = clean_message!(result)
      end
      result
    end

    def clean_message!(message, options = {:raise => true})
      title = clean_message = ""
      if message.is_a? String then
        split_message = message.split("\n")
        split_message.delete_if {|i| i =~ /^\s*\#\$\#/}
        if split_message.join("").strip == "" then
          if options[:raise] then
            raise IssueError.new "Empty issue message, Aborting"
          end
        else
          title = split_message[0].strip
          clean_message = split_message[1..-1].map{|l| l.rstrip}.join("\n").strip
        end
      end
      [title, clean_message]
    end

    def validate_sha(sha)
      if !Helper.is_partial_sha?(sha) then
        raise IssueError.new "#{sha} does not appear to be an issue-id"
      end
      sha
    end

    def get_issue(sha)
      if sha == nil
        nil
      else
        issues = Issue.find(sha)
        if issues.length == 0 then
          raise IssueError.new "could not find an issue matching issue-id #{sha}"
        elsif issues.length > 1 then
          raise IssueError.new "multiple issues match #{sha}: #{issues.map{|i| i.issue_id}.inspect}"
        end
        issues.shift
      end
    end

    def get_editor
      visual= ENV['VISUAL']
      editor = ENV['EDITOR']
      git = GitWorker.editor
      fallback = 'vim'
      Helper.first_non_blank [visual, editor, git, fallback]
    end
    def get_pager
      git = GitWorker.pager
      fallback = 'less'
      Helper.first_non_blank [git, fallback]
    end

    def write_output(str)
      lines = str.split("\n").length

      if lines > Helper.terminal_size.rows then
        Dir.mktmpdir do |d|
          filename = "#{d}/tmp.txt"
          File.open(filename, 'w') do |f|
            f.write(str)
          end
          system "#{get_pager} #{filename}"
        end
      else
        puts str
      end
    end
  end
end
#end of base_command.rb


module GitIssue
  class CloseCommand < BaseCommand
    def parse(args)
      options = OpenStruct.new()
      options.sha = validate_sha(args.shift)
      options.message = ""
      options.tags = {}
      parser = OptionParser.new do |opts|
        opts.on("-m", "--message MESSAGE", "notes on closing this issue") do |message|
          options.message = message
        end
        opts.on("-t", "--tag TAGS", "tags to place on this Issue") do |tags|
          options.tags = Tag.parse(tags)
        end
        opts.on("-e", "--editor", "opens an editor for a closing comment") do
          options.editor = true
        end
      end
      parser.parse!(args)

      options
    end

    def run(options)
      issue = get_issue(options.sha)
      title = ""
      message = options.message or ""
      message += @@default_message
      if options.editor then
        title, message = get_message_from_editor(message)
      end
      GitWorker.work_on_issues_branch do
        Tag.create(issue, {:status => "closed"}.merge(options.tags))
        if message != ""
          Event.create(issue, {:event_type => :close,
                               :title => title,
                               :description => message})
        end
      end
      write_output "#{issue.short_id} successfully closed"

    end
@@default_message = '

#$# Add a reason for closing this issue.
#$# Lines beginning with a "#$#"" are ignored, and an empty message aborts the
#$# operation.
'
@@help_message ='Closes an issue.  Closed issues have a status of \"closed\", and do not appear
in the query results of `git issue list`, unless `--all` is passed to that
command.  Currently, the only way to re-open an issue is with the `tag`
command, e.g. `git issue tag <issue_id> status:open`

Usage:   git issue close <issue_id> [options]

Options:
  -t, --tag TAGSTR
    applies the tags given in TAGSTR to the issue.  This may be useful, for
    example, to tag an issue with a common reason for closure, like deferral.
  -m, --message, MESSAGE
    specifies a message for a closing comment.
  -e, --editor
    opens an editor to accept a closing comment.

Examples:
  git issue close cc4
  git issue close 3cf91 -t deferred:true
  git issue close a3e --editor
'
  end
end
#end of close_command.rb


module GitIssue
  class CommentCommand < BaseCommand
    def parse(args)
      options = OpenStruct.new()
      options.message = ""
      options.tags = {}
      parser = OptionParser.new do |opts|
        opts.on("-m", "--message MESSAGE", "notes on closing this issue") do |message|
          options.message = message
        end
        opts.on("-d", "--delete COMMENT_ID", "delete comment") do |comment_id|
          options.delete = comment_id
        end
      end
      parser.parse!(args)
      unless options.delete then
        options.sha = validate_sha(args.shift)
      end
      options
    end

    def run(options)
      if options.delete then
        Event.delete(options.delete)
      else
        issue = get_issue(options.sha)
        title = options.message
        if title == "" then
          title, description = get_message_from_editor(@@default_message)
        end
        event = Event.create(issue, {:event_type => :comment,
                                     :title => title,
                                     :text => description})
        write_output "comment #{event.short_id} successfully created"
      end

    end
@@default_message = '

#$# Add a comment above.  The first line will be treated as the title of the
#$# comment, remaining lines as the body.
#$# Lines beginning with a "#$#" are ignored, and an empty message aborts the
#$# operation.
'
@@help_message ='Adds a comment to an issue.  Comments can be viewed using the `git issue show`
command.

Usage:   git issue comment <issue_id> [options]
         git issue command --delete <comment_id>

Options:
  -m, --message, MESSAGE
    specifies a message for the comment.
  -d, --delete, COMMENT_ID
    deletes the specified comment.

Examples:
  git issue comment cc4
  git issue comment 3cf91 -m "I love git-issue"
  git issue comment -d ae
'
  end
end
#end of comment_command.rb


module GitIssue
  class DeleteCommand < BaseCommand
    def parse(args)
      options = OpenStruct.new()
      options.sha = validate_sha(args.shift)
      parser = OptionParser.new do |opts|
        #no switches defined
      end
      parser.parse!(args)

      options
    end

    def run(options)
      issue = get_issue(options.sha)
      issue.delete
    end

    @@help_message ="\
Deletes an issue.  While deleted issues will no longer appear in
`git issue list` results, they will still exist in the history of the git
repository.

Usage:   git issue delete <issue_id>

Examples:
  git issue delete cc4
"
  end
end
#end of delete_command.rb


module GitIssue
  class EditCommand < BaseCommand
    def parse(args)
      options = OpenStruct.new()
      options.sha = validate_sha(args.shift)
      parser = OptionParser.new do |opts|

      end
      parser.parse!(args)

      options
    end

    def run(options)
      issue = get_issue(options.sha)

      initial_text = "\
#{issue.title}

#{issue.description}

#{@@default_message}
"
      title, description = get_message_from_editor(initial_text)
      if issue.title != title or issue.description!= description then
        issue.update({:title => title, :description => description})
      end
      write_output "#{issue.short_id} successfully updated"
    end

@@default_message ='
#$# Edit the title and description for this Issue.  The title should occupy the
#$# first line of the file, and be followed by a blank line.  On the lines
#$# following that blank line, feel free to add a more detailed description.
#$# Lines beginning with a "#$#" are ignored, and an empty message aborts the
#$# operation.
'
@@help_message ='Opens an editor to edits the title and description of an issue. The first line
of the editor result will be interpreted as the new title, and the remaining
lines as the description of the issue.

Usage:   git issue edit <issue_id>

Examples:
  git issue edit cc4
'
  end
end
#end of edit_command.rb


module GitIssue
  class ListCommand < BaseCommand
    def parse(args)
      options = OpenStruct.new()
      options.tags = []
      options.not_tags = []
      options.formatter = GitWorker.list_format
      parser = OptionParser.new do |opts|
        opts.on("-a", "--all", "also show closed issues") do |message|
          options.all = true
        end
        opts.on("-t", "--tag TAGS", "filter to issues with tag") do |tags|
          options.tags = Tag.parse(tags)
        end
        opts.on("-f", "--find SEARCH_STRING", "filter to issues with text") do |search_string|
          options.find = search_string
        end
        opts.on("-n", "--not-tag TAGS", "filter to issues without tag") do |not_tags|
          options.not_tags = Tag.parse(not_tags)
        end
        opts.on("-p", "--pretty FORMATTER", "specify output format") do |formatter|
          options.formatter = formatter
        end
      end
      parser.parse!(args)

      cl_tag = args.shift
      if cl_tag then
        options.tags << Tag.new({:key => GitWorker.default_tag, :value => cl_tag})
      end

      options
    end

    def run(options)
      issues = Issue.all

      if !options.all then
        issues.delete_if{|i| i.status == "closed"}
      end
      if options.tags.any? then
        issues.delete_if{|i| !Helper.tags_match(options.tags, i.tags)}
      end
      if options.not_tags.any? then
        issues.delete_if{|i| Helper.tags_match(options.not_tags, i.tags)}
      end
      if options.find then
        issues.delete_if{|i| !(i.title.include?(options.find) or
                               i.description.include?(options.find))}
      end

      issues = issues.sort_by{|i| i.created_at}
      write_output [Issue.table_header(options.formatter),
                    issues.map{|i| i.as_table_row(options.formatter) }.join("\n")].join("\n")
    end
    @@help_message ="\
Queries the issue datastore.

Usage:   git issue list [default_tag_specifier] [options]

Default tag specifier:
  When present, the default tag specifier restricts results to issues with the
  default tag set to the queried value. For example, if the default tag is
  'kind' (which is the default default tag), then `git issue list feature` will
  present all open issues with the tag kind:feature.

Options:
  -a, --all
    includes all issues in the resultset, including closed issues.
  -t, --tag TAGS
    limits results to issues with tags matching TAGS
  -n, --not-tag TAGS
    limits results to issues without tags matching TAGS
  -f, --find SEARCH_STRING
    limits results to issues containing SEARCH_STRING in the title or
    description.
  -p, --pretty FORMAT=withtags
    specifies the output format for the matching issues.  Currently available
    options are 'oneline', 'withtags', and 'detail'

Examples:
  git issue list                       # all open issues
  git issue list bug -t priority:high  # all open bugs with priority=high
  git issue list feature -n milestone  # open features without a milestone
  git issue list feature -t milestone:v2.1.?  # features for version 2.1.x
  git issue list bug -t assigned_to:c* # bugs assigned to chris, cathy, etc.
"
  end
end
#end of list_command.rb


module GitIssue
  class NewCommand < BaseCommand
    def parse(args)
      options = OpenStruct.new()
      options.message = ""
      options.tags = []
      allrawtags = []
      parser = OptionParser.new do |opts|
        opts.on("-m", "--message MESSAGE", "Title for this Issue") do |message|
          options.message = message
        end
        opts.on("-t", "--tag TAGS", "tags to place on this Issue") do |tags|
          allrawtags << tags
        end
      end
      parser.parse!(args)

      cl_tag = args.shift
      if cl_tag then
        allrawtags << "#{GitWorker.default_tag}:#{cl_tag}"
      end
      options.tags = Tag.parse(allrawtags.join(","))
      options
    end

    def run(options)
      title, description = clean_message!(options.message, :raise => false)
      if title == "" then
        title, description = get_message_from_editor(@@default_message)
      end

      issue = Issue.create_new(title, description, options.tags)
      write_output "#{issue.short_id} successfully created"
    end

@@default_message = '

#$# Add a title and description for this Issue.  The title should occupy the
#$# first line of the file, and be followed by a blank line.  On the lines
#$# following that blank line, feel free to add a more detailed description.
#$# Lines beginning with a "#$#" are ignored, and an empty message aborts the
#$# operation.
'
@@help_message ='
Creates a new issue.

Usage:   git issue new [default_tag_specifier] [options]

Default tag specifier:
  When present, the default tag will be set on the new issue with the provided
  value.  For instance, `git issue new feature` is a shorthand equivalent to
  `git issue new -t kind:feature`.  The default tag may be configured using git
  config to set the issue.default-tag key to another value.

Options:
  -t, --tag TAGSTR
    applies the tags given in TAGSTR to the issue.
  -m, --message, MESSAGE
    specifies a message. If a message is not specified, an editor will be
    opened to solicit a title and description for the issue.  The first line of
    the result will be interpreted as the title, and the remaining lines as the
    description.

Examples:
  git issue new
  git issue new feature -t milestone:2.0.5,priority:high
  git issue new bug -m "login screen broken"
'
  end
end
#end of new_command.rb


module GitIssue
  class PublishCommand < BaseCommand
    def parse(args)
      options = OpenStruct.new()
      options.branch = "gh-pages"
      options.folder = "issues"
      parser = OptionParser.new do |opts|
        opts.on("-b", "--branch BRANCH_NAME", "branch to publish to") do |branch|
          options.branch = branch

        end
        opts.on("-f", "--branch FOLDER_NAME", "folder to publish to") do |folder|
          options.folder = folder

        end
      end
      parser.parse!(args)

      options
    end

    def run(options)
      issues = Issue.all.sort_by{|i| i.created_at}.reverse
      GitWorker.work_on_branch(options.branch) do |base_folder|
        working_folder = "#{base_folder}/#{options.folder}"
        FileUtils.mkdir_p(working_folder) #this way rm_rf can't complain
        FileUtils.rm_rf(working_folder)
        FileUtils.mkdir_p(working_folder)
        create_page(working_folder, issues)
        GitWorker.add(working_folder)
        GitWorker.commit("git-issue publish issues")
      end
    end

    def create_page(working_folder, issues)
      write_index("#{working_folder}/index.html", issues)
      issues.each do |i|
        write_issue(working_folder, i)
      end
    end
    def write_index(filename, all_issues)
      closed, others = all_issues.partition{|i| i.status == "closed"}
      text = "\
---
layout: default
title: Issues
---
<div>
  <p>Open issues: (#{others.count})</p>
  <ul class=\"issue_list\">
    #{(others.map{|i| one_issue_for_index(i)}).join("\n")}
  </ul>
  <p>Closed issues: (#{closed.count})</p>
  <ul class=\"issue_list\">
    #{(closed.map{|i| one_issue_for_index(i)}).join("\n")}
  </ul>
  <p class=\"generation_time\">Generated at #{Time.now}</p>
</div>
"
      File.open(filename, 'w') do |f|
        f.write(text)
      end
    end

    def one_issue_for_index(i)
      "\
    <li>
      <div class=\"issue_list_entry\">
        <a href=\"#{i.issue_id}\">#{i.title}</a>
        <div class=\"issue_list_meta\">
          <span>
            \##{i.short_id} created on #{Time.at(i.created_at)}
          </span>
          #{(i.custom_tags.map do |_k, t|
"\
<div class=\"tag_string\">#{t.detail_string}</div>
"
             end).join("\n")}
        </div>
      </div>
    </li>"
    end

    def write_issue(parent_folder, issue)
      issue_folder = "#{parent_folder}/#{issue.issue_id}"
      FileUtils.mkdir_p(issue_folder)
      text = "\
---
layout: default
---
[back to issues](..)

\#\# \\\##{issue.short_id}: #{issue.title}

\#\#\#Status: **#{issue.status.capitalize}**
_created by #{issue.creator} at #{Time.at(issue.created_at)}_

#{issue.description}

#{issue.events.sort_by{|e| e.created_at}.map{|e| e.markdown_string}.delete_if{|r| r == ""}.join("\n\n---\n")}
"

      File.open("#{issue_folder}/index.md", "w") do |f|
        f.write(text)
      end
    end
    @@help_message ="\
Publishes all issues to the specified branch for use with Jekyll.

Usage:   git issue publish [options]

Options:
  -f, --folder FOLDER_NAME
    Specifies the folder to use as the root.  Defaults to \"issues\".

  -b, --branch BRANCH_NAME
    Specifies which branch to publish to.  Defaults to \"gh-pages\".
Examples:
  git issue publish
"
  end
end
#end of publish_command.rb


module GitIssue
  class ShowCommand < BaseCommand
    def parse(args)
      options = OpenStruct.new()
      parser = OptionParser.new do |opts|
        opts.on("-c", "--config", "show git config keys") do
          options.config = true

        end
      end
      parser.parse!(args)
      options.sha = validate_sha(args.shift) unless options.config

      options
    end

    def run(options)
      if options.config then
        write_output config_string
      else
        issue = get_issue(options.sha)
        write_output issue.detail_string
      end
    end

    def config_string
      output = ["`git config` key       Value", ("-" * 79)] +
      {"issue.user" =>
         "Custom user identifier. Defaults to \"`user.name` (`user.email`)\"",
       "issue.branch" =>
         "Branch to store issue information. Defaults to \"issue\"",
       "issue.folder" =>
         "Folder to store issue information. Defaults to \".issue\"",
       "issue.default-tag" =>
         "Default tag for convenience syntax. Defaults to \"kind\"",
       "issue.list-format" =>
         "Default format to use with --pretty switch. Defaults to \"withtags\"",
      }.map do |k, desc|
        value = GitWorker.config(k)
        if value.nil? then
          value = "<default>"
        end
        "#{k.ljust(17)}  =>  #{value}\n      #{desc}"
      end
      output.join("\n")
    end
    @@help_message ="\
Shows the full detail of an issue, including all tags, comments and events.
Can also be used to show the current git-issue configuration.

Usage:   git issue show <issue_id>
         git issue show --config

Options:
  -c, --config
    Lists the currently configuration for git-issue.

Examples:
  git issue show cc4
"
  end
end
#end of show_command.rb

module GitIssue
  class TagCommand < BaseCommand
    def parse(args)
      options = OpenStruct.new()
      parser = OptionParser.new do |opts|
        opts.on("-d", "--delete TAGSTR", "delete a tag from an issue") do |tagstr|
          options.delete = Tag.parse(tagstr)
        end
        opts.on("-l", "--list", "list all tags") do
          options.list = true
        end

      end
      parser.parse!(args)
      if options.list then
        # skip sha and tag validation
      else
        options.sha = validate_sha(args.shift)
        options.tags = Tag.parse(args.shift)
      end
      options
    end

    def run(options)
      issue = get_issue(options.sha)

      if options.delete then
        Tag.delete(issue, options.delete)
        write_output "successfully deleted tag(s) '#{options.delete.keys.join(", ")}' from #{issue.short_id}"
      elsif options.list then
        result_map = {}
        Tag.all.each do |t|
          result_map[t.key] ||= []
          result_map[t.key] << t.value unless result_map[t.key].include? t.value
        end
        result_map.delete_if{|k, v_ary| Issue.standard_tags.include? k}
        result = result_map.map do |k, v|
          "#{k}: #{v.join(",")}"
        end
        write_output result.join("\n")
      else
        Tag.create(issue, options.tags)
        tag_str = options.tags.map{|k,t| "#{k}:#{t.value}"}
        write_output "successfully added tag(s) '#{tag_str.join(", ")}' to #{issue.short_id}"
      end
    end
    @@help_message ="\
Various tag utilities.

Usage:   git issue tag <issue_id> TAGSTR
         git issue tag <issue_id> --delete TAGSTR
         git issue tag --list

Options:
  TAGSTR
    applies the tags given in TAGSTR to the issue.
  -d, --delete, TAGSTR
    removes the specified tags from the issue.
  -l, --list
    lists all tags and their values.

Examples:
  git issue tag cc4 deliverable
  git issue tag 3cf91 --delete priority
  git issue tag --list
"
  end
end
#end of tag_command.rb


module GitIssue
  class HelpCommand < BaseCommand
    def parse(args)
      options = OpenStruct.new()
      parser = OptionParser.new do |opts|
        opts.on("-s", "--summary", "summarize commands and options") do
          options.summary = true
        end
      end
      parser.parse!(args)
      options.sub_command = args.shift
      options
    end

    def run(options)
      if options.summary then
        write_output @@summary_help
      else
        command = HelpCommand
        begin
          command = GitIssue.parse_command(options.sub_command)
        rescue IssueError
        end
        write_output command.help_message
      end
    end
@@help_message = "\
'git issue' is a series of commands providing lightweight project managment
features, such as feature, bug, and todo list tracking, while storing such
data within the repository.

Available commands:
  git issue help            - help for git-issue and sub commands
  git issue list            - list existing issues
  git issue new             - create a new issue
  git issue edit            - edit an existing issue
  git issue tag             - manipulate issue tags
  git issue comment         - comment on an issue
  git issue close           - close an issue
  git issue delete          - delete an issue

Type 'git issue help <command>' for more information on any of these commands,
or 'git issue help -s' for a short summary of all commands.
"
@@summary_help = "\
git issue new
          new feature
          new -m|--message MESSAGE
          new -t|--tag priority:high
git issue close <id>
          close <id> -m|--message MESSAGE
          close <id> -t|--tag deferred:true
          close <id> -e|--editor
git issue tag <id> kind:feature
          tag <id> kind:bug,priority:high
          tag <id> -d|--delete worthless_tag
          tag -l|--list
git issue delete <id>
git issue comment <id>
          comment <id> -m|--message MESSAGE
          comment -d|--delete <comment_id>
git issue list
          list feature
          list -a|--all
          list -t|--tag priority
          list -t|--tag priority:high
          list -t|--tag milestone:2.*
          list -f|--find <search_string>
          list -n|--not-tag milestone
          list -p|--pretty with-tags|oneline
git issue show <id>
          show -c|--config
git issue edit <id>
git issue help
          help <command>
          help -s|--summary
"
  end
end
#end of help_command.rb


#end of all.rb


module GitIssue
  @@version = "0.0.3"
  def self.parse_and_run(args)
    result = 1 # pessimistic- assume the script will fail somewhere
    begin
      GitWorker.validate_repo_state
      command = parse_command(args.shift)
      runner = command.new
      args = runner.parse(args)
      runner.run(args)
      result = 0
    rescue GitError => ge
      warn "error in git command: #{ge.message}\n#{ge.backtrace}"
    rescue IssueError => ie
      warn ie.message
    end
    result
  end

  def self.parse_command(str)
    classname = "#{str}".capitalize + "Command"
    # dynamically find the class GitIssue::<command>Command, e.g.
    # GitIssue::NewCommand
    if const_defined?(classname) then
      const_get(classname)
    elsif "#{str}" == "" then
      raise IssueError.new("git-issue version #{@@version}.  Try 'git issue help'.")
    else
      raise IssueError.new("unknown command #{str}")
    end
  end
end
#end of git_issue.rb


# note the ENV bailout so we can load the code without running the app, e.g.:
#  `GIT_ISSUE_TEST=true irb`, and then `irb> load 'git-issue'`
GitIssue.parse_and_run(ARGV) unless ENV['GIT_ISSUE_TEST']
#end of entry_script.rb