20210915のGitに関する記事は4件です。

OpenAPI等のジェネレータツールによる更新作業の自動化、失敗時の自動ロールバックの実現

OpenAPI等のジェネレータツールによる更新作業の自動化、失敗時の自動ロールバックの実現 こんにちは、 最近はOpenAPIやgRPCであったり、それにまつわるジェネレータツールがとても熱いですね。 新規にジェネレータを走らせるのは何の憂いもなく素晴らしいのですが、 開発が進むにつれて、ディレクトリ構造が追加、変更されたり、中間処理を挟んだりと、素のジェネレータでは対応できない場合があります。 また、複数のジェネレータを使う場合、それぞれのジェネレータのご機嫌を伺わなければならない状況が発生し、そもそも更新作業にはジェネレータを使わないようになる場合も多いと思います。 ジェネレートした結果が意図した結果にならなかった場合、その問題を解決するために、いちいちGit操作を行ったり、例えば、ある段階のcommitを取り消したりrebaseしたりする操作を行うとそれだけで丸1日が潰れてしまう経験をお持ちの方もいるのではないでしょうか。 今回は、ジェネレータをもっと効率的に使うために、ジェネレート作業の自動化と適切なロールバックを行うことができるスクリプトを作ってみました。 今回の検証環境では以下のようになっています。 ツールや言語等 macOS BigSur 11.2.3 Git (git version 2.24.3 (Apple Git-128)) Bash (GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin20)) Docker (Docker version 20.10.7, build f0df350) OpenAPI (Version 3.0.3) openapitools/openapi-generator-cli Go (go version go1.16.3 darwin/amd64) 前提条件 まず、前提として、Git管理されているプロジェクトのホームディレクトリ等の上でスクリプトを走らせる必要があります。 また、今回はOpenAPIのopenapitools/openapi-generator-cliにおけるジェネレート作業を取り扱うので、その他のツールでは、reset_* generate_* replace_* の内容に関して読み替えを行う必要があります。 また、チケット単位で考えるとき、人が行う作業とジェネレート作業を完全に切り分ける必要があります。 ただし、作業を分担する上で、複数人がジェネレート作業を行わなければならない場合は、そのチケットに取り掛かる一番初めにこのジェネレート作業を行うという制約を設定する必要があります。 つまり、要件定義の更新作業を別チケットで対応し(主にopenapi.ymlの修正)所定のブランチにマージします。 その後、実装の修正更新作業は別チケットで対応します。 その実装作業を始める一番最初にこのスクリプトを走らせるという前提です。 スクリプト解説 今回はBashを使うのでシバン等を次のように設定します。 #!/bin/bash set -eux また、ロールバックを制御するためにerrtraceを設定します。 set -o errtrace 今回はロールバックにGitを使います。 また、失敗した段階で自動でロールバックを行いたいので、trapを設定します。 function trap_common() { trap "git stash && git clean -fd ; echo Failed ;" 2 trap "git stash && git clean -fd ; echo Failed ; exit 1" ERR } trapではスクリプトのステータスコードによって処理を分岐できるので、今回はエラー時と中断時にロールバックが走るように設定します。 次にリセット作業を行います。 更新する際にリセットを行わなければならないと思いますが、こちらはあらかじめ予約語を設定しておいて、ツールが出力する一定の法則に基づいたファイル名を利用してワイルドカードを駆使してリセットを行うというのが妥当だと思います。 次の例ではmodelとapiのリセットを行なっています。 function reset_model_phase() { git rm "${SET_PATH}"/web-api/go/model/model_* mkdir "${SET_PATH}"/web-api/go/model } function reset_api_phase() { rm "${SET_PATH}"/web-api/go/api/api_* rm "${SET_PATH}"/web-api/go/api/routers.go } 今回はmodelとapiに対するディレクトリ構造を変更しているので対象のディレクトリの対象のファイルをワイルドカードを使って削除します。 心配な方はこの作業の一つ上でバックアップ処理を挟んでも良いかもしれません。 次にリセットができれば、ジェネレートを行いましょう。 function genarate_phase() { docker run --rm -v "${SET_PATH}":/app openapitools/openapi-generator-cli generate -i /app/api/openapi.yml -g go-gin-server -o /app/web-api rm "${SET_PATH}"/web-api/Dockerfile } この際、ジェネレート時に作成される不要なファイル等があればこの関数内で削除します。つまり、generate時に生成されるファイル自体に改変を行いたい場合はこの関数内で適応するべきということです。 次に新しいディレクトリ構造に対応する作業を行います function replace_api_model() { mv "${SET_PATH}"/web-api/go/api_*.go "${SET_PATH}"/web-api/go/routers.go "${SET_PATH}"/web-api/go/api mv "${SET_PATH}"/web-api/go/model_* "${SET_PATH}"/web-api/go/model } 今回はmodelとapiの場所を追加しそれに則り変更したいので、mvコマンドでリプレースを行います。 次に、ジェネレートが行なった変更と手動で行う修正作業とを完全に切り分けるために自動でcommitを生成します。 まずはadd作業です。 function git_add_phase() { git add "${SET_PATH}"/web-api/.openapi-generator/FILES "${SET_PATH}"/web-api/api/openapi.yaml "${SET_PATH}"/web-api/go/README.md "${SET_PATH}"/web-api/go/model/model_* "${SET_PATH}"/web-api/go/api/routers.go git checkout "${SET_PATH}"/web-api/main.go git add -p git diff --cached } この際、ジェネレート作業において強制的に変更されてしまう内容を無視したいファイルがある場合にはcheckoutを個別に行なって変更をなかったことにします。 またaddのpオプションで更新するべきファイルとそうでないファイルをインタラクティブに判断します。 最後にdiffでステージ上の変更内容を改めて確認します。 この段階で正しくない更新が発見されたらその箇所の差分等をメモしておいて、処理をctl+cで中断させましょう。 先ほど設定した標準のロールバック処理が走ります。 問題がなければ次に、commitを行います。 commitまでする必要は無いのではと思う方もおられるかもしれませんが、ジェネレータの更新と人が行う更新を完全に切り分ける方がクリーンで効率的なので、commitを自動で行うようにしています。 ここでロールバック処理をカスタマイズします、そもそもスクリプトが勝手にcommitを行うのは大変危険なのでこの処理もロールバックは必須です。 ここではerrtraceを別にセットします。 set +o errtrace また、カスタマイズされたロールバックを元に戻すには再度errtraceを次のように設定しましょう。 set -o errtrace commitを行います。 この関数内でtrapを設定し直し、ロールバック処理のカスタマイズを行います。 function commit_phase() { git status trap "git stash && git clean -fd && git reset --hard HEAD^ ; echo Failed ; exit 1" ERR git commit -m "generate code from openapi.yml" } 次に新規にジェネレータによって生成されたファイルに関してはGit上ではトラッキングされていないので、そのためのcommitを行います。 function new_file_commit_phase() { git stash CHECK_UNTRACKED="$(git status -s)" ; readonly CHECK_UNTRACKED if [[ "${CHECK_UNTRACKED}" = "??"* ]]; then git add "${SET_PATH}"/web-api/go/api/api_* trap "git stash && git clean -fd && git reset --hard HEAD^ ; echo Failed ; exit 1" ERR git commit -m "generate new api_* files from openapi.yml" else echo "Skip this phase" fi } まず、すでにcommit済みなのでstashを行いトラッキングされていないファイルのみを作業中のステージに残るようにします。 次に、statusでトラッキングされていないファイルが存在するかどうかを判定します。 s オプションでstatusを省略表記で確認できます。 この際、「??」が存在する時点でトラッキングされていないファイルが存在することになるので、それをフラグとして条件分岐を行います。 条件を満たす場合にはステージにaddしcommitを行います。 条件を満たさない場合にはスキップします。 最後にmain関数で標準ロールバックとカスタマイズロールバックを制御します。 この関数を追加する場合は先ほど時点でのerrtraceの設定は必要ありません。 function main() { trap_common reset_model_phase reset_api_phase genarate_phase replace_api_model git_add_phase set +o errtrace commit_phase new_file_commit_phase set -o errtrace } スクリプトの全体像は次のようになります。 #!/bin/bash set -eux set -o errtrace SET_PATH=$(pwd) ; readonly SET_PATH function trap_common() { trap "git stash && git clean -fd ; echo Failed ;" 2 trap "git stash && git clean -fd ; echo Failed ; exit 1" ERR } function reset_model_phase() { git rm "${SET_PATH}"/web-api/go/model/model_* mkdir "${SET_PATH}"/web-api/go/model } function reset_api_phase() { rm "${SET_PATH}"/web-api/go/api/api_* rm "${SET_PATH}"/web-api/go/api/routers.go } function genarate_phase() { docker run --rm -v "${SET_PATH}":/app openapitools/openapi-generator-cli generate -i /app/api/openapi.yml -g go-gin-server -o /app/web-api rm "${SET_PATH}"/web-api/Dockerfile } function replace_api_model() { mv "${SET_PATH}"/web-api/go/api_*.go "${SET_PATH}"/web-api/go/routers.go "${SET_PATH}"/web-api/go/api mv "${SET_PATH}"/web-api/go/model_* "${SET_PATH}"/web-api/go/model } function git_add_phase() { git add "${SET_PATH}"/web-api/.openapi-generator/FILES "${SET_PATH}"/web-api/api/openapi.yaml "${SET_PATH}"/web-api/go/README.md "${SET_PATH}"/web-api/go/model/model_* "${SET_PATH}"/web-api/go/api/routers.go git checkout "${SET_PATH}"/web-api/main.go git add -p git diff --cached } function commit_phase() { git status trap "git stash && git clean -fd && git reset --hard HEAD^ ; echo Failed ; exit 1" ERR git commit -m "generate code from openapi.yml" } function new_file_commit_phase() { git stash CHECK_UNTRACKED="$(git status -s)" ; readonly CHECK_UNTRACKED if [[ "${CHECK_UNTRACKED}" = "??"* ]]; then git add "${SET_PATH}"/web-api/go/api/api_* trap "git stash && git clean -fd && git reset --hard HEAD^ ; echo Failed ; exit 1" ERR git commit -m "generate new api_* files from openapi.yml" else echo "Skip this phase" fi } function main() { trap_common reset_model_phase reset_api_phase genarate_phase replace_api_model git_add_phase set +o errtrace commit_phase new_file_commit_phase set -o errtrace } main echo "done" 最後に 今までは属人的にジェネレート作業は〇〇さんにお願いします、という感じで進んでいました。 仕様が変更されるたびにそれにまつわるジェネレート作業が発生する状況の場合、ジェネレート作業を終えた後に純粋にマージしたくても、マージ条件をクリアできない場合が多々あると思います。 その際、〇〇さんの本来の作業ではないロジックの修正までもマージ条件の制約上、行わなくてはならない状況が発生することが容易に予想できます。 私自身も〇〇さん状況になってしまった経験があります。 この経験があって今回このようなスクリプトを書きました。 今回の内容で、ジェネレート作業そのものをスクリプト化しそれを走らせるだけで、誰でもジェネレート作業を行うことができるようになります。 これで、作業を分担できるようになるので、プロジェクトの開発効率が一気に上がるのでは無いでしょうか。 今回は紹介していませんが、これに加えてさらにDockerでスクリプトを走らせるようにするとさらに良いですね。 今回はBashで自動化を実現したので、汎用性が高いと思います、ジェネレート作業に関係なく、さまざまな場面で応用できると思います。 今後git_add_phaseに関しては、改良の余地があると思うので、引き続き最適化していきたいと思います。 参考文献 「OpenAPI Specification」https://swagger.io/specification/ 「APIのコードを自動生成させたいだけならgRPCでなくてもよくない?」https://www.m3tech.blog/entry/2019/08/15/110000 「bashのtrap(ERR)が関数内に継承されないわけがない」https://qiita.com/kurapooh/items/67687ae3c4540d4b7d40 「シグナルとtrapコマンド」https://shellscript.sunone.me/signal_and_trap.html
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gitで重要なブランチ(develop/master/main)以外のブランチを一括で削除する

放っておくとブランチが溜まってしまう。 これ知るまで、何回もgit branch -D xxxしてた。 便利なので使ってください。 Gitで重要なブランチ(develop/master/main)以外のブランチを一括で削除するコマンド git branch | grep -Ev "master|develop|main" | xargs git branch -D 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

macOSのzshではこれだけはやっておこう

Homebrew を使います まだ Homebrew をインストールしていない場合はこちら: zsh % /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 1. パスの設定 M1 Mac (Apple Silicon) の場合は、Homebrew 向けにパスを通しておく必要があります。 zsh # デフォルトのパスを確認 zenn@mba ~ % echo $path /usr/local/bin /usr/bin /bin /usr/sbin /sbin /Library/Apple/usr/bin /Library/Frameworks/Mono.framework/Versions/Current/Commands Intel Mac であればこのままでも差し支えありませんが、Apple Silicon Mac では Homebrew のインストール先が /opt/homebrew 以下となるので、ここへパスを通しておきます。 ホームディレクトリ (/Users/ユーザー名) に .zshrc を作成します。 ~/.zshrc typeset -U path PATH path=( /opt/homebrew/bin(N-/) /opt/homebrew/sbin(N-/) /usr/bin /usr/sbin /bin /sbin /usr/local/bin(N-/) /usr/local/sbin(N-/) /Library/Apple/usr/bin ) 優先してほしい順にパスを指定していきます。 Intel Mac では /usr/local/bin と /usr/local/sbin を優先させてください。 (N-/) は、もしそのディレクトリが存在していれば PATH に追加し、そうでなければ無視してくれるオプションです。 上のパス設定を反映させるには ~/.zshrc を読み込みます。 zsh zenn@mba ~ % source ~/.zshrc 2. zsh-completions のインストール コマンド入力にバシバシ補完を効かせてくれる zsh-completions をインストールします。 zsh zenn@mba ~ % brew install zsh-completions ~ 略 ~ => Caveats To activate these completions, add the following to your .zshrc: if type brew &>/dev/null; then FPATH=$(brew --prefix)/share/zsh-completions:$FPATH autoload -Uz compinit compinit fi You may also need to force rebuild `zcompdump`: rm -f ~/.zcompdump; compinit Additionally, if you receive "zsh compinit: insecure directories" warnings when attempting to load these completions, you may need to run this: chmod -R go-w '/opt/homebrew/share/zsh' ~ 略 ~ インストール時の Caveats には、/opt/homebrew/share/zsh (Intel Mac の場合は /usr/local/share/zsh) のみを go-w せよと表示されていますが、実際には share ディレクトリそのものを chmod -R go-w する必要があります。 zsh zenn@mba ~ % chmod -R go-w /opt/homebrew/share あとは Caveats の指示にしたがって、~/.zshrc への追記と補完キャッシュファイルの再生成をおこないます。 ~/.zshrc if type brew &>/dev/null; then FPATH=$(brew --prefix)/share/zsh-completions:$FPATH autoload -Uz compinit && compinit fi zsh zenn@mba ~ $ source ~/.zshrc zenn@mba ~ $ rm -f ~/.zcompdump; compinit これでコマンド入力中に Tab キーまたは Ctrl+I を打鍵するとその後の候補を補完表示してくれます。 _ はカーソル位置を表しています。 zsh % sysctl _ # <-- ここで Tab または \C-I を入力 % sysctl _ audit hw kperf machdep security vfs debug kern ktrace net user vm % sysctl hw._ # <-- ここでふたたび Tab % sysctl hw._ activecpu cpufrequency l3cachesize busfrequency cpufrequency_max logicalcpu busfrequency_max cpufrequency_min logicalcpu_max busfrequency_min cpusubfamily memsize 3. プロンプトの表示を変更 プロンプトのデフォルト値を確認します。 zsh zenn@mba ~ $ echo $PROMPT %u@%h %1~ %# これらは以下のことを意味しています。 %u: ユーザー名 %h: ホスト名 %~: カレントディレクトリ(1 はディレクトリ深度) %#: 一般ユーザーのときは %、root になったときは # を表示 カレントディレクトリの表示形式にはいくつかのバリエーションがあります。 PROMPT 機能 実際の表示 %d フルパス /Users/zenn/Downloads/myapp:% _ %~ フルパス 2 ~/Downloads/myapp:% _ %c 相対パス myapp:% _ %2c ディレクトリ深度 2 Downloads/myapp:% _ ここでは次のように設定しました。 ~/.zshrc PROMPT="%n ($(arch)):%~"$'\n'"%# " $(): カッコ内のコマンド結果を表示します "$'\n'": プロンプトが長くなってしまったので途中に改行を挿入します zsh % source ~/.zshrc zenn (arm64):~/Downloads/myapp % _ 4. コマンド実行結果のあとに空行を挿入する コマンドの実行結果が表示されたあと、すぐにプロンプトが表示されるのはやや窮屈です。 zsh zenn (arm64):~/Downloads % ls -a . .. .DS_Store .localized myapp zenn (arm64):~/Downloads % _ zsh には、bash における PROMPT_COMMAND と同等のフック関数 precmd() が用意されています。 ~/.zshrc へ以下を追記します。 ~/.zshrc add_newline() { if [[ -z $PS1_NEWLINE_LOGIN ]]; then PS1_NEWLINE_LOGIN=true else printf '\n' fi } precmd() { add_newline } コマンド実行後に空行が挿入されるようになりました。 zsh zenn (arm64):~/Downloads % source ~/.zshrc zenn (arm64):~/Downloads % _ 5. プロンプトへ色を付ける zsh では簡単にプロンプトへ色をつけることができます。 まず、~/.zshrc の冒頭で色を扱うモジュールを有効化し、 ~/.zshrc autoload -Uz colors && colors プロンプト内の文字列に色を指定します。 構文 %F{色の名前または色番号}hoge%f %F ~ %f に挟まれた文字列 (hoge) に指定した色が付きます。 色の名前: black, red, green, yellow, blue, magenta, cyan or white ~/.zshrc PROMPT="%F{green}%n%f %F{cyan}($(arch))%f:%F{blue}%~%f"$'\n'"%# " 色番号を調べるには以下のスクリプトを実行してみてください。 palette.sh #!/bin/sh # # 256色のカラーパレットを表示する # bash と zsh にて実行可能 # target_shell=$1 if [ -z "$1" ]; then target_shell=$(basename "$SHELL") fi if [ "$target_shell" = "bash" ]; then bash <<< 'for code in {0..255}; do echo -n "[38;05;${code}m $(printf %03d $code)"; [ $((${code} % 16)) -eq 15 ] && echo; done' elif [ "$target_shell" = "zsh" ]; then zsh <<< 'for code in {000..255}; do print -nP -- "%F{$code}$code %f"; [ $((${code} % 16)) -eq 15 ] && echo; done' else echo "error: Invalid argument ($target)" echo "Usage: $0 [bash|zsh]" fi 6. プロンプトへ Git レポジトリの状態を表示する まず、zsh-git-prompt をインストールします。 zsh % brew install zsh-git-prompt ~ 略 ~ ==> Caveats Make sure zsh-git-prompt is loaded from your .zshrc: source "/opt/homebrew/opt/zsh-git-prompt/zshrc.sh" ~ 略 ~ Caveats の指示に従い、~/.zshrc の中でスクリプトを読み込み、 ~/.zshrc source $(brew --prefix)/opt/zsh-git-prompt/zshrc.sh プロンプトの中で git_super_status 変数を展開するだけです。 ~/.zshrc PROMPT='%F{034}%n%f %F{036}($(arch))%f:%F{020}%~%f $(git_super_status)' PROMPT+=""$'\n'"%# " $(git_super_status) は '' (シングルクオート)で囲まないと機能しません。 逆に "$'\n'" は ダブルクオートが必要です。 zsh zenn (arm64):~ % source ~/.zshrc zenn (arm64):~ (:|✔) % cd ~/Downloads/zenn ~/Downloads/zenn: (main|●1) % _ Git レポジトリ以外では $(git_super_status) を表示させたくない場合 precmd() フックへ、そのディレクトリが Git レポジトリかどうか判定する関数を追加します。 ~/.zshrc git_prompt() { if [ "$(git rev-parse --is-inside-work-tree 2> /dev/null)" = true ]; then PROMPT='%F{034}%n%f %F{036}($(arch))%f:%F{020}%~%f $(git_super_status)' PROMPT+=""$'\n'"%# " else PROMPT="%F{034}%n%f %F{036}($(arch))%f:%F{020}%~%f "$'\n'"%# " fi } precmd() { git_prompt add_newline } ただし、この手法ではコマンド実行ごとに関数が実行されてしまうため、それなりにコストがかかります。 7. あたらしくインストールされたコマンドを即認識させる デフォルト状態の zsh では、あたらしくインストールされたコマンドをただちに認識してはくれません。 zsh ~% brew install yarn ~% yarn --version zsh: command not found: yarn ~/.zshrc の冒頭へ以下の行を追記しましょう。 ~/.zshrc zstyle ":completion:*:commands" rehash 1 8. tarball へ macOS の特殊ファイルを含めないようにする macOS で作成した tar.gz を Windows などの他の OS で解凍すると、.DS_Store ファイルや ._ (ドットアンダーバー) ファイルなどの特殊ファイルが tarball に含まれてしまっていることがよくあります。 これを防ぐためのエイリアス関数を用意しましょう。 ~/.zshrc tgz() { env COPYFILE_DISABLE=1 tar zcvf $1 --exclude=".DS_Store" ${@:2} } 使い方: zsh ~% tgz dotfiles.tgz .zshrc .gitconfig .ssh/ a .zshrc a .gitconfig a .ssh a .ssh/id_ed25519 a .ssh/id_ed25519.pub a .ssh/config a .ssh/known_hosts ~% ls dotfiles.tgz ここまでの ~/.zshrc 全文 ~/.zshrc autoload -Uz colors && colors zstyle ":completion:*:commands" rehash 1 typeset -U path PATH path=( /opt/homebrew/bin(N-/) /opt/homebrew/sbin(N-/) /usr/bin /usr/sbin /bin /sbin /usr/local/bin(N-/) /usr/local/sbin(N-/) /Library/Apple/usr/bin ) if type brew &>/dev/null; then FPATH=$(brew --prefix)/share/zsh-completions:$FPATH autoload -Uz compinit && compinit source $(brew --prefix)/opt/zsh-git-prompt/zshrc.sh fi # PROMPT="%F{green}%n%f %F{cyan}($(arch))%f:%F{blue}%~%f"$'\n'"%# " # PROMPT='%F{034}%n%f %F{036}($(arch))%f:%F{020}%~%f $(git_super_status)' # PROMPT+=""$'\n'"%# " git_prompt() { if [ "$(git rev-parse --is-inside-work-tree 2> /dev/null)" = true ]; then PROMPT='%F{034}%n%f %F{036}($(arch))%f:%F{020}%~%f $(git_super_status)' PROMPT+=""$'\n'"%# " else PROMPT="%F{034}%n%f %F{036}($(arch))%f:%F{020}%~%f "$'\n'"%# " fi } add_newline() { if [[ -z $PS1_NEWLINE_LOGIN ]]; then PS1_NEWLINE_LOGIN=true else printf '\n' fi } precmd() { git_prompt add_newline } tgz() { env COPYFILE_DISABLE=1 tar zcvf $1 --exclude=".DS_Store" ${@:2} } 関連記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ワンライナーtips

業務で使える一行コマンドを、列挙しました。 windows コマンドプロンプト シャットダウン shutdown -s -t 0 -f windowsのファイル検索 ファイル検索 dir -s -q -b xxxx.jpg ファイル検索2 where /r c:/users xxxx.jpg git コマンド windowsの場合、コマンドプロンプトで実行すること!!(powershellだと文字化けが解決しない) おそらく文字化けするので、最初に実行する。 SETX LANG ja_JP.UTF-8 gitで差分をパッチファイル出力 git diff > aaaaa.patch パッチファイルを適用する git apply aaaaa.patch 変更の取り消し git checkout . 正規表現 平仮名3文字 [\u3040-\u309F]{3} カタカナ3文字 [\u30A0-\u30FF]{3} 半角カタカナ3文字 [\uFF61-\uFF9F]{3} ワンライナーではないけど、便利なtips コマンドウィンドウをここで開く エクスプローラで該当のフォルダまで開く → アドレスバーにcmdを入力 → enter
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む