diff --git a/doc/history/2026-09-16-8차.md b/doc/history/2026-09-16-8차.md index 2eb0035..61baf06 100644 --- a/doc/history/2026-09-16-8차.md +++ b/doc/history/2026-09-16-8차.md @@ -112,3 +112,48 @@ cherry-pick 의 BASE 제목에 꼬리가 없는 것은 맞다 — 그때 BASE "가져오기 전 내 쪽" 이라고 하면 틀린 말이 된다(제목은 `14a9476 · Weld/Unit1.cs`, HEAD 는 `45bdbf5`). 하네스 18벌 914건 통과(MergeSaveTest 148). 빌드 경고 0 · 오류 0. 설정 파일은 바이트 그대로 되돌림. + +## B-2. 바뀌는 것 없는 cherry-pick 에서 [커밋] 이 켜지고 누르면 실패한다 (2026-09-16, 완료) + +### 고치기 전에 다시 났다 (새로 만든 저장소, 이력에서 HEAD 커밋을 골라 [Cherry-pick]) + +| 때 | 결과 | +|---|---| +| [Cherry-pick] | 로그 `cherry-pick 적용됨 - 확인한 뒤 충돌 탭의 [커밋] 으로 끝냅니다.` · `.git` 에 `CHERRY_PICK_HEAD` | +| 충돌 탭 | 머리 `cherry-pick 진행 중` · 진행 줄 **`충돌 정리 끝 - [커밋] 으로 cherry-pick 을 끝냅니다`** · **[커밋] 켜짐** | +| [커밋] 을 눌러 봄 | 로그 **`[cherry-pick 커밋] 실패: No changes; nothing to commit.`** · HEAD 그대로 · 알림은 읽히지 않음 | +| 그 뒤 | 진행 줄 · [커밋] 이 그대로라 **같은 단추를 또 누르게 된다.** [작업 취소] 로는 빠져나온다 | + +### 원인 + +- 끝내기 단추(`CanFinishOperation`)는 "작업이 진행 중 · 충돌 0" 만 봤다. **커밋할 것이 있는지는 보지 않았다.** +- 진행 줄(`OperationLabels.ProgressText`)도 충돌이 0 이면 무조건 "끝낼 수 있다" 고 말했다. +- 시작 로그(`BuildPatchResult`)는 적용 결과가 `Applied` 면 늘 "확인한 뒤 [커밋] 으로 끝냅니다" 였다. +- 실제로 막은 것은 libgit2 였고, 그 말(`No changes; nothing to commit.`)이 로그에 그대로 나왔다. + +### 고친 것 + +| 파일 | 무엇 | +|---|---| +| `Services/GitService.cs` | `HasStagedChanges()` — 인덱스가 HEAD 와 다른가. cherry-pick 적용 로그가 이걸 보고 **"cherry-pick 했지만 가져올 변경이 없습니다 - [작업 취소] 로 끝냅니다."** 로 적는다(revert 는 "되돌릴"). `CommitPendingOperation` 은 커밋 전에 같은 것을 보고 사람 말로 막는다 | +| `Services/IGitService.cs` | 선언 | +| `Services/GitWorkspace.cs` | `PendingHasChanges` — 상태를 읽을 때 함께 읽어 둔다. 진행 줄이 이 값을 쓴다 | +| `Services/OperationLabels.cs` | `ProgressText(…, hasChanges)` — 충돌 0 인데 가져올 것이 없는 cherry-pick · revert 는 **"가져올(되돌릴) 변경이 없습니다 - [작업 취소] 로 끝냅니다"**. 병합 · rebase 는 따지지 않는다(병합 커밋은 트리가 같아도 정상이다) | +| `ViewModels/GitViewModel.cs` | `CanFinishOperation` — cherry-pick · revert 는 커밋할 것이 있어야 켜진다 | +| 하네스 `MergeSaveTest` | 진행 줄 검사 7건(없음 · revert · 있음 · 충돌 남음 · 병합 · rebase · 기본값이 예전과 같은지) | + +**병합 커밋에는 걸지 않았다.** 처음에 `CommitMerge` 에도 같은 막음을 넣었다가 뺐다 — 병합은 가져올 변경이 없어도(양쪽이 같게 고친 경우 등) 커밋이 정상이다. + +### 확인한 것 (저장소를 새로 만들어) + +| 경우 | 결과 | +|---|---| +| ① HEAD 커밋을 다시 [Cherry-pick] | 로그 **`cherry-pick 했지만 가져올 변경이 없습니다 - [작업 취소] 로 끝냅니다.`** · 진행 줄 **`가져올 변경이 없습니다 - [작업 취소] 로 끝냅니다`** · **[커밋] 꺼짐** · [작업 취소] 켜짐 | +| ① [작업 취소] | `CHERRY_PICK_HEAD` 없어짐 · 머리 표시 · 진행 줄 사라짐 | +| ② 회귀 - 충돌 24건짜리 보통 cherry-pick | 충돌 중 [커밋] 꺼짐 → 다 풀면 진행 줄 `충돌 정리 끝 - [커밋] 으로 cherry-pick 을 끝냅니다` · [커밋] 켜짐 → 누르면 `cherry-pick 커밋: e35f7b7 상대 쪽 작업`(부모 하나 · 원래 메시지) | + +하네스 18벌 921건 통과(MergeSaveTest 155). 빌드 경고 0 · 오류 0. 설정 파일은 바이트 그대로 되돌림. + +### 정하지 않은 것 + +- git 의 `--allow-empty` 처럼 **빈 cherry-pick 을 커밋으로 남기는 길**은 지시서대로 두지 않았다. 사용자와 정한 뒤에 넣는다. diff --git a/doc/history/changelog.md b/doc/history/changelog.md index 0d9ffb6..733d16e 100644 --- a/doc/history/changelog.md +++ b/doc/history/changelog.md @@ -8,6 +8,7 @@ | 날짜 | 항목 | 커밋 | |---|---|---| +| 2026-09-16 | **8차 B-2** — 이미 들어 있는 커밋을 cherry-pick 하면 [커밋] 이 켜지는데 누르면 로그에 "실패: No changes; nothing to commit." 만 남고 진행 줄도 그대로라 같은 단추를 또 누르게 됐다. 커밋할 것이 있는지(HasStagedChanges)를 상태로 들고 있다가 [커밋] 을 끄고, 진행 줄 · 적용 로그가 "가져올(되돌릴) 변경이 없습니다 - [작업 취소] 로 끝냅니다" 라고 말한다. 커밋 직전에도 사람 말로 막는다. 병합 커밋은 트리가 같아도 정상이라 걸지 않음 | 아래 | | 2026-09-16 | **8차 B-1** — rebase · cherry-pick 에서 마지막 충돌을 [Save] 하면 파일 비교 탭이 "남은 차이 6줄" 로 떨어져 충돌 탭과 다른 말을 했다(7차 A-1 이 병합 전용이었고 화면도 MERGE_HEAD 기준으로 물었다). 요약 · BASE 제목을 작업 상태(GitOperationState)에 맞춰 OperationLabels 로 옮김 — 끝말은 충돌 탭 진행 줄과 같은 FinishHint, 차이 줄은 작업별로(받은 · 올린 · 가져온 · 되돌린 변경), 제목 꼬리는 병합 "병합 전 내 쪽" · rebase "올릴 자리" 등. 병합 글은 그대로 | 아래 | | 2026-09-16 | **8차 A-1** — 파일 비교 탭을 처음 여는 것이 [3분할로 열기] 면 고른 파일 대신 첫 충돌 파일이 보였고, 그 화면을 [Save] 하면 **고른 파일에 그 내용이 써졌다**(Unit47 이 Unit15 내용으로 스테이징). 탭을 만들 때의 목록 세우기와 고른 파일 열기가 겹쳐, 늦게 끝난 읽기가 화면을 덮은 것. 읽기는 경로 · 커밋이 바뀌었으면 버리고 한 번 더 읽게, 쓰기(저장 · Save & Commit · 파일 지우기/남기기)는 화면에 실린 파일과 다르면 아무것도 쓰지 않게 했다. 네 길 모두 고른 파일이 열리고 저장이 그 파일에만 감, 병합은 사람 순서 그대로 끝까지(줄끝 0/33 · 내용 검사 0) | 아래 | | 2026-09-15 | **8차 지시서** — 7차 전부 끝. D-1 에서 새로 걸린 넷(A-1 고른 파일에 다른 파일 내용이 저장됨 · B-1 rebase / cherry-pick "남은 차이" · B-2 빈 cherry-pick [커밋] · B-3 MERGED 번호)과 7차에서 남긴 것(revert 끝까지 · 원래 작성자 · 창 크기 스냅 등) · 웹 문서 갱신 · 다섯 길 재확인(파일 비교 탭을 미리 열지 않고). 코드 변경 없음 | 아래 | diff --git a/doc/진행상황.md b/doc/진행상황.md index e21f610..8fbcd06 100644 --- a/doc/진행상황.md +++ b/doc/진행상황.md @@ -206,7 +206,7 @@ | 상태 | 항목 | |---|---| | **완료** | B-1 rebase · cherry-pick 에서 마지막 저장 뒤 "남은 차이" 가 뜬다 — 요약 · BASE 제목이 작업(병합 · rebase · cherry-pick · revert)을 따른다. rebase 는 "충돌 정리 끝 · [계속] 으로 rebase 를 이어 갑니다 · 올릴 자리 대비 올린 변경 N줄" · 제목 "올릴 자리". 병합 글은 그대로 | -| 대기 | B-2 바뀌는 것 없는 cherry-pick 에서 [커밋] 이 켜지고 누르면 실패한다 | +| **완료** | B-2 바뀌는 것 없는 cherry-pick 에서 [커밋] 이 켜지고 누르면 실패한다 — 커밋할 것이 없으면 [커밋] 이 꺼지고 진행 줄 · 로그가 "가져올 변경이 없습니다 - [작업 취소] 로 끝냅니다". 병합 커밋은 그대로(트리가 같아도 정상). `--allow-empty` 는 사용자와 정한 뒤 | | 대기 | B-3 MERGED 칸 줄 번호가 결과 파일 번호가 아니다 (먼저 사용자와 정함) | ### C. 7차에서 남긴 것 diff --git a/src/MergeHub/Services/GitService.cs b/src/MergeHub/Services/GitService.cs index 76f13e5..a7d2c1f 100644 --- a/src/MergeHub/Services/GitService.cs +++ b/src/MergeHub/Services/GitService.cs @@ -650,7 +650,10 @@ namespace MergeHub.Services // 사람이 읽는 로그다 - 예전에는 "Commit() 을 호출하세요" 처럼 코드 이름을 적었다(7차 C-2). PatchApplyOutcome.Applied => commitOnSuccess ? $"{operation} 완료 ({Shorten(sha)})" - : $"{operation} 적용됨 - 확인한 뒤 충돌 탭의 [커밋] 으로 끝냅니다.", + // 이미 들어 있는 커밋을 가져오면 적용은 됐는데 커밋할 것이 없다(8차 B-2). + : HasStagedChanges() + ? $"{operation} 적용됨 - 확인한 뒤 충돌 탭의 [커밋] 으로 끝냅니다." + : $"{operation} 했지만 {(operation == "revert" ? "되돌릴" : "가져올")} 변경이 없습니다 - [작업 취소] 로 끝냅니다.", PatchApplyOutcome.Conflicts => $"{operation} 충돌 {conflicts.Count}건 - 풀고 나서 충돌 탭의 [커밋] 으로 끝냅니다.", PatchApplyOutcome.NothingToDo => $"{operation} 할 변경이 없습니다.", _ => $"{operation} 실패" @@ -1168,6 +1171,18 @@ namespace MergeHub.Services : $"Revert \"{commit.MessageShort}\"\n\nThis reverts commit {commit.Sha}."; } + /// + /// 지금 커밋할 것이 있는가 - 인덱스가 HEAD 와 다른가(8차 B-2). + /// 이미 들어 있는 커밋을 cherry-pick 하면 가져올 변경이 없다. 그때 [커밋] 은 눌러 봐야 실패한다. + /// + public bool HasStagedChanges() + { + var head = Repo.Head?.Tip; + if (head is null) return Repo.Index.Count > 0; + + return Repo.Diff.Compare(head.Tree, DiffTargets.Index).Count > 0; + } + public string CommitPendingOperation(string? message = null) { var state = GetOperationState(); @@ -1178,6 +1193,12 @@ namespace MergeHub.Services if (remaining.Count > 0) throw new InvalidOperationException($"아직 해결되지 않은 충돌이 {remaining.Count}건 있습니다."); + // 가져올 것이 없으면 libgit2 가 "No changes; nothing to commit" 으로 막는다. 사람 말로 먼저 막는다(8차 B-2). + if (!HasStagedChanges()) + throw new InvalidOperationException(state.Kind == GitOperationKind.CherryPick + ? "가져올 변경이 없습니다. [작업 취소] 로 끝내세요." + : "되돌릴 변경이 없습니다. [작업 취소] 로 끝내세요."); + if (string.IsNullOrWhiteSpace(message)) message = GetPendingCommitMessage() ?? (state.Kind == GitOperationKind.CherryPick ? "cherry-pick" : "revert"); diff --git a/src/MergeHub/Services/GitWorkspace.cs b/src/MergeHub/Services/GitWorkspace.cs index 56f4679..027d8b2 100644 --- a/src/MergeHub/Services/GitWorkspace.cs +++ b/src/MergeHub/Services/GitWorkspace.cs @@ -189,7 +189,12 @@ namespace MergeHub.Services /// public string ConflictProgressText => // 작업마다 다 푼 뒤 할 일이 다르다 - 병합 커밋 · [계속] · [커밋](7차 C-2). 병합의 글은 예전과 같다. - OperationLabels.ProgressText(OperationState.Kind, Conflicts.Count, MergeConflictTotal); + OperationLabels.ProgressText(OperationState.Kind, Conflicts.Count, MergeConflictTotal, PendingHasChanges); + + /// 진행 중인 작업이 커밋할 것을 들고 있는가(8차 B-2). 없으면 끝낼 수 없고 [작업 취소] 로 나간다. + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ConflictProgressText))] + private bool _pendingHasChanges = true; /// 진행 중인 git 작업과 두 쪽 이름(7차 C-2). 상태를 다시 읽을 때마다 갱신한다. [ObservableProperty] @@ -359,6 +364,7 @@ namespace MergeHub.Services var rebase = Git.GetRebaseStatus(); var remotes = Git.GetRemotes(); var mergeInProgress = Git.IsMergeInProgress; + var pendingHasChanges = !operation.IsInProgress || Git.HasStagedChanges(); var conflictTotal = mergeInProgress ? Git.GetMergeConflictTotal() : null; var remote = remotes.FirstOrDefault(r => r.Name == RemoteName) ?? remotes.FirstOrDefault(); @@ -383,6 +389,7 @@ namespace MergeHub.Services IsMergeInProgress = mergeInProgress; IsRebaseInProgress = rebase.IsInProgress; OperationState = operation; + PendingHasChanges = pendingHasChanges; // cherry-pick · revert 도 "병합 진행 중" 이라고 했다. 작업 이름으로 적는다(7차 C-2). PendingOperationText = OperationLabels.PendingText(operation); diff --git a/src/MergeHub/Services/IGitService.cs b/src/MergeHub/Services/IGitService.cs index 62f3359..eb14417 100644 --- a/src/MergeHub/Services/IGitService.cs +++ b/src/MergeHub/Services/IGitService.cs @@ -101,6 +101,9 @@ namespace MergeHub.Services /// cherry-pick · revert 를 끝낼 커밋의 기본 메시지(원래 메시지). 그 작업이 아니면 null(7차 C-2). string? GetPendingCommitMessage(); + /// 인덱스가 HEAD 와 다른가 - 지금 커밋할 것이 있는가(8차 B-2). + bool HasStagedChanges(); + /// 충돌을 다 푼 cherry-pick · revert 를 커밋해 끝낸다. 병합이면 와 같다(7차 C-2). string CommitPendingOperation(string? message = null); diff --git a/src/MergeHub/Services/OperationLabels.cs b/src/MergeHub/Services/OperationLabels.cs index aed060b..1429f9b 100644 --- a/src/MergeHub/Services/OperationLabels.cs +++ b/src/MergeHub/Services/OperationLabels.cs @@ -1,4 +1,4 @@ -using MergeHub.Models; +using MergeHub.Models; namespace MergeHub.Services { @@ -139,10 +139,16 @@ namespace MergeHub.Services /// 충돌 탭 목록 위의 진행 한 줄(6차 C-1 을 작업별로). 처음 충돌 수를 모르면 남은 수만 적는다. /// 병합의 글은 예전과 같다. /// - public static string ProgressText(GitOperationKind kind, int left, int? total) + public static string ProgressText(GitOperationKind kind, int left, int? total, bool hasChanges = true) { if (kind == GitOperationKind.None) return string.Empty; + // 가져올(되돌릴) 것이 없는 cherry-pick · revert(8차 B-2). 끝낼 수 없으니 나가는 길을 말한다. + if (left == 0 && !hasChanges && kind is GitOperationKind.CherryPick or GitOperationKind.Revert) + return kind == GitOperationKind.CherryPick + ? "가져올 변경이 없습니다 - [작업 취소] 로 끝냅니다" + : "되돌릴 변경이 없습니다 - [작업 취소] 로 끝냅니다"; + var finish = FinishHint(kind); if (total is not int t || t < left) return left == 0 ? $"충돌 정리 끝 - {finish}" : $"남은 충돌 {left}개"; diff --git a/src/MergeHub/ViewModels/GitViewModel.cs b/src/MergeHub/ViewModels/GitViewModel.cs index ea54e94..99a2b87 100644 --- a/src/MergeHub/ViewModels/GitViewModel.cs +++ b/src/MergeHub/ViewModels/GitViewModel.cs @@ -296,7 +296,14 @@ namespace MergeHub.ViewModels /// 충돌 탭의 끝내기 단추 글자 — 병합 커밋 · 계속 · 커밋(7차 C-2). public string FinishOperationLabel => OperationLabels.FinishButtonLabel(_workspace.OperationState.Kind); - private bool CanFinishOperation() => _workspace.OperationState.IsInProgress && _workspace.Conflicts.Count == 0; + private bool CanFinishOperation() + { + if (!_workspace.OperationState.IsInProgress || _workspace.Conflicts.Count > 0) return false; + + // cherry-pick · revert 는 커밋할 것이 있어야 끝낼 수 있다. 이미 들어 있는 커밋을 가져오면 없다(8차 B-2). + return _workspace.OperationState.Kind is not (GitOperationKind.CherryPick or GitOperationKind.Revert) + || _workspace.PendingHasChanges; + } /// /// 충돌을 다 푼 작업을 끝낸다(7차 C-2). 병합은 병합 커밋, rebase 는 계속, cherry-pick · revert 는 원래 메시지로 커밋.