gitでラクに納品するためのカスタム`git archive`

# 前書き
こんにちは，@ukaznilです。普段は大学院で機械学習研究の傍ら，趣味で[Androidアプリ](https://play.google.com/store/apps/developer?id=ukaznil)をちまちま作って公開しています。[サイト](http://ukaznil.com)もよければご覧ください。

さて今回はエンジニアのの強い味方，普段の生活に役立つ__「git」__をちょっと便利にするTipをご紹介しようと思います。



# gitで「納品」してますか？
もはやエンジニアの必須ツールであるgit。趣味でプログラムを書いている方はもちろん，お仕事で日々コミットしている方も多いですよね。
ご存じの通り，gitの素晴らしい点はその履歴が取れることです。変更の多いプログラムを書いているのであれば，「あとから戻れる」という安心感は何事にも変えられませんね。

さて，それでは「開発」が終わった後のことを考えましょう。
趣味として個人でプログラムを書いているのであればあまり関係のないことかもしれませんが，gitで管理しつつ作成したプロジェクト・ソースコードを他人に渡す，という機会は少なからず存在します。むしろ，他人に渡すために作っているものをgit管理している，と言った方が正しいのかもしれませんね。

__そんな時，どうやってソースコードを「納品」すればいいのでしょう？__

普通にプロジェクトをzip化すると，いわゆる「gitのゴミ」（`.git/`とか`.gitignore`とか）みたいなものも含まれてしまいます。せっかくgit管理下ではignoreしていたのに，納品段階に至って手動で削除するような事態になれば二度手間です。
というか二度手間で済めば良い方で，場合によっては本当に必要なファイルを消してしまって納品先でコンパイルが通らない，なんてことになってしまったら目も当てられません。キーなどの機密情報を消し忘れたら，なんて考えたくもない人もいらっしゃると思います。

これを，どう自動化するか。
__`.gitignore`で無視しているファイルを含めない形で，ラクに圧縮・納品したい。__

これが今回の目的になります。



# ポイント
考えるにあたってのポイントは，__「gitで管理されているものだけを圧縮すればよい」__という至極簡単なものです。

この考え方は，「相手に渡したくないものはgitにも管理させたくない」という意識・事実に依存しています。APIキーなどはもちろんですが，プログラムによっては自動生成されるもの（Android Studioであれば`*.iml`ファイルや`/build/`ディレクトリなど）は通常`.gitignore`で無視していると思います。

そう思ってgitのコマンドを調べていくと，`git archive`という，まさにぴったりなものが見つかりました。
> git archive - Create an archive of files from a named tree
> https://git-scm.com/docs/git-archive

更に調べていくと，同様の取り組みをされている方も多く見つかります。下のリンクはあくまで参考ですが，皆さん似たようなことを考えています。
> Git で管理しているファイルをアーカイブする
> http://tnakamura.hatenablog.com/entry/20090508/1241789584

> git archiveでgzip以外の圧縮フォーマットを使用する
> http://qiita.com/habu1010/items/53e9f22abf593a20bbd1

しかし，実際使っていくには関数化していくのがよいでしょう。
さぁ実装するぞ，という段になって，いくつか標準の`git archive`だけでは達成できない要求があることに気がつきました。

- いちいち出力先のzipファイル名を決めるのが面倒。
    - どのコミットを出力したものなのか，あとからでも一目で分かるような名前にしたい。
- git statusがcleanでない状態では出力できないようにしたい。
    - 上記とも関連しますが，コミットの状態と出力ファイルを一意に対応づけるためです。
- git管理されているファイルのみ含めるのはいいが，例えば`.gitignore`などは除外したい。
    - 見落としがちなのですが，`.gitignore`もしっかりgitに追跡されているので`git archive`には含まれてしまいます。でも渡された側からすれば，`.gitignore`ファイルは無用の長物なのでなるべく含めたくはないのです。

まぁざっくり言えば上のような要求仕様を満たす関数を作る必要がありました。
上の2つはgitコマンドでどうにもなりそうですが，問題は3つ目の「git管理されているけど含めたくないファイル」，具体的には`.gitignore`ファイルの存在です。

さらに調べていくと，`.gitattributes`というファイルの存在を知りました。
> gitでsvn export的なことをやる
> http://qiita.com/scalper/items/1905b47209989dda5648

どうやら，`.gitattributes`というファイルに`export-ignore`と追記することで，`git archive`の対象から除外されるらしい。見事に，欲しい機能を見つけました。

つまるところ，`.gitattributes`には以下のような記述があれば事足りることになります。

```shell-session:.gitattributes
*~ export-ignore # backupファイルは絶対に納品しない
.DS_Store export-ignore # DS_Storeファイルも絶対いらない
.gitignore export-ignore # .gitignoreだけもらっても，先方には無用の長物
.gitattributes export-ignore # .gitattributes自身も納品する必要はない←ポイント！
```

当然，このような`.gitattributes`を自前で書くのは面倒なので自動生成してしまいましょう。
実現の目処が立ったので，あとは関数をガリガリ書いていくだけです。



# できました
結果，できあがったものが以下になります。

zsh環境ではfunction名にハイフン(-)が使えるので，`git-archive`として私は運用していますが，bash環境の方のことも考えて以下では`git_archive`と表記しています。


```shell-session:.bashrc/.zshrc
function git_archive() {
  # 現在の場所
  readonly local CURR_DIR=`\pwd`

  # gitリポジトリのroot
  readonly local REPOSITORY_DIR=`\git rev-parse --show-toplevel 2> /dev/null`

  # gitリポジトリかチェック
  if [ -z "${REPOSITORY_DIR}" ]; then
    echo '### This is not the repository root'
    return
  fi

  # リポジトリrootにcd
  \cd ${REPOSITORY_DIR} > /dev/null

  # .gitattributesの作成（存在していなかった場合）
  readonly local GIT_ATTRIBUTES_FILENAME='.gitattributes'
  if [ ! -f ${GIT_ATTRIBUTES_FILENAME} ]; then
  {
    echo '*~ export-ignore'
    echo '.DS_Store export-ignore'
    echo '.gitignore export-ignore'
    echo "${GIT_ATTRIBUTES_FILENAME} export-ignore"
  } > ${GIT_ATTRIBUTES_FILENAME}
  fi

  # リポジトリがcleanかチェック
  if [ -n "$(\git status --porcelain)" ]; then
    echo '### There are uncommited changes'
    \git status
    \cd ${CURR_DIR} > /dev/null
    return
  fi

  # ディレクトリ名取得，先頭のドットがあれば除去する
  readonly local REPOSITORY_DIRNAME=`echo $(\basename ${REPOSITORY_DIR}) | sed s:^[\.]*::`

  # パス取得
  readonly local REPOSITORY_PARENT_DIR=`\dirname ${REPOSITORY_DIR}`

  # ブランチ名取得
  readonly local BRANCH_NAME=`echo $(\git symbolic-ref --short HEAD) | sed s:/:-:g`

  # hash値取得
  readonly local HASH=`\git rev-parse --short=7 HEAD`

  # 納品!!
  readonly local ZIP_NAME="${REPOSITORY_PARENT_DIR}/${REPOSITORY_DIRNAME}-${BRANCH_NAME}-${HASH}.zip"
  \git archive --format=zip HEAD > ${ZIP_NAME} && {
    echo '#========#'
    echo '# Result #'
    echo '#========#'
    echo "Archived this repository as ${ZIP_NAME}"
  }

  # 元の場所に戻る
  \cd ${CURR_DIR} > /dev/null
}
```
cdコマンドのあとに`> /dev/null`しているのは，私のzsh環境が「`cd`したあとに自動で`ls`」設定になっているためです。不要なら削除してください。

__上のfunctionをお使いのシェルの設定ファイルに追加するだけで，魔法の呪文`git_archive`が使えるようになります！__


# 使い方についての補足
説明の都合上，git管理されているプロジェクトのパスを`/path/to/hoge/`と表記します（つまり`/path/to/hoge/.git/`が存在するということです）。

- git管理されているプロジェクトの，どこにいても`git_archive`関数は利用できます。
    - `/path/to/hoge/piyo`とかにいる状態でも問題ありません。
- 出力zip名は，`${projectName}-${branchName}-${commitHash}.zip`です。hashは先頭7桁を切り出すようにしています。
    - 今回の例では，`hoge-master-a1b2c3d.zip`のようになります（masterブランチの場合）。
    - ブランチがfeatureの場合（`feature/some_impl`など），`${brachName}`は`feature-some_impl`などのようにスラッシ(/)ュはハイフン(-)に置換されます。
    - （好みの問題だと思いますが）ドット(.)から始まるzipが気持ち悪いので，`${projectName}`は先頭にドットがあったら消しています。例えば，`.zsh.d/`以下を`git_archive`する場合，`zsh.d-${branchName}-${commitHash}.zip`のようになります。
- zipの出力先はプロジェクトの親ディレクトリです。
    - つまり，`/path/to/hoge-master-a1b2c3d.zip`が生成されます。
- statusがcleanでない場合は出力できません。これは，いつ，どのタイミングでzip化したとしてもzipの内容に一意性を持たせるためです。



以上です。
長文にお付き合いいただき，ありがとうございました。

__皆さんもカスタム`git archive`で楽しい納品ライフを！！__