適当にブランチを作成・マージした履歴を持つGitリポジトリを作成するシェルスクリプト

Gitのブランチ操作に関して練習したり、その方法を記事に纏めたりしたい。そのためには**多少複雑なコミット履歴が必要になる**のだが、どう用意しようか困っていた。

* 手動で作る
	* 10コミット作るのも大変
	* ランダム性が低く、あまり複雑な形にならない
* 実際のOSSのリポジトリを使う
	* 手頃なものを見つける必要がある
	* 間違えても本家にpushしてはいけない
	* 練習とはいえ、意味の無いコミットをしづらい
* 実業務のリポジトリを使う
	* 論外 最悪クビになる（だけでは済まないかもしれない…）

なので、**シェルスクリプトで自動的に作ることにした**。自動化するためには普段使わないようなgitコマンドやオプションなどが必要だが、一度書いてしまえば済むので案外問題にならなかった。

## 実行例
後に示す `create_git_sample.sh` で、20コミットのリポジトリを作ってみる。

```shell-session
$ mkdir -p ~/projects/git-test && cd ~/projects/git-test

$ bash ~/create_git_sample.sh 20
#1: grow branch_1
#2: grow branch_2
#3: grow branch_3
#4: grow branch_1
#5: grow branch_5
#6: merge branch_5
#7: grow branch_2
#8: grow branch_8
#9: grow branch_3
#10: grow branch_10
#11: merge branch_1
#12: grow branch_3
#13: grow branch_2
#14: merge branch_3
#15: grow branch_10
#16: grow branch_16
#17: merge branch_16
#18: grow branch_10
#19: grow branch_8
#20: grow branch_20

$ git branch
  branch_10
  branch_2
* branch_20
  branch_8
  master

$ git log --all --graph --pretty='format:%h  %s%d%n'  # 結果は以下
```

![git-test-tree.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/127130/e42b21f6-2c14-ca02-044f-67f2274d5bd4.png)

rebaseなどの練習ができそうな、いい感じに複雑な履歴になっている。

リポジトリを消すときは次のコマンドを使う。（他のリポジトリのディレクトリで実行しないこと）

```bash
rm -rf * .gitignore .git/
```

## スクリプト
動作は以下の通り。

0. リポジトリを新規作成する
0. 指定回数だけ以下のいずれかを実行する
	* ランダムに選んだブランチを伸ばす
		* masterや現在と同じブランチを選んだときは、ブランチを新規作成して伸ばす
		* ファイルに書き込むデータは何でもいいので、とりあえず `git log --oneline` の結果としている
	* ランダムに選んだブランチをmasterにマージして削除する
		* master自身を選んでしまったときは、上記のブランチを伸ばす操作に切り替える

Gitのバージョンは `2.7.4` 。別のバージョンではオプションが使えなかったり、もっと良いオプションが使える可能性がある。

```bash:create_git_sample.sh
#!/bin/bash
set -eu

function setup_git_repository() {
	git init
	git config --local user.email "xxx@example.com"
	git config --local user.name  "xxx"

	cat << EOF > .gitignore
!*.txt
!.gitignore
EOF
	git add .
	git commit -m "commit #0"
}

function show_branch_name() {
	git symbolic-ref --short HEAD
}

function pick_branch_name() {
	git for-each-ref --format='%(refname:short)' 'refs/heads/*' | shuf -n1
}

function grow_branch() {
	current_branch="$(show_branch_name)"
	target_branch="$(pick_branch_name)"
	if [ -z "${target_branch}" \
	     -o "${target_branch}" = "master" \
	     -o "${target_branch}" = "${current_branch}" ]; then
		target_branch="branch_${i}"
		git branch "${target_branch}" "$(shuf -e -n1 -- 'HEAD' 'master')"
	fi
	git checkout "${target_branch}" 2> /dev/null

	git log --oneline > "${target_branch}.txt"
	git add .
	git commit -m "commit #${1}"

	echo "#${1}: grow ${target_branch}" 1>&2
}

function merge_branch() {
	target_branch="$(pick_branch_name)"
	if [ -z "${target_branch}" ]; then return 1; fi
	if [ "${target_branch}" = "master" ]; then return 2; fi

	git checkout master 2> /dev/null
	git merge --no-ff -X "theirs" -m "commit #${1}" -- "${target_branch}"
	git branch -d "${target_branch}"

	echo "#${1}: merge ${target_branch}" 1>&2
}

#--- main ---#

if [ -d ".git" ]; then
	echo "repository already exists." 2> /dev/null
	exit 1
fi

setup_git_repository > /dev/null

for i in $(seq "${1}"); do
	case "$(shuf -e -n1 -- 'grow' 'grow' 'merge')" in
		"grow"  ) grow_branch "${i}" ;;
		"merge" ) merge_branch "${i}" || grow_branch "${i}" ;;
	esac
done > /dev/null
```

### コマンド等のメモ

* シェルコマンド
	* `set` : シェルの動作を設定・確認する
		* `-e` : コマンドがエラーを返したら即座に終了する（if文などエラーを利用するものは除く）
		* `-u` : 未設定の変数を使ったらエラーとする
	* `shuf` : 入力の並びをランダムにする
		* `-n` : 出力個数を制限する → 1個にすると、**ランダムに1個を選択**してくれる
		* `-e` : コマンドライン引数を入力列とする
* gitコマンド
	* `symbolic-ref` : 今回は現在のブランチ名を取得するのに使っている
	* `for-each-ref` : 今回はブランチ一覧を取得するのに使っている ※ `git branch` では出力を改めて整形する必要がある
	* `merge` : ブランチをマージする
		* `--no-ff` : fast-forwardを使わない → 「マージした」という履歴を必ず残せる
		* `-X` : マージ戦略を指定する → 今回はconflictで手動対処が必要になるのを防ぐために使っている
			* `theirs` : conflict時は相手の版を採用する

## 課題

* ランダムなので、同じ形のリポジトリを再現できない
	* 生成手順を保存して流し込めればいい？（ただしコミットのハッシュ値は変わる）
* いい具合に複雑な履歴とならないことがそこそこある
	* `shuf -e -n1 -- 'grow' 'grow' 'merge'` と同じ引数を入れて、確率を調整してみている
	* 新しいブランチを作る時も、 `shuf -e -n1 -- 'HEAD' 'master'` としてmasterから分岐する確率を上げている

## 参考

* [How to pick a random element from the output of a command? - Unix & Linux Stack Exchange](https://unix.stackexchange.com/q/269422)
* [How to get the current branch name in Git? - Stack Overflow](https://stackoverflow.com/q/6245570/9140111)
* [全てのgitブランチを取得したいならfor-each-refコマンドが使えるかも - エンジニアはこわくない](http://tsujitaku50.hatenablog.com/entry/2017/04/20/220934)
* [Git - git-merge Documentation](https://git-scm.com/docs/git-merge)