diff --git a/doc/history/2026-09-14-7차.md b/doc/history/2026-09-14-7차.md
index 4fb6c43..754b4d7 100644
--- a/doc/history/2026-09-14-7차.md
+++ b/doc/history/2026-09-14-7차.md
@@ -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, 추가 요청, 완료)
사용자: "폴더비교 탭 첫번째 드래그 첫번째 폴더 경로에 들어가고 두번째는 드래그앤드롭은 두번째로 들어가게해줘 경로텍스트 박스에 안넣어도 그냥 화면에다 넣어둬 기능 동작하도록".
diff --git a/doc/history/changelog.md b/doc/history/changelog.md
index 3a882c7..50feeb6 100644
--- a/doc/history/changelog.md
+++ b/doc/history/changelog.md
@@ -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개" 는 이어진다 | 아래 |
diff --git a/doc/진행상황.md b/doc/진행상황.md
index 9fc531a..a013429 100644
--- a/doc/진행상황.md
+++ b/doc/진행상황.md
@@ -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. 확인
diff --git a/src/MergeHub/Models/GitModels.cs b/src/MergeHub/Models/GitModels.cs
index 62e4bd0..a9084b0 100644
--- a/src/MergeHub/Models/GitModels.cs
+++ b/src/MergeHub/Models/GitModels.cs
@@ -114,6 +114,42 @@ namespace MergeHub.Models
Both
}
+ /// 지금 저장소에서 진행 중인 git 작업의 종류(7차 C-2).
+ public enum GitOperationKind
+ {
+ None,
+ Merge,
+ Rebase,
+ CherryPick,
+ Revert
+ }
+
+ ///
+ /// 진행 중인 작업과 두 쪽의 이름(7차 C-2). 충돌을 풀 때 "누가 LOCAL 이고 누가 REMOTE 인지" 가 작업마다 다르다.
+ ///
+ /// 병합 · cherry-pick · revert: ours = 내 브랜치, theirs = 들어오는 브랜치 · 커밋
+ /// rebase: ours = 올릴 자리(onto), theirs = 다시 올리는 내 커밋 — 병합과 거꾸로다
+ ///
+ ///
+ /// 병합 · cherry-pick · revert 는 내 브랜치, rebase 는 올릴 자리 브랜치 이름.
+ /// 병합은 상대 브랜치, rebase 는 다시 올리는 브랜치 이름.
+ /// 병합은 MERGE_HEAD, rebase 는 지금 올리는 커밋, cherry-pick · revert 는 그 커밋.
+ /// 그 커밋의 한 줄 메시지.
+ 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
{
/// 이미 최신 - 병합할 것이 없음.
diff --git a/src/MergeHub/Services/GitService.cs b/src/MergeHub/Services/GitService.cs
index ff41215..76f13e5 100644
--- a/src/MergeHub/Services/GitService.cs
+++ b/src/MergeHub/Services/GitService.cs
@@ -287,7 +287,7 @@ namespace MergeHub.Services
// ================= 히스토리 =================
- public IReadOnlyList GetCommits(string? committish = null, int maxCount = 200)
+ public IReadOnlyList GetCommits(string? committish = null, int maxCount = 200, bool allBranches = false)
{
var refsByTarget = new Dictionary>(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(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(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;
+ }
+ }
+
+ /// 파일 첫 줄의 40자 해시. 없으면 null.
+ 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;
+ }
+
+ /// 그 커밋을 끝으로 가리키는 브랜치 이름. 로컬을 먼저 본다.
+ 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(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(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;
diff --git a/src/MergeHub/Services/GitWorkspace.cs b/src/MergeHub/Services/GitWorkspace.cs
index 5e28b2b..56f4679 100644
--- a/src/MergeHub/Services/GitWorkspace.cs
+++ b/src/MergeHub/Services/GitWorkspace.cs
@@ -187,21 +187,28 @@ namespace MergeHub.Services
/// 처음 충돌 수는 병합할 때 .git 에 적어 두므로 앱을 다시 켜도 이어진다.
/// MergeHub 밖에서 시작한 병합이라 처음 수를 모르면 남은 수만 적는다.
///
- public string ConflictProgressText
+ public string ConflictProgressText =>
+ // 작업마다 다 푼 뒤 할 일이 다르다 - 병합 커밋 · [계속] · [커밋](7차 C-2). 병합의 글은 예전과 같다.
+ OperationLabels.ProgressText(OperationState.Kind, Conflicts.Count, MergeConflictTotal);
+
+ /// 진행 중인 git 작업과 두 쪽 이름(7차 C-2). 상태를 다시 읽을 때마다 갱신한다.
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(IsOperationInProgress))]
+ [NotifyPropertyChangedFor(nameof(ConflictProgressText))]
+ private GitOperationState _operationState = GitOperationState.None;
+
+ ///
+ /// 병합 · rebase · cherry-pick · revert 중 무엇이든 진행 중인지. 충돌 수와 따로 본다 -
+ /// 예전에는 충돌이 0 이 되면 cherry-pick 이 진행 중이라는 표시도 [작업 취소] 도 사라졌다(7차 C-2).
+ ///
+ public bool IsOperationInProgress => OperationState.IsInProgress;
+
+ /// 이력에 모든 브랜치의 커밋을 보일지(7차 C-2). 다른 브랜치 커밋을 골라 cherry-pick 할 때 켠다.
+ [ObservableProperty] private bool _showAllBranchesInHistory;
+
+ partial void OnShowAllBranchesInHistoryChanged(bool value)
{
- get
- {
- if (!IsMergeInProgress) return string.Empty;
-
- var left = Conflicts.Count;
- if (MergeConflictTotal is not int total || total < left)
- return left == 0 ? "충돌 정리 끝 - 병합 커밋을 할 수 있습니다" : $"남은 충돌 {left}개";
-
- var done = total - left;
- return left == 0
- ? $"충돌 정리 끝 · 푼 것 {done}개 - 병합 커밋을 할 수 있습니다"
- : $"남은 충돌 {left}개 · 푼 것 {done}개 ({done}/{total})";
- }
+ 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";
diff --git a/src/MergeHub/Services/IGitService.cs b/src/MergeHub/Services/IGitService.cs
index fa93730..62f3359 100644
--- a/src/MergeHub/Services/IGitService.cs
+++ b/src/MergeHub/Services/IGitService.cs
@@ -49,7 +49,8 @@ namespace MergeHub.Services
void Push(string? branchName = null, string remoteName = "origin");
// ---- 히스토리 / 조회 ----------------------------------------------
- IReadOnlyList GetCommits(string? committish = null, int maxCount = 200);
+ /// committish 가 없을 때 모든 브랜치에서 닿는 커밋까지(7차 C-2). 아니면 HEAD 에서 닿는 것만.
+ IReadOnlyList 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
///
string? GetMergeTheirsName();
+ /// 진행 중인 작업(병합 · rebase · cherry-pick · revert)과 두 쪽 이름(7차 C-2).
+ GitOperationState GetOperationState();
+
+ /// cherry-pick · revert 를 끝낼 커밋의 기본 메시지(원래 메시지). 그 작업이 아니면 null(7차 C-2).
+ string? GetPendingCommitMessage();
+
+ /// 충돌을 다 푼 cherry-pick · revert 를 커밋해 끝낸다. 병합이면 와 같다(7차 C-2).
+ string CommitPendingOperation(string? message = null);
+
/// Accept Mine / Theirs / Both 버튼 처리. 파일에 기록하고 인덱스에서 충돌 해제.
void ResolveConflict(string path, ConflictResolution resolution);
diff --git a/src/MergeHub/Services/OperationLabels.cs b/src/MergeHub/Services/OperationLabels.cs
new file mode 100644
index 0000000..b4aaf1b
--- /dev/null
+++ b/src/MergeHub/Services/OperationLabels.cs
@@ -0,0 +1,114 @@
+using MergeHub.Models;
+
+namespace MergeHub.Services
+{
+ ///
+ /// 진행 중인 git 작업(병합 · rebase · cherry-pick · revert)을 화면이 사람 말로 부르는 글(7차 C-2).
+ ///
+ /// 예전에는 모든 충돌을 "병합" 으로만 말했다. rebase 에서는 LOCAL 이 올릴 자리(상대 브랜치)이고 REMOTE 가
+ /// 내 커밋인데도 "REMOTE · THEIRS" 라고 했고, cherry-pick 은 머리에 "병합 진행 중" 이 떴다.
+ /// 충돌을 다 푼 뒤 할 일도 작업마다 다르다 — 병합은 [병합 커밋], rebase 는 [계속], cherry-pick · revert 는 [커밋].
+ /// 저장소 없이 검사하도록 글만 여기 모았다.
+ ///
+ 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();
+
+ /// 머리의 진행 표시. 진행 중이 아니면 빈 글.
+ 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
+ };
+
+ /// 네 칸의 LOCAL 칸 제목. rebase 는 올릴 자리, 그 밖은 내 브랜치.
+ public static string OursTitle(GitOperationState s, string? currentBranch) =>
+ s.Kind == GitOperationKind.Rebase
+ ? $"올릴 자리 {Onto(s)}"
+ : !string.IsNullOrWhiteSpace(currentBranch) ? currentBranch!.Trim() : s.OursName ?? "LOCAL";
+
+ /// 네 칸의 REMOTE 칸 제목("REMOTE · " 뒤).
+ 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()
+ };
+
+ /// 충돌 네 칸일 때 3-Way 콤보 자리에 보이는 글(7차 A-2 의 "병합 충돌 · REMOTE = …" 을 작업별로).
+ 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())}"
+ };
+
+ /// Take Theirs 로그의 괄호 안 이름. 병합은 브랜치, 그 밖은 "커밋 해시".
+ 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();
+ }
+
+ /// Take Mine 로그의 괄호 안 이름. rebase 는 올릴 자리, 그 밖은 내 브랜치.
+ public static string? MineLogName(GitOperationState s, string? currentBranch) =>
+ s.Kind == GitOperationKind.Rebase ? s.OursName : currentBranch;
+
+ /// 충돌을 다 푼 뒤 할 일.
+ public static string FinishHint(GitOperationKind kind) => kind switch
+ {
+ GitOperationKind.Rebase => "[계속] 으로 rebase 를 이어 갑니다",
+ GitOperationKind.CherryPick => "[커밋] 으로 cherry-pick 을 끝냅니다",
+ GitOperationKind.Revert => "[커밋] 으로 revert 를 끝냅니다",
+ _ => "병합 커밋을 할 수 있습니다"
+ };
+
+ /// 충돌 탭의 끝내기 단추 글자.
+ public static string FinishButtonLabel(GitOperationKind kind) => kind switch
+ {
+ GitOperationKind.Rebase => "계속",
+ GitOperationKind.CherryPick or GitOperationKind.Revert => "커밋",
+ _ => "병합 커밋"
+ };
+
+ ///
+ /// 충돌 탭 목록 위의 진행 한 줄(6차 C-1 을 작업별로). 처음 충돌 수를 모르면 남은 수만 적는다.
+ /// 병합의 글은 예전과 같다.
+ ///
+ 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})";
+ }
+ }
+}
diff --git a/src/MergeHub/ViewModels/GitViewModel.cs b/src/MergeHub/ViewModels/GitViewModel.cs
index b3d8d25..ea54e94 100644
--- a/src/MergeHub/ViewModels/GitViewModel.cs
+++ b/src/MergeHub/ViewModels/GitViewModel.cs
@@ -245,43 +245,83 @@ namespace MergeHub.ViewModels
/// 병합(또는 pull) 결과를 받은 뒤 할 일. 작업 스레드에서 부른다(7차 C-1 에서 [Merge] 와 pull 이 함께 쓰게 뺐다).
///
/// 커밋 메시지 칸이 비어 있고 병합 커밋이 남았을 때 채울 메시지.
- 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);
+
+ ///
+ /// 충돌이 날 수 있는 작업(병합 · pull · rebase · cherry-pick · revert) 뒤에 할 일. 작업 스레드에서 부른다.
+ /// 7차 C-2 에서 rebase · cherry-pick · revert 도 같은 길을 쓰게 했다 - 머지 주석 자동 풀기 · 알림 · 커밋 메시지.
+ ///
+ /// 커밋 메시지 칸이 비어 있으면 채울 글. 없으면 null.
+ private void AfterConflicts(bool hasConflicts, int conflictCount, string resultMessage, string? fillMessage, GitOperationKind kind)
{
// 머지 주석만 다른 충돌은 사람에게 묻지 않고 푼다. 양쪽 주석을 다 남긴다(6차 B-5).
- IReadOnlyList noteOnly = result.HasConflicts && _workspace.Settings.IgnoreMergeNotes
+ IReadOnlyList noteOnly = hasConflicts && _workspace.Settings.IgnoreMergeNotes
? _workspace.Git.ResolveNoteOnlyConflicts()
: Array.Empty();
- 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());
+ /// 충돌 탭의 끝내기 단추 글자 — 병합 커밋 · 계속 · 커밋(7차 C-2).
+ public string FinishOperationLabel => OperationLabels.FinishButtonLabel(_workspace.OperationState.Kind);
+
+ private bool CanFinishOperation() => _workspace.OperationState.IsInProgress && _workspace.Conflicts.Count == 0;
+
+ ///
+ /// 충돌을 다 푼 작업을 끝낸다(7차 C-2). 병합은 병합 커밋, rebase 는 계속, cherry-pick · revert 는 원래 메시지로 커밋.
+ /// 예전에는 [병합 커밋] 하나뿐이라 cherry-pick 은 충돌이 0 이 되면 끝낼 단추가 없었다.
+ ///
+ [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();
diff --git a/src/MergeHub/ViewModels/HomeViewModel.cs b/src/MergeHub/ViewModels/HomeViewModel.cs
index c06dcb1..54fb862 100644
--- a/src/MergeHub/ViewModels/HomeViewModel.cs
+++ b/src/MergeHub/ViewModels/HomeViewModel.cs
@@ -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
// 병합 커밋 메시지 기본값은 병합 전체의 이름이다(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;
+
+ /// 충돌 네 칸의 머리 글. 작업(병합 · rebase · cherry-pick · revert)마다 다르다(7차 C-2).
+ private string _conflictHeaderText = string.Empty;
/// 병합 충돌 파일을 네 칸으로 보는 중. 머리에서 3-Way 콤보 대신 상대 이름을 보인다.
public bool IsConflictThreeWay => IsGitMode && _conflictThreeWay;
@@ -1710,7 +1720,9 @@ namespace MergeHub.ViewModels
public bool ShowBranchCombo => !IsConflictThreeWay;
/// 충돌 네 칸일 때 콤보 자리에 보이는 글.
- public string ConflictSideText => $"병합 충돌 · REMOTE = {_theirsSideName ?? "THEIRS"}";
+ public string ConflictSideText => _conflictHeaderText.Length > 0
+ ? _conflictHeaderText
+ : $"병합 충돌 · REMOTE = {_theirsSideName ?? "THEIRS"}";
private void SetConflictThreeWay(bool on)
{
diff --git a/src/MergeHub/Views/GitView.xaml b/src/MergeHub/Views/GitView.xaml
index c5dfa13..a299e43 100644
--- a/src/MergeHub/Views/GitView.xaml
+++ b/src/MergeHub/Views/GitView.xaml
@@ -242,7 +242,7 @@
+ Visibility="{Binding Workspace.IsOperationInProgress, Converter={StaticResource BoolToVis}}"/>
@@ -298,7 +298,9 @@
ToolTip="내 파일 다음에 상대 파일을 통째로 이어 붙입니다. 누르면 먼저 묻습니다"
Background="{DynamicResource Ubisam.Brush.Warning}"
Foreground="{DynamicResource Ubisam.Brush.Accent.OnFill}"/>
-
+
+
@@ -432,6 +434,10 @@
Text="{Binding DiffText, Mode=OneWay}"/>
+
+