#!/usr/bin/env bash
#
# git-hub: Do GitHub operations from the `git` command
#
# Copyright (c) 2013-2014 Ingy döt Net

set -e

{
  Bash:version-check() {
    test "$1" -ge 4 && return
    test "$1" -eq 3 -a "$2" -ge 2 && return
    echo "Bash version 3.2 or higher required for 'git hub'" >&2
    exit 1
  }
  Bash:version-check "${BASH_VERSINFO[@]}"
  unset -f Bash:version-check
}

GIT_HUB_VERSION=0.1.4

OPTIONS_SPEC="\
git hub <command> <options> <arguments>

The most commonly used commands:
  help, setup, upgrade, config, url, open, keys,
  user, orgs, org, teams, members, followers, following, follow, stars, star,
  clone, repos, repo, repo-new, repo-delete, forks, fork, collabs, trust,
  issues, issue, issue-update, issue-close, comment,
  pr-new, pr-list, pr-diff, pr-merge, pr-fetch

See 'git hub help' for more help.

Options:
--
h           Show the command summary
help        Browse the complete 'git-hub' documentation
 
remote=     Remote name (like 'origin')
branch=     Branch name (like 'master')
org=        GitHub organization name
 
c,count=    Number of list items to show
a,all       Show all list items
 
q,quiet     Show minimal output
v,verbose   Show verbose output
r,raw       Show output data in a raw form
j,json      Show output data in JSON
 
A,use-auth  Force the use of authentication. (Get around rate limits)
C,no-cache  Don't use cached responses.
token=      Explicitly specify the GitHub v3 API Authentication Token
d,dryrun    Check arguments but don't actually run the API command
T           Show (don't hide) API token in the verbose output
 
O           Debug - Show response output
H           Debug - Show reponse headers
J           Debug - Show parsed JSON response
R           Debug - Repeat last command without contacting server
x           Debug - Turn on Bash trace (set -x) output
"

# source bash+ :std
SELFDIR="$(cd -P `dirname $BASH_SOURCE` && pwd -P)"
source "$SELFDIR/git-hub.d/bash+.bash"
bash+:import :std can

#------------------------------------------------------------------------------
main() {
  local OK=0 split_words=false
  init-env
  if run-for-each "$@"; then
    pager_in_use=true
    run-each "$@" | $GIT_HUB_PAGER
  else
    run-command "$@"
  fi
  exit $OK
}

run-command() {
  set -e
  local Infinity=9999999
  local command command_arguments command_sha1 api_token
  local command_header_file command_output_file command_error_file
  local user org owner repo pairs fields title list_size remote_name
  local msg_ok msg_fail status_code ERROR
  local count_option do_all=false use_auth no_cache
  local quiet_output verbose_output
  local raw_output=false json_output=false interactive=
  local show_output=false show_headers=false show_json=false
  local dry_run=false show_token repeat_command=false
  local terminal_lines terminal_cols
  init-vars

  get-opts "$@"
  assert-env

  callable-or-source "$command" ||
    error "unknown 'git hub' command '$command'"
  "command:$command" "$@"

  if OK; then
    if can "ok:$command"; then
      "ok:$command"
    else
      if [ "$msg_ok" != '0' ]; then
        say "${msg_ok:-"'git hub $command' command successful"}"
      fi
    fi
  else
    local status_msg="msg_$status_code"
    if [ -n "${!status_msg}" ]; then
      nay "${!status_msg}"
    elif [ -n "$status_code" ] && can "status-$status_code:$command"; then
      "status-$status_code:$command"
    elif [ -n "$msg_fail" ]; then
      nay "$msg_fail"
    elif can "fail:$command"; then
      "fail-$command"
    else
      local msg="Error: 'git hub $command' command failed"
      ! "$verbose_output" && [ -n "$status_code" ] &&
        msg+=": $status_code"
      nay "$msg"
    fi
    if "$verbose_output" && [ -n "$ERROR" ]; then
      nay
      [ -n "$status_code" ] &&
        nay "API status code: $status_code\n"
      [ -n "$ERROR" ] &&
        nay "API error output:\n$ERROR\n"
      nay "See files in: $GIT_HUB_CACHE/$command_sha1/"
    fi
  fi
  true
}

run-each() {
  local i line before=() after=() seen=false
  for ((i = 1; i <= $#; i++)); do
    if [[ "${!i}" =~ ^[-=]$ ]]; then
      seen=true
    elif ! "$seen"; then
      before+=( "${!i}" )
    else
      after+=("${!i}")
    fi
  done
  while read -r line; do
    local lines=("$line")
    "$split_words" && lines=($line)
    local arg
    for arg in "${lines[@]}"; do
      options=()
      options+=("${before[@]}")
      options+=($arg)
      options+=("${after[@]}")
      run-command "${options[@]}"
      OK || return
    done
  done
}

#------------------------------------------------------------------------------
# `git hub` command functions:
#------------------------------------------------------------------------------
command:help() {
  source-ext help-functions.bash
  local cmd="${command_arguments[0]}"
  if [ -n "$cmd" ]; then
    if can "help:$cmd"; then
      "help:$cmd"
      echo
    else
      err "No help found for '$cmd'"
    fi
  elif $do_all; then
    help:all
  else
    cat <<'...'
git-hub -- The GitHub Subcommand for Git

Try the following commands to get more help:

  git hub help --all        # Show all commands
  git hub help <command>    # Get help for a specific command
  git help hub              # Browse the complete 'git-hub' documentation
  git hub --help            # Browse the complete 'git-hub' documentation
  git hub -h                # Show the short documentation

Or read the documentation online:

  https://github.com/ingydotnet/git-hub#readme

If you find bugs, or missing features, just run:

  git hub issue-new ingydotnet/git-hub

Enjoy GitHubbing in the comfort of your terminal.

  -- Ingy döt Net

...
  fi
  msg_ok=0
}

# NOTE: All the commands used to be defined here but now they have been moved
# into various files under the `cmd` directory for better grouping and code
# organization.

#------------------------------------------------------------------------------
# API calling functions:
#------------------------------------------------------------------------------
api-get() { api-call GET "$1" "$2"; }
api-post() { api-call POST "$1" "$2"; }
api-put() { api-call PUT "$1" "$2"; }
api-patch() { api-call PATCH "$1" "$2"; }
api-delete() { api-call DELETE "$1" "$2"; }

# Build a command to make the HTTP call to the API server, make the call, and
# check the result.
api-call() {
  format-curl-command "$@"

  if "$verbose_output"; then
    local cc="${curl_command[@]}"
    if ! "$show_token"; then
      [ -n "$api_token" ] &&
        cc="${cc/token $api_token/token ********}"
      [ -n "$GIT_HUB_PASSWORD" ] &&
        cc="${cc/$login:$GIT_HUB_PASSWORD/$login:********}"
    fi
    echo "$cc"$'\n' >&2
  fi

  if "$dry_run"; then
    nay '*** NOTE: This is a dry run only. ***'
    msg_ok=0
    return 0
  elif "$repeat_command"; then
    # Use the cache
    [ -e "$command_header_file" ] ||
      error "-R flag not valid. Command not previously run."
    rc=0
  else
    local cache_dir="$GIT_HUB_CACHE/$command_sha1"
    mkdir -p "$cache_dir"
    # Actually run the curl command!
    (
      set +e
      # set -x  # for debugging the real command
      "${curl_command[@]}"
    )
    rc=$?
    cache-response-files
  fi

  "$show_headers" && cat "$command_header_file" >&2

  if [ -s "$command_output_file" ]; then
    JSON__cache="$(cat "$command_output_file" | JSON.load)"
    "$show_output" && cat "$command_output_file" >&2
    "$show_json" && JSON.cache >&2
  fi

  check-api-call-status $rc

  true
}

# Build curl command in a global array. This is the only way to preserve
# quoted whitespace.
format-curl-command() {
  local action="$1"
  local url="$2"
  local data="$3"

  [[ "$url" =~ [a-zA-Z0-9]/(/|$) ]] &&
    error "API url '$url' looks suspiciously wrong"

  "$use_auth" && require-auth 1

  local user_agent="git-hub-$GIT_HUB_VERSION"
  # Cheap trick to make github pretty-print the JSON output.
  "$show_output" && user_agent="curl-$user_agent"

  [[ "$url" =~ ^https?: ]] || url="$GIT_HUB_API_URI$url"

  curl_command=(
    curl
      --request "$action"
    "$url"
  )
  [ -n "$data" ] || [ "$action" = "PUT" ] && curl_command+=(-d "$data")
  if [ -n "$basic_auth" ]; then
    local login="$(get-login)"
    if [ -n "$GIT_HUB_PASSWORD" ]; then
      curl_command+=(-u "$login:$GIT_HUB_PASSWORD")
    else
      curl_command+=(-u "$login")
    fi
    if [ -n "$GIT_HUB_2FA_OTP" ]; then
      curl_command+=(--header "X-GitHub-OTP: $GIT_HUB_2FA_OTP")
    fi
  elif [ -n "$api_token" ]; then
    curl_command+=(--header "Authorization: token $api_token")
  fi

  local sha1seed="${curl_command[@]}"
  [ -n "$api_token" ] && [ -n "$GIT_HUB_TEST_MAKE" ] &&
    sha1seed="${sha1seed/$api_token/0123456789abcdef0123456789abcdef01234567}"
  # echo "$sha1seed" >> sha1seed
  "$show_output" && sha1seed+=' -O'
  command_sha1="$(echo "$sha1seed" | $sha1sum | cut -d ' ' -f1)"
  command_header_file="$GIT_HUB_CACHE/$command_sha1/head"
  command_output_file="$GIT_HUB_CACHE/$command_sha1/out"
  command_error_file="$GIT_HUB_CACHE/$command_sha1/err"

  if ! "$no_cache" &&
    [[ ! "$url" =~ hooks$ ]] &&
    [ -e "$command_header_file" ]
  then
    etag="$(grep -Em1 '^ETag:' "$command_header_file" | tr -d '\r')"
    if [ -n "$etag" ]; then
      curl_command+=(--header "${etag/ETag/If-None-Match}")
    fi
  fi
  curl_command+=(
    --user-agent "$user_agent"
    --dump-header "$GIT_HUB_CACHE/head"
    --output "$GIT_HUB_CACHE/out"
    --stderr "$GIT_HUB_CACHE/err"
    --silent
    --show-error
  )
}

cache-response-files() {
  grep -E '^Status: 304' "$GIT_HUB_CACHE/head" &> /dev/null || {
    for f in head out err; do
      [ -e "$GIT_HUB_CACHE/$f" ] &&
        cp "$GIT_HUB_CACHE/$f" "$cache_dir"
    done
  }
}

check-api-call-status() {
  OK=$1
  if [ ! -f "$command_header_file" ]; then
    ERROR="$(head -n1 $command_error_file)"
    if [ $OK == 6 ]; then
      ERROR+=$'\n'$'\n'"Check your internet connection."
      verbose_output=true
    fi
    return
  fi
  status_code="$(
    grep -E 'HTTP/1.1 [2-9]' $command_header_file |
    head -n1 | cut -d ' ' -f2
  )"
  [[ "$status_code" =~ ^[0-9]{3}$ ]] ||
    die "Can't get the status code from $command_header_file"
  case "$status_code" in
    200|201|202|204)
      OK=0
      ;;
    403)
      OK=1
      ERROR="$(JSON.get -s '/message' -)"
      if [[ "$ERROR" =~ rate\ limit\ exceeded ]]; then
        ERROR="$ERROR Try the --use-auth option."
      fi
      local width=81
      osx && width=78
      ERROR="$(echo "$ERROR" | fmt -w $width)"
      ;;
    *)
      OK=1
      ERROR="$(head -n1 "$command_header_file" | cut -d ' ' -f3-)"
      ;;
  esac
}

OK() { return $OK; }

#------------------------------------------------------------------------------
# Config management:
#------------------------------------------------------------------------------
check-config() {
  [ -e "$GIT_HUB_CONFIG" ] || config-not-setup
  read-config-value login && login="$value" || login=
  if [ -z "$login" ]; then
    config-not-setup
  fi
}

dump-config() {
  [ ! -e "$GIT_HUB_CONFIG" ] && config-not-setup
  say "Your git-hub config file (at $GIT_HUB_CONFIG):\n"
  cat "$GIT_HUB_CONFIG" || return 1
}

read-config-value() {
  value=
  value="$(git config -f $GIT_HUB_CONFIG "git-hub.$1")" || {
    value="$(git config -f $GIT_HUB_CONFIG "github.$1")" || return 1

    # This auto-migrates github to git-hub. Added 2014/08/11.
    # TODO Remove after a few months.
    git config -f $GIT_HUB_CONFIG "git-hub.$1" "$value"
    git config -f $GIT_HUB_CONFIG --unset "github.$1"
    if [ -z "$(git config -f $GIT_HUB_CONFIG --get-regex github)" ]; then
      git config -f $GIT_HUB_CONFIG --remove-section github 2> /dev/null || true
    fi

  }
}

write-config-value() {
  git config -f $GIT_HUB_CONFIG "git-hub.$1" "$2" || return 1
}

delete-config-value() {
  git config -f $GIT_HUB_CONFIG --unset "git-hub.$1" || {
    git config -f $GIT_HUB_CONFIG --unset "github.$1" || return 1
  }
}

#------------------------------------------------------------------------------
# Argument parsing functions:
#------------------------------------------------------------------------------
get-args() {
  local specs=("$@")
  local args=("${command_arguments[@]}")

  while [ ${#specs[@]} -gt 0 ]; do
    local optional=false
    local spec="${specs[0]}"
    local arg="${args[0]}"
    if [[ "$spec" =~ ^\? ]]; then
      optional=true
      spec="${spec#\?}"
      if [[ "$spec" =~ / ]] &&
        [[ ! "$spec" =~ : ]] &&
        [[ ! "$arg" =~ / ]]
      then
        specs=("${specs[@]:1}")
        continue
      fi
      if [ ${#args[@]} -lt ${#specs[@]} ] &&
        [[ ! "${specs[1]}" =~ ^\? ]]
      then
        if [[ "$spec" =~ : ]]; then
          arg=''
          spec-assign
        else
          printf -v "$spec" ""
        fi
      else
        spec-assign
        args=("${args[@]:1}")
      fi
    elif [[ "$spec" =~ ^-- ]]; then
      keyword-assign
    else
      if [ ${#args[@]} -eq 0 ]; then
        spec-assign
      else
        spec-assign
        args=("${args[@]:1}")
      fi
    fi
    specs=("${specs[@]:1}")
  done
  if [ ${#args[@]} -gt 0 ]; then
    error "unknown argument(s) '${args[*]}'"
  fi
}

keyword-assign() {
  local kargs=(${args[@]})
  args=()
  for ((i = 0; i < ${#kargs[@]}; i++)); do
    if [ "${kargs[$i]}" == "$spec" ]; then
      i=$((i + 1))
      local var="${spec#--}"
      local val="${kargs[$i]}"
      [ "$val" == '@' ] && val="$login"
      printf -v "$var" "$val"
    elif [[ "${kargs[$i]}" =~ ^$spec=(.*) ]]; then
      local var="${spec#--}"
      local val="${BASH_REMATCH[1]}"
      [ "$val" == '@' ] && val="$login"
      printf -v "$var" "$val"
    else
      args+=("${kargs[$i]}")
    fi
  done
}

spec-assign() {
  [[ "$spec" =~ / ]] && slash-assign && return
  [[ "$spec" =~ ^% ]] && hash-assign && return
  [[ "$spec" =~ ^@ ]] && array-assign && return
  local spec="$spec"
  local arg="$arg"
  [ "$arg" == '@' ] && arg="$login"
  local var="${spec/:*/}"
  [[ "$spec" =~ : ]] && spec="${spec#$var:}" || spec=
  if [ -z "$arg" ]; then
    for func in ${spec/:/ }; do
      if [ "$func" == empty ]; then
        printf -v "$var" ""
        return
      elif [[ "$func" =~ ^\'(.*)\'$ ]]; then
        printf -v "$var" "${BASH_REMATCH[1]}"
        return
      fi
      arg="$($func)"
      [ -n "$arg" ] && break
    done
  fi
  if [ -n "$arg" ]; then
    printf -v "$var" "$arg"
  else
    error "can't find a value for '$var'"
  fi
}

slash-assign() {
  local spec1="${spec/\/*/}"
  local spec2="${spec#$spec1/}"
  local arg1= arg2=
  if [[ "$arg" =~ / ]]; then
    local arg1="${arg/\/*/}"
    local arg2="${arg#$arg1/}"
  elif "$optional"; then
    local arg1=''
    local arg2="$arg"
  else
    error "invalid value '$arg' for '$spec'"
  fi
  local spec="$spec1"
  local arg="$arg1"
  spec-assign
  local spec="$spec2"
  local arg="$arg2"
  spec-assign
}

hash-assign() {
  if [ $(( ${#args[@]} % 2)) -eq 1 ]; then
    error "odd number of items for key/value pairs"
  fi
  local spec="${spec#%}"
  eval $spec=\(\"\${args[@]}\"\)
  args=()
}

array-assign() {
  local spec="${spec#@}"
  eval $spec=\(\"\${args[@]}\"\)
  args=()
}

get-user() {
  get-owner || get-login || return 1
  return 0
}

get-login() {
  if read-config-value login; then
    echo "$value"
  fi
}

get-owner() {
  local owner="$(git config github.owner)"
  if [ -n "$owner" ]; then
    echo "$owner"
    return 0
  fi
  get-repo-config owner
}

get-repo() {
  get-repo-config repo
}

get-remote-name() {
  value=
  [ -n "$remote_name" ] && value="$remote_name" && return
  value="$(get-default-remote-name)"
  [ -n "$value" ] && return 0 || return 1
}

get-default-remote-name() {
  [ -n "$remote_name" ] && return
  local owner="$(get-repo-config owner)"
  local repo="$(get-repo-config repo)"
  remote_name="$(
    git remote -v |
    grep "$owner/$repo" |
    grep "(fetch)" |
    head -n1 |
    cut -f 1
  )"
}

get-branch-name() {
  [ -n "$branch_name" ] && return
  branch_name="$(git rev-parse --abbrev-ref HEAD)"
}

get-parent-owner-repo() {
  [ -n "$parent_owner_repo" ] && return
  get-parent-remote-name
  local remote_name="$parent_remote_name"
  local parent_owner="$(get-repo-config owner)"
  local parent_repo="$(get-repo-config repo)"
  parent_owner_repo="$parent_owner/$parent_repo"
}

get-parent-remote-name() {
  [ -n "$parent_remote_name" ] && return
  local owner="$(get-owner)"
  local repo="$(get-repo)"
  api-get "/repos/$owner/$repo"
  OK || {
    abort "Repo '$owner/$repo' not found"
  }
  local value="$(JSON.get -a "/parent/full_name" -)"
  if [ -z "$value" ]; then
    # parent for PR is self
    get-default-remote-name
    parent_remote_name="$remote_name"
  else
    parent_remote_name="$(git remote -v | grep "$value" | grep "(fetch)" | cut -f 1)"
  fi
}

get-parent-base() {
  [ -n "$parent_base" ] && return
  get-parent-owner-repo
  parent_base="$(git hub repo-get $parent_owner_repo default_branch)"
}

get-repo-config() {
  local login="$(get-login)"
  local remote="$remote_name"
  [ -n "$remote" ] && remote="^$remote"$'\t'
  urls=($(
    git remote -v 2> /dev/null |
    grep "$remote" |
    grep 'github\.com' |
    grep -v '^subrepo/'
  ))
  for x in "$login" '.*'; do
    for url in "${urls[@]}"; do
      url="${url% (fetch)}"
      url="${url% (push)}"
      [[ "$url" =~ .*://(.*) ]] &&
        url="${BASH_REMATCH[1]}"
      local re1="^[^:/]*[:/]($x)/(.*)\.git$"
      local re2="^[^:/]*[:/]($x)/(.*)$"
      if [[ "$url" =~ $re1 ]] || [[ "$url" =~ $re2 ]]; then
        if [ "$1" == owner ]; then
          echo "${BASH_REMATCH[1]}"
        else
          echo "${BASH_REMATCH[2]}"
        fi
        return 0
      fi
    done
  done
  return 1
}

require-auth() {
  [ -z "$api_token" ] && get-var api-token
  if [ "$api_token" == XXX ]; then
    api_token=
    [ -n "$1" ] && return
    echo "This command requires an API token in your config, but you have that disabled."
    exit 0
  fi
  [ -z "$api_token" ] && need-api-token
  true
}

basic-auth() {
  basic_auth=1
}

check-token-id() {
  local id="$1"
  [ ${#id} -eq 40 ] &&
    error "'$command' requires a *token-id*, not a *token-value*."
  local regex='^[0-9]+$'
  [[ "$id" =~ $regex ]] ||
    error "'$id' is invalid token-id. Must be an integer value."
  true
}

#------------------------------------------------------------------------------
# List processing functions:
#------------------------------------------------------------------------------
report-list() {
  if interactive && $do_all && ! $pager_in_use; then
    pager_in_use=true
    msg_ok=0
    report-list-process "$@" | $GIT_HUB_PAGER
  else
    report-list-process "$@"
  fi
}

report-list-process() {
  msg_ok=0
  local url="$1" fields=($2) ii
  local list_max=${list_size:-$(($terminal_lines - 2))}
  local line_max=$terminal_cols
  local per_page=100
  if ! $json_output; then
    [ $per_page -gt $list_max ] && per_page=$list_max
  fi
  url="${url/PER_PAGE/$per_page}"
  [ -n "$title" ] && say "$title"
  local list_counter=1
  while true; do
    api-get "$url"
    if $json_output; then
      pretty-json-list "${fields[@]}"
      return 0
    fi
    OK || return 0
    json-var-list "${fields[@]}"
    if [ -n "$key_prefix" ]; then
      local page_size="$(JSON.cache | grep "^$key_prefix" | tail -n1 | cut -d '/' -f3)"
    else
      local page_size="$(JSON.cache | tail -n1 | cut -d '/' -f2)"
    fi
    [ -z "$page_size" ] && say '--None--' && break
    local counter=0
    for ((ii = 0; ii <= page_size && counter < list_max; ii++)); do
      local -a values=()
      for field in "${fields[@]}"; do
        var="${field//\//__}"_"$ii"
        values+=("${!var}")
      done
      local output_line=
      if can "format-entry:$command"; then
        "format-entry:$command" $list_counter "${values[@]}"
      else
        default-format-entry $list_counter "${values[@]}"
      fi
      if [ -n "$output_line" ]; then
        if interactive; then
          out "$output_line" | cut -c 1-$line_max
        else
          out "$output_line"
        fi
      fi
      let counter=counter+1
      let list_counter=list_counter+1
      [ -n "$list_size" ] && [ $list_counter -gt $list_size ] && return
    done
    url=$(get-next-page-url)
    if interactive; then
      if [ $list_counter -gt $list_max ]; then
        if [ -n "$url" -o $ii -lt $page_size ]; then
          local prompt_msg='--More-- (press Enter or CTRL-C)'
          prompt
        fi
      fi
    fi
    [ -n "$url" ] || break
  done
}

get-next-page-url() {
  local regexp='Link: <(https:[^>]+)>; rel="next"'
  [[ -e "$command_header_file" ]] &&
    [[ "$(< "$command_header_file")" =~ $regexp ]] &&
    echo "${BASH_REMATCH[1]}"
  true
}

default-format-entry() {
  if "$raw_output"; then
    out "$2"
  else
    printf "%d) %s\n" "$1" "$2"
  fi
}

normalize-multiline-text-output() {
  text="$1"
  local indent="$2"
  text="${text//\\r/}"
  text="${text//\\n/$'\n'}"
  text="$(
    echo "$text" | (
      local start=false blank=false para=false code=false output=''
      IFS=''
      while read -r line; do
        if [[ "$line" =~ [^[:space:]] ]]; then
          if "$blank" && "$start"; then
            output+=$'\n'
          fi
          if [[ "$line" =~ ^\  ]]; then
            output+="$indent$line"$'\n'
            code=true para=false
          else
            local width=81
            osx && width=78
            output+="$( echo "$indent$line" | fmt -w $width )"$'\n'
            para=true code=false
          fi
          start=true blank=false
        else
          if "$para" || "$code"; then
            para=false code=false
          fi
          blank=true
        fi
      done
      if [ -n "$output" ] && ! "$code"; then
        output+=$'\n'
      fi
      echo -n "$output"
    )
  )"
}

#------------------------------------------------------------------------------
# Report formatting functions:
#------------------------------------------------------------------------------
report-data() {
  if interactive && ! $pager_in_use; then
    pager_in_use=true
    msg_ok=0
    report-data-process "$@" | $GIT_HUB_PAGER
  else
    report-data-process "$@"
  fi
}

report-data-process() {
  if "$json_output"; then
    pretty-json-object "${fields[@]}"
  else
    json-prune-hash "${fields[@]}"
    for field in "${fields[@]}"; do
      local skip="skip_field_$field"
      [ -n "${!skip}" ] && continue
      report-value "$field"
    done
    ! "$raw_output" && echo
  fi
}

report-value() {
  local key="${1//__/\/}"
  local value="$(JSON.get -a "/$key" -)"
  local label="$key"
  if [ "$key" == created_at ]; then
    value="${value%T*}"
  elif [ "$key" == pushed_at ]; then
    value="${value/T/ }"
    value="${value/Z/}"
  fi
  if ! "$raw_output"; then
    local var="label_$1"
    label="${!var}"
    if [ -z "$label" ]; then
      label="$1"
      label="${label/__*/}"
      label="$(echo "$label" | tr '_' ' ')"
      label="$(
        for word in $label; do
          title=`echo "${word:0:1}" | tr a-z A-Z`${word:1}
          echo -n "$title "
        done
      )"
      label="${label% }"
    fi
  fi
  if [ -n "$label" -a -n "$value" ]; then
    if "$raw_output"; then
      printf "%s\t%s\n" "$label" "$value"
    else
      value="${value%%\\n}"
      if [ ${#value} -ge 80 ] ||
         [[ "$value" =~ \\n ]] ||
         [ "$label" == Description ]
      then
        echo "$label:"
        normalize-multiline-text-output "$value" " "
        echo "$text"
      else
        printf "%-15s %s\n" "$label" "$value"
      fi
    fi
  fi
}

label_blog='Web Site'
label_watchers='Stars'
label_homepage='Web Site'
label_html_url='GitHub Page'
label_ssh_url='Remote URL'
label_parent__full_name='Forked From'
label_source__full_name='Fork Parent'
label_number='ID #'

#------------------------------------------------------------------------------
# Initial setup functions:
#------------------------------------------------------------------------------
init-env() {
  [ -z "$HOME" ] && die "Cannot determine HOME directory"

  pager_in_use=false
  sha1sum=sha1sum
  shopt_opt='shopt -s nullglob globstar'
  if [[ "$OSTYPE" =~ ^darwin|^freebsd ]]; then
    sha1sum=shasum
    shopt_opt='shopt -s nullglob'
  fi
  check-system-commands curl cut git grep head ln mv readlink "$sha1sum" tr

  : "${GIT_HUB_API_URI:=https://api.github.com}"

  : "${GIT_HUB_EXEC_PATH:=$SELFDIR}"
  : "${GIT_HUB_EXT_PATH:=$GIT_HUB_EXEC_PATH/git-hub.d}"
  : "${GIT_HUB_PLUGIN_PATH:=${GIT_HUB_EXEC_PATH%lib}plugin}"

  if [ -z "$GIT_HUB_PLUGIN_LIBS" ]; then
    GIT_HUB_PLUGIN_LIBS="$(
      $shopt_opt
      set -- $GIT_HUB_PLUGIN_PATH/*/lib
      IFS=':'; echo "$*"
    )"
  fi

  : "${GIT_HUB_USER_DIR:=$HOME/.git-hub}"
  : "${GIT_HUB_CONFIG:=$GIT_HUB_USER_DIR/config}"
  : "${GIT_HUB_CACHE:=$GIT_HUB_USER_DIR/cache}"

  : "${GIT_HUB_EDITOR:=${EDITOR:-vim}}"
  [[ $GIT_HUB_EDITOR =~ ^g?vim?$ ]] && GIT_HUB_EDITOR+=' +1'

  : "${GIT_HUB_PAGER:=${PAGER:-less}}"
  [ "$GIT_HUB_PAGER" == less ] && GIT_HUB_PAGER='less -FRX'

  if [ -z "$GIT_HUB_MSG_FILE" ]; then
    GIT_HUB_MSG_FILE="$(git rev-parse --git-dir 2>/dev/null || true)"
    [ -z "$GIT_HUB_MSG_FILE" ] && GIT_HUB_MSG_FILE=/tmp
    GIT_HUB_MSG_FILE+=/GIT_HUB_EDIT_MSG
  fi

  [ -d "$GIT_HUB_EXT_PATH" ] ||
    die "Hmm... git-hub does not seem to be properly installed"
  true
}

init-vars() {
  terminal_cols="${GIT_HUB_TEST_COLS:-$(tput cols)}"
  terminal_lines="${GIT_HUB_TEST_LINES:-$(tput lines)}"
  get-var list-size
  get-var-bool use-auth
  get-var-bool no-cache
  get-var-bool quiet-output
  get-var-bool verbose-output
  get-var-bool show-token
}

run-for-each() {
  local elem
  for elem in "$@"; do
    [ "$elem" == '=' ] && split_words=true
    [[ "$elem" =~ ^[-=]$ ]] && return 0
  done
  return 1
}

assert-env() {
  assert-env-var EXEC_PATH EXT_PATH API_URI \
    USER_DIR CONFIG CACHE

  source-ext json-setup.bash
}

check-system-commands() {
  local missing=false
  local cmd
  for cmd in $@; do
    if ! $(type $cmd &> /dev/null); then
      echo "Required shell command not found: '$cmd'"
      missing=true
    fi
  done
  "$missing" && exit 1
  true
}

source-ext() {
  PATH="$GIT_HUB_EXT_PATH:$GIT_HUB_PLUGIN_LIBS:$PATH" source "$1"
}

source-ext-maybe() {
  set +e
  PATH="$GIT_HUB_EXT_PATH:$GIT_HUB_PLUGIN_LIBS:$PATH" source "$1" 2> /dev/null
  local rc=$?
  set -e
  return $rc
}

assert-env-var() {
  local var
  for var in $@; do
    var="GIT_HUB_$var"
    [ -n "${!var}" ] ||
      abort "'$var' is not set"
  done
}

get-opts() {
  [ $# -eq 0 ] && set -- --help

  # TODO This is a hack for per-command args. We need to make this be closer
  # to the commands that need them.
  local args=() need_var=
  for arg; do
    if [ -n "$need_var" ]; then
      printf -v "$need_var" "$arg"
      need_var=
    elif [[ "$arg" =~ ^--parent(=.*)?$ ]]; then
      set-var parent_remote "${BASH_REMATCH[1]#=}"
      [ -n "${BASH_REMATCH[1]#=}" ] || need_var=parent_remote
    elif [[ "$arg" =~ ^--base(=.*)?$ ]]; then
      set-var parent_base "${BASH_REMATCH[1]#=}"
      [ -n "${BASH_REMATCH[1]#=}" ] || need_var=parent_base
    else
      args+=("$arg")
    fi
  done

  set -- "${args[@]}"

  eval "$(
    echo "$OPTIONS_SPEC" |
      git rev-parse --parseopt -- "$@" ||
    echo exit $?
  )"

  while [ $# -gt 0 ]; do
    local option="$1"; shift
    case "$option" in
      -c)
        list_size="$1"
        count_option="$1"
        shift
        ;;
      -a)
        list_size="$Infinity"
        do_all=true
        ;;
      --org)
        set-var organization "$1"
        shift
        ;;
      --remote)
        set-var remote_name "$1"
        shift
        ;;
      --branch)
        set-var branch_name "$1"
        shift
        ;;

      -q) quiet_output=true ;;
      -v) verbose_output=true ;;
      -r)
        raw_output=true
        quiet_output=true
        ;;
      -j) json_output=true ;;

      -A) use_auth=true ;;
      -C) no_cache=true ;;
      --token)
        api_token="$1"
        shift
        ;;
      -d) dry_run=true ;;
      -T)
        show_token=true
        verbose_output=true
        quiet_output=false
        ;;

      --) break ;;

      # Debug options:
      -O) show_output=true ;;
      -H) show_headers=true ;;
      -J) show_json=true ;;
      -x) set -x ;;
      -R) repeat_command=true ;;

      *) error "unexpected option '$option'" ;;
    esac
  done

  [ -z "$list_size" ] && ! interactive && list_size="$Infinity"

  command="$1"; shift
  command_arguments=("$@")

  [[ "$command" == "setup" ]] ||
  [[ "$command" = "config" ]] ||
  [[ "$command" = "config-unset" ]] ||
  ! (callable-or-source "$command") ||
    check-config

  true
}

interactive() {
  if [ -n "$interactive" ]; then
    if $interactive; then
      return 0
    else
      return 1
    fi
  fi
  if [ -t 0 -a -t 1 ]; then
    return 0
  else
    return 1
  fi
}

osx() {
  if [ "$(uname)" == Darwin ]; then
    return 0
  else
    return 1
  fi

}

pipeline() {
  if [ -t 0 -a -t 1 ]; then
    return 1
  else
    return 0
  fi
}

set-var() {
  local name="$1"
  local value="$2"
  if [ "$value" == '@' ]; then
    login="$(git config -f "$GIT_HUB_CONFIG" github.login || echo '')"
    value="$login"
  fi
  printf -v "$name" "$value"
}

get-var() {
  local var="${1//-/_}"
  [ -n "${!var}" ] && return
  local env="GIT_HUB_$(echo "$var" | tr 'a-z' 'A-Z')"
  printf -v "$var" "${!env}"
  [ -n "${!var}" ] && return
  read-config-value "$1" || true
  printf -v "$var" "$value"
  true
}

get-var-bool() {
  get-var "$1"
  local var="${1//-/_}"
  [ -z "${!var}" ] && printf -v "$var" false
  [ "${!var}" != false ] && printf -v "$var" true
  true
}

#------------------------------------------------------------------------------
# Detailed error messages:
#------------------------------------------------------------------------------
need-api-token() {
  cat <<...

Can't determine your Personal API Access Token, which is required by this
command. Usually you put your token into the ~/.git-hub/config file, like this:

  git hub config api-token <your-personal-api-access-token>

You can also specify the token using the --token= commandline option, or via
the GIT_HUB_API_TOKEN environment variable.

To list your API tokens:

  git hub tokens

To create a new api token:

  git hub token-new "my git-hub token"
  # You should probably add at least the 'repo' and 'user' scopes:
  git hub scope-add <token-id> repo user

You can also just run setup command, and it will guide you:

  git hub setup

Would you like to run it now?

...
  prompt-to-run-setup
}

config-not-setup() {
  cat <<...

Greetings!!!

You seem to be a brand new 'git hub' user, since your config is not set up.

The 'git-hub' should soon be your new best friend. Configuration is really
easy. Just run the following command and answer a few simple questions:

  git hub setup

This will guide you through the setup process. Press <ENTER> to run it now.

NOTE: You can also do the setup process by hand using the 'config', 'token'
      and 'scope' commands. See the 'git help hub' documentation for more
      information on these commands. Or just manually create a config file
      in ~/.git-hub/config that looks like this:

  [github]
          login = your-github-login-id
          api-token = c956f87abab2da54882cc1f1ade42efcc2b15dc7
          json-lib = json-perl.bash
          use-auth = 1

Would you like to start the automated 'setup' process right now? (Do it!)

...
  prompt-to-run-setup
}

prompt-to-run-setup() {
  prompt
  source-ext git-hub-setup
  command:setup || exit 1
  exit 0
}

#------------------------------------------------------------------------------
# Reusable helper functions:
#------------------------------------------------------------------------------
editor-title-body() {
  rm -f "$GIT_HUB_MSG_FILE"

  echo "$1" > "$GIT_HUB_MSG_FILE"

  $GIT_HUB_EDITOR "$GIT_HUB_MSG_FILE"
  local line
  body=''
  title="$(head -n1 "$GIT_HUB_MSG_FILE")"
  if [[ ! "$title" =~ [^[:space:]] ]]; then
    abort "no title provided"
  fi
  line="$(head -n2 "$GIT_HUB_MSG_FILE" | tail -n1)"
  if [[ ! "$line" =~ ^\s*$ ]] && [[ ! "$line" =~ ^\# ]]; then
    error "malformed message in '$GIT_HUB_MSG_FILE'"
  fi
  local count=0
  local ifs="$IFS"
  IFS=''
  while read -r line; do
    [[ $((count++)) -lt 2 ]] && continue
    [[ "$line" =~ ^\# ]] && break
    body+="$line"$'\n'
  done < "$GIT_HUB_MSG_FILE"
  IFS="$ifs"
}

editor-comment() {
  rm -f "$GIT_HUB_MSG_FILE"

  echo "$1" > "$GIT_HUB_MSG_FILE"

  $GIT_HUB_EDITOR "$GIT_HUB_MSG_FILE"
  local line
  body=''
  local ifs="$IFS"
  IFS=''
  while read -r line; do
    [[ "$line" =~ ^\# ]] && break
    body+="$line"$'\n'
  done < "$GIT_HUB_MSG_FILE"
  IFS="$ifs"
}

editor-comment-state() {
  rm -f "$GIT_HUB_MSG_FILE"

  comment= title= state= assignee= milestone=

  echo "$1" > "$GIT_HUB_MSG_FILE"

  $GIT_HUB_EDITOR "$GIT_HUB_MSG_FILE"
  local line started=false

  local ifs="$IFS"
  IFS=''
  while read -r line; do
    [[ "$line" =~ ^\# ]] && break
    if $started; then
      if [[ "$line" =~ ^title:\ +(.*) ]]; then
        title="${BASH_REMATCH[1]}"
      elif [[ "$line" =~ ^state:\ +(.*) ]]; then
        state="${BASH_REMATCH[1]}"
      elif [[ "$line" =~ ^assignee:\ +(.*) ]]; then
        assignee="${BASH_REMATCH[1]}"
      elif [[ "$line" =~ ^milestone:\ +(.*) ]]; then
        milestone="${BASH_REMATCH[1]}"
      fi
    else
      if [[ "$line" == '---' ]]; then
        started=true
      elif [ -n "$comment" ] || [[ "$line" =~ [^[:space:]] ]]; then
        comment+="$line"$'\n'
      fi
    fi
  done < "$GIT_HUB_MSG_FILE"
  IFS="$ifs"

  if [[ ! "$title" =~ [^[:space:]] ]]; then
    abort "no title provided."
  fi
}

#------------------------------------------------------------------------------
# General purpose functions:
#------------------------------------------------------------------------------

quiet_output=false
say() { ! "$quiet_output" && out "$@"; true; }
say-() { ! "$quiet_output" && out- "$@"; true; }
nay() { ! "$quiet_output" && err "$@"; true; }
nay-() { ! "$quiet_output" && err- "$@"; true; }
out() { echo -e "$@" >&1; }
out-() { echo -en "$@" >&1; }
err() { echo -e "$@" >&2; }
err-() { echo -en "$@" >&2; }

error() {
  local msg="Error: $1" usage=
  source-ext help-functions.bash
  if can "help:$command"; then
    msg=$'\n'"$msg"$'\n'"$("help:$command")"$'\n'
  fi
  echo "$msg" >&2
  exit 1
}

abort() { echo "Abort: $1" >&2; exit 1; }

prompt() {
  local msg answer default yn=false password=false
  case $# in
    0) msg="${prompt_msg:-Press <ENTER> to continue, or <CTL>-C to exit.}" ;;
    1)
      msg="$1"
      if [[ "$msg" =~ \[yN\] ]]; then
        default=n
        yn=true
      elif [[ "$msg" =~ \[Yn\] ]]; then
        default=y
        yn=true
      fi
      ;;
    2)
      msg="$1"
      default="$2"
      ;;
    *) die "Invalid usage of prompt" ;;
  esac
  if [[ "$msg" =~ [Pp]assword ]]; then
    password=true
    msg=$'\n'"$msg"
  fi
  while true; do
    if "$password"; then
      read -s -p "$msg" answer
    else
      read -p "$msg" answer
    fi
    [ $# -eq 0 ] && return 0
    [ -n "$answer" -o -n "$default" ] && break
  done
  if "$yn"; then
    [[ "$answer" =~ ^[yY] ]] && echo y && return 0
    [[ "$answer" =~ ^[nN] ]] && echo n && return 0
    echo "$default"
    return 0
  fi
  if [ -n "$answer" ]; then
    echo "$answer"
  else
    echo "$default"
  fi
}

callable-or-source() {
  if can "command:$1"; then
    return 0
  else
    if source-ext-maybe git-hub-"${1/:*/}" && can "command:$1"; then
      return 0
    elif source-ext-maybe git-hub-"${1%s}" && can "command:$1"; then
      return 0
    elif source-ext-maybe git-hub-other && can "command:$1"; then
      return 0
    elif source-ext-maybe git-hub-"${1/-*/}" && can "command:$1"; then
      return 0
    fi
  fi
  return 1
}

assert-inside-git-repo() {
  inside-git-repo ||
    abort "not inside a git repo"
}

assert-repo-top-level() {
  [ "$(git rev-parse --git-dir)" == '.git' ] ||
    abort "not at top level directory of repo"
}

assert-git-repo-is-clean() {
  # Repo is in a clean state:
  git update-index -q --ignore-submodules --refresh
  git diff-files --quiet --ignore-submodules ||
    abort "unstaged changes."
  git diff-index --quiet --ignore-submodules HEAD ||
    abort "working tree has changes."
  git diff-index --quiet --cached --ignore-submodules HEAD ||
    abort "index has changes."
}

inside-git-repo() {
  (git rev-parse --is-inside-work-tree &> /dev/null)
}

#------------------------------------------------------------------------------
# Test overrides:
#------------------------------------------------------------------------------
if [ -n "$GIT_HUB_TEST_RUN" ]; then
  source git-hub-test-run
fi

if [ -n "$GIT_HUB_TEST_MAKE" ]; then
  source git-hub-subclass
fi

#------------------------------------------------------------------------------
# Begin at the end!
#------------------------------------------------------------------------------
[ "${BASH_SOURCE[0]}" == "$0" ] && main "$@"

true

# vim: set lisp:
