Pull-requestをレビューする時に使う関数を書いた

# 2019.09.12 追記

[git-extras](https://github.com/tj/git-extras) というのがあった。このレポジトリの中の `git-pr` コマンドがまさにやりたかったことだ。皆さんにはぜひこれを使ってほしい。

以下は shell command を function で作るための参考記事として読んでほしい。

----

# 概要

https://qiita.com/tarr1124/items/d807887418671adbc46f

以下のコマンド

```bash
$ git fetch <remote-name> pull/<ID>/head:<branch-name>
```

でプルリクエストを簡単に fetch して手元で実行できるらしい。

コマンドがわかりにくいし打ちにくいので、shell function にした。

ついでに、引数で`ID`や`remote-name`を受け取れるようにした。

# できたもの

```shell-session
(master) $ git-fetch-pull-request 42
Execute git fetch upstream pull/42/head:pull-request/42...
remote: Counting objects: ---, done.
remote: Compressing objects: 100% (---/---), done.
remote: Total --- (delta ---), reused --- (delta ---)
Unpacking objects: 100% (---/---), done.
From https://---
 * [new branch]      refs/pull/42/head -> pull-request/42
Checkout to pull-request/42...
Switched to branch 'pull-request/42'
(pull-request/42) $ 
(pull-request/42) $ git-fetch-pull-request --help
Usage: git-fetch-pull-request [-h,--help] [--upstream UPSTREAM] NUMBER

Fetch pull request to a new branch and checkout to it.

Positional arguments:
  NUMBER (int): id for the target pull request.

Optional arguments:
  -h, --help: show this message and exit.
  --upstream UPSTREAM (str): remote upstream name. default: 'upstream'.
(pull-request/42) $ 
```

# 引数を処理する

## やりたいこと

`ID`は必須項目。`remote-name`はdefaultを用意して、引数によって変えられるようにしたい。`branch-name`は固定で良い。

せっかくなので、`<function-name> --help` とか打ったら usage が出てほしい。

## やったこと

http://dojineko.hateblo.jp/entry/2016/06/30/225113
ここを参考に、こんな感じで実装した。

```bash
git-fetch-pull-request() {
  local function_name='git-fetch-pull-request'

  local help="Usage: ${function_name} [-h,--help] [--upstream UPSTREAM] NUMBER

Fetch pull request to a new branch and checkout to it.

Positional arguments:
  NUMBER (int): id for the target pull request.

Optional arguments:
  -h, --help: show this message and exit.
  --upstream UPSTREAM (str): remote upstream name. default: 'upstream'.
  "

  _git-fetch-pull-request() {
    local number=$1
    local upstream=$2

    # ...
    # メイン処理
    # ...
    
    return 0
  }

  show_help () {
    echo $help
  }

  invalid_arguments () {
    local argument=${1:-}
    echo "${function_name}: illegal option ${argument}"
    show_help
  }

  parameter_required () {
    local argument=${1:-}
    echo "${function_name}: argument required for ${argument}"
    show_help
  }

  # Parse optional arguments
  local upstream='upstream'
  local PARAM=()
  for OPT in "$@"; do
    case $OPT in
      '-h' | '--help' )
        show_help
        return 0
        ;;
      '--upstream' )
        if [[ -z $2 ]] || [[ $2 =~ ^-+ ]]; then
          parameter_required '--upstream'
          return 1
        fi
        upstream=$2
        shift 2
        ;;
      '--' | '-' )
        # Treat following all arguments as positional arguments
        shift
        PARAM+=( "$@" )
        break
        ;;
      -* )
        invalid_arguments $1
        return 1
        ;;
      * )
        if [[ -n $1 ]] && [[ ! $1 =~ ^-+ ]]; then
          PARAM+=( "$1" )
          shift
        fi
        ;;
    esac
  done
  # Get positional arguments
  number=$PARAM; PARAM=( "${PARAM[@]:1}" )
  if [[ -z $number ]]; then
    parameter_required 'NUMBER'
    return 1
  fi
  # Check remains
  if [[ -n "${PARAM[@]}" ]]; then
    invalid_arguments $PARAM
    return 1
  fi

  # Main
  _git-fetch-pull-request ${number} ${upstream}
}
```

## 解説

来たオプションを前から順番に処理して、処理できないものはとりあえず`PARAM`に詰めていく。
途中`--upstream`のような追加引数のあるオプションが来た場合、その次まで読んで、その分2回`SHIFT`する。

最後に残った`PARAM`を処理して終了。メイン処理（`_git-fetch-pull-request()`）に入る。

# `Fetch`・`Checkout`

## やりたいこと

前項で引数をいい感じに処理して`ID`と`remote-name`を得たので、それを使って`git fetch`する。

git repository 以外で叩いたときや、存在しないプルリクに対して実行したときには、適切にエラーを吐いて止まってくれるようにする。

## やったこと

```bash
_git-fetch-pull-request() {
  local number=$1
  local upstream=$2

  local branch_name="pull-request/${number}"
  if [[ -z `git rev-parse --is-inside-work-tree 2> /dev/null` ]]; then
    # Not a git repo
    echo "Not a git repository (or any of the parent directories). Stop."
    return 1
  fi
  if [[ -n `git branch --list ${branch_name}` ]]; then
    # Branch exists
    echo "Branch ${branch_name} already exists. Stop."
    return 1
  fi
  if ! git remote show ${upstream} 2>&1 > /dev/null; then
    # Upstream not exists
    echo "Remote ${upstream} not exists. Stop."
    return 1
  fi

  # Fetch
  local fetching="git fetch upstream pull/${number}/head:${branch_name}"
  echo "Execute ${fetching}..."
  if ! eval ${fetching}; then
    echo "Something went wrong. Stop."
    return 1
  fi

  # Checkout
  local checkout="git checkout ${branch_name}"
  echo "Checkout to ${branch_name}..."
  if ! eval ${checkout}; then
    echo "Something went wrong. Stop."
    return 1
  fi
  
  return 0
}
```

何が起こるかわからないので、丁寧にエラー処理をする。
Git の repository にいるかどうかの判定は、ここ ( https://stackoverflow.com/questions/2180270/check-if-current-directory-is-a-git-repository ) を参考にした。

せっかくなので、 `checkout` もしてもらった。

# 最終的なコード

以下のようになった。
これを自分の`.<shell-name>rc`に書き込んで完成。

```bash
git-fetch-pull-request() {
  local function_name='git-fetch-pull-request'

  local help="Usage: ${function_name} [-h,--help] [--upstream UPSTREAM] NUMBER

Fetch pull request to a new branch and checkout to it.

Positional arguments:
  NUMBER (int): id for the target pull request.

Optional arguments:
  -h, --help: show this message and exit.
  --upstream UPSTREAM (str): remote upstream name. default: 'upstream'.
  "

  _git-fetch-pull-request() {
    local number=$1
    local upstream=$2

    local branch_name="pull-request/${number}"
    if [[ -z `git rev-parse --is-inside-work-tree 2> /dev/null` ]]; then
      # Not a git repo
      echo "Not a git repository (or any of the parent directories). Stop."
      return 1
    fi
    if [[ -n `git branch --list ${branch_name}` ]]; then
      # Branch exists
      echo "Branch ${branch_name} already exists. Stop."
      return 1
    fi
    if ! git remote show ${upstream} 2>&1 > /dev/null; then
      # Upstream not exists
      echo "Remote ${upstream} not exists. Stop."
      return 1
    fi

    # Fetch
    local fetching="git fetch upstream pull/${number}/head:${branch_name}"
    echo "Execute ${fetching}..."
    if ! eval ${fetching}; then
      echo "Something went wrong. Stop."
      return 1
    fi

    # Checkout
    local checkout="git checkout ${branch_name}"
    echo "Checkout to ${branch_name}..."
    if ! eval ${checkout}; then
      echo "Something went wrong. Stop."
      return 1
    fi
    
    return 0
  }

  show_help () {
    echo $help
  }

  invalid_arguments () {
    local argument=${1:-}
    echo "${function_name}: illegal option ${argument}"
    show_help
  }

  parameter_required () {
    local argument=${1:-}
    echo "${function_name}: argument required for ${argument}"
    show_help
  }

  # Parse optional arguments
  local upstream='upstream'
  local PARAM=()
  for OPT in "$@"; do
    case $OPT in
      '-h' | '--help' )
        show_help
        return 0
        ;;
      '--upstream' )
        if [[ -z $2 ]] || [[ $2 =~ ^-+ ]]; then
          parameter_required '--upstream'
          return 1
        fi
        upstream=$2
        shift 2
        ;;
      '--' | '-' )
        # Treat following all arguments as positional arguments
        shift
        PARAM+=( "$@" )
        break
        ;;
      -* )
        invalid_arguments $1
        return 1
        ;;
      * )
        if [[ -n $1 ]] && [[ ! $1 =~ ^-+ ]]; then
          PARAM+=( "$1" )
          shift
        fi
        ;;
    esac
  done
  # Get positional arguments
  number=$PARAM; PARAM=( "${PARAM[@]:1}" )
  if [[ -z $number ]]; then
    parameter_required 'NUMBER'
    return 1
  fi
  # Check remains
  if [[ -n "${PARAM[@]}" ]]; then
    invalid_arguments $PARAM
    return 1
  fi

  # Main
  _git-fetch-pull-request ${number} ${upstream}
}
```