背景
個人開発の管理ツールを、Claude Codeと一緒に「1ユーザーストーリー(US)=1ブランチ・1PR」というルールで運用しています。
ある日、既存の機能A(画面のツリー表示化)の作業をしていたブランチの上で、そのまま別の新機能B(並び替え機能)の相談を始めてしまい、気づいたら同じファイルの中にAとBの変更が混ざった状態でコミット前の作業ツリーができあがっていました。
「1US=1ブランチ」のルールに反するので分離したかったのですが、両方ともまだ1回もコミットしていない状態です。
この記事は、そこから両方の変更を壊さずに、それぞれ別ブランチ・別PRに分離した手順の記録です。
前提条件
- 対象は同じファイル(今回はReactのページコンポーネント1つ)に、機能Aと機能Bのコードが交互に混ざって書き足された状態
- 機能Aは既にコミットしても矛盾が無い状態、機能Bは機能Aの構造(新しく作った関数やstate)に依存している
- どちらもまだ1回もコミットしていない(作業ツリー上の変更のみ)
手順
1. 混在した状態をバックアップしておく
まずは今の状態(AとB両方入り)をそのままコピーして退避します。あとで機能Bの分を復元するのに使います。
cp frontend/src/pages/StoryTaskPage.tsx /tmp/StoryTaskPage.full.tsx
2. 機能Bのコードを手作業で取り除き、「機能Aだけの版」を作る
エディタの差分機能に頼らず、自分が直前に追加した行を思い出しながら1つずつ取り除きます。今回は具体的には以下のようなものでした。
- 機能B用に追加したimport文
- 機能B用に追加した
useState - 機能B用に追加した関数(イベントハンドラ)
- 機能B用にJSXへ追加したボタン
// Before(AとBが混在)
import { createUserStory, updateUserStory, deleteUserStory, reorderUserStory } from "../services/userStoryApi";
// ...
const [reorderingStoryId, setReorderingStoryId] = useState<number | null>(null);
// ...
const handleStoryReorder = async (story, direction, e) => { /* 機能Bのロジック */ };
// After(Aだけ)
import { createUserStory, updateUserStory, deleteUserStory } from "../services/userStoryApi";
// reorderingStoryId, handleStoryReorder は削除
3. 「機能Aだけの版」でテスト・ビルドを確認してからコミットする
npm run build
npx vitest run
git add frontend/src/pages/StoryTaskPage.tsx
git commit -m "feat: 機能Aを追加"
4. 残った変更(機能B分)をstashで退避する
git status --porcelain
# → 機能B関連のファイルだけが残っていることを確認
git stash push -u -m "feature-b-in-progress"
-uを付けるのがポイントです。機能Bのために新規追加した(まだgit管理下に無い)ファイルも一緒に退避できます。
5. 機能Aのコミットの上から、機能B用の新規ブランチを作る
git checkout -b feature/b
機能Bは機能Aの構造に依存しているので、機能Aをまだ含んでいないdevelop等ではなく、今いるブランチ(機能Aのコミット済み)からそのまま分岐します。いわゆる「スタック型ブランチ」です。
6. stashを戻し、退避しておいたバックアップで上書きする
git stash pop
cp /tmp/StoryTaskPage.full.tsx frontend/src/pages/StoryTaskPage.tsx
stash popだけだと「機能Aだけの版」に対する機能Bの差分がそのまま復元されるだけなので、ファイルによっては競合が起きたり、思っていたのと違う形で合成されることがあります。今回は素直に「AB両方入りのバックアップ」で丸ごと上書きしてしまう方が確実でした。
7. テストを確認してコミットする
npm run build
npx vitest run
git add -A
git commit -m "feat: 機能Bを追加"
機能AのPRがマージされた後
機能Bのブランチはまだ機能Aに依存した状態(スタック型)なので、機能AがPR経由でdevelopにマージされたら、developに向けてrebaseし直します。
git checkout develop
git pull
git checkout feature/b
git rebase develop
ドキュメント(READMEや仕様書など)の同じ節に両方の変更が追記されていると、ここで競合が起きやすいです。ただ内容自体は大抵両立するので、手動でマージして進めれば問題ありませんでした。
# 競合ファイルを編集して解決したら
git add README.md
git rebase --continue
まとめ
- コミット前の変更はstashで安全に退避・復元できるので、「後から分離する」こと自体は十分可能
- ただし依存関係のあるコードを手作業で分離するのはミスが起きやすいので、「分離前の全部入りバックアップをファイルコピーで取っておき、後半の分離作業はそのファイルで丸ごと上書きする」ようにすると、差分の再構築ミスを避けられる
- そもそも論として、新しい機能に着手する前に「今どのブランチにいるか」を確認する習慣がいちばんの予防策
参考
- 特になし(自身の作業記録に基づく内容です)