## 2026-05-05 — SourceTree 스타일 멀티 브랜치 Push 다이얼로그

### 배경

기존 Git 패널의 Push는 툴바 버튼 한 번 누르면 곧장 현재 브랜치를 `origin`으로 밀어내는 단일 동작이었다. 다음 시나리오에선 사용자가 다시 터미널로 돌아갈 수밖에 없었다 — (1) 태그 동기화(`git push --tags`), (2) 여러 브랜치를 한 번에 밀기, (3) 처음 push 하는 새 브랜치에 upstream 설정(`-u`), (4) 강제 push(쓸 일은 적지만 rebase/amend 후 필수). SourceTree의 Push 다이얼로그를 레퍼런스로 같은 워크플로를 그대로 가져와서 GUI에서 종결되도록 정리.

### 주요 변경

#### 1. `gitPush(dir, opts)` 객체 시그니처 + `--force-with-lease` + `getRemotes()` (`3b3eddc`)

`worktree-manager.ts`의 push 시그니처가 객체형으로 일반화되어 단일 `git push` 호출로 다중 브랜치/태그/force를 합칠 수 있게 됨. 강제 push는 항상 `--force-with-lease`라 stale한 로컬 뷰가 원격을 덮을 수 없음.

- **`src/server/services/worktree-manager.ts`**:
  - `GitPushOptions { remote?, branches?, pushAllTags?, force? }` + `PushBranchSpec { local, remote, setUpstream }` 타입 export.
  - `gitPush(dir, opts)` — `--force-with-lease` / `--tags`를 단일 `git push <remote> <flags...> <refspec...>` 호출에 묶고, upstream을 설정하는 행만 두 번째 `-u` invocation으로 분리. upstream 호출 쪽은 `--tags`를 떼어내(일부 git 버전에서 `-u` + refspec과 결합 시 invalid) 첫 호출에서 이미 푸시된 태그가 중복되지 않도록.
  - `getRemotes(dirPath)` — `simpleGit.getRemotes(true)`로 `{name, url}` 페어 반환. push URL 우선, 없으면 fetch URL fallback.

#### 2. 라우트 — body 확장 + 레거시 어댑터 + `GET /git-remotes` (`3b3eddc`)

- **`src/server/routes/projects.ts`**:
  - `POST /api/projects/:id/git-push` body가 `{remote, branches[], pushAllTags, force}`로 확장됨.
  - 레거시 `{branch, setUpstream}` 페이로드는 어댑터로 흡수해서 `branches: [{local: branch, remote: branch, setUpstream}]`로 변환 — 기존 post-commit / 컨텍스트 메뉴 콜러는 무수정.
  - `GET /api/projects/:id/git-remotes` 신규 — `[{name, url}]` 반환.

#### 3. 클라이언트 API + `PushDialog` 컴포넌트 (`3b3eddc`)

- **`src/client/src/api/projects.ts`** — `gitPush(id, payload?)` 객체 시그니처로 변경, `GitPushPayload`/`PushBranchSpec`/`GitRemote` 타입 export, `getGitRemotes(id)` 함수 추가.
- **`src/client/src/components/PushDialog.tsx`** (NEW, 329 lines):
  - 상단: 원격 select + URL 표기.
  - 중간: 4-컬럼 브랜치 테이블 — `Push?` 체크박스 / 로컬 브랜치명 / 원격 브랜치 select(+직접 입력 fallback) / `Track?` 체크박스(upstream 토글). 헤더의 `Select all` 토글로 전체 on/off.
  - 하단: `Push all tags` / `Force push` 토글.
  - 푸터: Push / Cancel 버튼.
- **`src/client/src/components/GitStatusPanel.tsx`** — Push 툴바 버튼이 즉시 push 대신 `PushDialog`를 열도록 전환. `ActionToolbar`에 `projectName` prop 전달(다이얼로그 헤더용).
- **`src/client/src/i18n.tsx`** — `git.pushDialog.*` 키 EN/KO 추가. KO 라벨은 SourceTree 한국어 UI를 그대로 따름.

### 수정된 주요 파일

| 파일 | 변경 내용 |
|------|----------|
| `src/server/services/worktree-manager.ts` | `gitPush(dir, opts)` 객체 시그니처 + `--force-with-lease`/`--tags` 단일 호출 머지 + upstream 분리 + `getRemotes()` 신규 |
| `src/server/routes/projects.ts` | `POST /git-push` body 확장(`branches[]`/`pushAllTags`/`force`) + 레거시 `{branch}` 어댑터 + `GET /git-remotes` 신규 |
| `src/client/src/api/projects.ts` | `gitPush(id, payload?)` 객체 시그니처 + `GitPushPayload`/`PushBranchSpec`/`GitRemote` 타입 + `getGitRemotes()` |
| `src/client/src/components/PushDialog.tsx` | NEW — 원격 select + 4-컬럼 브랜치 테이블 + 태그/force 토글 다이얼로그 |
| `src/client/src/components/GitStatusPanel.tsx` | Push 버튼이 즉시 push 대신 `PushDialog` 오픈 |
| `src/client/src/i18n.tsx` | `git.pushDialog.*` 키 EN/KO |

### 아키텍처 결정

1. **객체 시그니처 + 레거시 어댑터로 양립**: 기존 `gitPush(dir, remote, branch, setUpstream)` 위치 인자형 호출자가 후속 커밋(post-commit auto-push, 브랜치 컨텍스트 메뉴)에 박혀 있어 한 번에 모두 바꾸기 위험. 서버 라우트 레벨에서 `{branch, setUpstream}` → `{branches: [{...}]}` 어댑터를 두면 클라이언트 전용 호출 경로만 새 시그니처로 옮기고 내부 콜러는 무수정 유지 가능.
2. **`--force-with-lease` 기본**: 일반 `--force`는 fetch 직후 stale한 로컬 ref로 원격을 덮을 수 있어 팀 공유 브랜치에서 사고 빈도가 높음. lease는 "내가 마지막으로 본 원격 ref가 여전히 그대로일 때만 push" 의미라 동시 push는 거절되며 사용자가 다시 fetch 하도록 유도. 다이얼로그의 `Force push` 토글은 항상 lease로 이어짐.
3. **태그는 첫 호출에서만**: `--tags`가 일부 git 버전에서 `-u` + refspec과 함께 쓰일 때 invalid 처리되는 케이스가 있어, plain push와 upstream push가 둘 다 있으면 첫 호출에만 `--tags`를 붙이고 두 번째에선 떼어냄. 태그는 repo-wide라 한 번이면 충분.
4. **`Track?`는 per-branch**: SourceTree와 같이 브랜치별로 upstream 추적을 토글할 수 있게 함. 신규 브랜치는 기본 ON, 이미 upstream이 있는 브랜치는 OFF가 자연스럽지만 현재 다이얼로그는 모두 OFF로 두고 사용자가 명시적으로 켜는 방식 — `-u`는 두 번째 invocation으로 분리되므로 ON이어도 추가 비용은 작음.

---
