リポジトリが10個~ (あるいは100個でも) あるときに、そのすべてのローカルGit を更新したいことがまれにあります。 Windows、macOS、Linux 各種OSでサブディレクトリにあるローカルGit で git pull を一気に行う必要があったのでスクリプトをおいておきます。
tl;dr;
- バッチファイル、PowerShellスクリプト、シェルスクリプトで同様にパラメーターを受け取り、動作するスクリプトが用意できる
- OS に応じて手元にあると便利。めったに使わないけど
gist においておきます。
https://gist.github.com/guitarrapc/2623623a0e1bc7fe86b5cf56e0c70d88
やりたいこと
現在のフォルダ以下に次のように複数の Gitリポジトリがクローンされた状態で、まとめてすべて git pull したいことがあります。
# たくさんフォルダがあるとして.... $ tree -L 1 . ├── Bar ├── Foo ├── Piyo ├── Hoge ├── ... └── NanikaSugoiRepo
コミットしていない変更の存在するリポジトリもあり得るため、手でやるのは時間がいくらあっても足りません。 とりあえずリポジトリを今の ref に対して最新に更新したい。これを目指します。
# ↓を並べるのはやりたくない cd ./Bar && git pull && cd - # stash や git reset --hard をしたいかもしれない cd ./Foo && git pull && cd - ...
スクリプト
ワンライナーで書くのもいいですが各種OS でサクッとやりたいので、Windowsバッチファイル、PowerShellスクリプト、シェルスクリプトそれぞれで同じような引数を受け取って、同じように動作するようスクリプトを書きます。
やりたくなることを見越して、パラメーターで git stash
と git reset --hard
もやるか選べるようにしましょう。git stash
はデフォルト有効でパラメーターで無効を指定 (--no-stash
)、git reset --hard
はデフォルト無効でパラメーターで有効を指定できる (--force
)、とします。
こんな感じの手触りをイメージして書きます。
# Windows バッチファイル git-pull.bat --force --no-stash # PowerShell スクリプト ./git-pull.ps1 -Force --NoStash # シェルスクリプト bash ./git-pull.sh --force --no-stash
さっそく見てみましょう。
Windowsバッチファイル
git-pull.bat
:: Script to run `git pull` inside all subdirectories which are git repositories. :: usage 1: keep local changes, then up to date. :: > git-pull.bat :: usage 2: abort local changes, reset if possible, then up to date. :: > git-pull.bat --force --no-stash :: usage 3: try keep local changes, reset if possible, then up to date. :: > git-pull.bat --force @echo off setlocal :parse IF "%~1"=="" GOTO endparse IF "%~1"=="--force" set _FORCE=true IF "%~1"=="--no-stash" set _NO_STASH=true SHIFT GOTO parse :endparse for /f "tokens=* delims=" %%i in ('dir /ad /b "."') do ( echo --- Working on %cd%\%%i pushd "%cd%\%%i" if NOT EXIST ".git\" ( echo x: \"%cd%\%%i\" is not a git repository, continue next directory. ) else ( if not "%_NO_STASH%"=="true" ( git stash --quiet ) if "%_FORCE%"=="true" ( git reset --hard ) git pull if not "%_NO_STASH%"=="true" ( git stash apply --quiet ) ) popd ) endlocal
PowerShellスクリプト
git-pull.ps1
# Script to run `git pull` inside all subdirectories which are git repositories. # usage 1: keep local changes, then up to date. # PS> ./git-pull.ps1 # usage 2: abort local changes, reset if possible, then up to date. # PS> ./git-pull.ps1 -Force -NoStash # usage 3: try keep local changes, reset if possible, then up to date. # PS> ./git-pull.ps1 -Force param( [Switch]$Force, [Switch]$NoStash ) foreach ($dir in Get-ChildItem -Directory) { try { pushd $dir.FullName echo "--- Working on ""$($dir.FullName)""" if ((Get-ChildItem -Force -Directory).Name -notcontains ".git") { echo " x: ""$($dir.FullName)"" is not a git repository, continue next directory." continue } if (!$NoStash) { git stash --quiet } if ($Force) { git reset --hard } git pull if (!$NoStash) { git stash apply --quiet } } finally { popd } }
シェルスクリプト
git-pull.sh
#!/bin/bash set -o pipefail # Script to run `git pull` inside all subdirectories which are git repositories. # usage 1: keep local changes, then up to date. # $ bash ./git-pull.sh # usage 2: abort local changes, reset if possible, then up to date. # $ bash ./git-pull.sh --force --no-stash # usage 3: try keep local changes, reset if possible, then up to date. # $ bash ./git-pull.sh --force while [ $# -gt 0 ]; do case $1 in --force) _FORCE=true; shift 1; ;; --no-stash) _NO_STASH=true; shift 1; ;; *) shift ;; esac done ## find is not a good way to control. Additionally, this include CURRENT directory and it is unexpected. # find . -maxdepth 1 -type d -exec bash -c 'echo "Working on $(realpath $1)"; git reset --force; git pull' shell {} \; for dir in ./*/; do echo "--- Working on \"$(realpath "${dir}")\"" pushd "$(realpath "${dir}")" > /dev/null if [[ ! -d ".git" ]]; then echo " x: \"$(realpath "${dir}")\" is not a git repository, continue next directory." popd > /dev/null continue fi if [[ "${_NO_STASH:=false}" != "true" ]]; then git stash --quiet; fi if [[ "${_FORCE:=false}" == "true" ]]; then git reset --hard; fi git pull if [[ "${_NO_STASH:=false}" != "true" ]]; then git stash apply --quiet; fi popd > /dev/null done
書くときのメモ
どのスクリプトも、フォルダが git フォルダじゃない場合は次に行きます。
また、エラーが出ても止まらないようにしています。もし止めたい場合は、set -e
(シェルスクリプト) や $ErrorActionPreference = "Stop"
(PowerShell)、あるいは%errorlevel%
チェック (Windowsバッチファイル) でできますが、数が多いとエラー無視して進めたくなるんですよね。
Windowsバッチファイル
パラメーター引数を受け取るのは Window バッチファイルでパラメーター入力を受け付ける で紹介したやり方です。
サブディレクトリで何かコマンドを実行する方法として for
を採用しました。ただ、このやり方だと 実行中に Ctrl+C
でキャンセルしようとしても Ctl+C連打で止まらず、キャンセルプロンプトで Y
を選ばないといません。これは結構不便です。1
PowerShellスクリプト
try/finally を使うことで、実行中に Ctl+C
でキャンセルしてもフォルダ移動しっぱなしが防げます、便利。
パラメーター受け取りから制御処理まで、全体的に小細工がなく、素直に書けばそのまま動くのは PowerShell えらい。
あとは、PowerShell 7がOSのデフォルトに入ればいいのですが、なかなか先は見えません。
シェルスクリプト
パラメーター入力はいくつかやりかた2 がありますが、私はもっぱら while
+ case
+ shift
がパラメーターごとに一行で収まり、また任意のパラメーター名にできるのが好みです。
フォルダ一覧を取得するなら定番は find
ですが、-exec
でコマンド並べるのは厳しいので雑に glob としています。
シェルスクリプト定番なことだけ使って書いていますが、もう少しいい書き方ないですかね?
参考
- 似た処理で参考にしたもの: https://gist.github.com/phette23/7620214