Technology Engineering

178inaba の技術ブログ

削除済みリモートブランチを追跡しているローカルブランチを1発で全削除するコマンド

リモートブランチはマージ後GitHubが削除するか聞いてくれるのでその時点で削除するようにしている。

f:id:i178inaba:20181103173338p:plain

だが、そのリモートブランチを追跡しているローカルブランチの削除を忘れて溜まっていることがよくある。

$ git branch -v
* develop           fef1cdd Merge pull request #30 from abcdef/fix/bazbarfoo
  feature/foo       e57408f [gone] Fix deploy
  feature/bar       a9b73ad [gone] Upload image
  feature/baz       b833113 [gone] Fix test
  feature/foobar    703e2ad [gone] Add foobar.c
  feature/foobaz    1587581 [gone] Fix foobaz.rb
  feature/barfoo    37265cb [gone] Add barfoo.php
  feature/barbaz    3c121ca [gone] Fix barbaz.py
  feature/bazfoo    cb69767 [gone] Add bazfoo.go
  feature/bazbar    bb8575f [gone] Fix bazbar.pl
  feature/foobarbaz 66d2807 [gone] Fix README.md
  fix/foobazbar     84a781e [gone] Add gitignore
  fix/bazfoobar     cd36a6b [gone] Fix ci
  master            0a41ce8 Merge pull request #20 from abcdef/develop

[gone] が付いているのがリモートブランチへの追跡が切れているローカルブランチ。

いつも git branch -d の後ろに1つずつブランチ名をコピペして削除していた。

$ git branch -d feature/foo feature/bar feature/baz feature/foobar feature/foobaz feature/barfoo feature/barbaz feature/bazfoo feature/bazbar feature/foobarbaz fix/foobazbar fix/bazfoobar
Deleted branch feature/foo (was e57408f).
Deleted branch feature/bar (was a9b73ad).
Deleted branch feature/baz (was b833113).
Deleted branch feature/foobar (was 703e2ad).
Deleted branch feature/foobaz (was 1587581).
Deleted branch feature/barfoo (was 37265cb).
Deleted branch feature/barbaz (was 3c121ca).
Deleted branch feature/bazfoo (was cb69767).
Deleted branch feature/bazbar (was bb8575f).
Deleted branch feature/foobarbaz (was 66d2807).
Deleted branch fix/foobazbar (was 84a781e).
Deleted branch fix/bazfoobar (was cd36a6b).

面倒なので1発コマンド打つだけで全部消えるコマンドを組もうと思い調べた。

コマンド

git for-each-ref --format '%(if)%(upstream:track)%(then)%(refname:short)%(end)' | xargs git branch -d

解説

最初は git branch コマンドを使って作ろうとしていた。
だが、先の記事にも書いた通り、 git branch を使ってスクリプトを書かないほうがいいらしい。

inaba.hatenablog.com

なので git for-each-ref を使って書いた。
これはGitレポジトリの各refの情報を条件に従って出力していくコマンド。

Git - git-for-each-ref Documentation

何点か解説を書いてみる。

ブランチ名

ブランチ名は %(refname:short) で取得。
short をつけないと refs/heads がついてしまい、ブランチ名として認識されないので注意が必要。

# `refs/heads` がついてしまう。
$ git for-each-ref --format '%(refname)'
refs/heads/develop
refs/heads/feature/foo
refs/heads/feature/bar
refs/heads/feature/baz
refs/heads/feature/foobar
refs/heads/feature/foobaz
refs/heads/feature/barfoo
refs/heads/feature/barbaz
refs/heads/feature/bazfoo
refs/heads/feature/bazbar
refs/heads/feature/foobarbaz
refs/heads/fix/foobazbar
refs/heads/fix/bazfoobar
refs/heads/master
refs/remotes/origin/HEAD
refs/remotes/origin/develop
refs/remotes/origin/feature/aaa
refs/remotes/origin/feature/bbb
refs/remotes/origin/ccc
refs/remotes/origin/master
refs/remotes/origin/try/ddd
refs/remotes/origin/try/eee

# shortをつければ正しいブランチ名が取得できる。
$ git for-each-ref --format '%(refname:short)'
develop
feature/foo
feature/bar
feature/baz
feature/foobar
feature/foobaz
feature/barfoo
feature/barbaz
feature/bazfoo
feature/bazbar
feature/foobarbaz
fix/foobazbar
fix/bazfoobar
master
origin/HEAD
origin/develop
origin/feature/aaa
origin/feature/bbb
origin/ccc
origin/master
origin/try/ddd
origin/try/eee

gone

削除対象は [gone] が付いたブランチなのでこれを取得する。

upstream の項で以下のように説明されている。

:track also prints "[gone]" whenever unknown upstream ref is encountered.

試すと

$ git for-each-ref --format '%(upstream:track)'

[gone]
[gone]
[gone]
[gone]
[gone]
[gone]
[gone]
[gone]
[gone]
[gone]
[gone]
[gone]








[gone] のみでは分かりづらいと思うのでブランチ名をつけてみる。

$ git for-each-ref --format '%(upstream:track) %(refname:short)'
 develop
[gone] feature/foo
[gone] feature/bar
[gone] feature/baz
[gone] feature/foobar
[gone] feature/foobaz
[gone] feature/barfoo
[gone] feature/barbaz
[gone] feature/bazfoo
[gone] feature/bazbar
[gone] feature/foobarbaz
[gone] fix/foobazbar
[gone] fix/bazfoobar
 master
 origin/HEAD
 origin/develop
 origin/feature/aaa
 origin/feature/bbb
 origin/ccc
 origin/master
 origin/try/ddd
 origin/try/eee

リモートへの追跡が切れていない develop, master やリモートにのみ存在しているブランチには [gone] がついておらず、リモートへの追跡が切れたローカルブランチにのみ [gone] がついている事がわかる。

if

git for-each-ref ではif文( %(if)…​%(then)…​%(end) )が使える。
if文を使い、 [gone] があるときだけブランチ名を表示するようにする。

$ git for-each-ref --format '%(if)%(upstream:track)%(then)%(refname:short)%(end)'

feature/foo
feature/bar
feature/baz
feature/foobar
feature/foobaz
feature/barfoo
feature/barbaz
feature/bazfoo
feature/bazbar
feature/foobarbaz
fix/foobazbar
fix/bazfoobar








xargs

取得できたブランチ名をxargsを使って git branch -d に流す。 ちなみにxargsだけ実行すると git branch -d にどんな値が渡っているのかわかる。

$ git for-each-ref --format '%(if)%(upstream:track)%(then)%(refname:short)%(end)' | xargs
feature/foo feature/bar feature/baz feature/foobar feature/foobaz feature/barfoo feature/barbaz feature/bazfoo feature/bazbar feature/foobarbaz fix/foobazbar fix/bazfoobar

こんな感じ。

別解

サブコマンドとして実行してもよい。

git branch -d $(git for-each-ref --format '%(if)%(upstream:track)%(then)%(refname:short)%(end)')

$(git branch)でカレントディレクトリのファイル一覧も出てしまう時の対処法

とあるシェルでgitのブランチ一覧を取りたくて echo $(git branch) と書いたのだが、なぜかファイル一覧も出てしまい戸惑った。

前提

下記のようなディレクトリがあって、それがgit管理されていたとする。

$ ls
README.md   main.go     main_test.go

そして以下のようなブランチが存在する。

$ git branch
  develop
  feature/bar
  feature/baz
  feature/foo
* master

事象

echo $(git branch) を実行すると下記のようにブランチ一覧の間にファイル一覧が差し込まれてしまう。

$ echo $(git branch)
develop feature/bar feature/baz feature/foo README.md main.go main_test.go master

原因

勘のいい人はもうお分かりかもしれませんが、 git branch した時にmasterの前に * がついていますよね。
それが echoワイルドカードとみなされてファイル一覧が出てしまいます。

$ echo *
README.md main.go main_test.go

と同じになるわけです。

対処法

ダブルクオートで囲めばワイルドカードが展開されずに出力されます。

$ echo "$(git branch)"
  develop
  feature/bar
  feature/baz
  feature/foo
* master

ブランチ一覧を1行で取得したい場合

tr を使って * をスペースに変換したリストをダブルクオート無しで echo すれば取得可能です。

$ echo $(echo "$(git branch | tr '*' ' ')")
develop feature/bar feature/baz feature/foo master

でも、そもそも git branch を使ってスクリプトを書かないほうがいいらしい。

stackoverflow.com

上記によれば、Gitはスクリプトで使用するために明示的に設計されたplumbingインターフェースを提供しているとのこと。
なので自分はplumbingインターフェースを使ってみようと思います。

参考

stackoverflow.com

特定のディレクトリ内にあるjavascriptを全てMinifyする

例えば static/js ディレクトリにjsが置いてあるとする。

$ tree
.
└── static
    └── js
        ├── bar.js
        ├── baz.js
        └── foo.js

そのディレクトリ内にあるjavascriptを全てMinifyしたい場合のbashスクリプト

for f in $(ls static/js/*.js); do
  uglifyjs -cm -o "${f/.js/.min.js}" "${f}"
done

Minifyには uglify-js を使用する。
オプションの c が単純なソース圧縮、 m が変数名の圧縮(1文字にする)、 o はアウトプットするファイルを指定。

これを minify.sh というファイル名で作成し、実行する。
注意点としては ${f/.js/.min.js}bashの文字列置換を使用しているのでbashで実行すること。

$ bash minify.sh

すると全てのjavascriptがMinifyされる。

$ tree
.
├── minify.sh
└── static
    └── js
        ├── bar.js
        ├── bar.min.js
        ├── baz.js
        ├── baz.min.js
        ├── foo.js
        └── foo.min.js

ちなみにMinifyしたjavascriptをgit管理したくない場合は .gitignore*.min.js を入れると無視してくれる。

もうひと工夫

シバンを入れて実行権限をつけるとスッキリして良い。
実行権限をつけるのでファイル名も minify にすると綺麗かも。

#!/bin/bash

for f in $(ls static/js/*.js); do
  uglifyjs -cm -o "${f/.js/.min.js}" "${f}"
done
$ mv minify.sh minify
$ chmod u+x minify
$ ./minify
$ tree
.
├── minify
└── static
    └── js
        ├── bar.js
        ├── bar.min.js
        ├── baz.js
        ├── baz.min.js
        ├── foo.js
        └── foo.min.js

参考

CircleCIのYAMLの仕様が変わった

CircleCIを使っている。
今週水曜(2018年10月10日)なぜか本番デプロイができなくなっていた。

調査すると設定を書いているYAMLの仕様が変更されたようで、キーの上書きができなくなっていた。

元々の仕様

デプロイにはFabricを使用している。

fab dev deploy で開発環境、 fab prod deploy で本番環境にデプロイされるように設定ファイルを書いている。

references:
  env_dev: &env_dev
    environment:
      ENV: dev

  env_prod: &env_prod
    environment:
      ENV: prod

jobs:
  deploy_dev: &deploy
    docker:
      - image: circleci/python:3.6
        environment:
          AWS_DEFAULT_REGION: ap-northeast-1
    steps:
      - checkout
      - run:
          name: Install python package
          command: sudo pip install -r requirements.txt
      - run:
          name: Deploy
          command: fab $ENV deploy
    <<: *env_dev

  deploy:
    <<: *deploy
    <<: *env_prod

Anchor(&name)とAlias(*name)を使用してコンパクトに書いている。

CircleCIは各jobページのConfigurationタブで展開されたYAMLを確認することができる。

f:id:i178inaba:20181014050410p:plain

展開されたYAMLを確認すると下記のようになっている。

jobs:
  deploy_dev:
    environment:
    - ENV: dev
    steps:
    - checkout
    - run:
        name: Install python package
        command: sudo pip install -r requirements.txt
    - run:
        name: Deploy
        command: fab $ENV deploy
    docker:
    - image: circleci/python:3.6
      environment:
        AWS_DEFAULT_REGION: ap-northeast-1
  deploy:
    environment:
    - ENV: prod
    docker:
    - image: circleci/python:3.6
      environment:
        AWS_DEFAULT_REGION: ap-northeast-1
    steps:
    - checkout
    - run:
        name: Install python package
        command: sudo pip install -r requirements.txt
    - run:
        name: Deploy
        command: fab $ENV deploy

ちゃんと environmentENV が上書きされて展開されているのがわかります。

仕様変更後

水曜日、本番デプロイのCIが失敗することで仕様変更を知る。
仕様変更後に展開されたYAMLを確認すると deploy jobの ENV が上書きされず、 dev のままになってしまっていた。

jobs:
  ...
  deploy:
    ...
    environment:
    - ENV: dev

Twitterでつぶやかれている方も居た。

修正

AnchorとAlias周りがおかしいのではないかという事はすぐわかったので、以下のように修正を行った。

   deploy:
-    <<: *deploy
-    <<: *env_prod
+    <<: [*env_prod, *deploy]

元々は *deploy を先に書き、そこに *env_prod を上書きするという意図で書いていた。
しかし、上書きされないため書く順番を逆にし、 *env_prod を先に書き、 *deploy を後に書いた。
この時、Aliasの書き方もCircleCIで推奨されている [] を使用した書き方に変更した。

すると下記のように以前の想定通り展開されました。

jobs:
  ...
  deploy:
    environment:
    - ENV: prod
    docker:
    - image: circleci/python:3.6
      environment:
        AWS_DEFAULT_REGION: ap-northeast-1
    steps:
    - checkout
    - run:
        name: Install python package
        command: sudo pip install -r requirements.txt
    - run:
        name: Deploy
        command: fab $ENV deploy

そして無事デプロイされるようになった。

ブログ再始動

お久しぶりです。

178inabaです。

久しぶりにブログを書いています。

最近、技術書典で技術書を販売した方のブログを見ました。

shu223.hatenablog.com

このブログを見て

(自分もいつかは本が書けたら楽しいだろうな)
(沢山の人が来場していて交流できるし、報酬も得られる!)

などと思い、

でも本を出すにはまず、休止状態のブログを再始動して技術的な記事とか書くべきだろうと思いこのブログを書いています。

Kyash始めてみた

Kyashは同僚と食事に行った時に割り勘代金を渡すために入れてみたのですが便利です。

上の技術書典のブログを読んだときに、KyashのAndroidエンジニアのこにふぁーさんのブログを思い出しました。

konifar.hatenablog.com

このこにふぁーさんのブログには投げ銭の話が書いてあって、上の技術書典の話と 『技術情報の発信で報酬を得る』 という所で通ずるなと思いました。

なので真似してKyashのIDとQRコードを貼ってみます。

kyash_id: 178inaba

f:id:i178inaba:20181012005104j:plain

今後

今後は定期的に技術情報をブログに書いていけたらと思います。
よろしくです!

GitHub Gistにファイルをアップロードするコマンドをgolangで作った

Gistを作る時、毎回catでファイル内容を表示してコピーして、GistのWeb画面にペーストしてたんだけど、
これが地味にめんどくなってきたので、ファイルを渡すとGistを作ってくれるコマンドをgolangで作った。

github.com

使い方

  • target.goをGistにアップロードする場合。
$ gistup target.go
  • 複数ファイルをGistにアップロードする場合。
$ gistup target.go target_test.go target_linux.go
  • 標準入力をGistにアップロードする場合。
$ stdin | gistup

アップロードが成功するとブラウザで作成されたGistのページを開きます。
(開けなかった場合はURLを表示。)

デフォルトでは最初にユーザ名とパスワードでログインを求められます。
(後述のオプションで匿名を選択すればログイン不要。)

また、デフォルトでは非公開のGistとして作成されます。
(こちらもオプションで公開のGistとして作成することも可能です。)

オプション

  • -a
    • 匿名のGistを投稿します。
  • -d <description>
    • Gistの説明を追加します。
  • -n <file_name>
    • 標準入力からGistにアップロードする場合のファイル名を設定します。
    • 指定しない場合はGistのデフォルトgistfile1.txtになります。
  • -p
    • パブリック(公開)のGistを投稿します。

所感など

入力時のコンテキストキャンセル検知

ユーザ名、パスワード入力は下記のようにコンテキストのキャンセルを検知できるようにしている。

func readString(ctx context.Context, hint string, readFunc func(t *tty.TTY) (string, error), t *tty.TTY) (string, error) {
    fmt.Printf("%s: ", hint)
    ch := make(chan string)
    errCh := make(chan error)
    go func() {
        s, err := readFunc(t)
        if err != nil {
            errCh <- err
        }
        ch <- s
    }()
    var s string
    select {
    case <-ctx.Done():
        return "", ctx.Err()
    case s = <-ch:
    case err := <-errCh:
        return "", err
    }
    return s, nil
}

理由はgistupはInterruptシグナル(Ctrl+C)を受けてコンテキストをキャンセルする仕組み(下記参照)になっているのだが、

sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)
ctx, cancel := context.WithCancel(context.Background())
go func() {
    <-sigCh
    cancel()
}()

これだと、入力中にCtrl+Cを押しても終わらない。
なので、ゴルーチンで入力処理を実行して、コンテキストのキャンセルと、入力終了をselectで同時に待つようにした。

結果、入力中にCtrl+Cを押すと終了するようにできた。

ユーザ名、パスワードの入力はTTYを使用

gistupではユーザ名とパスワードの入力にTTYを使用している。

理由は、最初、標準入力からユーザ名とパスワードを取得していたけど、
標準入力をGistにアップロードする箇所を実装してる時にユーザ名、パスワードの取得ができなくなる事に気づいた。

いろいろ調べたけど引き続き標準入力からユーザ名、パスワードを取得するのは難しそうで、調べて出てきたのはperlの質問でTTYを使っているやつだった。

stackoverflow.com

「goでTTYって確かmattnさんが作ってたよな」と探したらあって、

github.com

ただ、パスワードを読むメソッドしか無かったのでReadStringメソッドをプルリクエストした。
その後、速攻でマージして頂けたのでユーザ名の入力にはReadStringを使用。

結果、ユーザ名、パスワードを入力しつつ、標準入力から取得した文字列をGistにアップロードすることができた。

まとめ

これでコピペせずにGistにアップロードできるようになった。

またなんか作ったら書きます。

あと、PR大歓迎です😆

Raspberry Piで使うSDカードをコマンドラインで作る

新しいRaspberry Piを買ったので起動SDを作るコマンドをメモしておきます。
使用するOSはRaspbianです。

# ダウンロード
$ wget https://downloads.raspberrypi.org/raspbian_lite_latest -O raspbian.zip

# zip解凍
$ unzip raspbian.zip

# SDカードのマウント箇所を確認
$ diskutil list
...
/dev/disk2 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *2.0 GB     disk2
...

# SDカードのマウントを解除
$ diskutil unmountDisk /dev/disk2

# SDカードにイメージを書き込む
$ sudo dd bs=1m if=2017-03-02-raspbian-jessie-lite.img of=/dev/rdisk2

イメージを書き込むのが結構時間かかります。

マウント箇所(上記だとdisk2)はPCによって異なります。
イメージファイル名(上記だと2017-03-02-raspbian-jessie-lite.img)もRaspbianがアップデートされると変わります。

参考:

www.raspberrypi.org