多すぎるブランチをまとめて削除するためにやったこと

こんにちは。リブセンスのエンジニアのkurobaraです。

リブセンスでは、会社の口コミサイトである転職会議のメディアチームに所属しています。

一応、転職会議メディアを開発することもありますが、基本は以下のことをやっています。

+ 転職会議メディアに関係する業務システムの開発(一応メイン担当)
+ サーバ運用監視
+ サーバ構成検討
+ etc...

チーム内で転職会議メディア開発以外の雑多なこと全部やっているという感じでしょうか。

### 今回のお話の発端
---
転職会議のメディアチームでは毎週１度チーム内のエンジニアでミーティングを行っています。

その時に以下の内容が議題に出ました

**「git branch --all が酷いのでMergedなブランチを一括削除したいと思います。」**

というのが発端だったりします。

因みにミーティング中、この議題に対して以下の内容が出てました

+ 古すぎるものやメンバーですら把握するのが難しいブランチがあるよね？
+ (議題に上がった時期にABテスト等を実施していたので)簡単に削除できないよね？
+ 爆発的に増えた要因は、[40日で登録数2.23倍。転職会議が行った32回のABテスト【前編:効果的なKPIとテスト設計のコツ】](http://jp-blog.kaizenplatform.com/tensyokukaigi_abtest)の結果では？

### というわけで軽い気持ちでブランチ数を数えてみる
---
( ＾ω＾) 今のブランチ数はっと  
  
( ＾ω＾)つ ッターン  

```
$git branch --remote | wc -l
289
```

(゜o゜;  
  
  
( ＾ω＾;) **開発人数に対して289個って多すぎやしませんかね・・・**
  
  
( ＾ω＾) ブランチ数多いし、これ手作業でやると大変だな


### 「40日で登録数2.23倍」の実施期間はどれくらいブランチ数増えてたの？
---
( ＾ω＾) 当該期間だけで、増えたブランチ数はっと・・・

|開始前ブランチ数|終了後ブランチ数|
|:--:|:--:|
|94個|174個|

(゜o゜;  期間中に80も増えてる・・・

( ＾ω＾) **登録数も２倍にしたけど、ブランチ数も2倍になってるwww**

(´；ω；｀)ﾌﾞﾜｯ < その前からブランチ運用がザルとか言わないで・・・

### そして話し合う・・・
---
( ＾ω＾) 要件決まらないなぁ・・・  
  
( ＾ω＾;) 数多いし、もはやどれが重要か把握出来ないんだろうなぁ  
  
(`･ω･´) とりあえず、それっぽいの作り始めるか  
  
最終的に決まった要件は以下の内容

+ 最終コミットが削除実行日よりも３ヶ月前のブランチは全て削除してもよい
+ masterなど、チーム内運用で使っている特別なブランチは削除してはいけない

### ちょっと面倒臭そうなものを調べる
---
( ＾ω＾) 実現できそうかどうか確認しておくか

##### 全ブランチを取得

```
$git branch --all
* master
  test
  hoge
  remotes/origin/hoge
  remotes/origin/fuga
```

##### hogeブランチで、最終コミットの日付だけを取得
```
$git log -n 1 --date=short --pretty=format:"%cd" hoge
2014-12-14
```

##### リモートにあるhogeブランチを削除
```  
git push origin :hoge
```

( ＾ω＾) いけるな！！

### つくった
---
色々雑だけど、5分かからず実装・・・

(｀・ｖ・´) ﾄﾞﾔｯ!

```
require 'rubygems'
require 'chronic'

NOT_DELETE_BRANCHES = ["remotes/origin/HEAD", "remotes/origin/master", "remotes/origin/hoge", "remotes/origin/HEAD -> origin/master"]
BRANCH_PREFIX = 'remotes/origin/'
delete_limit = Chronic.parse('3 months ago')

branches = `git branch --all`.split("\n  ")
excluded_branches = (branches - NOT_DELETE_BRANCHES).map { |branch_name| branch_name if branch_name =~ /remotes\/origin\/*/ }.compact
excluded_branches.each do |branch_name|
  date = `git log -n 1 --date=short --pretty=format:"%cd" #{branch_name}`
  last_commit_date = Chronic.parse(date)

  if last_commit_date <= delete_limit
    delete_branch_name = branch_name.gsub(BRANCH_PREFIX, '')
    `git push origin :#{delete_branch_name}`
  end
end
```

文字列から日付処理は面倒だったので、[chronic](https://github.com/mojombo/chronic)を使いました

\# cronで回せば問答無用でブランチ削除できるな

### 一呼吸
---
( ＾ω＾) 作ったので実行してみるか〜  
  
  
( ＾ω＾;) 待て、これそのまま使って事故起きたらやばいぞ・・・
  
  
（(；ﾟДﾟ)ｶﾞｸｶﾞｸﾌﾞﾙﾌﾞﾙ  


手元ならまだしも、リモートの作業をするわけなので確認無かったら怖いですよね

### ブラッシュアップ
---
(`･ω･´) いきなり削除はヤバイから、削除するかのトリガーを引けるようにするか


というわけで、機能を追加

+ 最終コミット内容を表示
+ 削除対象ブランチ名を表示
+ Y/n/qのような感じで実行するかどうかトリガーを引けるようにする


色々と雑だけど、さらっとやるならこんな感じかな

```
loop do
  p "[Y/n/q]> "
  input = $stdin.gets.gsub(/\n/, '')
  next if input.nil? || input.empty?

  if %w{quit exit q}.include? input
    p 'exit'
    exit
  end

  if %w{no N n}.include? input
    break
  end

  if %w{yes Y y}.include? input
    delete_branch_name = branch_name.gsub(BRANCH_PREFIX, '')
    `git push origin :#{delete_branch_name}`
    break
  end
end
```

( ＾ω＾) これで安心して実行できるな

### 最終実行版
---
多分、もっと短くできそうだなぁと思いつつも使い捨てなのでこれでいいだろうという感じ


```
require 'rubygems'
require 'chronic'

SEPARATE_LINE = '##################################################'
NOT_DELETE_BRANCHES = ["remotes/origin/HEAD", "remotes/origin/master", "remotes/origin/hoge", "remotes/origin/HEAD -> origin/master"]
BRANCH_PREFIX = 'remotes/origin/'

delete_limit = Chronic.parse('3 months ago')

branches = `git branch --all`.split("\n  ")

excluded_branches = (branches - NOT_DELETE_BRANCHES).map { |branch_name| branch_name if branch_name =~ /remotes\/origin\/*/ }.compact

excluded_branches.each do |branch_name|
  date = `git log -n 1 --date=short --pretty=format:"%cd" #{branch_name}`
  last_commit_date = Chronic.parse(date)

  if last_commit_date <= delete_limit
    commit_log = `git log -n 1 --date=short #{branch_name}`

    puts SEPARATE_LINE
    puts "branch name : #{branch_name}"
    puts commit_log
    puts SEPARATE_LINE

    loop do
      p "[Y/n/q]> "
      input = $stdin.gets.gsub(/\n/, '')
      next if input.nil? || input.empty?

      if %w{quit exit q}.include? input
        p 'exit'
        exit
      end

      if %w{no N n}.include? input
        break
      end

      if %w{yes Y y}.include? input
        delete_branch_name = branch_name.gsub(BRANCH_PREFIX, '')
        `git push origin :#{delete_branch_name}`
        break
      end
    end
  end
end
```


( ＾ω＾) こんなもの作る時間が勿体無いような・・・

### とりあえず言いたいこと
---
**登録数2倍で、ブランチ数も2倍に増えるので開発やリリースサイクルがいい感じに回っていて嬉しいね**

とはいえ・・・

**マージ済み等で不要になったブランチは削除しましょう!!**

### 明日は
---
ウィザードである@thermesさんです。

よろしくお願いしますm(_ _)m