0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

1つのブランチに2つの機能の変更を混ぜてしまった時に、gitでスタック型ブランチへ分離する手順

0
Posted at

背景

個人開発の管理ツールを、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で安全に退避・復元できるので、「後から分離する」こと自体は十分可能
  • ただし依存関係のあるコードを手作業で分離するのはミスが起きやすいので、「分離前の全部入りバックアップをファイルコピーで取っておき、後半の分離作業はそのファイルで丸ごと上書きする」ようにすると、差分の再構築ミスを避けられる
  • そもそも論として、新しい機能に着手する前に「今どのブランチにいるか」を確認する習慣がいちばんの予防策

参考

  • 特になし(自身の作業記録に基づく内容です)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?