gitのカスタムコマンドで自動バージョン更新＆タグ付けする

開発しているコードをリリースするときにバージョンを更新するのは地味に面倒です。

- [semver](https://semver.org/) に則って新バージョンを決める or カスタムで決める
- package.json 等の管理情報のバージョンを書き換えて commit する
- 最終的に `git tag` でバージョンのタグ付けを行う

リリースのたびにこのような定型的な作業を行うのは大変なのと、人力でやることでミスを誘発する可能性も高くなるので、これらの一連の作業を自動で行うカスタムコマンド `git-release` を作って安全・安心なタグ付けをしましょう。

## できること

```
Usage: git release [-p] [<newversion> | major | minor | patch]
```

`git release NEWVERSION` で、指定したバージョンのタグを付与します。
package.json or build.sbt がある場合には、ファイル内のバージョン情報を上書きして commit します。

package.json or build.sbt で semver を利用している場合、以下のような自動 semver 更新が利用可能です。

`git release patch` で 1.1.1 -> 1.1.2 のように patch 部分がインクリメントされます。
`git release minor` で 1.1.1 -> 1.2.0 のように minor 部分がインクリメントされます。
`git release major` で 1.1.1 -> 2.0.0 のように major 部分がインクリメントされます。

`git release -p patch` のように `-p` をつけると、origin に自動で push されます。

## 動作条件

- git-flow（[gitflow-avh](https://github.com/petervanderdoes/gitflow-avh) 等）を導入済みであること
    - Mac であれば `brew install git-flow-avh` で OK
- git リポジトリが `git flow init` されていること

> リリース時のタグ付けを `git flow release` で行っているだけなので、その他のブランチ運用を行っている場合は適宜個々人で改修してください。

## 設置方法

以下の bash script を`chmod 755`した上でパスの通っているところに設置するだけです。
> `/usr/local/bin` 等に設置しても良いですが、`~/bin` 等 $HOME 以下に閉じたほうが良さげ。

```bash:git-release
#!/bin/bash
set -e

usage() {
  echo "Usage: git release [-p] [<newversion> | major | minor | patch]" >&2
  exit 1
}

getVersion() {
  local file=$1
  case $file in
    package.json)
      echo $(grep '"version":' $file | cut -d'"' -f4)
      ;;
    build.sbt)
      echo $(grep 'version\s*:=' $file | cut -d'"' -f2)
      ;;
  esac
}

setVersion() {
  local file=$1
  local version=$2
  case $file in
    package.json)
      sed -i '' -e '/"version"/s/"[^"]*",$/"'$version'",/' $file
      ;;
    build.sbt)
      sed -i '' -e '/^version :=/s/"[^"]*"$/"'$version'"/' $file
      ;;
  esac
}

incSemver() {
  local version=$1
  local type=$2
  local semver=($(echo $version | tr '.' ' '))

  if [ ${#semver[@]} -ne 3 ]; then
    echo "$version is not semver." >&2
    exit
  fi

  case $type in
    major)
      semver[0]=$(expr ${semver[0]} + 1)
      semver[1]=0
      semver[2]=0
      ;;
    minor)
      semver[1]=$(expr ${semver[1]} + 1)
      semver[2]=0
      ;;
    patch)
      semver[2]=$(expr ${semver[2]} + 1)
      ;;
  esac

  echo $(echo ${semver[@]} | tr ' ' '.')
}

while getopts hp OPT
do
  case $OPT in
    p)
      USEPUSH=1
      ;;
    h)
      usage
      ;;
    *)
      usage
      ;;
  esac
done
shift $((OPTIND - 1))

if [ $# -eq 0 ]; then
  echo "VERSION required." >&2
  usage
fi

VERSION=$1

for VERFILE in package.json build.sbt ""; do
  [ -f $VERFILE ] && break
done

case $VERSION in
  major|minor|patch)
    if [ -z $VERFILE ]; then
      echo "package.json or build.sbt not found." >&2
      exit 1
    fi
    VERSION=$(incSemver $(getVersion $VERFILE) $VERSION)
    ;;
esac

git flow release start $VERSION > /dev/null

if [ -n "$VERFILE" ]; then
  setVersion $VERFILE $VERSION
  git add $VERFILE
  git ci -m "build: bump version to $VERSION" > /dev/null
fi

GIT_MERGE_AUTOEDIT=no git flow release finish -m release $VERSION > /dev/null

if [ -n "$USEPUSH" ]; then
  git push origin $VERSION
  git push origin master
  git push origin develop
fi

echo "$VERSION released!"
```

## 最後に

類似のものは他にもありますが、こういった類のものは行いたい処理が環境によって異なるのと、1スクリプト＝導入がシンプル＆カスタマイズ性が高いので、この程度であれば自作のほうが使い勝手が良いと思います。

github 上には [git-semver](https://github.com/markchalloner/git-semver) のようなツールもありますので、より高度なことを行いたい場合には検討してみても良いでしょう。