CATHODE COASTaccess to tools
9 min read

git worktree で stash を卒業する — 天気予報CLIで「機能追加」と「緊急修正」を同時に回す

git worktree を初めて実践。天気予報CLIを題材に、main を汚さず機能追加と緊急修正を別フォルダで同時に進め、マージ・片付けまでの一連の手順をまとめました。

shareB!

はじめに

git worktree はずっと名前だけ知っていて、触らずにきたコマンドでした。ブランチを切り替えたいとき、私はこれまで git stash で作業を退避するか、中途半端な一時コミットを積んで対応していました。動くには動くのですが、退避したことを忘れたり、後で stash pop の順番に悩んだりと、地味に神経を使います。

その悩みを git worktree が解けると聞いたので、今回は手を動かして確かめました。題材は、天気予報APIを叩いて結果をターミナルに装飾表示する小さなCLIです。実装言語は Python 3(標準ライブラリのみ、追加インストール不要)。天気データは APIキー不要で使える Open-Meteoを利用しました。作業環境は macOS、gitpython3 はOS標準に入っているものを使っています。

この記事は、その一連の手順を再現できる形でまとめた作業記録です。特別なトラブルは起きず、最初から最後まで素直に通りました。なので「派手な失敗談」ではなく、「worktree を実務の流れで一周する」ことに主眼を置いています。

先にまとめ

3行でいうと、こうなりました。

  • できたこと:1つのリポジトリから main / 機能ブランチ / 修正ブランチを別々のフォルダに同時展開し、main を一切触らずに並行開発 → マージ → 片付けまで通せた。
  • いちばんの勘所:worktree は「履歴(.git)を共有したまま、作業ディレクトリだけを増やす」仕組み。だから stash も一時コミットも要らない。ただし同じブランチを2つのツリーで同時にチェックアウトはできない
  • 体感:ブランチ切り替えのたびの stash 往復が消えた。エディタとターミナルを2枚開いて、装飾機能と緊急修正を文字通り同時に進められた。

この記事の主役である worktree の操作だけを抜き出すと、再現手順は次の通りです(アプリのコードは本編に掲載)。

# 土台(main)で作業を始めたあと……
# 1) 機能開発用のツリーを別フォルダに生やす
git worktree add -b feature/emoji ../weather-emoji
 
# 2) 緊急修正用のツリーも生やす(機能開発は中断しない)
git worktree add -b hotfix/city-not-found ../weather-hotfix
 
# 3) 今どんなツリーがあるか確認
git worktree list
 
# 4) main に取り込む(マージは「今いるブランチに取り込む」操作)
cd ../weather-cli
git merge hotfix/city-not-found
git merge feature/emoji
 
# 5) 片付け(ツリー削除とブランチ削除は別操作)
git worktree remove ../weather-emoji
git worktree remove ../weather-hotfix
git branch -d feature/emoji hotfix/city-not-found

worktree の考え方(前提)

普段の Git では、リポジトリ(.git)と作業ディレクトリが1対1で結びついています。そのため一度にチェックアウトできるブランチは1つだけで、別ブランチに移るには今の変更を退避するしかありません。

git worktree は、この結びつきをほどきます。履歴・ブランチ・オブジェクトが入った .git 本体は1つのまま共有し、作業ディレクトリ(実際にファイルを編集する場所)だけを複数持てるようにするものです。追加した作業ツリーはそれぞれ別のブランチをチェックアウトでき、物理的に別フォルダなので、片方で編集中でももう片方には影響しません。

公式ドキュメントによると、追加した作業ツリー(linked worktree)の .git は実体はファイルで、本体リポジトリの履歴と参照を指し示すだけです(git-worktree Documentation)。この機能は Git 2.5(2015年7月リリース)で導入されました(Git 2.5 の紹介記事 — GitHub Blog)。ここ数年の Git であればまず問題なく使えます。バージョンは git --version で確認できます。

前提として押さえておくとよいのは、次の3点です。履歴とブランチは全ツリーで共有される(どこでコミットしても全ツリーから見える)。作業ディレクトリ・HEAD・インデックスはツリーごとに独立している。そして、同じブランチを2つの作業ツリーで同時にチェックアウトすることはできない(後述)。

以下、実際の手順です。

1. 土台を作る(main に初版を置く)

まず普通のリポジトリを作り、天気予報CLIの初版を main に置きます。

mkdir weather-cli && cd weather-cli
git init
git branch -M main        # 初期ブランチ名を main に統一

weather.py の初版は、都市名から現在の気温を表示するだけの素朴なものです。

weather.py
#!/usr/bin/env python3
import sys
import json
import urllib.request
import urllib.parse
 
def geocode(city):
    """都市名から緯度・経度を得る"""
    url = "https://geocoding-api.open-meteo.com/v1/search?" + urllib.parse.urlencode(
        {"name": city, "count": 1, "language": "ja"}
    )
    with urllib.request.urlopen(url) as res:
        data = json.load(res)
    if not data.get("results"):
        print(f"都市が見つかりません: {city}")
        sys.exit(1)
    r = data["results"][0]
    return r["latitude"], r["longitude"], r["name"]
 
def fetch_weather(lat, lon):
    url = "https://api.open-meteo.com/v1/forecast?" + urllib.parse.urlencode(
        {"latitude": lat, "longitude": lon, "current": "temperature_2m,weather_code"}
    )
    with urllib.request.urlopen(url) as res:
        return json.load(res)
 
def main():
    city = sys.argv[1] if len(sys.argv) > 1 else "Tokyo"
    lat, lon, name = geocode(city)
    data = fetch_weather(lat, lon)
    cur = data["current"]
    print(f"{name}の現在の気温: {cur['temperature_2m']}°C")
 
if __name__ == "__main__":
    main()

動作を確認してコミットします。

python3 weather.py Tokyo      # 「東京 の現在の気温: 27.3°C」のように表示される
git add weather.py
git commit -m "初期実装: 都市名から現在の気温を表示"

これで main に土台ができました。ここが基準になります。

2. 装飾機能を別ツリーで開発する

「出力に絵文字と色をつけて見やすくする」機能を作ります。従来なら同じフォルダでブランチを切り替えるところですが、今回は main を触らず、別フォルダで開発してみます。

git worktree add -b feature/emoji ../weather-emoji

feature/emoji という新しいブランチを作り(-b)、それを1つ上の階層に並ぶ ../weather-emoji フォルダにチェックアウトする、という意味です。状態を確認します。

git worktree list
/path/to/weather-cli     abc1234 [main]
/path/to/weather-emoji   abc1234 [feature/emoji]

作業ツリーが2つ並びました。weather-climain のまま、隣の weather-emojifeature/emoji。ここが worktree の気持ちよさで、main 側のファイルは1文字も変わっていません。

新しいフォルダに移動して開発します。

cd ../weather-emoji

weather.py を、天気コードから絵文字と説明を引き、気温を寒暖で色分けし、枠線で囲う版に書き換えます。装飾はANSIエスケープシーケンスなので、これも追加ライブラリは不要です。

weather.py
#!/usr/bin/env python3
import sys
import json
import urllib.request
import urllib.parse
 
# weather_code → (説明, 絵文字)
WEATHER_CODES = {
    0: ("快晴", "☀️"), 1: ("晴れ", "🌤️"), 2: ("一部曇り", ""), 3: ("曇り", "☁️"),
    45: ("", "🌫️"), 48: ("霧氷", "🌫️"), 51: ("霧雨", "🌦️"),
    61: ("", "🌧️"), 63: ("", "🌧️"), 65: ("強い雨", "🌧️"),
    71: ("", "🌨️"), 80: ("にわか雨", "🌦️"), 95: ("雷雨", "⛈️"),
}
 
def geocode(city):
    url = "https://geocoding-api.open-meteo.com/v1/search?" + urllib.parse.urlencode(
        {"name": city, "count": 1, "language": "ja"}
    )
    with urllib.request.urlopen(url) as res:
        data = json.load(res)
    if not data.get("results"):
        print(f"都市が見つかりません: {city}")
        sys.exit(1)
    r = data["results"][0]
    return r["latitude"], r["longitude"], r["name"]
 
def fetch_weather(lat, lon):
    url = "https://api.open-meteo.com/v1/forecast?" + urllib.parse.urlencode(
        {"latitude": lat, "longitude": lon, "current": "temperature_2m,weather_code"}
    )
    with urllib.request.urlopen(url) as res:
        return json.load(res)
 
def colorize_temp(temp):
    if temp < 10:
        color = "\033[36m"   # シアン(寒い)
    elif temp < 25:
        color = "\033[32m"   # 緑(快適)
    else:
        color = "\033[31m"   # 赤(暑い)
    return f"{color}{temp}°C\033[0m"
 
def render(name, temp, code):
    desc, icon = WEATHER_CODES.get(code, ("不明", ""))
    bar = "\033[33m" + "" * 32 + "\033[0m"
    print(bar)
    print(f"  {icon}  \033[1m{name}\033[0m の天気")
    print(f"  天気: {desc}")
    print(f"  気温: {colorize_temp(temp)}")
    print(bar)
 
def main():
    city = sys.argv[1] if len(sys.argv) > 1 else "Tokyo"
    lat, lon, name = geocode(city)
    data = fetch_weather(lat, lon)
    cur = data["current"]
    render(name, cur["temperature_2m"], cur["weather_code"])
 
if __name__ == "__main__":
    main()

動かして、コミットします。

python3 weather.py Osaka      # 枠線・絵文字・色つきで表示される
git add weather.py
git commit -m "機能追加: 天気を絵文字・色・枠線で装飾表示"

ここで一度、main 側が無傷であることを確かめておきます。

cd ../weather-cli
python3 weather.py Osaka      # こちらは装飾なしの素朴な表示のまま

同じ瞬間に、装飾版(weather-emoji)と素朴版(weather-cli)が両方手元にある状態です。stash では得られない感覚でした。

3. 緊急修正をもう1つのツリーで割り込む

worktree がありがたいのはこういう場面です。装飾機能を作っている最中に、「存在しない都市名を渡したときのメッセージが不親切」という直したい点が出てきたとします。装飾の作業を中断・退避せず、さらに3つ目の作業ツリーを生やして対応します。

main 側(weather-cli)にいる状態で実行します。

git worktree add -b hotfix/city-not-found ../weather-hotfix
cd ../weather-hotfix

geocode 関数のエラー処理を、少し親切にします。該当箇所だけ差し替えます。

weather.py
    if not data.get("results"):
        print(f"'{city}' に一致する都市が見つかりませんでした。")
        print("英語表記(例: Tokyo, Osaka)や、より大きな都市名で試してください。")
        sys.exit(1)

コミットします。

python3 weather.py ZZZZZ       # 親切なメッセージが出れば OK
git add weather.py
git commit -m "修正: 都市が見つからない時の案内を親切にする"

この時点で git worktree list を実行すると、main / feature/emoji / hotfix/city-not-found の3つが並びます。装飾機能の作業は weather-emoji に手つかずで残ったままです。「機能追加」と「緊急修正」が別フォルダで同時に生きている、というのがこの記事で一番伝えたい状態でした。

4. マージして片付ける

作業が済んだので、main に両方のブランチを取り込みます。マージは「今いるブランチに取り込む」操作なので、main の作業ツリーで行います。

cd ../weather-cli          # main の作業ツリー
git merge hotfix/city-not-found
git merge feature/emoji

mainweather.py に、親切なエラーメッセージと装飾表示の両方が入りました。確認します。

python3 weather.py Tokyo

最後に、役目を終えた作業ツリーとブランチを片付けます。ここで一点、作業ツリーの削除とブランチの削除は別操作である点に注意します。

git worktree remove ../weather-emoji
git worktree remove ../weather-hotfix
git branch -d feature/emoji hotfix/city-not-found
git worktree list          # main だけが残っていれば完了

git worktree remove はフォルダごと安全に削除してくれます。未コミットの変更が残っている場合はうっかり消さないよう止めてくれるので、本当に捨てていいときだけ --force を付けます。

つまずきやすいポイント

今回は素直に通りましたが、事前に知っておくと救われる挙動が3つあります。いずれも仕様として理解しておくと安心です。

症状原因対処
fatal: '<branch>' is already checked out at ...同じブランチを2つのツリーで同時にチェックアウトしようとした別ブランチにするか、新しいブランチを切る。これは「1ブランチ=1ツリー」を守るための意図的な制約
rm -rf でフォルダを消したのに worktree が残っている扱いになるgit worktree remove を使わず手動削除すると管理情報が残るgit worktree prune で古い情報を掃除する
どこに作るか迷う置き場所に決まりはない隣のフォルダ(../name)か、専用ディレクトリにまとめる。本体フォルダの には作らないのが無難

同じブランチを2重にチェックアウトできない制約は、公式にも明記されています。2つのツリーが同じブランチを別々に更新できてしまうと、コミットが失われるおそれがあるため、あえて禁止されています(git-worktree Documentation)。

やってみての所感

結論として、worktree は「並行作業」に効くと感じました。今回のように、機能開発の途中で別の修正が割り込む状況では、stash の退避・復帰や一時コミットの巻き戻しが完全に不要になります。main を汚さずに別バージョンを同時に手元へ持てるのは、頭のなかの状態管理がそのままフォルダの分割になった感覚で、思っていたよりずっと素直でした。

一方で、1つの作業を最初から最後まで一直線に進めるだけの日には、フォルダが増えるぶん少し大げさに感じる場面もありそうです。効いてくるのは「割り込みが入る」「複数を行き来する」ときで、そこは stash と使い分ければよさそうです。次は既存ブランチ(新規作成ではないもの)を worktree で開くパターンや、長時間かかるビルド・テストを別ツリーで走らせながら本体で作業を続ける使い方も試してみたいところです。

まとめ

git worktree は、.git を共有したまま作業ディレクトリだけを増やすコマンドです。今回は天気予報CLIを題材に、main を土台として「装飾機能」と「エラー文言の修正」を別フォルダで同時に進め、マージと片付けまでを一周しました。特別なトラブルもなく通り、stash 往復から解放されたのが一番の収穫でした。ブランチの切り替えでいつも息を止めていた人は、一度この静かな並行作業を試してみる価値があります。


環境・バージョン:macOS、Python 3(標準ライブラリのみ)、git worktree(Git 2.5 以降で利用可能)。執筆時点は 2026年7月。

参考リンク

天気データは Open-Meteo を利用しています。非商用利用は無料ですが、データは CC BY 4.0 ライセンスのもとで提供されており、利用時は帰属表示が必要です。