diff --git a/doc/history/2026-09-17-9차.md b/doc/history/2026-09-17-9차.md
new file mode 100644
index 0000000..acad5de
--- /dev/null
+++ b/doc/history/2026-09-17-9차.md
@@ -0,0 +1,70 @@
+# 9차 진행 기록 (2026-09-17 ~)
+
+지시서: [../작업지시/9차.md](../작업지시/9차.md)
+
+## A-1. cherry-pick 마지막 저장 뒤 "내 쪽 대비" 라고 하지만 실제 기준은 이력에서 고른 커밋이다 (2026-09-17, 완료)
+
+사용자 결정(2026-09-17): **작업 중 충돌을 다 풀면 두 칸의 기준을 HEAD 로 옮긴다.**
+("기준은 고른 커밋 그대로 두고 글을 맞춘다" 는 고르지 않음.)
+
+### 고치기 전에 다시 났다 (길마다 저장소를 새로 만들어, 파일 비교 탭을 미리 열지 않고)
+
+8차 E-1 순서 그대로 — 이력에서 커밋을 골라 시작 → 한 파일만 남기고 파일 단위 Accept Theirs → 남은 파일을 [3분할로 열기] → 충돌마다 Take Theirs → [Save].
+
+| | cherry-pick ([모든 브랜치] 켬) | revert ([모든 브랜치] 끔) |
+|---|---|---|
+| 이력에서 고른 커밋 | `상대 쪽 작업` | `내 쪽 작업` |
+| 마지막 Save 뒤 두 칸 기준(Selected Commit) | **`뒤이은 수정`** — HEAD(`내 쪽 작업`)도 고른 커밋도 아닌 다른 브랜치 커밋 | `뒤이은 수정` = HEAD |
+| BASE 제목 | `ace42e3 · Weld/Unit1.cs` (꼬리 없음) | `… · Motion/Unit0.cs · 되돌리기 전 내 쪽` |
+| 요약 | 충돌 정리 끝 · [커밋] 으로 cherry-pick 을 끝냅니다 · **내 쪽 대비** 가져온 변경 6줄 | … · 내 쪽 대비 되돌린 변경 6줄 |
+
+- 8차 E-1 에서 따로 잰 한 번은 기준이 **고른 커밋**이었다. 이번 cherry-pick 은 **목록 맨 위(다른 브랜치 커밋)**, revert 는 **HEAD**.
+ 같은 순서인데 기준이 셋 중 하나로 그때그때 달랐다 — N1 과 N1-덧(9차 A-2)은 뿌리가 같다.
+- 파일 비교 탭 왼쪽 이력에서 선택된 줄이 맨 위 `뒤이은 수정` 이었다(스크린샷 `d9-a1-pick-before-1`).
+
+### 원인
+
+- 새로고침은 이력 목록(`Commits`)을 **비웠다가 다시 채운다.** 그때 Git 탭 이력 목록(`SelectedItem` 바인딩)이 선택을 놓치면 **null 을 밀어 넣어** 고른 커밋이 빈다.
+ 놓치는지는 그때 화면 상태에 따라 달랐다.
+- 비었으면 `SelectedCommit = Commits[0]` — **목록 맨 위**로 채웠다. [모든 브랜치] 를 끄면 맨 위가 HEAD 라 티가 안 났고,
+ 켜면 맨 위는 가장 최근의 다른 브랜치 커밋이다.
+- 안 비었으면 이력에서 고른 커밋(cherry-pick 할 커밋)이 그대로 기준으로 남았다.
+- 요약(`OperationLabels.ResolvedSummary`)은 기준이 HEAD 인지 보지 않고 "내 쪽 대비" 를 붙였다. BASE 제목은 HEAD 일 때만 작업 글을 붙여서 둘이 다른 말을 했다.
+
+### 고친 것
+
+- `Services/CommitSelection.cs`(새로) — 새로고침 뒤 고를 자리:
+ 작업 중 충돌이 **막 0 이 된** 새로고침이면 HEAD, 아니면 새로고침 전에 고른 커밋을 **해시로** 되찾고, 없으면 맨 위가 아니라 HEAD(HEAD 도 목록에 없을 때만 맨 위).
+ "막 0 이 된" 때만 옮긴다 — 그 상태에서 사람이 다른 커밋을 골라 보면 스테이징 · 새로고침마다 HEAD 로 끌어오지 않는다.
+- `GitWorkspace.ReadState` — 목록을 갈아 끼우기 **전에** 고른 커밋의 해시를 적어 두고, 채운 뒤 `CommitSelection.Choose` 로 새 목록의 같은 커밋 객체를 고른다.
+- `OperationLabels.ResolvedSummary(…, baseIsHead)` — 기준이 HEAD 가 아니면 작업 글을 쓰지 않는다(보통 요약). `HomeViewModel` 은 BASE 제목과 같은 조건(`SelectedCommit.ShortSha == HeadSha`)을 넘긴다.
+- 하네스 `MergeSaveTest` 18건 추가(201건): 해시로 되찾기 · 순서가 바뀐 목록 · 비었을 때 HEAD · 사라졌을 때 HEAD · 막 0 이 되면 HEAD · 머무는 동안 안 끌어옴 · 기준이 HEAD 가 아니면 보통 요약.
+
+### 확인한 것 (고친 앱, 같은 순서)
+
+| | cherry-pick ([모든 브랜치] 켬) | revert |
+|---|---|---|
+| 마지막 Save 뒤 Selected Commit | **`내 쪽 작업` = HEAD** | `뒤이은 수정` = HEAD |
+| BASE 제목 | `e039373 · Weld/Unit1.cs · 가져오기 전 내 쪽` | `6dde274 · Motion/Unit0.cs · 되돌리기 전 내 쪽` |
+| 요약 | 충돌 정리 끝 · [커밋] 으로 cherry-pick 을 끝냅니다 · 내 쪽 대비 가져온 변경 6줄 | … · 내 쪽 대비 되돌린 변경 6줄 |
+| git 과 대조 | `git diff -U0 HEAD -- Weld/Unit1.cs` = 바뀐 줄 6 + 머지 주석 줄 6. 주석 줄은 규칙(무시: 머지 주석)대로 안 셈 → **6 이 맞다** | 같은 구성(+12 −6) |
+
+곁 확인(`d9-a1-keep.ps1`):
+
+| 경우 | 결과 |
+|---|---|
+| 충돌을 다 푼 cherry-pick 저장소를 다시 열기 | 기준 HEAD · 요약 작업 글 |
+| 그 상태에서 파일 비교 탭 이력으로 `바탕` 을 고름 → [새로고침] 세 번 | 세 번 모두 `바탕` 그대로(HEAD 로 안 끌려감) · 제목 꼬리 없음 · 요약 "차이 없음"("내 쪽 대비" 안 함) |
+| 작업 없음 · [모든 브랜치] 켬 | 맨 위 `뒤이은 수정` 이 아니라 HEAD `내 쪽 작업` 그대로 |
+| Git 탭 이력에서 `상대 쪽 작업` 을 고름 → [새로고침] 세 번 | 세 번 모두 `상대 쪽 작업` 그대로 |
+| [모든 브랜치] 를 꺼 고른 커밋이 목록에서 빠짐 | HEAD 로 |
+
+- 하네스 22벌 1,112건 통과(+18) · 빌드 경고 0 · 오류 0.
+- 병합 · pull · rebase 의 요약 · 제목은 기본값(`baseIsHead` 참)으로 8차 B-1 검사가 그대로 통과한다. 화면으로는 9차 D-1 에서 다시 본다.
+
+### 알게 된 것 · 남긴 것
+
+- **Git 탭 이력에서 커밋을 고르면 파일 비교 탭의 Selected Commit 은 새로고침 전까지 따라오지 않는다**(Git 탭은 `_workspace.SelectedCommit` 만 바꾸고, 파일 비교 탭은 새로고침 알림으로만 다시 읽는다).
+ 이번에 바꾼 것이 아니라 전부터 그랬다. 고치지 않고 적어 둔다.
+- A-2(한 번은 다른 브랜치 커밋이 기준)는 이번에 원인(목록 맨 위로 채움)이 드러나 함께 고쳐졌다. 지시서대로 A-2 에서 같은 순서를 다섯 번 넘게 되풀이해 확인한다.
+- 자동 조작 쪽: 첫 확인 스크립트는 Git 탭에서 HEAD 를 골라 파일 비교 탭을 봤는데 위의 까닭으로 안 바뀌어, 파일 비교 탭 이력에서 고르게 고쳤다.
diff --git a/doc/history/changelog.md b/doc/history/changelog.md
index b264b36..bd3b1c3 100644
--- a/doc/history/changelog.md
+++ b/doc/history/changelog.md
@@ -8,6 +8,7 @@
| 날짜 | 항목 | 커밋 |
|---|---|---|
+| 2026-09-17 | **9차 A-1** — cherry-pick 마지막 [Save] 뒤 두 칸의 기준이 고른 커밋 · 다른 브랜치 커밋 · HEAD 중 하나로 그때그때 달랐는데 요약은 늘 "내 쪽 대비" 라고 했다. 새로고침이 이력 목록을 갈아 끼우며 선택을 놓치면 **목록 맨 위**로 채웠기 때문([모든 브랜치] 에서 맨 위는 HEAD 가 아님). 사용자 결정대로 작업 중 충돌이 막 0 이 되면 기준을 HEAD 로 옮기고, 그 밖에는 해시로 되찾고 없으면 HEAD. 요약은 기준이 HEAD 일 때만 작업 글. 하네스 +18 | 아래 |
| 2026-09-17 | **9차 지시서** — 8차 전부 끝. E-1 에서 새로 걸린 N1(A-1 cherry-pick 마지막 저장 뒤 요약은 "내 쪽 대비" 인데 두 칸의 기준은 이력에서 고른 커밋 — 먼저 정함 · A-2 한 번은 다른 브랜치 커밋이 기준 — 새로고침 뒤 선택 복원, 원인부터)과 전에 남긴 것(B-1 MERGED 주석 줄 "무시" 색 · B-2 폴더 트리 1초) · C-1 웹 문서(F-1 · 숫자) · D-1 cherry-pick · revert 를 마지막 파일까지 네 칸으로. 코드 변경 없음 | 아래 |
| 2026-09-17 | **8차 E-1** — 다섯 길(병합 · pull · rebase · cherry-pick · revert)을 새 저장소에서 **파일 비교 탭을 미리 열지 않고** MergeHub 만으로 끝까지(제품 코드 변경 없음). 다섯 길 모두 커밋(rebase 끝)까지 · 줄끝 · BOM 바뀐 파일 0 · 충돌 표식 0 · class 이름 검사 0. 7차 A-1 · A-2 · B-1, 8차 A-1 · B-1 · B-2 다시 안 남, F-1 병합 · pull 본문 26개. 새로 걸린 것: N1 cherry-pick 요약 "내 쪽 대비 가져온 변경" 이 실제 기준(이력에서 고른 커밋)과 다름 · 한 번 다른 브랜치 커밋이 기준으로 잡힘(재현 안 됨) → 9차. 8차 전부 끝 | 아래 |
| 2026-09-17 | **8차 F-1 (추가 요청)** — 충돌 탭 파일 단위 Accept 는 소스에 주석이 안 남고, 병합 커밋 본문은 비어 있었고, Git 탭 로그는 끄면 사라져 **누가 어느 파일을 어떻게 풀었는지 남는 곳이 없었다**. 병합 · pull 중 푼 방법(줄 단위 · Accept Mine/Theirs/Both · 파일 남기기/지우기 · 머지 주석 자동)을 `.git/MERGEHUB_RESOLUTIONS` 에 적고, [병합 커밋] 본문에 "충돌 해결 N개 · 이름" 과 갈래별 파일 목록을 붙인다(Git 탭 메시지 칸에 미리 보임, 사람이 고쳐도 커밋 때 붙음). 기록은 병합 커밋 · 작업 취소 때 치운다. 병합 26개 · pull 26개 · Accept Mine · 취소 확인. 스타일 맞춤 선택지는 만들지 않기로(사용자) | 아래 |
diff --git a/doc/진행상황.md b/doc/진행상황.md
index 59d0143..4e5f277 100644
--- a/doc/진행상황.md
+++ b/doc/진행상황.md
@@ -243,13 +243,14 @@
## 9차 — 두 칸이 무엇과 견주는지 사실대로 · 8차에서 남긴 것 (2026-09-17 ~)
+진행 기록: [history/2026-09-17-9차.md](history/2026-09-17-9차.md)
지시서: [작업지시/9차.md](작업지시/9차.md). 8차 E-1 에서 새로 걸린 N1(cherry-pick 요약의 기준)과 그 곁가지 · 전에 남겨 둔 작은 것 둘 · 웹 문서.
### A. 두 칸이 무엇과 견주는지 사실대로
| 상태 | 항목 |
|---|---|
-| 대기 | A-1 cherry-pick 마지막 저장 뒤 "내 쪽 대비" 라고 하지만 실제 기준은 이력에서 고른 커밋이다 (먼저 사용자와 정함) |
+| **완료** | A-1 cherry-pick 마지막 저장 뒤 "내 쪽 대비" 라고 하지만 실제 기준은 이력에서 고른 커밋이다 — 사용자가 "HEAD 로 옮긴다" 를 고름(2026-09-17). 새로고침이 고른 커밋을 놓치면 목록 맨 위(모든 브랜치에서는 다른 브랜치 커밋)로 채우던 것을 해시로 되찾고 없으면 HEAD, 작업 중 충돌이 막 0 이 되면 HEAD 로. 요약은 기준이 HEAD 일 때만 작업 글. cherry-pick · revert 마지막 Save 뒤 제목 · 요약 · 기준이 같음(6줄 = git 대조) |
| 대기 | A-2 한 번은 기준이 다른 브랜치 커밋이었다 — 새로고침 뒤 이력 선택 복원 (원인부터) |
### B. 전에 남겨 둔 작은 것
diff --git a/src/MergeHub/Services/CommitSelection.cs b/src/MergeHub/Services/CommitSelection.cs
new file mode 100644
index 0000000..1574b92
--- /dev/null
+++ b/src/MergeHub/Services/CommitSelection.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+
+namespace MergeHub.Services
+{
+ ///
+ /// 새로고침 뒤 이력에서 어느 커밋을 고른 채로 둘지(9차 A-1 · A-2).
+ ///
+ /// 따로 뺀 까닭 - 예전에는 "고른 커밋이 비었으면 목록 맨 위" 였다. 새로고침은 이력 목록을 통째로 갈아 끼우고,
+ /// 그때 Git 탭 목록이 선택을 놓치면 고른 커밋이 비기도 하고 안 비기도 했다. [모든 브랜치] 를 켜면 맨 위는 HEAD 가 아니라
+ /// 가장 최근의 다른 브랜치 커밋이다. 그래서 cherry-pick 마지막 [Save] 뒤 두 칸의 기준이 고른 커밋 · 다른 브랜치 커밋 · HEAD 중
+ /// 하나로 그때그때 달랐고, 요약은 늘 "내 쪽 대비" 라고 했다(8차 E-1 N1).
+ ///
+ /// 정한 것(2026-09-17 사용자) - 작업(병합 · rebase · cherry-pick · revert) 중 충돌이 0 이 되는 순간 기준을 HEAD 로 옮긴다.
+ /// 병합 · rebase 와 같아지고 "내 쪽 대비 가져온 변경 N줄" 이 사실이 된다(커밋하면 무엇이 바뀌는지).
+ /// 그 밖에는 사람이 고른 커밋을 해시로 되찾고, 없으면 맨 위가 아니라 HEAD 로 채운다.
+ ///
+ public static class CommitSelection
+ {
+ /// 새로 읽은 목록()에서 고를 자리. 목록이 비면 -1.
+ /// 새로고침 전에 고른 커밋. 없으면 null.
+ /// 지금 HEAD. 목록에 없을 수 있다(이력 쪽수 밖).
+ /// 이번 새로고침에서 "작업 중 · 충돌 0" 이 막 된 것인지().
+ public static int Choose(IReadOnlyList shas, string? previousSha, string? headSha, bool enteredResolvedOperation)
+ {
+ if (shas is null || shas.Count == 0) return -1;
+
+ var head = IndexOf(shas, headSha);
+ if (enteredResolvedOperation && head >= 0) return head;
+
+ var kept = IndexOf(shas, previousSha);
+ if (kept >= 0) return kept;
+
+ return head >= 0 ? head : 0;
+ }
+
+ ///
+ /// 이번 새로고침에서 "작업이 진행 중이고 충돌이 0" 이 막 되었는지.
+ /// 그 상태에 머무는 동안 사람이 다른 커밋을 골라 보면 새로고침(스테이징 등)마다 HEAD 로 끌어오지 않는다.
+ ///
+ public static bool EntersResolvedOperation(bool wasResolved, bool inProgress, int conflictCount) =>
+ IsResolvedOperation(inProgress, conflictCount) && !wasResolved;
+
+ /// 작업이 진행 중이고 남은 충돌이 없는가.
+ public static bool IsResolvedOperation(bool inProgress, int conflictCount) => inProgress && conflictCount == 0;
+
+ private static int IndexOf(IReadOnlyList shas, string? sha)
+ {
+ if (string.IsNullOrEmpty(sha)) return -1;
+
+ for (var i = 0; i < shas.Count; i++)
+ if (string.Equals(shas[i], sha, StringComparison.OrdinalIgnoreCase)) return i;
+
+ return -1;
+ }
+ }
+}
diff --git a/src/MergeHub/Services/GitWorkspace.cs b/src/MergeHub/Services/GitWorkspace.cs
index 0107a17..ef5b85c 100644
--- a/src/MergeHub/Services/GitWorkspace.cs
+++ b/src/MergeHub/Services/GitWorkspace.cs
@@ -221,6 +221,9 @@ namespace MergeHub.Services
[ObservableProperty] private bool _isRebaseInProgress;
[ObservableProperty] private string _pendingOperationText = string.Empty;
[ObservableProperty] private CommitInfo? _selectedCommit;
+
+ /// 지난 새로고침 때 "작업 중 · 충돌 0" 이었는지. 그 상태가 막 된 순간에만 선택을 HEAD 로 옮긴다(9차 A-1).
+ private bool _wasResolvedOperation;
[ObservableProperty] private BranchInfo? _selectedBranch;
[ObservableProperty] private string _log = string.Empty;
@@ -369,6 +372,7 @@ namespace MergeHub.Services
var mergeInProgress = Git.IsMergeInProgress;
var pendingHasChanges = !operation.IsInProgress || Git.HasStagedChanges();
var conflictTotal = mergeInProgress ? Git.GetMergeConflictTotal() : null;
+ var resolvedOperation = CommitSelection.IsResolvedOperation(operation.IsInProgress, conflicts.Count);
var remote = remotes.FirstOrDefault(r => r.Name == RemoteName) ?? remotes.FirstOrDefault();
var head = branches.FirstOrDefault(b => b.IsCurrentHead);
@@ -403,6 +407,10 @@ namespace MergeHub.Services
IsWorkingTreeClean = status.IsClean;
WorkingTreeSummary = status.IsClean ? "Clean" : $"{status.Files.Count} files";
+ // 목록을 갈아 끼우기 전에 고른 커밋을 적어 둔다. Git 탭 이력 목록이 비워지는 순간 선택을 놓치면
+ // 바인딩이 null 을 밀어 넣는다 - 그 뒤에 보면 고른 커밋을 이미 잃었다(9차 A-2).
+ var previousSha = SelectedCommit?.Sha;
+
Replace(Branches, branches);
Replace(Commits, commits);
Replace(CommitNodes, nodes);
@@ -415,7 +423,12 @@ namespace MergeHub.Services
Replace(Submodules, submodules);
Replace(Stashes, stashes);
- if (SelectedCommit is null && Commits.Count > 0) SelectedCommit = Commits[0];
+ // 예전에는 "비었으면 맨 위" 였다. [모든 브랜치] 에서 맨 위는 HEAD 가 아니어서, cherry-pick 마지막 [Save] 뒤
+ // 두 칸의 기준이 다른 브랜치 커밋이 되기도 했다. 해시로 되찾고, 없으면 HEAD. 작업 중 충돌이 막 0 이 되면 HEAD 로(9차 A-1).
+ var entered = CommitSelection.EntersResolvedOperation(_wasResolvedOperation, operation.IsInProgress, conflicts.Count);
+ _wasResolvedOperation = resolvedOperation;
+ var pick = CommitSelection.Choose(commits.Select(c => c.Sha).ToList(), previousSha, info.HeadSha, entered);
+ SelectedCommit = pick < 0 ? null : Commits[pick];
});
}
diff --git a/src/MergeHub/Services/OperationLabels.cs b/src/MergeHub/Services/OperationLabels.cs
index 1429f9b..2af0d83 100644
--- a/src/MergeHub/Services/OperationLabels.cs
+++ b/src/MergeHub/Services/OperationLabels.cs
@@ -92,10 +92,13 @@ namespace MergeHub.Services
/// 마지막 충돌을 저장하면 파일 비교 탭은 보통 화면으로 돌아간다. 거기서 "남은 차이 3줄" 이라고 하면
/// 방금 받아 놓은 줄이 아직 할 일로 읽혀 덜 푼 것처럼 보인다. 충돌 탭과 같은 말을 하고,
/// 그 줄이 무엇인지 적는다. 진행 중이 아니거나 충돌이 남았으면 null - 그때는 보통 요약이 맞다.
+ ///
+ /// - 두 칸의 기준이 HEAD 일 때만 "내 쪽 대비" 가 사실이다. 작업 중에 사람이 이력에서
+ /// 다른 커밋을 골라 보면 보통 요약으로 둔다. 예전에는 기준을 보지 않아, 기준이 고른 커밋이어도 "내 쪽 대비" 라고 했다(9차 A-1).
///
- public static string? ResolvedSummary(GitOperationKind kind, int conflictsLeft, int diffLines)
+ public static string? ResolvedSummary(GitOperationKind kind, int conflictsLeft, int diffLines, bool baseIsHead = true)
{
- if (kind == GitOperationKind.None || conflictsLeft > 0) return null;
+ if (kind == GitOperationKind.None || conflictsLeft > 0 || !baseIsHead) return null;
var done = $"충돌 정리 끝 · {FinishHint(kind)}";
return diffLines <= 0 ? done : $"{done} · {DiffLead(kind)} {diffLines}줄";
diff --git a/src/MergeHub/ViewModels/HomeViewModel.cs b/src/MergeHub/ViewModels/HomeViewModel.cs
index 24b2c24..32e0bd3 100644
--- a/src/MergeHub/ViewModels/HomeViewModel.cs
+++ b/src/MergeHub/ViewModels/HomeViewModel.cs
@@ -3452,10 +3452,12 @@ namespace MergeHub.ViewModels
: incoming == 0 ? $"남은 충돌 {conflicts}개" : $"남은 충돌 {conflicts}개 · 받을 변경 {incoming}줄";
}
else if (IsGitMode &&
- OperationLabels.ResolvedSummary(_workspace.OperationState.Kind, _workspace.Conflicts.Count, diff) is { } resolved)
+ OperationLabels.ResolvedSummary(_workspace.OperationState.Kind, _workspace.Conflicts.Count, diff,
+ baseIsHead: SelectedCommit?.ShortSha == _workspace.HeadSha) is { } resolved)
{
// 작업(병합 · rebase · cherry-pick · revert) 중 충돌을 다 푼 뒤의 두 칸(HEAD ↔ 워킹트리).
// 차이는 할 일이 아니라 방금 받아 놓은 변경이다(7차 A-1 · 8차 B-1).
+ // 기준이 HEAD 일 때만 그렇다 - BASE 제목이 "가져오기 전 내 쪽" 을 붙이는 조건과 같다(9차 A-1).
work = resolved;
}