#!/usr/bin/env ruby
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with this
# work for additional information regarding copyright ownership.  The ASF
# licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
# License for the specific language governing permissions and limitations under
# the License.

require 'optparse'
require 'ostruct'
require 'fileutils'

module GitFlow
  extend self

  attr_accessor :should_run, :trace, :program

  self.program = 'gitflow'
  self.should_run = true # should we run at exit?

  HELP = <<-HELP

GitFlow is a tool to create custom git commands implemented in ruby.
It is generic enougth to be used on any git based project besides Apache Buildr.

OVERVIEW:

gitflow is intended to help developers with their daily git workflow,
performing repetitive git commands for them. It is implemented in
ruby so you can do anything, from invoking rake tasks to telling
people on twitter you are having trouble with their code :P.

To get help for a specific command use:
    gitflow.rb help command
    gitflow.rb command --help

For convenience you can create an alias to be execute using git.
The following example registers buildr-git.rb, which provides apache
svn and git synchronization commands:

    git config alias.apache '!'"ruby $PWD/doc/scripts/buildr-git.rb"

After that you can use
    git apache command --help

EXTENDING YOUR WORKFLOW:

You can create your own gitflow commands, to adapt your development
workflow.

Simply create a ruby script somewhere say ~/.buildr/gitflow.rb
And alias it in your local repo:

    git config alias.flow '!'"ruby ~/.buildr/gitflow.rb"
    git config alias.work '!'"ruby ~/.buildr/gitflow.rb my-flow sub-work"

A sample command would look like this.. (you may want to look at buildr-git.rb)

    #!/usr/bin/env ruby
    require /path/to/gitflow.rb

    class MyCommand < GitFlow/'my-flow'

      @help = "Summary to be displayed when listing commands"
      @documentation = "Very long help that will be paged if necessary. (for --help)"

      # takes an openstruct to place default values and option values.
      # returns an array of arguments given to optparse.on
      def options(opts)
        opts.something = 'default'
        [
         ['--name NAME', lambda { |n| opts.name = n }],
         ['--yes', lambda { |n| opts.yes = true }]
        ]
      end

      # takes the opts openstruct after options have been parsed and
      # an argv array with non-option arguments.
      def execute(opts, argv)
        # you can run another command using
        run('other-command', '--using-this', 'arg')
        some = git('config', '--get', 'some.property').chomp rescue nil
        page { puts "This will be paged on terminal if needed" }
      end

      class SubCommand < MyCommand/'sub-work'
        ... # implement a subcommand
      end

    end

You would then get help for your command with

    git flow my-flow --help
    git work --help

Using gitflow you can customize per-project git interface.

HELP

  # Pager from http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby
  def pager
    return if RUBY_PLATFORM =~ /win32/
    return unless STDOUT.tty?

    read, write = IO.pipe

    unless Kernel.fork # Child process
      STDOUT.reopen(write)
      STDERR.reopen(write) if STDERR.tty?
      read.close
      write.close
      return
    end

    # Parent process, become pager
    STDIN.reopen(read)
    read.close
    write.close

    ENV['LESS'] = 'FSRX' # Don't page if the input is short enough

    Kernel.select [STDIN] # Wait until we have input before we start the pager
    pager = ENV['PAGER'] || 'less'
    exec pager rescue exec '/bin/sh', '-c', pager
  end

  # Return a class to be extended in order to register a GitFlow command
  # if command name is nil, it will be registered as the top level command.
  # Classes implementing commands also provide this method, allowing for
  # sub-command creation.
  def /(command_name)
    command_name = command_name.to_s unless command_name.nil?
    cls = Class.new { include GitFlow::Mixin }
    (class << cls; self; end).module_eval do
      attr_accessor :help, :documentation, :command
      define_method(:/) do |subcommand|
        raise "Subcommand cannot be nil" unless subcommand
        GitFlow/([command_name, subcommand].compact.join(' '))
      end
      define_method(:inherited) do |subclass|
        subclass.command = command_name
        GitFlow.commands[command_name] = subclass
      end
    end
    cls
  end

  def commands
    @commands ||= Hash.new
  end

  def optparse
    optparse = opt = OptionParser.new
    opt.separator ' '
    opt.separator 'OPTIONS'
    opt.separator ' '
    opt.on('-h', '--help', 'Display this help') do
      GitFlow.pager; puts opt; throw :exit
    end
    opt.on('--trace', 'Display traces') { GitFlow.trace = true }
    optparse
  end

  def command(argv)
    cmds = []
    argv.each_with_index do |arg, i|
      arg = argv[0..i].join(' ')
      cmds << commands[arg] if commands.key?(arg)
    end
    cmds.last || commands[nil]
  end

  def run(*argv)
    catch :exit do
      command = self.command(argv).new
      argv = argv[command.class.command.split.length..-1] if command.class.command
      parser = optparse
      parser.banner = "Usage: #{GitFlow.program} #{command.class.command} [options]"
      options = OpenStruct.new
      if command.respond_to?(:options)
        command.options(options).each { |args| parser.on(*args) }
      end
      if command.class.documentation && command.class.documentation != ''
        parser.separator ' '
        parser.separator command.class.documentation.split(/\n/)
      end
      parser.parse!(argv)
      command.execute(options, argv)
    end
  end

  module Mixin
    include FileUtils

    # Override this method in your command class if it
    # needs to parse command line options.
    #
    # This method takes an openstruct object as argument
    # allowing you to store default values on it, and
    # set option values.
    #
    # The return value must be an array of arguments
    # given to optparse.on
    def options(opt)
      []
    end

    # Override this method in your command class to implement
    # the command.
    # First argument is the openstruct object after
    # it has been populated by the option parser.
    # Second argument is the array of non-option arguments.
    def execute(opt, argv)
      fail "#{self.class.command} not implemented"
    end

    # Run the command line given on argv
    def run(*argv, &block)
      GitFlow.run(*argv, &block)
    end

    # Yield paging the blocks output if necessary.
    def page
      GitFlow.pager
      yield
    end

    def trace(*str)
      STDERR.puts(*str) if GitFlow.trace
    end

    def git(*args)
      cmd = 'git ' + args.map { |arg| arg[' '] ? %Q{"#{arg}"} : arg }.join(' ')
      trace cmd
      `#{cmd}`.tap {
        fail "GIT command `#{cmd}` failed with status #{$?.exitstatus}" unless $?.exitstatus == 0
      }
    end

    def sh(*args)
      `#{args.join(' ')}`.tap {
        fail "Shell command `#{args.join(' ')}` failed with status #{$?.exitstatus}" unless $?.exitstatus == 0
      }
    end

    def expand_path(path, dir=Dir.pwd)
      File.expand_path(path, dir)
    end
  end

  class NoSuchCommand < GitFlow/nil
    @documentation = HELP

    def execute(opts, argv)
      page do
        puts "Command not found: #{argv.join(' ').inspect}"
        puts "Try `#{GitFlow.program} help` to obtain a list of commands."
      end
    end
  end

  class HelpCommand < GitFlow/:help
    @help = "Display help for a command or show command list"
    @documentation = "Displays help for the command given as argument"

    def execute(opts, argv)
      if argv.empty?
        opt = GitFlow.optparse
        opt.banner = "Usage: #{GitFlow.program} command [options]"
        opt.separator ' '
        opt.separator 'COMMANDS'
        opt.separator ' '
        commands = GitFlow.commands.map { |name, cls| [nil, name, cls.help] }.
          sort_by { |a| a[1] || '' }
        commands.each { |a| opt.separator("%-2s%-25s%s" % a) if a[1] }
        opt.separator ' '
        opt.separator 'You can also obtain help for any command giving it --help.'
        page { puts opt }
      else
        run(*(argv + ['--help']))
      end
    end
  end

end

at_exit { GitFlow.run(*ARGV) if GitFlow.should_run }