7차 C-2: rebase · cherry-pick 충돌을 작업에 맞게 부르고 MergeHub 만으로 끝까지
화면이 모든 충돌을 "병합" 으로 불렀다 - rebase REMOTE 가 "THEIRS", cherry-pick 에 "병합 진행 중", 다 풀면 cherry-pick 을 끝낼 단추가 없고, 로그에 RebaseContinue() · Commit(), 다른 브랜치 커밋은 고를 수 없었다. GitOperationState 로 칸 제목(올릴 자리 · 다시 올리는 내 커밋 · 가져오는 커밋) · 머리 글 · 진행 표시 · 끝내기 단추(병합 커밋 · 계속 · 커밋)를 정한다. cherry-pick · revert 는 원래 메시지 · 작성자로 커밋. 머지 주석 자동 풀기 · 알림도 같은 뒷일. 이력 탭 [모든 브랜치]. rebase · cherry-pick 모두 줄끝 바뀐 파일 0, MergeSaveTest 125건 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
165b5ea295
commit
aebfd6b089
@ -170,6 +170,97 @@ MergeSaveTest 88건 통과. 빌드 경고 0 · 오류 0. 설정 파일은 바이
|
||||
|
||||
MergeSaveTest 93건 통과. 빌드 경고 0 · 오류 0. 설정 파일은 바이트 그대로 되돌림.
|
||||
|
||||
## C-2. rebase · cherry-pick 충돌을 네 칸으로 끝까지 (2026-09-15, 완료)
|
||||
|
||||
### 준비
|
||||
|
||||
시험 저장소(`C1Repo`)를 그대로 썼다. `feature/mine`(내 쪽 작업) · `feature/theirs`(상대 쪽 작업)가 같은 바탕에서 커밋 하나씩이라
|
||||
**rebase**(`feature/mine` 을 `feature/theirs` 위로)도 **cherry-pick**(상대 쪽 작업을 `feature/mine` 에)도 병합과 같은 26개 충돌을 낸다.
|
||||
지시서는 생성기에 갈래를 더하라고 했지만 필요 없어서 바꾸지 않았다.
|
||||
|
||||
### 고치기 전에 다시 났다
|
||||
|
||||
**rebase** (Git 탭 [선택 브랜치 위로 rebase])
|
||||
|
||||
| # | 무엇 | 근거 |
|
||||
|---|---|---|
|
||||
| R1 | 네 칸 제목이 두 쪽을 말하지 않는다. rebase 에서는 LOCAL 이 **올릴 자리(`feature/theirs`)**, REMOTE 가 **다시 올리는 내 커밋**인데 | LOCAL `feature/theirs` · REMOTE **`REMOTE · THEIRS`** · 머리 글 **`병합 충돌 · REMOTE = THEIRS`** |
|
||||
| R2 | Take Theirs 가 내 커밋 줄을 가져오는데 화면은 그 사정을 모른다 | `Common/Util/Unit15.cs` Take Theirs → `feature/mine` 에만 있는 줄 1/1 들어감 |
|
||||
| R3 | 로그가 코드 이름을 적는다 | `rebase 1/1 단계에서 충돌 26건 - 해결 후 RebaseContinue() 를 호출하세요.` |
|
||||
| R4 | 머지 주석 자동 풀기가 안 걸린다 | 충돌 26(병합은 24) |
|
||||
| - | 끝내는 길은 됐다 | 다 풀자 [계속] 켜짐 · [병합 커밋] 꺼짐, [계속] → `rebase 완료 (1/1 단계)`, 이력 `내 쪽 작업` 이 `상대 쪽 작업` 위에 |
|
||||
|
||||
**cherry-pick**
|
||||
|
||||
| # | 무엇 | 근거 |
|
||||
|---|---|---|
|
||||
| C1 | **다른 브랜치의 커밋을 고를 수 없다.** Git 탭 이력은 HEAD 에서 닿는 커밋만 보인다 | 이력 목록에 `상대 쪽 작업` 없음. `GetCommits` 가 시작점 없이 `CommitFilter` 를 써서 libgit2 기본(HEAD)만 |
|
||||
| C2 | 이미 있는 커밋(HEAD)을 cherry-pick 하면 `CHERRY_PICK_HEAD` 가 남는데 **진행 표시도 [작업 취소] 도 없다** — 빠져나올 길이 없다 | 로그 `cherry-pick 적용됨 - 확인 후 Commit() 을 호출하세요.` · 머리 표시 없음 · [작업 취소] 꺼짐 |
|
||||
| C3 | 충돌 중 머리가 **"병합 진행 중"**, REMOTE `REMOTE · THEIRS`, 머리 글 `병합 충돌 · REMOTE = THEIRS` | git 으로 cherry-pick 을 시작해 앱으로 연 화면 |
|
||||
| C4 | 충돌을 다 풀면 **진행 표시 · [작업 취소] · [병합 커밋] 이 모두 사라진다.** 끝내려면 변경 · 커밋 탭에서 메시지를 직접 쳐야 한다(원래 메시지 안 채워짐) | 다 푼 뒤 머리 표시 없음 · 작업 취소 꺼짐 · 병합 커밋 꺼짐. 메시지를 쳐서 [커밋] 하면 `CHERRY_PICK_HEAD` 는 치워짐 |
|
||||
|
||||
### 원인
|
||||
|
||||
- 화면이 충돌을 모두 "병합" 으로만 다뤘다. 진행 중인지는 `IsMergeInProgress`(병합 표시 **또는 충돌 있음**)로만 봐서, cherry-pick 은 충돌이 0 이 되면 진행 중이 아니게 됐다.
|
||||
- 상대 이름은 MERGE_HEAD 로만 찾았다(`GetMergeTheirsName`) — rebase · cherry-pick 에는 MERGE_HEAD 가 없어 "THEIRS".
|
||||
- 머지 주석 자동 풀기 · 알림은 [Merge] · pull 에만 걸려 있었다(7차 C-1).
|
||||
|
||||
### 고친 것
|
||||
|
||||
| 파일 | 무엇 |
|
||||
|---|---|
|
||||
| `Models/GitModels.cs` | `GitOperationKind`(None · Merge · Rebase · CherryPick · Revert) · `GitOperationState`(두 쪽 이름 · 들어오는 커밋 · 단계) |
|
||||
| `Services/OperationLabels.cs` (새 파일) | 작업별 글 — 진행 표시(`rebase 1/1` · `cherry-pick 진행 중`), LOCAL 제목(rebase 는 `올릴 자리 feature/theirs`), REMOTE 제목(`다시 올리는 내 커밋 …` · `가져오는 커밋 …` · `되돌리는 커밋 …`), 네 칸 머리 글, 로그 이름, 끝내기 단추(병합 커밋 · 계속 · 커밋), 진행 줄(병합 글은 예전 그대로) |
|
||||
| `Services/GitService.cs` | `GetOperationState` — git 작업 표시로 판단: rebase 는 `rebase-merge/onto` · `head-name` · 지금 단계 커밋, cherry-pick · revert 는 `CHERRY_PICK_HEAD` · `REVERT_HEAD`. `GetPendingCommitMessage`(원래 메시지, # 줄 뺌). `CommitPendingOperation` — cherry-pick 은 git 처럼 **원래 작성자**, 커밋한 사람은 지금 사용자. `GetCommits(allBranches)`. 로그의 `Commit()` · `RebaseContinue()` 문구를 [커밋] · [계속] 으로 |
|
||||
| `Services/GitWorkspace.cs` | `OperationState` · `IsOperationInProgress`(충돌 수와 따로) · `ShowAllBranchesInHistory`. 진행 표시 · 진행 줄이 작업을 따른다 |
|
||||
| `ViewModels/GitViewModel.cs` | 뒷일(`AfterConflicts`)을 병합 · pull · rebase 시작 · rebase 계속 · cherry-pick · revert 가 함께 쓴다 — 머지 주석 자동 풀기 · 알림("다 풀면 [계속] 으로 …") · cherry-pick · revert 메시지 채우기. `FinishOperationCommand` — 병합 커밋 / 계속 / 커밋. [작업 취소] 가 진행 중인 작업이면 켜짐 |
|
||||
| `Views/GitView.xaml` | 충돌 탭 끝내기 단추 글자 · 명령이 작업을 따른다. 진행 줄은 `IsOperationInProgress` 로 보인다. 이력 탭에 **[모든 브랜치]** 체크(사용자 결정, 기본 꺼짐) |
|
||||
| `ViewModels/HomeViewModel.cs` | 충돌 파일을 열 때 작업 상태로 LOCAL · REMOTE 제목 · 머리 글 · Take 로그 이름을 정한다. 병합 커밋 메시지 자동 채우기는 병합일 때만 |
|
||||
| 하네스 `MergeSaveTest` | 작업별 글 검사 32건(진행 표시 · 칸 제목 · 머리 글 · 로그 이름 · 끝내기 단추 · 진행 줄, 병합 글이 예전과 같은지) |
|
||||
|
||||
### 확인한 것 (모두 저장소를 새로 만들어 MergeHub 만으로)
|
||||
|
||||
**① rebase**
|
||||
|
||||
| 때 | 결과 |
|
||||
|---|---|
|
||||
| [선택 브랜치 위로 rebase] | 알림 **`충돌 24건 (머지 주석만 다른 2건은 자동으로 풀림) - … 다 풀면 [계속] 으로 rebase 를 이어 갑니다.`** · 로그 `… 충돌 26건 - 풀고 나서 [계속] 을 누르세요.` + 자동 해결 두 줄 |
|
||||
| 충돌 중 | 머리 **`rebase 1/1`** · [작업 취소] 켜짐 · 진행 줄 `남은 충돌 24개` · 끝내기 단추 **[계속]**(꺼짐) |
|
||||
| 네 칸 (`Unit15.cs`) | LOCAL **`올릴 자리 feature/theirs`** · REMOTE **`REMOTE · 다시 올리는 내 커밋 a0fead2 내 쪽 작업`** · 머리 글 `rebase 충돌 · LOCAL = 올릴 자리 feature/theirs · REMOTE = 다시 올리는 내 커밋 a0fead2` · "3-Way 안 씀" 없음 |
|
||||
| Take Theirs → Save | 로그 **`10행 Theirs(커밋 a0fead2) 반영`** |
|
||||
| 23개 파일 단위 Accept Theirs · `Io/LineEnding.cs` 는 네 칸에서 Take Theirs → Save | 진행 줄 **`충돌 정리 끝 - [계속] 으로 rebase 를 이어 갑니다`** · [계속] 켜짐 |
|
||||
| [계속] | `rebase 완료 (1/1 단계)` · 진행 표시 · [작업 취소] 사라짐. 이력 `c1296a7 내 쪽 작업` ← `1bdc05b 상대 쪽 작업` ← 바탕 |
|
||||
| 결과 커밋 (부모와 바이트로) | 바뀐 파일 32 · **줄끝 · BOM · 인코딩이 부모와 다른 파일 0** · 충돌 표식 0 |
|
||||
|
||||
**② cherry-pick**
|
||||
|
||||
| 때 | 결과 |
|
||||
|---|---|
|
||||
| 이력 [모든 브랜치] | 끄면 `상대 쪽 작업` 없음 → 켜면 보임, 골라 [Cherry-pick] |
|
||||
| 시작 | 알림 `충돌 24건 (…) - … 다 풀면 [커밋] 으로 cherry-pick 을 끝냅니다.` · 로그 `cherry-pick 충돌 26건 - 풀고 나서 충돌 탭의 [커밋] 으로 끝냅니다.` + 자동 해결 |
|
||||
| 충돌 중 | 머리 **`cherry-pick 진행 중`** · [작업 취소] 켜짐 · 끝내기 단추 **[커밋]**(꺼짐) |
|
||||
| 네 칸 | LOCAL `feature/mine` · REMOTE **`REMOTE · 가져오는 커밋 0862180 상대 쪽 작업`** · 머리 글 `cherry-pick 충돌 · REMOTE = 가져오는 커밋 …` · 로그 `10행 Theirs(커밋 …) 반영` |
|
||||
| 23개 Accept Theirs(24 → 1) · `LineEnding.cs` 네 칸 → Save | 진행 줄 **`충돌 정리 끝 - [커밋] 으로 cherry-pick 을 끝냅니다`** · [커밋] 켜짐 · 진행 표시 그대로 |
|
||||
| [커밋] (메시지 칸 비움) | `cherry-pick 커밋: ebe2a64 상대 쪽 작업` — **원래 메시지**, 부모 하나, `CHERRY_PICK_HEAD` 없어짐, 진행 표시 · [작업 취소] 사라짐 |
|
||||
| 결과 커밋 | 바뀐 파일 32 · **줄끝 · BOM · 인코딩이 부모와 다른 파일 0** · 충돌 표식 0 · `LineEnding.cs` BOM · CRLF 그대로 |
|
||||
|
||||
**③ 바뀌는 것 없는 cherry-pick (HEAD 커밋을 다시)**
|
||||
|
||||
| 때 | 결과 |
|
||||
|---|---|
|
||||
| [Cherry-pick] | `CHERRY_PICK_HEAD` 생김 · 머리 **`cherry-pick 진행 중`** · **[작업 취소] 켜짐** (예전에는 둘 다 없었다) |
|
||||
| [작업 취소] | `CHERRY_PICK_HEAD` 없어짐 · 표시 사라짐 |
|
||||
|
||||
MergeSaveTest 125건 · EncTest 48 · FolderTest 62 · ReportTest 31 · SearchTest 63 통과(`GitModels.cs` 를 함께 컴파일). 빌드 경고 0 · 오류 0. 설정 파일은 바이트 그대로 되돌림.
|
||||
|
||||
### 확인하며 알게 된 것 · 남긴 것
|
||||
|
||||
- **파일 단위 Accept 는 고른 쪽 파일을 바이트째 쓴다.** 상대가 줄끝을 CRLF → LF 로 바꾼 `Io/LineEnding.cs` 를 파일 단위로 Accept Theirs 하면 그 줄끝이 그대로 들어온다(처음 확인에서 1개로 잡힘). 결함이 아니라 고른 대로다 — 6차 D-1 처럼 이런 파일은 네 칸에서 풀면 내 쪽(rebase 는 올릴 자리) 줄끝을 따른다.
|
||||
- **원래 작성자 남기기는 이 저장소로 구별되지 않았다.** 시험 저장소의 모든 커밋 작성자가 같다(`시험`). 코드로만 확인했다.
|
||||
- **③ 에서 [커밋] 도 켜진다.** 바뀐 것이 없어 누르면 빈 커밋 오류가 날 것으로 보인다. 로그 `적용됨 - 확인한 뒤 [커밋] 으로 끝냅니다` 도 이 경우엔 맞지 않다. 고치지 않고 적어 둔다(D-1 에서 다시 본다).
|
||||
- revert 충돌은 같은 길(`AfterConflicts` · `CommitPendingOperation`)을 쓰지만 화면으로 끝까지는 안 봤다.
|
||||
- 자동 조작 쪽: cherry-pick 첫 확인에서 [모든 브랜치] 를 켠 직후 목록 선택이 앱에 안 닿아 Accept 가 한 개만 풀렸다. 기다림을 넣고 다시 해 24 → 1 로 확인했다.
|
||||
|
||||
## E-2. 폴더 비교 탭에 폴더를 하나씩 끌어다 놓으면 첫 칸 → 둘째 칸 차례로 (2026-09-14, 추가 요청, 완료)
|
||||
|
||||
사용자: "폴더비교 탭 첫번째 드래그 첫번째 폴더 경로에 들어가고 두번째는 드래그앤드롭은 두번째로 들어가게해줘 경로텍스트 박스에 안넣어도 그냥 화면에다 넣어둬 기능 동작하도록".
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
| 날짜 | 항목 | 커밋 |
|
||||
|---|---|---|
|
||||
| 2026-09-15 | **7차 C-2** — rebase · cherry-pick 충돌을 화면이 모두 "병합" 으로 불렀다(REMOTE · THEIRS, cherry-pick 에 "병합 진행 중", 다 풀면 끝낼 단추가 없음, 로그에 `RebaseContinue()` · `Commit()`, 다른 브랜치 커밋을 cherry-pick 할 수 없음). 작업 상태(`GitOperationState`)로 칸 제목 · 머리 글 · 진행 표시 · 끝내기 단추(병합 커밋 · 계속 · 커밋)를 정하고, cherry-pick · revert 는 원래 메시지 · 작성자로 커밋. 머지 주석 자동 풀기도 함께. 이력 탭 [모든 브랜치]. 두 길 모두 MergeHub 만으로 끝까지, 줄끝 바뀐 파일 0 | 아래 |
|
||||
| 2026-09-14 | **7차 지시서에 E-4 추가** — 사용자 요청 "창 크기를 마음대로 조절". 지금은 셸이 최대화 전용(테두리 없음 · 크기 고정 · 끌면 다시 최대화). 걸리는 곳(크기 조절 테두리 · 배율 다른 모니터 · 크기 기억 · MergeHub 고정 폭과 네 칸 정렬 · 모든 소비 프로그램)과 판정 기준을 적음. 코드 변경 없음 | 아래 |
|
||||
| 2026-09-14 | **7차 C-1** — pull 로 받은 충돌은 머지 주석만 다른 파일까지 사람에게 물었고(26 · 0/26), 알림도 커밋 메시지도 없었다. [Merge] 의 뒷일을 `AfterMerge` 로 빼 pull 도 같게(24 · 2/26 · 같은 알림). pull 병합 커밋 메시지는 git 과 같은 "Merge remote-tracking branch 'origin/feature/mine' into feature/mine" | 아래 |
|
||||
| 2026-09-14 | **7차 B-1** — 병합 때 적는 처음 충돌 목록(`.git/MERGEHUB_CONFLICTS`)이 병합 커밋 · 작업 취소 · 터미널에서 끝낸 병합 뒤에도 남았다. MERGE_HEAD 가 없거나 다른 병합의 것이면 병합 커밋 · 작업 취소 직후와 상태를 다시 읽을 때마다 지운다. 병합 중에는 그대로라 "푼 것 M개" 는 이어진다 | 아래 |
|
||||
|
||||
@ -164,7 +164,7 @@
|
||||
| 상태 | 항목 |
|
||||
|---|---|
|
||||
| **완료** | C-1 pull 이 낸 충돌에는 머지 주석 자동 풀기가 안 걸린다 — pull 도 [Merge] 와 같은 뒷일(주석 자동 풀기 · 알림 · 진행 줄), 커밋 메시지 "Merge remote-tracking branch 'origin/…' into …" |
|
||||
| 대기 | C-2 rebase · cherry-pick 충돌을 네 칸으로 끝까지 |
|
||||
| **완료** | C-2 rebase · cherry-pick 충돌을 네 칸으로 끝까지 — 작업별 칸 제목(올릴 자리 · 다시 올리는 내 커밋 · 가져오는 커밋) · 진행 표시 · 끝내기 단추(병합 커밋 · 계속 · 커밋) · 원래 메시지, 이력 [모든 브랜치]. 두 길 모두 줄끝 바뀐 파일 0 |
|
||||
|
||||
### D. 확인
|
||||
|
||||
|
||||
@ -114,6 +114,42 @@ namespace MergeHub.Models
|
||||
Both
|
||||
}
|
||||
|
||||
/// <summary>지금 저장소에서 진행 중인 git 작업의 종류(7차 C-2).</summary>
|
||||
public enum GitOperationKind
|
||||
{
|
||||
None,
|
||||
Merge,
|
||||
Rebase,
|
||||
CherryPick,
|
||||
Revert
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 진행 중인 작업과 두 쪽의 이름(7차 C-2). 충돌을 풀 때 "누가 LOCAL 이고 누가 REMOTE 인지" 가 작업마다 다르다.
|
||||
/// <list type="bullet">
|
||||
/// <item>병합 · cherry-pick · revert: ours = 내 브랜치, theirs = 들어오는 브랜치 · 커밋</item>
|
||||
/// <item>rebase: ours = <b>올릴 자리(onto)</b>, theirs = <b>다시 올리는 내 커밋</b> — 병합과 거꾸로다</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
/// <param name="OursName">병합 · cherry-pick · revert 는 내 브랜치, rebase 는 올릴 자리 브랜치 이름.</param>
|
||||
/// <param name="TheirsName">병합은 상대 브랜치, rebase 는 다시 올리는 브랜치 이름.</param>
|
||||
/// <param name="TheirsSha">병합은 MERGE_HEAD, rebase 는 지금 올리는 커밋, cherry-pick · revert 는 그 커밋.</param>
|
||||
/// <param name="TheirsMessage">그 커밋의 한 줄 메시지.</param>
|
||||
public sealed record GitOperationState(
|
||||
GitOperationKind Kind,
|
||||
string? OursName,
|
||||
string? TheirsName,
|
||||
string? TheirsSha,
|
||||
string? TheirsMessage,
|
||||
int Step,
|
||||
int TotalSteps)
|
||||
{
|
||||
public static readonly GitOperationState None =
|
||||
new GitOperationState(GitOperationKind.None, null, null, null, null, 0, 0);
|
||||
|
||||
public bool IsInProgress => Kind != GitOperationKind.None;
|
||||
}
|
||||
|
||||
public enum MergeOutcome
|
||||
{
|
||||
/// <summary>이미 최신 - 병합할 것이 없음.</summary>
|
||||
|
||||
@ -287,7 +287,7 @@ namespace MergeHub.Services
|
||||
|
||||
// ================= 히스토리 =================
|
||||
|
||||
public IReadOnlyList<CommitInfo> GetCommits(string? committish = null, int maxCount = 200)
|
||||
public IReadOnlyList<CommitInfo> GetCommits(string? committish = null, int maxCount = 200, bool allBranches = false)
|
||||
{
|
||||
var refsByTarget = new Dictionary<string, List<string>>(StringComparer.Ordinal);
|
||||
foreach (var branch in Repo.Branches)
|
||||
@ -318,6 +318,14 @@ namespace MergeHub.Services
|
||||
?? throw new ArgumentException($"커밋/브랜치를 찾을 수 없습니다: {committish}");
|
||||
filter.IncludeReachableFrom = start;
|
||||
}
|
||||
else if (allBranches)
|
||||
{
|
||||
// 모든 브랜치(원격 추적 포함)와 HEAD 에서 닿는 커밋. 다른 브랜치의 커밋을 골라 cherry-pick 하려면 필요하다(7차 C-2).
|
||||
// 넣지 않으면 libgit2 는 HEAD 에서 닿는 것만 준다.
|
||||
var tips = Repo.Branches.Select(b => b.Tip).Where(t => t is not null).ToList();
|
||||
if (Repo.Head?.Tip is { } headTip) tips.Add(headTip);
|
||||
if (tips.Count > 0) filter.IncludeReachableFrom = tips;
|
||||
}
|
||||
|
||||
var result = new List<CommitInfo>(Math.Min(maxCount, 256));
|
||||
foreach (var c in Repo.Commits.QueryBy(filter))
|
||||
@ -639,10 +647,11 @@ namespace MergeHub.Services
|
||||
|
||||
var message = outcome switch
|
||||
{
|
||||
// 사람이 읽는 로그다 - 예전에는 "Commit() 을 호출하세요" 처럼 코드 이름을 적었다(7차 C-2).
|
||||
PatchApplyOutcome.Applied => commitOnSuccess
|
||||
? $"{operation} 완료 ({Shorten(sha)})"
|
||||
: $"{operation} 적용됨 - 확인 후 Commit() 을 호출하세요.",
|
||||
PatchApplyOutcome.Conflicts => $"{operation} 충돌 {conflicts.Count}건 - 해결이 필요합니다.",
|
||||
: $"{operation} 적용됨 - 확인한 뒤 충돌 탭의 [커밋] 으로 끝냅니다.",
|
||||
PatchApplyOutcome.Conflicts => $"{operation} 충돌 {conflicts.Count}건 - 풀고 나서 충돌 탭의 [커밋] 으로 끝냅니다.",
|
||||
PatchApplyOutcome.NothingToDo => $"{operation} 할 변경이 없습니다.",
|
||||
_ => $"{operation} 실패"
|
||||
};
|
||||
@ -729,7 +738,7 @@ namespace MergeHub.Services
|
||||
RebaseOutcome.NothingToDo => "재배치할 커밋이 없습니다.",
|
||||
RebaseOutcome.Conflicts =>
|
||||
$"rebase {result.CompletedStepCount + 1}/{result.TotalStepCount} 단계에서 충돌 {conflicts.Count}건 " +
|
||||
"- 해결 후 RebaseContinue() 를 호출하세요.",
|
||||
"- 풀고 나서 [계속] 을 누르세요.",
|
||||
RebaseOutcome.Stopped => $"rebase 정지 ({result.CompletedStepCount}/{result.TotalStepCount} 단계)",
|
||||
_ => "rebase 실패"
|
||||
};
|
||||
@ -1066,6 +1075,124 @@ namespace MergeHub.Services
|
||||
return null;
|
||||
}
|
||||
|
||||
public GitOperationState GetOperationState()
|
||||
{
|
||||
var head = Repo.Head;
|
||||
|
||||
GitOperationState Picked(GitOperationKind kind, string headFile)
|
||||
{
|
||||
var sha = ReadFirstShaLine(Path.Combine(Repo.Info.Path, headFile));
|
||||
var commit = sha is null ? null : Repo.Lookup<Commit>(sha);
|
||||
return new GitOperationState(kind, head?.FriendlyName, null, commit?.Sha ?? sha, commit?.MessageShort, 0, 0);
|
||||
}
|
||||
|
||||
switch (Repo.Info.CurrentOperation)
|
||||
{
|
||||
case CurrentOperation.Rebase:
|
||||
case CurrentOperation.RebaseInteractive:
|
||||
case CurrentOperation.RebaseMerge:
|
||||
{
|
||||
// rebase 는 병합과 두 쪽이 거꾸로다: ours = 올릴 자리(onto), theirs = 다시 올리는 내 커밋.
|
||||
var dir = Path.Combine(Repo.Info.Path, "rebase-merge");
|
||||
var onto = ReadFirstShaLine(Path.Combine(dir, "onto"));
|
||||
var headName = File.Exists(Path.Combine(dir, "head-name"))
|
||||
? File.ReadAllText(Path.Combine(dir, "head-name")).Trim()
|
||||
: null;
|
||||
var step = Repo.Rebase.GetCurrentStepInfo();
|
||||
return new GitOperationState(
|
||||
GitOperationKind.Rebase,
|
||||
OursName: BranchNameAt(onto) ?? (onto is null ? null : Shorten(onto)),
|
||||
TheirsName: headName is not null && headName.StartsWith("refs/heads/", StringComparison.Ordinal)
|
||||
? headName.Substring("refs/heads/".Length)
|
||||
: headName,
|
||||
TheirsSha: step?.Commit?.Sha,
|
||||
TheirsMessage: step?.Commit?.MessageShort,
|
||||
Step: (int)(Repo.Rebase.GetCurrentStepIndex() + 1),
|
||||
TotalSteps: (int)Repo.Rebase.GetTotalStepCount());
|
||||
}
|
||||
case CurrentOperation.CherryPick:
|
||||
case CurrentOperation.CherryPickSequence:
|
||||
return Picked(GitOperationKind.CherryPick, "CHERRY_PICK_HEAD");
|
||||
case CurrentOperation.Revert:
|
||||
case CurrentOperation.RevertSequence:
|
||||
return Picked(GitOperationKind.Revert, "REVERT_HEAD");
|
||||
case CurrentOperation.Merge:
|
||||
return new GitOperationState(GitOperationKind.Merge, head?.FriendlyName, GetMergeTheirsName(),
|
||||
ReadMergeHeadSha(), null, 0, 0);
|
||||
default:
|
||||
// 작업 표시 없이 충돌만 남은 경우(stash 적용 등)는 지금까지처럼 병합으로 본다.
|
||||
return Repo.Index.Conflicts.Any()
|
||||
? new GitOperationState(GitOperationKind.Merge, head?.FriendlyName, null, null, null, 0, 0)
|
||||
: GitOperationState.None;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>파일 첫 줄의 40자 해시. 없으면 null.</summary>
|
||||
private static string? ReadFirstShaLine(string path)
|
||||
{
|
||||
if (!File.Exists(path)) return null;
|
||||
foreach (var line in File.ReadAllLines(path))
|
||||
{
|
||||
var sha = line.Trim();
|
||||
if (sha.Length >= 40) return sha.Substring(0, 40);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>그 커밋을 끝으로 가리키는 브랜치 이름. 로컬을 먼저 본다.</summary>
|
||||
private string? BranchNameAt(string? sha)
|
||||
{
|
||||
if (sha is null) return null;
|
||||
return Repo.Branches.Where(b => !b.IsRemote && b.Tip?.Sha == sha).Select(b => b.FriendlyName).FirstOrDefault()
|
||||
?? Repo.Branches.Where(b => b.IsRemote && b.Tip?.Sha == sha).Select(b => b.FriendlyName).FirstOrDefault();
|
||||
}
|
||||
|
||||
public string? GetPendingCommitMessage()
|
||||
{
|
||||
var state = GetOperationState();
|
||||
if (state.Kind is not (GitOperationKind.CherryPick or GitOperationKind.Revert)) return null;
|
||||
|
||||
// libgit2 가 남긴 MERGE_MSG(원래 메시지)가 있으면 # 줄을 빼고 쓴다. 없으면 그 커밋에서 만든다.
|
||||
var raw = ReadMergeMessage();
|
||||
if (raw is not null)
|
||||
{
|
||||
var cleaned = string.Join("\n", raw.Replace("\r\n", "\n").Split('\n')
|
||||
.Where(l => !l.TrimStart().StartsWith("#", StringComparison.Ordinal))).Trim();
|
||||
if (cleaned.Length > 0) return cleaned;
|
||||
}
|
||||
|
||||
var commit = state.TheirsSha is null ? null : Repo.Lookup<Commit>(state.TheirsSha);
|
||||
if (commit is null) return null;
|
||||
return state.Kind == GitOperationKind.CherryPick
|
||||
? commit.Message.TrimEnd()
|
||||
: $"Revert \"{commit.MessageShort}\"\n\nThis reverts commit {commit.Sha}.";
|
||||
}
|
||||
|
||||
public string CommitPendingOperation(string? message = null)
|
||||
{
|
||||
var state = GetOperationState();
|
||||
if (state.Kind is not (GitOperationKind.CherryPick or GitOperationKind.Revert))
|
||||
return CommitMerge(message);
|
||||
|
||||
var remaining = GetConflicts();
|
||||
if (remaining.Count > 0)
|
||||
throw new InvalidOperationException($"아직 해결되지 않은 충돌이 {remaining.Count}건 있습니다.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
message = GetPendingCommitMessage() ?? (state.Kind == GitOperationKind.CherryPick ? "cherry-pick" : "revert");
|
||||
|
||||
// cherry-pick 은 git 처럼 원래 작성자를 남기고, 커밋한 사람은 지금 사용자로 둔다.
|
||||
var committer = BuildSignature();
|
||||
var author = committer;
|
||||
if (state.Kind == GitOperationKind.CherryPick && state.TheirsSha is { } sha && Repo.Lookup<Commit>(sha) is { } picked)
|
||||
author = new Signature(picked.Author.Name, picked.Author.Email, picked.Author.When);
|
||||
|
||||
var commit = Repo.Commit(message, author, committer);
|
||||
ForgetStaleMergeConflicts();
|
||||
Report($"{(state.Kind == GitOperationKind.CherryPick ? "cherry-pick" : "revert")} 커밋: {Shorten(commit.Sha)} {commit.MessageShort}");
|
||||
return commit.Sha;
|
||||
}
|
||||
|
||||
public string? GetMergeTheirsName()
|
||||
{
|
||||
if (Repo.Info.CurrentOperation != CurrentOperation.Merge && !Repo.Index.Conflicts.Any()) return null;
|
||||
|
||||
@ -187,21 +187,28 @@ namespace MergeHub.Services
|
||||
/// 처음 충돌 수는 병합할 때 .git 에 적어 두므로 앱을 다시 켜도 이어진다.
|
||||
/// MergeHub 밖에서 시작한 병합이라 처음 수를 모르면 남은 수만 적는다.
|
||||
/// </summary>
|
||||
public string ConflictProgressText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsMergeInProgress) return string.Empty;
|
||||
public string ConflictProgressText =>
|
||||
// 작업마다 다 푼 뒤 할 일이 다르다 - 병합 커밋 · [계속] · [커밋](7차 C-2). 병합의 글은 예전과 같다.
|
||||
OperationLabels.ProgressText(OperationState.Kind, Conflicts.Count, MergeConflictTotal);
|
||||
|
||||
var left = Conflicts.Count;
|
||||
if (MergeConflictTotal is not int total || total < left)
|
||||
return left == 0 ? "충돌 정리 끝 - 병합 커밋을 할 수 있습니다" : $"남은 충돌 {left}개";
|
||||
/// <summary>진행 중인 git 작업과 두 쪽 이름(7차 C-2). 상태를 다시 읽을 때마다 갱신한다.</summary>
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsOperationInProgress))]
|
||||
[NotifyPropertyChangedFor(nameof(ConflictProgressText))]
|
||||
private GitOperationState _operationState = GitOperationState.None;
|
||||
|
||||
var done = total - left;
|
||||
return left == 0
|
||||
? $"충돌 정리 끝 · 푼 것 {done}개 - 병합 커밋을 할 수 있습니다"
|
||||
: $"남은 충돌 {left}개 · 푼 것 {done}개 ({done}/{total})";
|
||||
}
|
||||
/// <summary>
|
||||
/// 병합 · rebase · cherry-pick · revert 중 무엇이든 진행 중인지. 충돌 수와 따로 본다 -
|
||||
/// 예전에는 충돌이 0 이 되면 cherry-pick 이 진행 중이라는 표시도 [작업 취소] 도 사라졌다(7차 C-2).
|
||||
/// </summary>
|
||||
public bool IsOperationInProgress => OperationState.IsInProgress;
|
||||
|
||||
/// <summary>이력에 모든 브랜치의 커밋을 보일지(7차 C-2). 다른 브랜치 커밋을 골라 cherry-pick 할 때 켠다.</summary>
|
||||
[ObservableProperty] private bool _showAllBranchesInHistory;
|
||||
|
||||
partial void OnShowAllBranchesInHistoryChanged(bool value)
|
||||
{
|
||||
if (IsRepositoryOpen) _ = RefreshAsync();
|
||||
}
|
||||
[ObservableProperty] private bool _isRebaseInProgress;
|
||||
[ObservableProperty] private string _pendingOperationText = string.Empty;
|
||||
@ -340,7 +347,9 @@ namespace MergeHub.Services
|
||||
|
||||
var info = Git.GetRepositoryInfo();
|
||||
var branches = Git.GetBranches();
|
||||
var commits = Git.GetCommits(maxCount: Settings.CommitPageSize <= 0 ? 200 : Settings.CommitPageSize);
|
||||
var commits = Git.GetCommits(maxCount: Settings.CommitPageSize <= 0 ? 200 : Settings.CommitPageSize,
|
||||
allBranches: ShowAllBranchesInHistory);
|
||||
var operation = Git.GetOperationState();
|
||||
var nodes = CommitGraphBuilder.Build(commits);
|
||||
var status = Git.GetStatus();
|
||||
var conflicts = Git.GetConflicts();
|
||||
@ -373,9 +382,9 @@ namespace MergeHub.Services
|
||||
|
||||
IsMergeInProgress = mergeInProgress;
|
||||
IsRebaseInProgress = rebase.IsInProgress;
|
||||
PendingOperationText =
|
||||
rebase.IsInProgress ? $"rebase {rebase.CurrentStep}/{rebase.TotalSteps}" :
|
||||
mergeInProgress ? "병합 진행 중" : string.Empty;
|
||||
OperationState = operation;
|
||||
// cherry-pick · revert 도 "병합 진행 중" 이라고 했다. 작업 이름으로 적는다(7차 C-2).
|
||||
PendingOperationText = OperationLabels.PendingText(operation);
|
||||
|
||||
IsWorkingTreeClean = status.IsClean;
|
||||
WorkingTreeSummary = status.IsClean ? "Clean" : $"{status.Files.Count} files";
|
||||
|
||||
@ -49,7 +49,8 @@ namespace MergeHub.Services
|
||||
void Push(string? branchName = null, string remoteName = "origin");
|
||||
|
||||
// ---- 히스토리 / 조회 ----------------------------------------------
|
||||
IReadOnlyList<CommitInfo> GetCommits(string? committish = null, int maxCount = 200);
|
||||
/// <param name="allBranches">committish 가 없을 때 모든 브랜치에서 닿는 커밋까지(7차 C-2). 아니면 HEAD 에서 닿는 것만.</param>
|
||||
IReadOnlyList<CommitInfo> GetCommits(string? committish = null, int maxCount = 200, bool allBranches = false);
|
||||
CommitInfo? GetCommit(string committish);
|
||||
string? GetMergeBaseSha(string committishA, string committishB);
|
||||
|
||||
@ -94,6 +95,15 @@ namespace MergeHub.Services
|
||||
/// </summary>
|
||||
string? GetMergeTheirsName();
|
||||
|
||||
/// <summary>진행 중인 작업(병합 · rebase · cherry-pick · revert)과 두 쪽 이름(7차 C-2).</summary>
|
||||
GitOperationState GetOperationState();
|
||||
|
||||
/// <summary>cherry-pick · revert 를 끝낼 커밋의 기본 메시지(원래 메시지). 그 작업이 아니면 null(7차 C-2).</summary>
|
||||
string? GetPendingCommitMessage();
|
||||
|
||||
/// <summary>충돌을 다 푼 cherry-pick · revert 를 커밋해 끝낸다. 병합이면 <see cref="CommitMerge"/> 와 같다(7차 C-2).</summary>
|
||||
string CommitPendingOperation(string? message = null);
|
||||
|
||||
/// <summary>Accept Mine / Theirs / Both 버튼 처리. 파일에 기록하고 인덱스에서 충돌 해제.</summary>
|
||||
void ResolveConflict(string path, ConflictResolution resolution);
|
||||
|
||||
|
||||
114
src/MergeHub/Services/OperationLabels.cs
Normal file
114
src/MergeHub/Services/OperationLabels.cs
Normal file
@ -0,0 +1,114 @@
|
||||
using MergeHub.Models;
|
||||
|
||||
namespace MergeHub.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 진행 중인 git 작업(병합 · rebase · cherry-pick · revert)을 화면이 사람 말로 부르는 글(7차 C-2).
|
||||
///
|
||||
/// 예전에는 모든 충돌을 "병합" 으로만 말했다. rebase 에서는 LOCAL 이 올릴 자리(상대 브랜치)이고 REMOTE 가
|
||||
/// <b>내 커밋</b>인데도 "REMOTE · THEIRS" 라고 했고, cherry-pick 은 머리에 "병합 진행 중" 이 떴다.
|
||||
/// 충돌을 다 푼 뒤 할 일도 작업마다 다르다 — 병합은 [병합 커밋], rebase 는 [계속], cherry-pick · revert 는 [커밋].
|
||||
/// 저장소 없이 검사하도록 글만 여기 모았다.
|
||||
/// </summary>
|
||||
public static class OperationLabels
|
||||
{
|
||||
private static string Short(string? sha)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sha)) return string.Empty;
|
||||
var t = sha!.Trim();
|
||||
return t.Length > 7 ? t.Substring(0, 7) : t;
|
||||
}
|
||||
|
||||
private static string CommitRef(GitOperationState s)
|
||||
{
|
||||
var sha = Short(s.TheirsSha);
|
||||
var msg = string.IsNullOrWhiteSpace(s.TheirsMessage) ? string.Empty : s.TheirsMessage!.Trim();
|
||||
if (sha.Length == 0) return msg;
|
||||
return msg.Length == 0 ? sha : $"{sha} {msg}";
|
||||
}
|
||||
|
||||
private static string Onto(GitOperationState s) =>
|
||||
string.IsNullOrWhiteSpace(s.OursName) ? "(올릴 자리)" : s.OursName!.Trim();
|
||||
|
||||
/// <summary>머리의 진행 표시. 진행 중이 아니면 빈 글.</summary>
|
||||
public static string PendingText(GitOperationState s) => s.Kind switch
|
||||
{
|
||||
GitOperationKind.Merge => "병합 진행 중",
|
||||
GitOperationKind.Rebase => s.TotalSteps > 0 ? $"rebase {s.Step}/{s.TotalSteps}" : "rebase 진행 중",
|
||||
GitOperationKind.CherryPick => "cherry-pick 진행 중",
|
||||
GitOperationKind.Revert => "revert 진행 중",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
/// <summary>네 칸의 LOCAL 칸 제목. rebase 는 올릴 자리, 그 밖은 내 브랜치.</summary>
|
||||
public static string OursTitle(GitOperationState s, string? currentBranch) =>
|
||||
s.Kind == GitOperationKind.Rebase
|
||||
? $"올릴 자리 {Onto(s)}"
|
||||
: !string.IsNullOrWhiteSpace(currentBranch) ? currentBranch!.Trim() : s.OursName ?? "LOCAL";
|
||||
|
||||
/// <summary>네 칸의 REMOTE 칸 제목("REMOTE · " 뒤).</summary>
|
||||
public static string TheirsTitle(GitOperationState s) => s.Kind switch
|
||||
{
|
||||
GitOperationKind.Rebase => $"다시 올리는 내 커밋 {CommitRef(s)}".TrimEnd(),
|
||||
GitOperationKind.CherryPick => $"가져오는 커밋 {CommitRef(s)}".TrimEnd(),
|
||||
GitOperationKind.Revert => $"되돌리는 커밋 {CommitRef(s)}".TrimEnd(),
|
||||
_ => string.IsNullOrWhiteSpace(s.TheirsName) ? "THEIRS" : s.TheirsName!.Trim()
|
||||
};
|
||||
|
||||
/// <summary>충돌 네 칸일 때 3-Way 콤보 자리에 보이는 글(7차 A-2 의 "병합 충돌 · REMOTE = …" 을 작업별로).</summary>
|
||||
public static string HeaderText(GitOperationState s) => s.Kind switch
|
||||
{
|
||||
GitOperationKind.Rebase => $"rebase 충돌 · LOCAL = 올릴 자리 {Onto(s)} · REMOTE = 다시 올리는 내 커밋 {Short(s.TheirsSha)}".TrimEnd(),
|
||||
GitOperationKind.CherryPick => $"cherry-pick 충돌 · REMOTE = 가져오는 커밋 {Short(s.TheirsSha)}".TrimEnd(),
|
||||
GitOperationKind.Revert => $"revert 충돌 · REMOTE = 되돌리는 커밋 {Short(s.TheirsSha)}".TrimEnd(),
|
||||
_ => $"병합 충돌 · REMOTE = {(string.IsNullOrWhiteSpace(s.TheirsName) ? "THEIRS" : s.TheirsName!.Trim())}"
|
||||
};
|
||||
|
||||
/// <summary>Take Theirs 로그의 괄호 안 이름. 병합은 브랜치, 그 밖은 "커밋 해시".</summary>
|
||||
public static string? TheirsLogName(GitOperationState s)
|
||||
{
|
||||
if (s.Kind is GitOperationKind.Rebase or GitOperationKind.CherryPick or GitOperationKind.Revert)
|
||||
return Short(s.TheirsSha) is { Length: > 0 } sha ? $"커밋 {sha}" : null;
|
||||
return string.IsNullOrWhiteSpace(s.TheirsName) ? null : s.TheirsName!.Trim();
|
||||
}
|
||||
|
||||
/// <summary>Take Mine 로그의 괄호 안 이름. rebase 는 올릴 자리, 그 밖은 내 브랜치.</summary>
|
||||
public static string? MineLogName(GitOperationState s, string? currentBranch) =>
|
||||
s.Kind == GitOperationKind.Rebase ? s.OursName : currentBranch;
|
||||
|
||||
/// <summary>충돌을 다 푼 뒤 할 일.</summary>
|
||||
public static string FinishHint(GitOperationKind kind) => kind switch
|
||||
{
|
||||
GitOperationKind.Rebase => "[계속] 으로 rebase 를 이어 갑니다",
|
||||
GitOperationKind.CherryPick => "[커밋] 으로 cherry-pick 을 끝냅니다",
|
||||
GitOperationKind.Revert => "[커밋] 으로 revert 를 끝냅니다",
|
||||
_ => "병합 커밋을 할 수 있습니다"
|
||||
};
|
||||
|
||||
/// <summary>충돌 탭의 끝내기 단추 글자.</summary>
|
||||
public static string FinishButtonLabel(GitOperationKind kind) => kind switch
|
||||
{
|
||||
GitOperationKind.Rebase => "계속",
|
||||
GitOperationKind.CherryPick or GitOperationKind.Revert => "커밋",
|
||||
_ => "병합 커밋"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 충돌 탭 목록 위의 진행 한 줄(6차 C-1 을 작업별로). 처음 충돌 수를 모르면 남은 수만 적는다.
|
||||
/// 병합의 글은 예전과 같다.
|
||||
/// </summary>
|
||||
public static string ProgressText(GitOperationKind kind, int left, int? total)
|
||||
{
|
||||
if (kind == GitOperationKind.None) return string.Empty;
|
||||
|
||||
var finish = FinishHint(kind);
|
||||
if (total is not int t || t < left)
|
||||
return left == 0 ? $"충돌 정리 끝 - {finish}" : $"남은 충돌 {left}개";
|
||||
|
||||
var done = t - left;
|
||||
return left == 0
|
||||
? $"충돌 정리 끝 · 푼 것 {done}개 - {finish}"
|
||||
: $"남은 충돌 {left}개 · 푼 것 {done}개 ({done}/{t})";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -245,43 +245,83 @@ namespace MergeHub.ViewModels
|
||||
/// 병합(또는 pull) 결과를 받은 뒤 할 일. 작업 스레드에서 부른다(7차 C-1 에서 [Merge] 와 pull 이 함께 쓰게 뺐다).
|
||||
/// </summary>
|
||||
/// <param name="defaultMessage">커밋 메시지 칸이 비어 있고 병합 커밋이 남았을 때 채울 메시지.</param>
|
||||
private void AfterMerge(MergeResultInfo result, string defaultMessage)
|
||||
private void AfterMerge(MergeResultInfo result, string defaultMessage) =>
|
||||
// 병합 커밋이 남는 경우에만 메시지를 채운다 - 이미 최신이거나 앞으로 감기면 커밋할 병합이 없다.
|
||||
AfterConflicts(result.HasConflicts, result.Conflicts.Count, result.Message,
|
||||
result.HasConflicts || result.Outcome == MergeOutcome.Merged ? defaultMessage : null,
|
||||
GitOperationKind.Merge);
|
||||
|
||||
/// <summary>
|
||||
/// 충돌이 날 수 있는 작업(병합 · pull · rebase · cherry-pick · revert) 뒤에 할 일. 작업 스레드에서 부른다.
|
||||
/// 7차 C-2 에서 rebase · cherry-pick · revert 도 같은 길을 쓰게 했다 - 머지 주석 자동 풀기 · 알림 · 커밋 메시지.
|
||||
/// </summary>
|
||||
/// <param name="fillMessage">커밋 메시지 칸이 비어 있으면 채울 글. 없으면 null.</param>
|
||||
private void AfterConflicts(bool hasConflicts, int conflictCount, string resultMessage, string? fillMessage, GitOperationKind kind)
|
||||
{
|
||||
// 머지 주석만 다른 충돌은 사람에게 묻지 않고 푼다. 양쪽 주석을 다 남긴다(6차 B-5).
|
||||
IReadOnlyList<string> noteOnly = result.HasConflicts && _workspace.Settings.IgnoreMergeNotes
|
||||
IReadOnlyList<string> noteOnly = hasConflicts && _workspace.Settings.IgnoreMergeNotes
|
||||
? _workspace.Git.ResolveNoteOnlyConflicts()
|
||||
: Array.Empty<string>();
|
||||
var remaining = noteOnly.Count > 0 ? _workspace.Git.GetConflicts().Count : result.Conflicts.Count;
|
||||
var remaining = noteOnly.Count > 0 ? _workspace.Git.GetConflicts().Count : conflictCount;
|
||||
var finish = OperationLabels.FinishHint(kind);
|
||||
|
||||
_workspace.OnUi(() =>
|
||||
{
|
||||
// result.Message("충돌 N건 - 해결이 필요합니다.")는 GitService 가 이미 로그에 적었다(Progress).
|
||||
// 여기서 또 적으면 두 번 찍히고, 자동 해결 줄 뒤에 다시 나와 순서도 헷갈렸다(6차 C-1).
|
||||
// 결과 문장은 GitService 가 이미 로그에 적었다(Progress). 여기서 또 적으면 두 번 찍힌다(6차 C-1).
|
||||
if (noteOnly.Count > 0)
|
||||
_workspace.AppendLog($"머지 주석만 다른 충돌 {noteOnly.Count}개 파일은 자동으로 풀었습니다. 남은 충돌 {remaining}건.");
|
||||
|
||||
// 병합 커밋이 남는 경우에만 채운다 - 이미 최신이거나 앞으로 감기면 커밋할 병합이 없다.
|
||||
if (string.IsNullOrWhiteSpace(CommitMessage) &&
|
||||
(result.HasConflicts || result.Outcome == MergeOutcome.Merged))
|
||||
CommitMessage = defaultMessage;
|
||||
if (string.IsNullOrWhiteSpace(CommitMessage) && !string.IsNullOrWhiteSpace(fillMessage))
|
||||
CommitMessage = fillMessage!;
|
||||
|
||||
var autoNote = noteOnly.Count > 0 ? $" (머지 주석만 다른 {noteOnly.Count}건은 자동으로 풀림)" : string.Empty;
|
||||
var then = kind == GitOperationKind.Merge ? string.Empty : $" 다 풀면 {finish}.";
|
||||
|
||||
if (result.HasConflicts && remaining > 0)
|
||||
MessageUtil.ShowWarningToast($"충돌 {remaining}건{autoNote} - 충돌 탭에서 파일을 두 번 누르면 파일 비교 탭에서 세 칸으로 풉니다.");
|
||||
else if (result.HasConflicts)
|
||||
MessageUtil.ShowSuccessToast($"머지 주석만 다른 충돌 {noteOnly.Count}건을 자동으로 풀었습니다 - 병합 커밋을 할 수 있습니다.");
|
||||
if (hasConflicts && remaining > 0)
|
||||
MessageUtil.ShowWarningToast($"충돌 {remaining}건{autoNote} - 충돌 탭에서 파일을 두 번 누르면 파일 비교 탭에서 세 칸으로 풉니다.{then}");
|
||||
else if (hasConflicts)
|
||||
MessageUtil.ShowSuccessToast($"머지 주석만 다른 충돌 {noteOnly.Count}건을 자동으로 풀었습니다 - {finish}.");
|
||||
else
|
||||
MessageUtil.ShowToast(result.Message);
|
||||
MessageUtil.ShowToast(resultMessage);
|
||||
});
|
||||
}
|
||||
|
||||
private bool CanAbort() => _workspace.IsMergeInProgress || _workspace.IsRebaseInProgress;
|
||||
private bool CanAbort() =>
|
||||
_workspace.IsOperationInProgress || _workspace.IsMergeInProgress || _workspace.IsRebaseInProgress;
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanAbort))]
|
||||
private Task AbortAsync() => _workspace.RunAndRefreshAsync("작업 취소",
|
||||
() => _workspace.Git.AbortOperation());
|
||||
|
||||
/// <summary>충돌 탭의 끝내기 단추 글자 — 병합 커밋 · 계속 · 커밋(7차 C-2).</summary>
|
||||
public string FinishOperationLabel => OperationLabels.FinishButtonLabel(_workspace.OperationState.Kind);
|
||||
|
||||
private bool CanFinishOperation() => _workspace.OperationState.IsInProgress && _workspace.Conflicts.Count == 0;
|
||||
|
||||
/// <summary>
|
||||
/// 충돌을 다 푼 작업을 끝낸다(7차 C-2). 병합은 병합 커밋, rebase 는 계속, cherry-pick · revert 는 원래 메시지로 커밋.
|
||||
/// 예전에는 [병합 커밋] 하나뿐이라 cherry-pick 은 충돌이 0 이 되면 끝낼 단추가 없었다.
|
||||
/// </summary>
|
||||
[RelayCommand(CanExecute = nameof(CanFinishOperation))]
|
||||
private Task FinishOperationAsync()
|
||||
{
|
||||
var kind = _workspace.OperationState.Kind;
|
||||
if (kind == GitOperationKind.Rebase) return RebaseContinueAsync();
|
||||
if (kind == GitOperationKind.Merge) return CommitMergeAsync();
|
||||
|
||||
var name = kind == GitOperationKind.CherryPick ? "cherry-pick" : "revert";
|
||||
return _workspace.RunAndRefreshAsync($"{name} 커밋", () =>
|
||||
{
|
||||
var message = string.IsNullOrWhiteSpace(CommitMessage) ? null : CommitMessage;
|
||||
var sha = _workspace.Git.CommitPendingOperation(message);
|
||||
_workspace.OnUi(() =>
|
||||
{
|
||||
CommitMessage = string.Empty;
|
||||
MessageUtil.ShowSuccessToast($"{name} 커밋 완료 ({sha.Substring(0, 7)})");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private bool CanCommitMerge() => _workspace.IsMergeInProgress && _workspace.Conflicts.Count == 0;
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanCommitMerge))]
|
||||
@ -414,15 +454,19 @@ namespace MergeHub.ViewModels
|
||||
[RelayCommand(CanExecute = nameof(HasCommit))]
|
||||
private Task RevertAsync() => _workspace.RunAndRefreshAsync("revert", () =>
|
||||
{
|
||||
// 결과 한 줄은 GitService 가 로그에 적는다. 여기서 또 적으면 두 번 찍힌다(6차 C-1).
|
||||
_workspace.Git.Revert(SelectedCommit!.Sha);
|
||||
// 결과 한 줄은 GitService 가 로그에 적는다. 여기서 또 적으면 두 번 찍힌다(6차 C-1). 뒷일은 병합과 같은 길로(7차 C-2).
|
||||
var result = _workspace.Git.Revert(SelectedCommit!.Sha);
|
||||
AfterConflicts(result.HasConflicts, result.Conflicts.Count, result.Message,
|
||||
_workspace.Git.GetPendingCommitMessage(), GitOperationKind.Revert);
|
||||
});
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasCommit))]
|
||||
private Task CherryPickAsync() => _workspace.RunAndRefreshAsync("cherry-pick", () =>
|
||||
{
|
||||
// 결과 한 줄은 GitService 가 로그에 적는다(6차 C-1).
|
||||
_workspace.Git.CherryPick(SelectedCommit!.Sha);
|
||||
// 결과 한 줄은 GitService 가 로그에 적는다(6차 C-1). 뒷일은 병합과 같은 길로(7차 C-2).
|
||||
var result = _workspace.Git.CherryPick(SelectedCommit!.Sha);
|
||||
AfterConflicts(result.HasConflicts, result.Conflicts.Count, result.Message,
|
||||
_workspace.Git.GetPendingCommitMessage(), GitOperationKind.CherryPick);
|
||||
});
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasCommit))]
|
||||
@ -435,8 +479,10 @@ namespace MergeHub.ViewModels
|
||||
[RelayCommand(CanExecute = nameof(CanRebase))]
|
||||
private Task RebaseAsync() => _workspace.RunAndRefreshAsync("rebase", () =>
|
||||
{
|
||||
// 결과 한 줄은 GitService 가 로그에 적는다(6차 C-1).
|
||||
_workspace.Git.RebaseStart(SelectedBranch!.FriendlyName);
|
||||
// 결과 한 줄은 GitService 가 로그에 적는다(6차 C-1). 뒷일은 병합과 같은 길로(7차 C-2).
|
||||
var result = _workspace.Git.RebaseStart(SelectedBranch!.FriendlyName);
|
||||
AfterConflicts(result.Outcome == RebaseOutcome.Conflicts, result.Conflicts.Count, result.Message,
|
||||
null, GitOperationKind.Rebase);
|
||||
});
|
||||
|
||||
private bool IsRebasing() => _workspace.IsRebaseInProgress;
|
||||
@ -444,8 +490,10 @@ namespace MergeHub.ViewModels
|
||||
[RelayCommand(CanExecute = nameof(IsRebasing))]
|
||||
private Task RebaseContinueAsync() => _workspace.RunAndRefreshAsync("rebase continue", () =>
|
||||
{
|
||||
// 결과 한 줄은 GitService 가 로그에 적는다(6차 C-1).
|
||||
_workspace.Git.RebaseContinue();
|
||||
// 결과 한 줄은 GitService 가 로그에 적는다(6차 C-1). 다음 단계에서 또 충돌하면 병합과 같은 뒷일(7차 C-2).
|
||||
var result = _workspace.Git.RebaseContinue();
|
||||
AfterConflicts(result.Outcome == RebaseOutcome.Conflicts, result.Conflicts.Count, result.Message,
|
||||
null, GitOperationKind.Rebase);
|
||||
});
|
||||
|
||||
[RelayCommand(CanExecute = nameof(IsRebasing))]
|
||||
@ -744,6 +792,8 @@ namespace MergeHub.ViewModels
|
||||
MergeCommand.NotifyCanExecuteChanged();
|
||||
AbortCommand.NotifyCanExecuteChanged();
|
||||
CommitMergeCommand.NotifyCanExecuteChanged();
|
||||
FinishOperationCommand.NotifyCanExecuteChanged();
|
||||
OnPropertyChanged(nameof(FinishOperationLabel));
|
||||
AcceptMineCommand.NotifyCanExecuteChanged();
|
||||
AcceptTheirsCommand.NotifyCanExecuteChanged();
|
||||
AcceptBothCommand.NotifyCanExecuteChanged();
|
||||
|
||||
@ -1204,6 +1204,7 @@ namespace MergeHub.ViewModels
|
||||
var branch = RemoteBranch;
|
||||
var baseOrigin = string.Empty;
|
||||
string? theirsName = null;
|
||||
var operation = GitOperationState.None;
|
||||
var deletion = default(DeletionConflict);
|
||||
|
||||
// 다른 작업이 게이트를 쥐고 있으면 RunAsync 는 아무것도 하지 않고 false 를 준다.
|
||||
@ -1220,7 +1221,9 @@ namespace MergeHub.ViewModels
|
||||
remoteText = three.TheirsText;
|
||||
isConflict = true;
|
||||
baseOrigin = "merge base";
|
||||
theirsName = _workspace.Git.GetMergeTheirsName();
|
||||
// 병합 · rebase · cherry-pick · revert 마다 두 쪽이 다르다. rebase 는 LOCAL 이 올릴 자리, REMOTE 가 내 커밋(7차 C-2).
|
||||
operation = _workspace.Git.GetOperationState();
|
||||
theirsName = OperationLabels.TheirsLogName(operation);
|
||||
|
||||
// 한쪽이 파일을 지운 충돌(6차 A-4). 정할 것은 "남길까 지울까" 하나라 줄 단위 3분할로 끌고 가지 않는다.
|
||||
// 끌고 가면 지운 쪽 칸이 통째로 비고, 범례는 남은 쪽에만 있는 줄을 거꾸로 센다("Theirs only 89").
|
||||
@ -1289,7 +1292,8 @@ namespace MergeHub.ViewModels
|
||||
|
||||
// 행을 싣기 전에 틀부터 바꾼다. 세 칸 행을 두 칸 틀에 실으면 REMOTE 칸이 안 보인다.
|
||||
_theirsSideName = isConflict ? theirsName ?? "THEIRS" : null;
|
||||
_mineSideName = null;
|
||||
_mineSideName = isConflict ? OperationLabels.MineLogName(operation, _workspace.CurrentBranch) : null;
|
||||
_conflictHeaderText = OperationLabels.HeaderText(operation);
|
||||
SetConflictThreeWay(isConflict && !deletion.IsDeletion);
|
||||
SetDeletion(deletion, theirsName);
|
||||
OnPropertyChanged(nameof(ConflictSideText));
|
||||
@ -1312,9 +1316,11 @@ namespace MergeHub.ViewModels
|
||||
: _workspace.IsMergeInProgress && commit.ShortSha == _workspace.HeadSha
|
||||
? MergeSavePlan.MergeBaseHeader(commit.ShortSha, path!)
|
||||
: $"{commit.ShortSha} · {path}";
|
||||
LocalHeader = _workspace.CurrentBranch;
|
||||
LocalHeader = isConflict
|
||||
? OperationLabels.OursTitle(operation, _workspace.CurrentBranch)
|
||||
: _workspace.CurrentBranch;
|
||||
RemoteHeader = isConflict
|
||||
? $"REMOTE · {theirsName ?? "THEIRS"}"
|
||||
? $"REMOTE · {OperationLabels.TheirsTitle(operation)}"
|
||||
: $"REMOTE · {branch}";
|
||||
|
||||
if (deletion.IsDeletion)
|
||||
@ -1346,7 +1352,8 @@ namespace MergeHub.ViewModels
|
||||
|
||||
// 병합 커밋 메시지 기본값은 <b>병합 전체의 이름</b>이다(6차 B-3). 예전에는 "Merge: 첫 충돌 파일 충돌 해결" 을
|
||||
// 채우고, 다른 파일로 옮겨도 그 이름이 굳어 남았다. 사람이 고쳐 적은 글은 건드리지 않는다.
|
||||
if (isConflict && (string.IsNullOrWhiteSpace(CommitMessage) || CommitMessage == _autoCommitMessage))
|
||||
if (isConflict && operation.Kind == GitOperationKind.Merge &&
|
||||
(string.IsNullOrWhiteSpace(CommitMessage) || CommitMessage == _autoCommitMessage))
|
||||
{
|
||||
_autoCommitMessage = MergeNames.BranchMergeMessage(theirsName, _workspace.CurrentBranch);
|
||||
CommitMessage = _autoCommitMessage;
|
||||
@ -1701,7 +1708,10 @@ namespace MergeHub.ViewModels
|
||||
|
||||
private string? TheirsSideName => TakesMergeSides ? _theirsSideName : RemoteBranch;
|
||||
|
||||
private string? MineSideName => _toolThreeWay ? _mineSideName : _workspace.CurrentBranch;
|
||||
private string? MineSideName => _mineSideName ?? _workspace.CurrentBranch;
|
||||
|
||||
/// <summary>충돌 네 칸의 머리 글. 작업(병합 · rebase · cherry-pick · revert)마다 다르다(7차 C-2).</summary>
|
||||
private string _conflictHeaderText = string.Empty;
|
||||
|
||||
/// <summary>병합 충돌 파일을 네 칸으로 보는 중. 머리에서 3-Way 콤보 대신 상대 이름을 보인다.</summary>
|
||||
public bool IsConflictThreeWay => IsGitMode && _conflictThreeWay;
|
||||
@ -1710,7 +1720,9 @@ namespace MergeHub.ViewModels
|
||||
public bool ShowBranchCombo => !IsConflictThreeWay;
|
||||
|
||||
/// <summary>충돌 네 칸일 때 콤보 자리에 보이는 글.</summary>
|
||||
public string ConflictSideText => $"병합 충돌 · REMOTE = {_theirsSideName ?? "THEIRS"}";
|
||||
public string ConflictSideText => _conflictHeaderText.Length > 0
|
||||
? _conflictHeaderText
|
||||
: $"병합 충돌 · REMOTE = {_theirsSideName ?? "THEIRS"}";
|
||||
|
||||
private void SetConflictThreeWay(bool on)
|
||||
{
|
||||
|
||||
@ -242,7 +242,7 @@
|
||||
<!-- 진행 한 줄(6차 C-1): "남은 충돌 N개 · 푼 것 M개 (M/전체)". 병합이 아니면 비어 접힌다. -->
|
||||
<StackPanel Grid.Row="0" Margin="0,0,0,6">
|
||||
<TextBlock Text="{Binding Workspace.ConflictProgressText}" FontWeight="Bold" Margin="0,0,0,2"
|
||||
Visibility="{Binding Workspace.IsMergeInProgress, Converter={StaticResource BoolToVis}}"/>
|
||||
Visibility="{Binding Workspace.IsOperationInProgress, Converter={StaticResource BoolToVis}}"/>
|
||||
<TextBlock Style="{StaticResource Muted}" TextWrapping="Wrap"
|
||||
Text="파일 전체를 한쪽으로 채택하려면 아래 버튼을 쓰고, 줄마다 고르려면 목록을 두 번 누르거나 [3분할로 열기] 를 누르세요. 파일 비교 탭에서 세 칸으로 열립니다. Ctrl · Shift 로 여러 파일을 고르면 Accept 가 한 번에 적용됩니다(Ctrl+A 전부)."/>
|
||||
</StackPanel>
|
||||
@ -298,7 +298,9 @@
|
||||
ToolTip="내 파일 다음에 상대 파일을 통째로 이어 붙입니다. 누르면 먼저 묻습니다"
|
||||
Background="{DynamicResource Ubisam.Brush.Warning}"
|
||||
Foreground="{DynamicResource Ubisam.Brush.Accent.OnFill}"/>
|
||||
<Button Content="병합 커밋" Command="{Binding CommitMergeCommand}" MinWidth="100"/>
|
||||
<!-- 작업마다 끝내는 법이 다르다 — 병합 커밋 · 계속(rebase) · 커밋(cherry-pick · revert)(7차 C-2). -->
|
||||
<Button Content="{Binding FinishOperationLabel}" Command="{Binding FinishOperationCommand}" MinWidth="100"
|
||||
ToolTip="충돌을 다 푼 작업을 끝냅니다. 병합은 병합 커밋, rebase 는 다음 단계로, cherry-pick · revert 는 원래 메시지로 커밋"/>
|
||||
<TextBlock Text="{Binding ConflictSelectionText}" Style="{StaticResource Muted}"
|
||||
VerticalAlignment="Center" Margin="8,0,0,0"/>
|
||||
</WrapPanel>
|
||||
@ -432,6 +434,10 @@
|
||||
Text="{Binding DiffText, Mode=OneWay}"/>
|
||||
|
||||
<WrapPanel Grid.Row="1" Grid.ColumnSpan="2" Margin="0,6,0,0">
|
||||
<!-- 끄면 지금 브랜치에서 닿는 커밋만. 켜면 다른 브랜치 커밋도 골라 cherry-pick 할 수 있다(7차 C-2). -->
|
||||
<CheckBox Content="모든 브랜치" IsChecked="{Binding Workspace.ShowAllBranchesInHistory}"
|
||||
VerticalAlignment="Center" Margin="0,0,14,0"
|
||||
ToolTip="다른 브랜치의 커밋까지 이력에 보입니다. cherry-pick 할 커밋을 고를 때 켭니다"/>
|
||||
<TextBlock Text="선택 커밋 →" Style="{StaticResource Muted}" Margin="0,0,6,0"/>
|
||||
<Button Content="Revert" Command="{Binding RevertCommand}"
|
||||
ToolTip="선택 커밋의 변경을 되돌리는 커밋 생성"/>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user