他の人がpushしたブランチをpullせずにpushしてrejectされた時の対処法

Git を使った開発において，リモートリポジトリへのプッシュがrejectされるケース...やらかしたことありませんか？？
特に複数人で開発している場合によく発生しますね？
本記事では，「push拒否」エラーの対処法として，`git reset`，`git stash`，`git pull`を組み合わせた効率的な解決方法を図とかを用いて説明してみます！

## pushが拒否される問題

以下のようなエラーメッセージを見たことがあるだろうか：

```bash
To github.com:username/repository.git
 ! [rejected]        feature/branch -> feature/branch (fetch first)
error: failed to push some refs to 'github.com:username/repository.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
```

このエラーは，リモートリポジトリ（GitHub）に誰かが先にコミットをプッシュしており，手元（ローカル）にはその変更が反映されていない状態で`git push`を実行した場合に発生する．

```mermaid
graph LR
    subgraph "ローカルリポジトリ"
        L1[コミットA] --> L2[コミットB] --> L4[あなたの新コミットD]
    end
    
    subgraph "リモートリポジトリ"
        R1[コミットA] --> R2[コミットB] --> R3[コミットC]
    end

    style R3 fill:#e85,stroke:#333,stroke-width:2px
    style L4 fill:#6ac,stroke:#333,stroke-width:2px
```

この図が示すように，リモートリポジトリにはコミットCがあるが，あなたのローカルにはそれがなく，代わりにあなた独自のコミットD（青色）がある．Gitはこの状態でのプッシュを拒否する．

## 簡単な解決方法：プル＆プッシュ？

最も単純な解決策は，エラーメッセージの通りgit pullを実行することだが，これは以下の理由から必ずしも最適ではない：

1. マージコンフリクトが発生する可能性がある，
2. 不要なマージコミットが生成される，
3. コミット履歴が複雑になる．

### git pull による解決方法とは？

Gitで以下のようなエラーが表示されることがある．
```
! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to 'origin'
hint: Updates were rejected because the tip of your current branch is behind
```
これはローカルのブランチがリモートより古いため，リモートの変更を取り込まずにpushしようとして拒否されたということだ．

この場合，次のコマンドを実行することで解決できる．
```bash
git pull origin main
```
これにより，リモートの変更をローカルにマージできるようになり，その後git pushが成功するようになる．

### なぜgit pullが最適とは限らないのか？

#### 1. マージコンフリクトが発生する可能性

git pullはデフォルトでgit fetch + git mergeを実行する．

ローカルとリモートの変更が同じファイルや同じ行にあると，マージコンフリクトが発生する．
```
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
```
このようなコンフリクトは手動で解消しなければならず，手間がかかる．

#### 2. 不要なマージコミットが生成される

git pullはマージ戦略をとるため，以下のようなコミットが発生することがある．
```
Merge branch 'main' of github.com:user/repo
```
このマージコミットは内容がないため，コミット履歴が乱雑になりやすい．

#### 3. コミット履歴が複雑になる

履歴が以下のように分岐・統合を繰り返すことで，git logやgit blameの読み解きが難しくなる．
```bash
*   8e3c9f4 Merge branch 'main'
|\
| * 234a1a2 Remote commit
* | b7c56d3 Local commit
|/
* 6d79a1e Common ancestor
```
履歴をできるだけシンプルに保ちたい場合には不向きだ．

### より良い代替案：git pull --rebase

より推奨されるのは，次のコマンドを使う方法だ．
```bash
git pull --rebase origin main
```
これは，リモートの変更をローカルのコミットの下に巻き直す操作で，履歴を直線的に保つことができる．

- 履歴がきれいになる，
- 不要なマージコミットが発生しない，
- ただし，rebase中のコンフリクト対応は少し難しいこともある．


| 方法                     | メリット                      | デメリット                     |
|--------------------------|-------------------------------|-------------------------------|
| git pull                 | 簡単で習慣化しやすい           | マージコミット，履歴の複雑化 |
| git pull --rebase        | 履歴がきれい，CI向き           | コンフリクト解決がやや複雑   |
| git fetch → git rebase   | 柔軟で安全な操作ができる       | 少し手間が増える              |

- 個人開発やシンプルな作業なら，git pull --rebaseを使うとよい．
- チーム開発やCIを意識するなら，git fetchとgit rebaseの組み合わせが望ましい．
- とにかく早く解決したいだけなら，git pullでも対応は可能だが，履歴が乱れることを理解しておく必要がある．


## より効率的な解決方法：reset, stash, pullの組み合わせ

場合によっては以下の方法で対処すると簡単に対処できるかもしれない．

```mermaid
sequenceDiagram
    participant リモート as リモートリポジトリ

    Note over ローカル: ローカルで変更・コミット後の状態
    ローカル->>リモート: git push
    リモート-->>ローカル: 拒否される（fast-forward でないため）

    Note over ローカル: 1. git reset --mixed HEAD^（コミットを取り消す）
    Note over ローカル: 2. git stash（作業内容を一時退避）

    リモート-->>ローカル: 3. git pull（リモートの変更を取得・マージ）

    Note over ローカル: 4. git stash apply（退避した作業内容を戻す）
    Note over ローカル: 5. 変更を add・commit する

    ローカル->>リモート: 6. git push（成功）

```

### ステップ1：コミットを取り消す（reset）

最初に，直前のコミットを取り消すが，変更内容は保持する：

```bash
git reset --mixed HEAD^
```

:::note info
`--mixed`オプションは，コミットを取り消しつつ，変更内容をワーキングディレクトリに残す．`HEAD^`は「1つ前のコミット」を意味する．
:::

```mermaid
graph TD
    subgraph "git reset --mixed HEAD^ 後"
        A2[コミットA] --> B2[コミットB]
        W[ワーキングディレクトリ<br>コミットDの変更内容]
    end
    subgraph "git reset --mixed HEAD^ 前"
        A[コミットA] --> B[コミットB] --> C[あなたのコミットD]
    end

```

### ステップ2：変更を一時保存（stash）

次に，取り消したコミットの変更内容を一時的に保存する：

```bash
git stash
```

これにより，ワーキングディレクトリの変更が一時的に退避され，「クリーン」な状態になる．

```mermaid
graph TD
    subgraph "git stash 後"
        A2[コミットA] --> B2[コミットB]
        S[スタッシュ<br>コミットDの変更内容]
    end
    
    subgraph "git stash 前"
        A[コミットA] --> B[コミットB]
        W[ワーキングディレクトリ<br>コミットDの変更内容]
    end
```

### ステップ3：リモートの変更を取得（pull）

ワーキングディレクトリがクリーンな状態になったので，リモートの変更を安全に取得できる：

```bash
git pull
```

これにより，リモートリポジトリの最新の変更（コミットC）がローカルに取り込まれる．

```mermaid
graph TD
    subgraph "git pull 後"
        A2[コミットA] --> B2[コミットB] --> C[リモートのコミットC]
    end

    S[スタッシュ<br>コミットDの変更内容]
    subgraph "git pull 前"
        A[コミットA] --> B[コミットB]
    end
```

### ステップ4：変更を復元（stash apply）

スタッシュした変更を復元する：

```bash
git stash apply
```

これにより，一時保存していた変更内容がワーキングディレクトリに戻る．

```mermaid
graph TD
    subgraph "git stash apply 後"
        A2[コミットA] --> B2[コミットB] --> C2[リモートのコミットC]
        W[ワーキングディレクトリ<br>コミットDの変更内容]
    end
    
    subgraph "git stash apply 前"
        A[コミットA] --> B[コミットB] --> C[リモートのコミットC]
        S[スタッシュ<br>コミットDの変更内容]
    end
```

この時点で，ワーキングディレクトリには，リモートリポジトリの最新の状態と，あなたの変更内容の両方が含まれている．もしコンフリクトがある場合は，この段階で解決する．

### ステップ5：変更を追加（add）

変更内容をステージングエリアに追加する：

```bash
git add .
```

### ステップ6：変更を確定(commit)

変更内容を新たにコミットする：

```bash
git commit -m "コミットメッセージ"
```

```mermaid
graph TD
    subgraph "コミット後"
        A[コミットA] --> B[コミットB] --> C[リモートのコミットC] --> D[あなたの新コミットD]
    end
```

### ステップ7：リモートリポジトリに同期(push)

最後に，変更をリモートリポジトリにプッシュする：

```bash
git push
```

これで，リモートリポジトリとローカルリポジトリが同期された状態になる．

```mermaid
graph LR
    subgraph "リモートリポジトリ"
        R1[コミットA] --> R2[コミットB] --> R3[コミットC] --> R4[あなたのコミットD]
    end

    subgraph "ローカルリポジトリ"
        L1[コミットA] --> L2[コミットB] --> L3[コミットC] --> L4[あなたのコミットD]
    end
```

---

上記のステップをまとめると，以下のコマンドシーケンスとなる：

```bash
# 1. コミットを取り消す（変更内容は保持）
git reset --mixed HEAD^

# 2. 変更を一時保存
git stash

# 3. リモートの変更を取得
git pull

# 4. 変更を復元
git stash apply

# コンフリクトがある場合はここで解決

# 5. 変更を追加
git add .

# 6. 変更をコミット
git commit -m "コミットメッセージ"

# 7. プッシュ
git push
```

`git push`が拒否される問題は，複数人での開発では日常的に発生する．本記事で紹介した`git reset`，`git stash`，`git pull`を組み合わせた方法は，やや手順は多いものの，変更内容を安全に保持しながらリモートリポジトリと同期できる効果的な方法である．

この方法に慣れることで，Git操作の理解が深まり，より複雑なバージョン管理のシナリオにも対応できるようになるといいですね