8차 B-2: 가져올 것이 없으면 [커밋] 을 끄고 나가는 길을 말한다

이미 들어 있는 커밋을 cherry-pick 하면 [커밋] 이 켜졌고, 누르면 로그에
"[cherry-pick 커밋] 실패: No changes; nothing to commit." 만 남았다. 진행 줄도 그대로였다.

- GitService.HasStagedChanges() 를 두고 적용 로그 · 커밋 앞 막음에 쓴다
- GitWorkspace.PendingHasChanges 를 상태로 들고, 진행 줄이 그 값을 본다
- CanFinishOperation 은 cherry-pick · revert 에서 커밋할 것이 있어야 켜진다
- 병합 커밋은 트리가 같아도 정상이라 걸지 않는다
- 하네스 MergeSaveTest 155건(진행 줄 7건), 18벌 921건 통과

확인: HEAD 커밋을 다시 cherry-pick → [커밋] 꺼짐 · "가져올 변경이 없습니다 - [작업 취소] 로 끝냅니다" ·
[작업 취소] 로 빠져나옴. 충돌 24건짜리 보통 cherry-pick 은 그대로 커밋까지(원래 메시지).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
김승준 2026-09-16 13:44:07 +09:00
parent c275cf6067
commit 90f662ba9b
8 changed files with 96 additions and 6 deletions

View File

@ -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 을 커밋으로 남기는 길**은 지시서대로 두지 않았다. 사용자와 정한 뒤에 넣는다.

View File

@ -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 끝까지 · 원래 작성자 · 창 크기 스냅 등) · 웹 문서 갱신 · 다섯 길 재확인(파일 비교 탭을 미리 열지 않고). 코드 변경 없음 | 아래 |

View File

@ -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차에서 남긴 것

View File

@ -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}.";
}
/// <summary>
/// 지금 커밋할 것이 있는가 - 인덱스가 HEAD 와 다른가(8차 B-2).
/// 이미 들어 있는 커밋을 cherry-pick 하면 가져올 변경이 없다. 그때 [커밋] 은 눌러 봐야 실패한다.
/// </summary>
public bool HasStagedChanges()
{
var head = Repo.Head?.Tip;
if (head is null) return Repo.Index.Count > 0;
return Repo.Diff.Compare<TreeChanges>(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");

View File

@ -189,7 +189,12 @@ namespace MergeHub.Services
/// </summary>
public string ConflictProgressText =>
// 작업마다 다 푼 뒤 할 일이 다르다 - 병합 커밋 · [계속] · [커밋](7차 C-2). 병합의 글은 예전과 같다.
OperationLabels.ProgressText(OperationState.Kind, Conflicts.Count, MergeConflictTotal);
OperationLabels.ProgressText(OperationState.Kind, Conflicts.Count, MergeConflictTotal, PendingHasChanges);
/// <summary>진행 중인 작업이 커밋할 것을 들고 있는가(8차 B-2). 없으면 끝낼 수 없고 [작업 취소] 로 나간다.</summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ConflictProgressText))]
private bool _pendingHasChanges = true;
/// <summary>진행 중인 git 작업과 두 쪽 이름(7차 C-2). 상태를 다시 읽을 때마다 갱신한다.</summary>
[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);

View File

@ -101,6 +101,9 @@ namespace MergeHub.Services
/// <summary>cherry-pick · revert 를 끝낼 커밋의 기본 메시지(원래 메시지). 그 작업이 아니면 null(7차 C-2).</summary>
string? GetPendingCommitMessage();
/// <summary>인덱스가 HEAD 와 다른가 - 지금 커밋할 것이 있는가(8차 B-2).</summary>
bool HasStagedChanges();
/// <summary>충돌을 다 푼 cherry-pick · revert 를 커밋해 끝낸다. 병합이면 <see cref="CommitMerge"/> 와 같다(7차 C-2).</summary>
string CommitPendingOperation(string? message = null);

View File

@ -1,4 +1,4 @@
using MergeHub.Models;
using MergeHub.Models;
namespace MergeHub.Services
{
@ -139,10 +139,16 @@ namespace MergeHub.Services
/// 충돌 탭 목록 위의 진행 한 줄(6차 C-1 을 작업별로). 처음 충돌 수를 모르면 남은 수만 적는다.
/// 병합의 글은 예전과 같다.
/// </summary>
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}개";

View File

@ -296,7 +296,14 @@ namespace MergeHub.ViewModels
/// <summary>충돌 탭의 끝내기 단추 글자 — 병합 커밋 · 계속 · 커밋(7차 C-2).</summary>
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;
}
/// <summary>
/// 충돌을 다 푼 작업을 끝낸다(7차 C-2). 병합은 병합 커밋, rebase 는 계속, cherry-pick · revert 는 원래 메시지로 커밋.