Ubisam-MergeTool/doc/history/2026-09-14-6차.md
ksj09406 5856ca0436 6차 C-1: 결과 로그 한 번, 충돌 목록에 종류와 푼 개수
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:07:56 +09:00

880 lines
64 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 6차 — 병합을 끝까지 믿고 쓸 수 있게
지시서: [../작업지시/6차.md](../작업지시/6차.md) · 재료: [2026-09-11-5차.md](2026-09-11-5차.md) 의 C-1
확인은 5차 C-1 과 같은 시험 저장소로 한다. 생성기(`C1Repo`)는 씨앗이 고정돼 있어 몇 번을 만들어도 같다.
만든 뒤 `feature/mine` 에서 `feature/theirs` 를 병합해 **충돌 22개** 상태로 두고 시작한다. 실제 소스는 열지 않는다.
---
## A-2. Save & Commit 이 "실패" 라는데 파일은 이미 써지고 스테이징됐다 (2026-09-14, 완료)
지시서 순서는 A-1 이 먼저지만 A-2 를 먼저 했다. 둘은 뿌리가 같다 - 병합 중에 "파일 하나만 풀어서 저장"
하는 길이 없고 커밋까지 묶여 있다. 그 길을 먼저 만들어야 A-1(줄끝)을 고칠 자리가 한 곳으로 정해진다.
### 고치기 전에 다시 났다
`Io/Unit12.cs` 의 충돌 하나를 3분할에서 Take Theirs 로 정하고 Save & Commit 을 눌렀다.
| 본 것 | 값 |
|---|---|
| 화면 | `Save & Commit 실패: 아직 해결되지 않은 충돌이 21건 있습니다.` |
| git 충돌 수 | 22 → **21** (실패라면서 줄었다) |
| 그 파일 상태 | `M` — 이미 써지고 스테이징됐다 |
| 편집 상태 | 여전히 "편집 1건", Save & Commit 단추 켜짐 |
### 무엇이 원인이었나
`SaveAndCommitAsync` 가 **쓰기 → 스테이징 → 커밋** 을 한 덩어리로 돌렸다. 남은 충돌은 마지막 커밋 단계
(`GitService.CommitMerge`)에서야 예외로 막혔다. 그 앞 두 단계는 되돌릴 수 없는 일인데 이미 끝난 뒤였다.
실패로 끝났으니 편집 상태도 안 치웠다 - 그래서 다른 파일을 고를 때마다 확인 창이 다시 떴다.
그리고 Git 모드에는 커밋 없이 저장하는 길이 **아예 없었다.** Ctrl+S 도 Save & Commit 이었다.
### 고친 것
| 자리 | 무엇 |
|---|---|
| `Services/MergeSavePlan.cs` (새 파일) | "지금 커밋해도 되는가" 를 가르는 판단만 따로 뺐다. 지금 저장하는 파일은 셈에서 빼고, 그 밖에 하나라도 남으면 막는다. 화면 코드에 두면 또 순서가 뒤바뀌기 쉬워서 뺐다 |
| `SaveAndCommitAsync` | 병합 중이면 **아무것도 쓰기 전에** 먼저 판단하고, 막히면 그대로 멈춘다. 알림은 "커밋하지 않았습니다. 파일은 바꾸지 않았습니다. [해결로 저장] 으로…" |
| `SaveResolvedAsync` (새 명령) · [해결로 저장] 단추 | 병합 중에만 보인다. 이 파일만 쓰고 스테이징한다. 커밋은 안 한다. 저장한 내용을 새 "원본" 으로 삼아 편집 상태를 치운다 |
| Ctrl+S | 병합 중이면 [해결로 저장]. 병합이 아니면 지금까지와 같다 |
| 확인 창의 "저장하고 계속" | 병합 중 충돌 파일이면 워킹트리에만 쓰지 않고 충돌까지 푼다. 예전에는 충돌 표시만 사라지고 git 은 여전히 충돌로 보는 어정쩡한 상태가 됐다 |
### 화면에서 해 보다 둘이 더 나왔다
**① 저장하자 파일 목록이 0개가 됐다.** 로그에 `[파일 목록] 다른 작업 진행 중이라 건너뜀`.
`LoadCommitAsync` 가 게이트에 막혀 건너뛰고도 **빈 결과로 목록을 갈아엎었다.** 본문 로드(`LoadFileAsync`)는
이미 `if (!loaded) return;` 으로 막고 있었는데 목록만 빠져 있었다.
새로고침은 저장소 상태를 읽는 도중(게이트를 쥔 채) 화면 목록을 바꾸고, 그 변화가 목록 다시 세우기를 부르므로
이 건너뜀은 **새로고침이 있을 때마다 난다.** 5차 C-1 의 "병합 뒤 파일 목록이 비어 있다"(6차 B-1)도 같은 원인으로 보인다.
- 건너뛰면 지금 목록을 그대로 둔다(`listed` 를 보고 돌아간다).
- 해결로 저장 뒤에는 **게이트가 풀릴 때까지 기다렸다가**(`GitWorkspace.WhenIdleAsync`) 목록을 다시 세운다.
처음에는 새로고침 직후 곧바로 불렀는데 5ms 뒤에도 또 건너뛰었다 - 다른 화면이 게이트를 잠깐 쥔다.
- 방금 푼 파일은 충돌 목록에서 빠지므로 **남은 충돌의 첫 파일이 저절로 열린다.**
**② 충돌을 전부 "내 것" 으로 정하면 저장할 수가 없었다.** Take Mine 은 글을 바꾸지 않는다(이미 내 것이다).
그래서 "고친 것 없음" 이 되어 단추가 꺼지고, git 은 계속 충돌로 봤다.
충돌로 열린 파일이고 3분할에서 충돌을 다 정했으면 고친 글이 없어도 저장할 수 있게 했다.
3분할이 아닐 때는 켜지 않는다 - 충돌 수를 셀 수 없는데 내 것으로 덮으면 안 된다.
### 확인한 것 (새로 만든 저장소, 충돌 22개에서 시작)
| 확인 | 결과 |
|---|---|
| 다른 충돌이 남은 채 Save & Commit | "커밋하지 않았습니다 · 파일은 바꾸지 않았습니다". git 충돌 **22 그대로**, 파일 `UU` 그대로, 커밋 없음, 편집 남음 |
| [해결로 저장] | "해결로 저장 · Unit12.cs · 남은 충돌 21건". git 충돌 **21**, 파일 `M`, 커밋 없음, 편집 상태 깨끗 |
| 저장 뒤 파일 목록 | **21개**로 다시 서고, 남은 충돌 첫 파일(`Common/Util/Unit15.cs`)이 세 칸으로 열림 |
| 그 뒤 다른 파일 고르기 | 확인 창 안 뜸, 파일 열림 |
| Take Mine 만 하고 저장 | 단추가 켜지고 저장됨. git 충돌 **20**, 내 줄이 들어감 |
| 병합 중 Ctrl+S | 해결로 저장이 불림. git 충돌 **19**, 커밋 없음, 목록 19개 |
| Git 탭 충돌 목록 | 푼 세 파일이 빠짐 |
화면 확인 **25건 통과**. 검사 하네스는 `MergeSaveTest` 18건을 새로 만들어 **21벌 716건 통과** · 빌드 경고 0 · 오류 0.
### 이번에 안 한 것
- **줄끝(CRLF → LF)은 그대로다.** 해결로 저장도 지금은 `ResolveConflictWithContent` 를 거치므로 A-1 이 남아 있다.
저장 길이 한 곳으로 모였으니 A-1 은 거기서 고친다.
- 로그의 첫 번째 "건너뜀" 은 여전히 찍힌다. 기다렸다 다시 세우므로 화면에는 티가 안 난다.
새로고침 도중에 목록을 다시 세우려는 것 자체는 B-1 에서 본다.
---
## A-1. 3분할로 푼 파일을 저장하면 CRLF 가 LF 로 바뀐다 (2026-09-14, 완료)
### 고치기 전에 다시 났다
새로 만든 저장소에서 A-2 의 [해결로 저장] 으로 두 파일을 풀었다.
| 파일 | 원래 | 저장 뒤 | git 에 나온 변경 |
|---|---|---|---|
| `Io/Unit12.cs` (200줄 · UTF-8 BOM) | CRLF | **CR 0 / LF 200** | 399줄 (실제로 고친 것은 1줄 + 머지 주석) |
| `Common/Util/한글설명.cs` (11줄 · CP949) | CRLF | **CR 0 / LF 11** | 21줄 |
### 무엇이 원인이었나
두 가지가 겹쳤다.
1. 편집기가 주는 글(`BuildLocalText`)은 줄마다 `\n` 이다. `GitService.ResolveConflictWithContent` 는
**인코딩만 맞추고 줄끝은 아예 보지 않고** 그 글을 그대로 썼다. 그래서 늘 LF 가 됐다.
2. 인코딩을 맞출 "원본" 으로 **워킹트리 파일**을 읽었다. 충돌 중의 워킹트리에는 git 이 LF 로 충돌 표시를 넣어 두므로
줄끝을 봤더라도 "혼합" 으로 읽혔을 것이다. 보통 저장(`TextFile.Write`)은 그 "혼합" 을 무조건 CRLF 로 썼다.
### 고친 것
| 자리 | 무엇 |
|---|---|
| `TextFile.Encode` (새 함수) | 글을 저장할 바이트로 만드는 규칙을 한 곳으로 모았다. "원본 유지" 가 무엇을 따를지를 **원본 바이트로 받는다.** 인코딩은 원본에서 판별, 줄끝은 원본에서 **가장 많은 것**, 원본에 줄끝이 없으면 CRLF. BOM 은 맨 앞에 한 번만 붙이고 글 첫머리의 U+FEFF 는 뗀다 |
| `TextFile.Write` | 디스크의 파일을 원본으로 넘겨 `Encode` 를 쓴다. "혼합" 은 이제 많은 쪽을 따른다(예전에는 무조건 CRLF) |
| `GitService.ResolveConflictWithContent` | 원본을 워킹트리가 아니라 **내 쪽(ours) blob** 으로 잡는다. 내 쪽이 없으면 상대 쪽, 그것도 없으면 공통 조상. 고른 저장 형식을 받는다 |
| `HomeViewModel` 세 자리 | 해결로 저장 · Save & Commit · 확인 창의 "저장하고 계속" 이 모두 고른 저장 형식을 넘긴다 |
로그 글도 따라 바뀌었다. `충돌 해결(직접 편집, ks_c_5601-1987)` 이 `충돌 해결(직접 편집, CP949 · CRLF)` 로 나온다
(6차 C-1 의 "로그가 CP949 를 ks_c_5601-1987 로 적는다" 가 이 자리였다).
### 확인한 것 (새로 만든 저장소, 3분할 + [해결로 저장])
| 파일 | 한 일 | 결과 |
|---|---|---|
| `Io/Unit12.cs` | Take Theirs 1 | CR 200 = LF 200 · BOM 한 번 · 내 쪽 대비 **+2 −1** (고른 줄 + 머지 주석) |
| `Motion/Unit0.cs` (3,363줄) | 충돌 6개 | CR 3,367 = LF 3,367 · 내 쪽 대비 **+8 −4** (5차 C-1 에서는 +3,367 −3,363) |
| `Common/Util/한글설명.cs` | Take Theirs 1 | CR 11 = LF 11 · CP949 그대로(UTF-8 로 안 읽힘) · '가접' 들어감 |
| `Io/LineEnding.cs` | 내 쪽 CRLF · 상대가 LF 로 바꾼 파일에서 Take Theirs | **내 쪽을 따라 CRLF** (CR 61 = LF 61) |
| `Io/Unit20.cs` | 저장 형식 줄바꿈을 **LF 로 고르고** 저장 | 고른 대로 LF (CR 0 · LF 182) |
화면 확인 **10건 통과**. `EncTest` 에 저장 바이트 규칙 22건을 넣었다(16 → 38).
### 검사 모음이 옛 실행 파일로 돌고 있었다
A-1 검사를 넣고 하네스를 돌리다 **세 벌이 빌드가 안 된다**는 것을 알았다. 검사 모음 스크립트가 빌드 출력을 버리고
`bin` 에 남아 있던 **옛 실행 파일**을 그대로 돌려서, 그동안 통과로 세어지고 있었다.
| 하네스 | 무엇이 깨졌나 | 언제부터 |
|---|---|---|
| `LangTest` (52건) | 토크나이저의 줄 넘김 상태를 `bool` 로 넘겼다. 5차 B-1 에서 `CarryOver` 로 바뀌었다. 고치자 **1건이 실제로 실패** - XAML 언어 이름을 옛 이름("XML / HTML")으로 기대하고 있었다 | 5차 B-1 (09-11) |
| `ReportTest` (31건) | 프로젝트에 `CarryOver.cs` · `MarkupTokenizer.cs` 가 빠져 있었다 | 5차 B-1 (09-11) |
| `TokenTest` (15건) | **`Program.cs` 가 없다.** 언제 왜 사라졌는지 모른다. 실행 파일은 09-06 것이었다 | 알 수 없음 |
- 5차 B-1 · B-2 · C-1 과 6차 A-2 기록의 "전체 N건 통과" 에는 **이 세 벌의 옛 결과가 섞여 있다.**
고치고 새로 빌드해 돌려 보니 LangTest 의 이름 기대값 1건 말고는 모두 통과했다 - 제품 코드가 틀린 것은 없었다.
- `LangTest` 는 부르는 모양과 이름 기대값만 새 형식으로 맞췄다. 검사 내용은 그대로다.
- `ReportTest` 는 빠진 파일 둘을 넣었다.
- `TokenTest` 는 되살릴 원본이 없어 **같은 범위(C# 줄 하나를 갈래로 쪼개기)로 새로 썼다**(22건). 예전 15건과 같은 검사는 아니다.
- 검사 모음은 이제 **모두 다시 빌드하고(`--no-incremental`), 이번에 새로 만든 실행 파일이 아니면 실패로 센다.**
그렇게 돌린 결과 **21벌 745건 통과** · 빌드 경고 0 · 오류 0.
---
## A-3. 충돌 파일을 고르면 상대(Theirs) 칸이 안 나온다 (2026-09-14, 완료)
### 고치기 전에 다시 났다
새로 만든 저장소에서 병합 중에 `Io/Unit12.cs` 를 골랐다.
| 본 것 | 값 |
|---|---|
| 3-Way 콤보 | `(3-Way 안 씀)` |
| 칸 | BASE 와 LOCAL 두 칸. **REMOTE · MERGED 없음** |
| BASE 제목 | `(merge base) Io/Unit12.cs` — 읽기는 충돌 갈래로 읽었다 |
### 무엇이 원인이었나
`LoadFileAsync` 는 충돌 파일이면 git 이 들고 있는 세 벌(BASE · 내 쪽 · 상대 쪽)을 읽고 행도 세 칸으로 만든다.
그런데 켠 것은 지역 변수 `threeWay` 뿐이었다. 화면 틀을 정하는 `IsThreeWay` 는 **콤보에서 브랜치를 골랐을 때만** 참이라,
세 칸짜리 행이 두 칸 틀에 실렸다. REMOTE 칸 제목도 늘 `REMOTE · THEIRS` 라 어느 브랜치를 끌어오는지 알 수 없었다.
### 고친 것
| 자리 | 무엇 |
|---|---|
| `HomeViewModel.IsThreeWay` | "콤보로 고른 3-Way" 또는 **"충돌 파일이라 켠 3분할"** 이면 참. 충돌 쪽은 따로 둔 깃발(`_conflictThreeWay`)이다 |
| `LoadFileAsync` | 행을 싣기 **전에** 틀부터 바꾼다. 읽기 갈래는 이제 **콤보로 고른 3-Way 만** 본다(`IsBranchThreeWay`) |
| `ClearPanes` | 깃발을 내린다 |
| `GitService.GetMergeTheirsName` (새 함수) | 들어오는 커밋(MERGE_HEAD)을 가리키는 브랜치 이름 → 병합 메시지의 이름 → 짧은 해시 차례로 찾는다 |
| `Services/MergeNames.cs` (새 파일) | 병합 메시지 첫 줄에서 브랜치·태그 이름을 꺼낸다. 저장소 없이 검사하려고 따로 뺐다 |
| REMOTE 칸 제목 | `REMOTE · feature/theirs` 처럼 상대 이름을 보인다 |
**콤보 값은 건드리지 않는다.** 콤보를 상대 브랜치로 바꿔 두는 쪽도 생각했지만, 그러면 병합이 끝난 뒤에도 보통 파일이
그 브랜치와 3-Way 로 열려 사람이 고르지 않은 비교가 남는다.
읽기 갈래를 콤보에만 묶은 까닭 - 앞 파일이 충돌이라 켜 둔 3분할을 그대로 들고 오면, 충돌이 아닌 다음 파일을
`(3-Way 안 씀)` 이라는 이름의 브랜치와 견주려다 깨진다.
`MergeHeads` 를 쓰려 했는데 이 프로젝트의 LibGit2Sharp(0.32)에는 없었다. `.git/MERGE_HEAD` 파일의 첫 줄을 읽는다.
### 확인한 것 (새로 만든 저장소)
| 확인 | 결과 |
|---|---|
| 콤보를 안 건드리고 충돌 파일(`Io/Unit12.cs`)을 연다 | 콤보는 `(3-Way 안 씀)` 그대로, LOCAL (Mine) · BASE · REMOTE (Theirs) · MERGED **네 칸**, 충돌 잡힘 |
| REMOTE 칸 제목 | `REMOTE · feature/theirs` |
| 다른 충돌 파일(XAML)로 옮김 | 네 칸, 충돌 잡힘 |
| 콤보 없이 Take Theirs → 해결로 저장(A-2) | 단추 켜짐, "남은 충돌 21건", 다음 충돌 파일도 네 칸 |
| `git merge --abort` 뒤 충돌 아닌 파일을 연다 | 콤보 `(3-Way 안 씀)`, **두 칸**, 해결로 저장 단추 안 보임 |
| 콤보로 브랜치를 고른다 / 되돌린다 | 네 칸(`REMOTE · feature/theirs`) / 두 칸 — 예전과 같다 |
화면 확인 **18건 통과**. `MergeSaveTest` 에 병합 메시지 이름 검사 14건을 넣었다(18 → 32).
검사 **21벌 759건 통과**(전부 새로 빌드한 실행 파일) · 빌드 경고 0 · 오류 0.
### 확인하다 본 것 (B-1 으로 넘긴다)
`git merge --abort` 뒤 파일 비교 탭의 [새로고침] 을 눌렀는데 **파일 목록이 병합 때의 21개 그대로**였다(커밋 파일은 303개).
A-2 에서 "건너뛴 목록 불러오기가 목록을 비우지 않게" 막았으므로 이제 **비지 않고 옛 목록이 남는다.**
새로고침 도중 게이트를 쥔 채로 목록 다시 세우기가 불리는 뿌리는 그대로라, B-1 에서 본다.
이번 확인에 쓴 `Io/Unit12.cs` 는 두 목록에 다 있어 결과에는 영향이 없다.
---
## A-4. 상대가 지운 파일을 3분할이 다룰 줄 모른다 (2026-09-14, 완료)
### 고치기 전에 다시 났다
시험 저장소 생성기에 **J. 내가 지웠는데 상대가 고침(DU)** 을 넣었다(지시서가 요구). 이제 충돌은 23개다.
| 파일 | git | 화면 |
|---|---|---|
| `Recipe/ToBeDeleted.cs` (상대가 지움) | `UD` | 네 칸, 지운 쪽 칸이 빔, 범례 **`Theirs only 89`** (실은 내 쪽에만 있는 줄), 충돌 1개, 지웠다는 말 없음 |
| `Recipe/ToBeDeletedByMe.cs` (내가 지움) | `DU` | 네 칸, `Theirs only 0`, 충돌 1개, 지웠다는 말 없음 |
### 무엇이 원인이었나
지운 쪽의 blob 이 없으면 `GetThreeWayContent` 는 그쪽 글을 빈 문자열로 준다. 화면은 그걸 "상대가 줄을 전부 지운 파일" 로 보고
줄 단위 3분할을 만들었다. 사람이 정할 것은 **"이 파일을 남길까 지울까"** 하나인데, 그 물음이 화면 어디에도 없었다.
파일 단위 해결(`ResolveConflict`)은 이미 "없는 쪽을 고르면 지운다" 로 바르게 동작했다 - 화면이 그 길로 안내하지 못했을 뿐이다.
### 고친 것
| 자리 | 무엇 |
|---|---|
| `Services/DeletionConflict.cs` (새 파일) | 어느 쪽이 지웠는지, **남기기 · 지우기가 각각 어느 쪽을 고르는 것인지**를 정한다. 지운 쪽에 따라 뒤바뀌므로(UD 는 남기기=내 것, DU 는 남기기=상대 것) 한 곳에 모았다 |
| `LoadFileAsync` | 한쪽이 지운 충돌이면 줄 단위 3분할로 안 간다. **남아 있는 파일을 BASE 와 두 칸으로** 보여 준다 - 남은 쪽이 무엇을 고쳤는지는 보인다. 칸 제목이 `feature/mine · 내 쪽 (상대는 지움)` 처럼 누가 지웠는지 말한다 |
| 범례 위 띠 | "상대(feature/theirs)가 이 파일을 지웠습니다. 내 쪽(feature/mine)은 고쳤습니다. 파일을 남길까요, 지울까요?" 와 [파일 남기기] · [파일 지우기] |
| [파일 남기기] | 파일이 있는 쪽을 골라 스테이징. **고친 것이 있으면 고친 대로 남긴다**(해결로 저장과 같다) |
| [파일 지우기] | 없는 쪽을 골라 지우고 스테이징. 고친 것이 있으면 한 번 묻는다 |
| 뒷정리 | 해결로 저장 · 남기기 · 지우기가 같은 `ReloadAfterResolveAsync` 를 쓴다. 남은 충돌 첫 파일이 열린다 |
### 확인한 것 (새로 만든 저장소, 충돌 23개에서 시작)
| 확인 | 결과 |
|---|---|
| 보통 충돌(`Io/Unit12.cs`) | 띠 없음, 네 칸 그대로 |
| 상대가 지운 파일을 연다 | 띠 "상대(feature/theirs)가 이 파일을 지웠습니다…", 단추 둘 켜짐, **두 칸**, 제목 `feature/mine · 내 쪽 (상대는 지움)`, 거꾸로 센 범례 없음, 콤보 그대로 |
| 거기서 [파일 지우기] | "지움 · ToBeDeleted.cs · 남은 충돌 22건", git 충돌 22, 워킹트리에서 파일 없어짐, 스테이징 `D `, 다음 충돌 파일이 열림 |
| 내가 지운 파일을 연다 | 띠 "내 쪽(feature/mine)에서 이 파일을 지웠습니다…", 제목 `feature/theirs · 상대 쪽 (나는 지움)`, 두 칸 |
| 거기서 [파일 남기기] | "남김 · ToBeDeletedByMe.cs · 남은 충돌 21건", git 충돌 21, 상대가 고친 줄이 들어간 파일이 생김, CRLF 그대로(CR 70 = LF 70), 스테이징 `A ` |
| Git 탭 충돌 목록 | 두 파일이 빠짐 |
화면 확인 **23건 통과**. `MergeSaveTest` 에 지운 충돌 검사 16건을 넣었다(32 → 48).
검사 **21벌 775건 통과**(전부 새로 빌드한 실행 파일) · 빌드 경고 0 · 오류 0.
### 화면을 보다 하나 더 고쳤다 — BASE 칸 제목
두 칸으로 보여 줄 때 BASE 칸 제목이 `a751a6b · Recipe/ToBeDeleted.cs` 로 나왔다. 지금 커밋과 견주는 것처럼 읽히지만
실제로 보이는 것은 **공통 조상**이다. 제목을 정하는 조건이 "3분할인가" 만 봤기 때문이다.
충돌 파일이면 두 칸이어도 `(merge base) 경로` 로 적게 했다.
다시 확인 **5건 통과** — 보통 충돌은 네 칸, 두 지운 파일 모두 BASE 제목이 `(merge base) …`, 지우기 → 남은 충돌 22건, 남기기 → 21건.
이 고침은 화면 코드(`HomeViewModel`)만 건드려 검사 하네스에는 걸리지 않는다. 앞의 775건은 마지막 서비스 코드 변경 뒤에 돌린 값이다.
### 시험 저장소 생성기가 반쯤 지운 채 멈췄다
다시 만들려는데 생성기가 `다른 프로세스가 사용 중` 으로 멈추고 저장소가 **빈 폴더**로 남았다. 앱을 끄는 명령과 생성기를
동시에 돌렸고, 그 뒤에도 셸이 이 폴더를 작업 폴더로 잡고 있어 **뿌리 폴더를 지울 수 없었다.**
생성기가 뿌리는 두고 **안만 비우게** 고쳤다. 제품 코드와는 상관없다.
---
## A-5. 양쪽 새 파일에 Accept Both 를 누르면 빌드가 깨진다 (2026-09-14, 완료)
### 고치기 전에 다시 났다
새로 만든 저장소에서 Git 탭 충돌 목록의 `Ui/Views/NewPanel.cs`(양쪽이 같은 이름으로 새로 만듦, `AA`)에 Accept Both 를 눌렀다.
| 본 것 | 값 |
|---|---|
| 누르기 전에 묻는가 | **안 묻고 바로 이어 붙였다** |
| 결과 파일의 BOM | **2개** — 맨 앞 하나, 가운데 하나 |
| `class NewPanel` | **2번** |
### 무엇이 원인이었나
`GitService.ResolveConflict` 의 Both 갈래가 두 blob 을 **바이트째** 이어 붙였다. 양쪽이 BOM 으로 시작하면 뒤쪽 BOM 이
결과 한가운데에 그대로 남는다. 줄끝이 다르면 한 파일 안에 CRLF 와 LF 가 섞인다.
그리고 파일 단위 "둘 다" 는 같은 이름의 형식이 두 번 나오는 결과를 거의 늘 만드는데, 누르기 전에 그걸 알릴 자리가 없었다.
### 고친 것
| 자리 | 무엇 |
|---|---|
| `Services/BothJoin.cs` (새 파일) | 두 벌을 각자 제 인코딩으로 **글로 풀어** 이어 붙인 뒤, **내 쪽 파일의 형식**(인코딩 · BOM · 줄끝)으로 다시 쓴다. 규칙은 A-1 의 `TextFile.Encode` 를 그대로 쓴다. 한쪽이 비면 다른 쪽을 손대지 않고 돌려준다 |
| `GitService.ResolveConflict` (Both) | 바이트 이어 붙이기 대신 `BothJoin.Join` |
| Git 탭 Accept Both | 누르면 먼저 묻는다 — "내 것 다음에 상대 것을 한 파일로 이어 붙입니다… 같은 형식이 두 번 나와 빌드가 안 될 수 있습니다. 줄마다 골라 둘 다 넣으려면 파일 비교 탭의 3분할에서 하세요." 단추에 같은 뜻의 도움말 |
**Both 를 없애지는 않았다.** 설정 파일이나 목록 파일처럼 이어 붙이는 게 맞는 경우가 있다. 대신 누르기 전에 결과를 알게 했다.
### 확인한 것 (새로 만든 저장소, 충돌 23개에서 시작)
| 확인 | 결과 |
|---|---|
| 양쪽 새 파일에 Accept Both | 먼저 묻는다(단추: 예 · 아니오) |
| [아니오] | git 은 여전히 `AA`, 파일 바이트 그대로, 충돌 23 |
| 다시 눌러 [예] | BOM **1개(맨 앞)**, CR 4 = LF 4, 내 것 다음에 상대 것, 가운데 U+FEFF 없음, git `M`, 충돌 22 |
| 양쪽이 고친 보통 충돌(`Io/Unit20.cs`)에 Accept Both | BOM 1개, CR 362 = LF 362, 양쪽 줄이 다 들어감, 충돌 21 |
화면 확인 **14건 통과**. `EncTest` 에 이어 붙이기 검사 10건을 넣었다(38 → 48). CP949 인 내 파일과 UTF-8 BOM 인 상대 파일을 이어도
CP949 로 쓰이고 양쪽 한글이 다 읽히는지까지 본다.
검사 **21벌 785건 통과**(전부 새로 빌드한 실행 파일) · 빌드 경고 0 · 오류 0.
### A 묶음을 마치며
A 의 다섯 가지(줄끝 · 반쯤 된 실패 · 상대 칸 · 지운 파일 · Both)가 모두 끝났다. 끝까지 풀어 커밋까지 가는 확인은 D-1 에서
처음부터 다시 한다. A 를 하다 B-1 으로 넘긴 것 - **새로고침 도중 게이트를 쥔 채 목록 다시 세우기가 불려 건너뛰는 뿌리** -
는 아직 그대로다(지금은 목록이 비지 않고 옛 목록이 남는다).
---
## B-1. 충돌 목록에서 3분할 화면까지 가는 길이 끊겨 있다 (2026-09-14, 완료)
### 고치기 전에 다시 났다
새로 만든 저장소에서 파일 비교 탭을 한 번 열어 둔 채 Git 탭으로 `feature/theirs` 를 병합했다(충돌 23개).
| 본 것 | 값 |
|---|---|
| 파일 비교 탭 파일 목록 ([새로고침] 안 누름) | **304개** — 병합 전 커밋 파일 그대로. 충돌 23개가 아니다 |
| 충돌 탭 목록을 두 번 누름 | **아무 일 없음** |
| 충돌 탭 안내 글 · 병합 알림 | "**홈 탭**의 3분할 화면" · "**홈 탭**에서 해결하세요" — 그런 탭은 없다 |
5차 C-1 때는 목록이 **0개** 였다. 6차 A-2 에서 "건너뛴 목록 불러오기가 목록을 비우지 않게" 막아서 이제는 **옛 목록이 남는다.**
### 무엇이 원인이었나
**목록.** 새로고침은 저장소 상태를 읽는 동안(게이트를 쥔 채) 화면 목록을 갈아 끼우고, 끝나자마자 Git 탭이 제 일로
게이트를 다시 잡는다. 파일 비교 화면은 새로고침 신호를 받자마자 목록을 다시 세우려다 **"다른 작업 진행 중" 으로 건너뛰었다.**
A-2 · A-4 에서는 저장한 뒤에만 기다렸다 다시 세우게 했고, 새로고침 자체는 그대로였다.
**두 번 누르기.** 충돌 목록과 파일 비교 화면을 잇는 통로가 없었다. 폴더 비교 → 파일 비교에는 있던 것이다.
### 고친 것
| 자리 | 무엇 |
|---|---|
| `HomeViewModel.OnWorkspaceRefreshed` | 새로고침 뒤 목록 세우기를 **게이트가 풀릴 때까지 기다렸다** 한다(`RebuildListAfterRefreshAsync`). 막히면 몇 번 더 해 본다 |
| `GitWorkspace.RequestOpenConflict` (새 통로) | "이 충돌 파일을 3분할로 열어 달라" 요청을 **남겨 두고** 파일 비교 탭으로 옮긴다. 폴더 비교의 넘겨주기와 같은 방식이다 - 파일 비교 화면은 탭을 처음 열 때 만들어지므로 받을 사람이 없을 수 있다 |
| `HomeViewModel.OpenConflictAsync` | 요청을 받으면 목록에 그 파일이 설 때까지 기다렸다 고른다. 충돌 파일이라 A-3 대로 **네 칸**으로 열린다. 이 요청 때문에 화면이 새로 만들어졌으면 **기본 저장소를 새로 열지 않고** 지금 저장소로 목록만 세운다 |
| Git 탭 충돌 목록 | 두 번 누르기 · Enter · **[3분할로 열기]** 단추. 이진 파일은 줄 단위로 못 본다고 알린다 |
| 두 번 누르기 받는 법 | `MouseBinding` 이 아니라 목록의 `MouseDoubleClick` - 항목이 마우스 누름을 먼저 처리해 `MouseBinding` 은 항목 위에서 안 온다 |
| 안내 글 · 병합 알림 | "…줄마다 고르려면 목록을 두 번 누르거나 [3분할로 열기] 를 누르세요. 파일 비교 탭에서 세 칸으로 열립니다." · "충돌 23건 - 충돌 탭에서 파일을 두 번 누르면 파일 비교 탭에서 세 칸으로 풉니다." |
### 확인한 것 (새로 만든 저장소, MergeHub 로 병합)
| 확인 | 결과 |
|---|---|
| 병합 알림 | "충돌 23건 - 충돌 탭에서 파일을 두 번 누르면…" (홈 탭 없음) |
| 병합 뒤 파일 비교 탭, [새로고침] 안 누름 | 목록 **23개** |
| 충돌 탭 안내 글 | 두 번 누르기 · [3분할로 열기] 를 알려 줌, 홈 탭 없음 |
| 목록 두 번 누르기 | 파일 비교 탭으로 넘어가 `Io/Unit20.cs` 가 네 칸으로 열림, 충돌 잡힘 (약 1초) |
| [3분할로 열기] | `Motion/Unit0.cs` 가 네 칸으로 (약 0.8초) |
| Enter | `Io/LineEnding.cs` 가 네 칸으로 (약 1초) |
| **파일 비교 탭을 한 번도 안 연 새 앱**에서 두 번 누르기 | 파일 비교 탭이 새로 만들어지며 `Io/Unit12.cs` 가 네 칸으로, 저장소는 `mergehub-c1` 그대로, 목록 23개 |
| `git merge --abort` 뒤 **Git 탭에서만** [새로고침] | 파일 비교 탭 목록이 커밋 파일 304개로 돌아옴, REMOTE 칸 없음 |
| 열기만 하는 동안 git 충돌 수 | 23 그대로 |
화면 확인 **17건 통과**(Enter 1건은 처음에 목록 아래쪽 행을 골랐다가 자동 조작이 못 찾아 실패 - 가상화된 목록이라 화면 밖 행은
안 만들어진다. 위쪽 행으로 다시 해 통과). 이번에 고친 파일은 검사 하네스가 쓰지 않아 앞의 785건이 그대로 유효하다.
빌드 경고 0 · 오류 0.
---
## B-2. 확인 창이 엉뚱한 파일 이름을 댄다 (2026-09-14, 완료)
### 고치기 전에 다시 났다
새로 만든 저장소(충돌 23개)에서 `Io/Unit12.cs` 를 3분할로 열어 Take Theirs 로 고친 뒤, 파일 콤보에서 `Io/Unit20.cs` 를 **마우스로** 골랐다.
| 본 것 | 값 |
|---|---|
| 확인 창 첫 줄 | "**Unit20.cs** 에 저장하지 않은 편집이 있습니다." — 고친 것은 Unit12.cs |
| [취소] 뒤 콤보 | **`Io/Unit20.cs`** — 새 파일을 가리킨 채 |
| [취소] 뒤 화면 | `(merge base) Io/Unit12.cs`, 고친 내용 그대로 — **콤보와 화면이 서로 다르다** |
(자동 조작은 콤보 항목을 UIA 로 고르지 않고 진짜 마우스로 눌렀다. UIA 로 고르면 확인 창이 뜨는 순간 그 호출이 안 돌아와 앱이 굳는다 - 5차 C-1 에서 겪었다.)
### 확인하다 찾은 것 둘
**① [저장하고 계속] 이 엉뚱한 파일에 썼다 — 이것이 가장 컸다.**
`Io/Unit20.cs` 를 고치고 `Io/Unit12.cs` 로 옮기며 [저장하고 계속] 을 눌렀다. 충돌은 23 → 22 로 줄었는데 `Io/Unit20.cs` 는 여전히 `UU` 였다.
저장소를 들여다보니 **`Io/Unit12.cs` 안에 `class Unit20` 이 들어가 있었고, 해결된 것(`M`)으로 스테이징돼 있었다.**
고친 파일의 글이 다른 파일을 덮어쓰고, 덮인 파일은 풀린 것으로 처리됐다. 병합 결과가 망가지는 버그다.
**② B-1 의 빈틈 — 저장소를 먼저 열고 파일 비교 탭을 처음 열면 목록이 끝내 0개.**
B-1 확인 때는 늘 파일 비교 탭을 먼저 열어 두었다. 순서를 바꾸자 파일 콤보가 0개였다.
그 화면의 자동 열기는 이미 열린 저장소면 아무것도 안 하고, 새로고침 신호는 화면이 생기기 전에 지나갔다.
### 무엇이 원인이었나
모두 한 뿌리다. 파일 콤보에서 다른 파일을 고르면 **`SelectedFile` 이 먼저 새 파일로 바뀐 뒤에** 변경 처리가 불린다.
그 안에서 뜨는 확인 창이 이름을 `SelectedFile` 에서 가져왔고, [저장하고 계속] 의 저장(`SaveWorkingCopy`)도 쓸 곳을 `SelectedFile` 에서 가져왔다.
[취소] 뒤 되돌리기는 콤보가 고르는 **도중에** 값을 되돌려, 콤보가 그 알림을 흘려 버렸다.
### 고친 것
| 자리 | 무엇 |
|---|---|
| `ConfirmDiscard` | 이름을 **지금 화면에 실린 파일**(`_lastLoadedFile`)에서 가져온다. Git 모드는 저장소 안 경로로 적는다 - 다른 폴더에 같은 이름 파일이 흔하다 |
| `SaveWorkingCopy` | 쓸 곳도 **실린 파일**로. 병합 중 충돌 파일이면 그 파일의 충돌을 푼다 |
| `OnSelectedFileChanged` | [취소] 의 되돌리기를 **콤보의 선택 처리가 끝난 다음 차례**에 한다 |
| 생성자 | 저장소가 이미 열려 있으면 그 저장소로 목록을 세운다(B-1 빈틈) |
### 확인한 것 (새로 만든 저장소, 저장소를 먼저 열고 파일 비교 탭을 나중에 처음 연다)
| 확인 | 결과 |
|---|---|
| 파일 비교 탭을 처음 연다, [새로고침] 안 누름 | 목록 **23개** |
| 고친 `Io/Unit12.cs` 에서 `Io/Unit20.cs` 를 고름 | "**Io/Unit12.cs** 에 저장하지 않은 편집이 있습니다. / 다른 파일을 엽니다: Io/Unit20.cs" |
| [취소] | 콤보 `Io/Unit12.cs`, 화면 `(merge base) Io/Unit12.cs`, 고친 내용 그대로 |
| [저장하지 않고 계속] | 콤보·화면 `Io/Unit20.cs`, git 충돌 23 그대로, `Io/Unit12.cs` 는 여전히 `UU` |
| `Io/Unit20.cs` 를 고치고 `Io/Unit12.cs` 로 옮기며 [저장하고 계속] | 확인 창이 **Io/Unit20.cs** 이름을 댐. git 충돌 22, **풀린 것은 `Io/Unit20.cs`**(`M`, 안의 형식 `Unit20`, 상대 줄 들어감), **`Io/Unit12.cs` 는 `UU` 그대로**(안의 형식 `Unit12`), 그다음 `Io/Unit12.cs` 가 열림 |
화면 확인 **18건 통과**. 고친 파일(`HomeViewModel`)은 검사 하네스가 쓰지 않아 앞의 785건이 그대로 유효하다. 빌드 경고 0 · 오류 0.
## B-3. 커밋 메시지가 엉뚱하게 남는다 (2026-09-14, 완료)
### 고치기 전에 다시 났다
새로 만든 저장소(충돌 23개)를 MergeHub 로 병합했다.
| 본 것 | 값 |
|---|---|
| 파일 비교 탭에서 `Common/Util/Unit15.cs` 를 연 뒤 `Io/Unit12.cs` · `Motion/Unit0.cs` 로 옮김 | 메시지 칸이 끝까지 "**Merge: Common/Util/Unit15.cs 충돌 해결**" — 처음 연 파일 이름이 굳음 |
| 충돌을 다 풀고 앱을 다시 켠 뒤 충돌 탭 [병합 커밋] | 첫 줄 "**Merge commit 'fc8b8f8…'**", 그 아래 **`#` 로 시작하는 줄 24개**(`#Conflicts:` 와 파일 목록). 부모 2개 |
### 무엇이 원인이었나
- 파일 비교 탭은 메시지 칸이 **비어 있을 때만** 채웠다. 첫 파일에서 한 번 채우면 그 뒤로는 사람이 적은 글과 구별이 안 돼 그대로 남았다. 병합이 끝나도 지우는 곳이 없었다.
- 메시지 없이 커밋하면 git 이 남긴 `MERGE_MSG` 를 **그대로** 썼다. git 명령은 커밋할 때 `#` 줄을 지우지만 MergeHub 는 파일을 통째로 읽어 넣었다.
또 MergeHub 는 브랜치를 커밋으로 풀어 병합하므로(libgit2) git 이 첫 줄을 해시로 적는다 — 이력에서 무엇을 합쳤는지 안 보였다.
### 고친 것
| 자리 | 무엇 |
|---|---|
| `MergeNames.BranchMergeMessage` | 기본 메시지 `Merge branch '상대' into 내 브랜치`. 파일 이름은 넣지 않는다 — 병합 커밋은 병합 전체를 담는다 |
| `MergeNames.CleanMergeMessage` | `MERGE_MSG` 에서 `#` 줄을 빼고 앞뒤 빈 줄을 정리. 첫 줄이 `Merge commit '해시'` 이고 상대 브랜치 이름을 알면 브랜치 모양으로 바꾼다. 남는 것이 없으면 기본 메시지 |
| `GitService.CommitMerge` | 메시지가 비면 `CleanMergeMessage` 를 거친 것을 쓴다 |
| `HomeViewModel` | 스스로 채운 글을 `_autoCommitMessage` 로 기억한다. 칸이 비었거나 **내가 채운 글 그대로**일 때만 다시 채우고, 사람이 고친 글은 건드리지 않는다. 병합이 끝나면(새로고침에서 병합 중이 아님 · 병합 아닌 파일을 열 때 · 커밋 성공) 내가 채운 글만 지운다 |
`MergeSaveTest` 에 메시지 경우를 넣어 **62건 통과**.
### 확인한 것 (새로 만든 저장소)
| 확인 | 결과 |
|---|---|
| 병합 뒤 `Io/Unit12.cs` 를 엶 | 칸 `Merge branch 'feature/theirs' into feature/mine` |
| `Motion/Unit0.cs` 로 옮김 | 같은 글. 파일 이름 없음 |
| 칸에 "냉각 로직 병합 - 사람이 적음" 을 쓰고 `Io/Unit20.cs` 로 옮김 | 적은 글 그대로 |
| 칸을 비우고 `Io/Unit12.cs` 로 옮김 | 기본 메시지가 다시 채워짐 |
| 충돌을 git 으로 다 풀고 **앱을 다시 켠 뒤** 충돌 탭 [병합 커밋] | 커밋 메시지 `Merge branch 'feature/theirs' into feature/mine` 한 줄, `#` 줄 **0개**, 부모 2개, MERGE_HEAD 사라짐 |
| 저장소를 새로 만들어 병합 → 파일을 열어 칸이 채워진 채로 충돌을 풀고 **앱을 끄지 않고** [병합 커밋] | 커밋 메시지 같은 한 줄, `#` 줄 0개, 부모 2개. **파일 비교 탭 메시지 칸이 비워짐** |
화면 확인 **7건 통과**. 빌드 경고 0 · 오류 0.
## B-4. MERGED 칸 줄 번호가 네 자리에서 잘린다 (2026-09-14, 완료)
### 고치기 전에 다시 났다
새로 만든 저장소(충돌 23개)를 병합하고 `Motion/Unit0.cs`(3,363줄)를 네 칸으로 열어 편집기에서 Ctrl+End 를 눌렀다.
| 칸 | 보인 번호 |
|---|---|
| LOCAL · BASE · REMOTE | `2949` · `2950` 온전 |
| **MERGED** | **`294` · `295` · `295`** — 2949 · 2950 · 2951 줄 |
### 무엇이 원인이었나
줄 여백 폭은 네 칸 모두 54px 로 같지만 **나눈 방법이 달랐다.**
| 칸 | 여백 나눔 | 번호 글자 자리 |
|---|---|---|
| BASE | 번호 54 (오른쪽 띄움 8) | 46px |
| LOCAL 원본 · REMOTE | 번호 34 (띄움 4) + [&lt;]/[&gt;] 20 | 30px |
| MERGED | 북마크 10 + 번호 24 (띄움 4) + [&gt;] 20 | **20px** |
Consolas 12 는 한 글자 약 6.6px 라 20px 에는 세 자리만 들어간다. MERGED 칸만 북마크 막대 자리를 따로 떼어 번호가 가장 좁았다.
### 고친 것 (`Views/HomeView.xaml` 만)
여백을 네 칸 모두 **66px** 로 넓혔다. 칸마다 폭이 같아야 워드랩에서 접히는 자리가 같다(한 칸만 넓히면 행이 어긋난다).
| 자리 | 전 | 후 | 번호 글자 자리 |
|---|---|---|---|
| BASE 템플릿 | 54 | 66 | 58px |
| LOCAL 원본 · REMOTE 템플릿 | 34 + 20 | 46 + 20 | 42px |
| MERGED 템플릿 | 10 + 24 + 20 | **6 + 40 + 20** | **36px** — 다섯 자리(99,999줄)까지 |
| 위에 겹친 편집기 `LocalEditor` 왼쪽 여백 | 54 | 66 | — 아래층 글자와 x 가 같아야 캐럿이 맞는다 |
북마크 막대는 너비 4 · 좌우 띄움 1 이라 6px 로 모자람이 없다. 칸마다 코드 자리가 12px 줄었다.
### 확인한 것
| 확인 | 결과 |
|---|---|
| C-1 저장소 `Motion/Unit0.cs` 네 칸, Ctrl+End | MERGED `2949` · `2950` · `2951` 온전. 확대해 보니 [&gt;] 단추와도 안 겹친다. 위 세 칸도 온전 |
| 만든 저장소(`Big7479.cs` 7,479줄, 2950 · 7470 줄 양쪽이 다르게 고침)를 MergeHub 로 병합, 네 칸, Ctrl+End | **네 칸 모두** `7462` ~ `7470` 온전. 충돌 2 |
| 7,479줄 두 파일을 `--merge` 인자로 열기(두 칸) | BASE · LOCAL `7440` ~ `7479` 온전 |
| 편집기와 아래층이 맞는가 | 7468 줄에서 Ctrl+Shift+→ 로 `public ` 을 고르니(UIA 로 읽은 선택 글도 `public `) 선택 영역이 아래층의 `public` 글자에 딱 겹친다 |
빌드 경고 0 · 오류 0. 고친 파일은 XAML 뿐이라 검사 하네스는 영향이 없다.
### 확인하다 본 것 두 가지 — 아래 "B-4 확인 중 발견 두 건" 에서 고쳤다
- 3분할에서 Ctrl+End 를 누르면 네 칸 모두 몇 줄만 보이고 **그 아래가 빈 채**로 그려진다. 고치기 전 화면에서도 같았다.
- 저장소 밖 파일을 `--merge --base --local --remote --merged` 로 넘기면 네 칸이 아니라 BASE · LOCAL 두 칸으로 열린다.
## B-4 확인 중 발견 두 건 (2026-09-14, 완료)
사용자가 "확인하다 본 것도 고쳐 달라" 고 했다.
### ① 멀리 건너뛰면 네 칸이 위로 밀려 아래가 빈다
**다시 났다.** 만든 저장소 `Big7479.cs`(7,479줄) 네 칸에서 Ctrl+End. 네 칸 모두 `7462` ~ `7470` 아홉 줄만 보이고 아래가 비었다. 휠을 한 칸 굴리면 멀쩡해졌다.
Ctrl+End 는 원래 "마지막 차이" 단축키(4차)라 마지막 충돌 줄로 가는 것은 설계대로다. 문제는 빈 화면이다.
**원인을 가른 과정.**
| 해 본 것 | 본 것 |
|---|---|
| UIA 로 MERGED 목록의 행 | 행은 만들어져 있다(7446 ~ 7471). 비어서 빈 것이 아니다 |
| 1px 굴렸다 되돌리는 코드를 넣음 | **그대로** — 이 길이 아니다(걷어냄) |
| 목록 칸과 편집기의 화면 좌표 | **MERGED 목록 칸이 편집기보다 279px(약 9줄) 위에 있었다.** 목록 위쪽은 머리글에 가리고 그만큼 아래가 빈다 |
표시층(색 · 줄 번호) 목록 넷은 편집기 글자에 맞추려고 **같은 세로 이동값**(`_decorationShift`)을 공유한다. 이 값은 배치가 끝날 때마다 "편집기의 N행 글자 위치" 와 "표시층 N행 위치" 를 견줘 정한다.
그런데 N 을 **처음 만들어진 목록 행**에서 골랐다. 멀리 건너뛴 직후 그 행은 화면 밖에 남은 행이고, TextBox 는 화면 밖 글자의 좌표를 **어림으로** 준다. 그 어림이 9줄쯤 틀려 네 칸이 한꺼번에 밀렸다. 휠을 굴리면 기준 행이 바뀌어 다시 맞았다.
**고친 것** (`Views/HomeView.xaml.cs`)
| 자리 | 무엇 |
|---|---|
| `AlignDecorationToEditor` | 기준 행을 **목록 칸 안에 위가 안 잘리고 보이는 첫 행**으로 고른다(`FirstVisibleContainer` - 행 판의 자식만 훑는다). 편집기 글자 좌표가 **편집기 화면 밖이면 맞추지 않고** 다음 배치 때 다시 본다 |
**확인** (`Big7479.cs` 네 칸)
| 동작 | MERGED 목록 칸 y − 편집기 y | 화면 |
|---|---|---|
| 처음 연 뒤 | −7px (고치기 전과 같은 값 - 칸 테두리 차) | 정상 |
| Ctrl+End | **−7px** (고치기 전 −279px) | 네 칸 `7453` ~ `7470` 끝까지 채워짐 |
| Ctrl+Home(첫 차이 2950) · 끝에서 Alt+↑ | 행 번호와 편집기 줄이 같은 눈높이 | 정상 |
### ② `git mergetool` 세 벌이 두 칸으로 열린다
**다시 났다.** 7,479줄 파일 셋을 `--merge --base --local --remote --merged` 로 넘기자 BASE · LOCAL 두 칸, "차이 없음".
**원인.** 켤 때 인자를 `DiffPair`(BASE, MERGED) 한 쌍으로만 넘겼다. 세 벌을 받는 길이 없었고, 네 칸(3-Way)은 `IsGitMode` 일 때만 켜졌다.
MERGED 는 git 이 충돌 표시를 넣어 둔 파일이라, 두 칸으로는 상대가 무엇을 바꿨는지 볼 수 없고 충돌 표시를 손으로 지워 가며 풀어야 했다.
**고친 것**
| 파일 | 무엇 |
|---|---|
| `Services/StartupArgs.cs` | `ThreeWayMerge` — `--merge` 에 LOCAL · REMOTE 가 다 있으면 `MergeToolRequest(Base, Local, Remote, Merged)`. BASE 는 없어도 되고(양쪽 새 파일), MERGED 가 없으면 LOCAL 에 쓴다 |
| `Services/GitWorkspace.cs` | 켤 때 세 벌이면 `RequestMergeTool` — 다른 요청처럼 화면이 생길 때까지 남겨 둔다 |
| `ViewModels/HomeViewModel.cs` | `OnMergeToolRequested` · `LoadMergeToolAsync` — 파일 비교 모드에서 네 칸으로 연다. MERGED 칸의 처음 글은 충돌 표시가 아니라 **LOCAL**. `IsThreeWay` 가 Git 이 아니면 mergetool 네 칸일 때 참. 파일 두 개 비교를 열면 접는다 |
| 같은 파일 | [저장] 은 안 고쳤어도 **충돌이 다 풀리면** 켜진다. MERGED 파일에 쓸 때 형식(인코딩 · 줄끝)은 **LOCAL 파일**에서 읽는다 - MERGED 는 충돌 표시 줄이 LF 라 섞여 보인다(A-1 과 같은 까닭) |
`ArgsTest` 에 10건을 넣어 **36건 통과**.
**확인** (지어낸 세 벌: 40줄 CRLF · BOM, 10줄은 양쪽이 다르게 고침, 30줄은 상대만 고침. MERGED 는 git 모양대로 충돌 표시를 넣고 LF)
| 확인 | 결과 |
|---|---|
| 인자로 켬 | **네 칸**. BASE `Weld_BASE_1234.cs`, REMOTE `REMOTE · Weld_REMOTE_1234.cs`, 범례 Conflict 1 · Theirs only 1, MERGED 는 내 쪽 글(`= 111; // 내 쪽`), [저장] 꺼짐 |
| 10줄 Take Theirs → [자동 병합] | "1줄 자동 병합 · 남은 충돌 없음", [저장] 켜짐 |
| [저장] 뒤 `Weld.cs` | BOM 있음 · CRLF 42 · 홀로 LF 0 · 충돌 표시 없음. 10줄 머지 주석, 11줄 `= 222; // 상대`, 31줄 머지 주석, 32줄 `= 3030; // 상대만 고침` |
위쪽 LOCAL(Mine) 칸 제목은 MERGED 칸과 같은 값을 쓴다(Git 모드에서는 둘 다 브랜치 이름). mergetool 에서는 결과 파일 이름 `Weld.cs` 가 보인다. 손대지 않았다.
빌드 경고 0 · 오류 0.
## B-5. 머지 주석은 소스에 넣고, 병합 뒤에는 그 줄을 무시한다 (2026-09-14, 완료)
사용자가 정한 것: 머지 주석은 지금처럼 소스에 넣는다. 비교하고 병합할 때 그 줄을 무시한다.
### 생성기에 넣은 경우 (`C1Repo`, D-1 에서도 쓴다)
| 이름 | 파일 | 내 쪽 | 상대 쪽 |
|---|---|---|---|
| K | `Ui/Views/Unit45.cs` | 13줄 코드 위에 `// [김승준] 2026-09-10` 한 줄 | 같은 자리에 `// [박지훈] 2026-09-11` 한 줄 |
| L | `Ui/Dialogs/Unit46.cs` | 13줄 끝에 ` // [김승준] 2026-09-10` | 같은 줄 끝에 ` // [박지훈] 2026-09-11` |
| M | `Common/Util/Unit47.cs` | 9줄 `= 111` 로 고치고 그 위에 내 주석 | `= 222` 로 고치고 그 위에 상대 주석 |
이제 병합하면 git 충돌은 23 → **26**개다.
### 고치기 전에 다시 났다
| 확인 | 결과 |
|---|---|
| MergeHub 로 병합 | git 충돌 **26**개, K · L · M 모두 충돌 |
| 두 칸 비교: 줄 끝 주석 1 · 주석 줄 1 만 다른 두 파일 | **남은 차이 2줄** |
| 네 칸: K · L · M | 모두 충돌로 물음 |
(파일을 바꾸고 5초 뒤에 읽은 값이 한 파일씩 밀려 있었다 - M 이라고 찍은 화면이 K 였다. 그래서 파일별 "예전 모습" 은 아래 "설정을 끄면" 에서 다시 읽은 값을 쓴다.)
### 무엇이 원인이었나
- 비교 규칙(`CompareRules`)에 머지 주석을 가리는 규칙이 없었다. 줄 필터는 사용자가 정규식을 적어야 했고, 줄 끝에 붙은 주석은 줄을 통째로 걸러야 해서 코드 차이까지 가려졌다.
- git(libgit2)은 주석 줄도 글자로 본다. 양쪽이 같은 자리에 다른 주석을 달면 그 줄끼리 충돌한다. MergeHub 는 그대로 사람에게 물었다.
### 고친 것
| 파일 | 무엇 |
|---|---|
| `Services/MergeNote.cs` | `TailStart` · `WithoutTail` · `TailOf` — 줄 끝 머지 주석 조각을 찾는다. 주석이 둘 이어 붙어도 첫 주석부터. 주석 기호 앞이 공백이 아니면 글 속 기호로 본다(`"http://..."`). XAML 은 닫는 기호까지 있어야 한다 |
| `Services/CompareRules.cs` | `IgnoreMergeNotes` · `NotePath`. 켜면 주석만 있는 줄은 걸러지고(줄 필터 켜짐과 따로), 비교 키는 **줄 끝 주석을 뗀 코드**. 주석 줄끼리는 같은 키라 이름 · 날짜가 달라도 짝지어지고, 빈 줄과는 짝지어지지 않는다. 상태바 "무시: 머지 주석" |
| `Services/NoteRows.cs` (새) | 정렬기가 **주석 줄과 코드 줄을 한 행에 짝지으면** 주석 행 · 코드 행으로 나누고, 떼어 낸 코드 조각은 칸이 안 겹치는 다음 행과 합친다. M 에서 BASE 코드 줄이 양쪽 주석 줄과 한 행이 되어 "BASE 와 다름" 이 하나 더 세지고, 코드 충돌이 BASE 없이 보이던 것을 막는다 |
| `Services/NoteConflict.cs` (새) | git 이 워킹트리에 남긴 충돌 표시를 읽어, **모든 덩어리가** 주석을 빼면 같은 코드일 때만 푼다. 양쪽 주석을 다 남긴다 - 줄 위 주석은 내 쪽 다음 상대 쪽(같은 줄은 한 번), 줄 끝 주석은 이어 붙인다. `|||||||` (diff3) 모양도 읽는다 |
| `Services/GitService.cs` · `IGitService.cs` | `ResolveNoteOnlyConflicts` — 양쪽 다 있는 글 파일 충돌에만. 쓰기는 직접 편집 해결과 같은 길(내 쪽 형식 따름, A-1) |
| `ViewModels/GitViewModel.cs` | [Merge] 뒤 충돌이 있으면 자동으로 풀고, 알림 · 로그에 몇 건 풀었는지와 남은 충돌 수 |
| `Services/FolderComparer.cs` | 켜면 주석 줄을 빼고 줄 끝 주석을 뗀 글이 같으면 **같음**. 뗀 것이 없거나 줄끝(`\r`)이 다르면 가리지 않는다 |
| `ViewModels/HomeViewModel.cs` · `FolderViewModel.cs` | 설정을 규칙에 넘기고 파일 경로로 주석 기호를 고른다. 두 칸 · 네 칸 정렬 뒤 `NoteRows` |
| `MergeHubSettings.cs` | [머지 주석] "비교·병합에서 머지 주석 무시 (사람이 '// [이름] 2026-01-01' 모양으로 쓴 주석도 무시됨)", 기본 켬 |
검사: NoteTest 29 → **64**, RulesTest 21 → **36**, FolderTest 51 → **57**. 검사 모음 21개를 모두 새로 빌드해 **865건 통과**.
### 확인한 것 (새로 만든 저장소)
| 확인 | 결과 |
|---|---|
| MergeHub 로 병합 | git 충돌 **24**개. 알림 "충돌 24건 (머지 주석만 다른 2건은 자동으로 풀림)", 로그에 파일별 "머지 주석만 다른 충돌 1곳 자동 해결" |
| K 스테이징된 글 | 13줄 `// [김승준] 2026-09-10`, 14줄 `// [박지훈] 2026-09-11`, 15줄 코드. BOM · CRLF 342 · 홀로 LF 0 · 충돌 표시 0. 워킹트리 = 인덱스 |
| L 스테이징된 글 | 13줄 `Log("step 5"); // [김승준] 2026-09-10 // [박지훈] 2026-09-11`. BOM · CRLF 242 · 홀로 LF 0 |
| M (코드도 다름) | 충돌로 남음(`UU`). 네 칸: 주석 행은 따로 **무시 1**, 코드 행은 BASE `101` · 내 쪽 `111` · 상대 `222` 가 한 행. **남은 충돌 1 · 남은 차이 1** |
| 두 칸 비교: 줄 끝 주석 1 · 주석 줄 1 | **남은 차이 0줄** |
### 설정을 끄면 (설정 파일을 백업해 끄고 확인한 뒤 원래 바이트로 되돌림 - 해시 같음)
| 확인 | 결과 |
|---|---|
| 병합 | git 충돌 **26**개, K · L · M 모두 남음 |
| 네 칸 K · L · M | 남은 충돌 1 · 1 · **2** (M 은 주석 줄끼리도 충돌) |
| 두 칸 비교 | 남은 차이 **2줄** |
빌드 경고 0 · 오류 0.
### 남긴 것
- 자동 풀기는 Git 탭 [Merge] 에만 걸었다. pull · rebase · cherry-pick 이 낸 충돌에는 아직 걸지 않았다.
- MERGED 칸에서 반영하며 새로 넣은 주석 줄은, 다시 정렬하면(타이핑 멈춤 · 규칙 변경) "무시" 색이 된다. 차이 수에는 원래도 세지 않던 줄이다.
## B-6. 충돌 탭에서 하나씩 누를 때마다 오래 기다린다 (2026-09-14, 완료)
### 고치기 전에 다시 났다
새로 만든 저장소를 병합(충돌 24)하고, 같은 줄 충돌 15개(`Vision/Unit10.cs` ~ `Motion/Unit24.cs`)를 충돌 탭에서 **하나씩 골라 Accept Mine**.
| 본 것 | 값 |
|---|---|
| 15개 합계 | **22.0초** |
| 한 개 평균 | **1.47초** (자동 조작의 고르기 0.3초 포함) |
| 여러 개 고르기 | 안 됨 - 목록이 한 개만 고른다 |
### 무엇이 원인이었나
- 목록이 `SelectionMode` 기본값(한 개)이었다.
- Accept 한 번이 작업 하나(`RunAndRefreshAsync`)라, 끝날 때마다 저장소 전체를 다시 읽었다.
### 고친 것
| 파일 | 무엇 |
|---|---|
| `Views/GitView.xaml` | 충돌 목록 `SelectionMode="Extended"`(Ctrl · Shift · Ctrl+A). 단추 옆에 "N개 고름 - Accept 는 고른 파일 모두에 한 번에 적용됩니다". 안내 글에 여러 개 고르기 |
| `Views/GitView.xaml.cs` | `SelectionChanged` 에서 고른 목록을 넘긴다(SelectedItems 는 바인딩이 안 된다) |
| `ViewModels/GitViewModel.cs` | `SetSelectedConflicts` · `ConflictTargets`. Accept Mine/Theirs/Both 가 고른 파일 모두를 **작업 하나**로 풀고 다시 읽기는 끝에 한 번. 한 파일이 실패해도 나머지는 풀고, 실패는 로그 · 알림. 로그 "충돌 N개를 Mine 로 한 번에 풀었습니다 (… ms)". Accept Both 확인 창은 "N개 파일 (첫 파일 외 N-1개)" |
### 확인한 것 (같은 저장소 · 같은 15개)
| 확인 | 결과 |
|---|---|
| 15개를 골라 Accept Mine 한 번 | "15개 고름" 표시. git 반영 **0.8초**, 목록이 9개로 다시 설 때까지 **1.2초** (고치기 전 22.0초) |
| 로그 | "충돌 15개를 Mine 로 한 번에 풀었습니다 (224ms · 저장소 다시 읽기는 끝에 한 번)" |
| 결과 | 충돌 24 → 9. 15개 모두 내 쪽(HEAD) 글과 같음 |
(자동 조작은 목록이 화면에 보이는 항목만 만들어 두는 탓에 처음에는 10개만 골랐다. 목록 항목을 모두 불러오게 고쳐 15개로 다시 쟀다.)
빌드 경고 0 · 오류 0.
## E-1. C# 코드 스타일 맞춤 (2026-09-14, 추가 요청, 완료)
사용자 요청: Visual Studio [텍스트 편집기 > C# > 코드 스타일 > 서식 > 줄 바꿈] 옵션 그림과 같은 스타일(AURORA)이 아니면 diff 가 많이 뜬다. C# 이면 그 스타일로 자동으로 맞춰 달라. AURORA 와 Allman 를 고를 수 있게.
### 그림에서 읽은 AURORA
| 옵션 | 켬 |
|---|---|
| 여는 중괄호 새 줄: 형식 · 메서드와 로컬 함수 | ✓ |
| 여는 중괄호 새 줄: 속성·인덱서·이벤트 · 접근자 · 익명 메서드 · 제어 블록 · 익명 형식 · 개체/컬렉션/배열 초기화 · 람다 | 끔(같은 줄) |
| "else" · "catch" · "finally" 새 줄 | 끔(`} else {`) |
| 개체 초기화 멤버 · 익명 형식 멤버 · 쿼리 절 새 줄 | ✓ (손대지 않는다) |
Allman 은 모두 새 줄(Visual Studio 기본값과 같다). 처음에는 Default 라고 불렀으나 사용자 요청으로 바꿨다 - 팀에서 Default 는 C++ 스타일을 가리킨다.
### 만든 것
| 파일 | 무엇 |
|---|---|
| `Services/CSharpStyle.cs` (새) | 중괄호 · else/catch/finally 의 **줄 위치만** 옮긴다. 주석 · 문자열 · 보간 문자열 · 전처리 줄은 가려서 본다. 중괄호 앞 머리를 거꾸로 읽어 갈래(형식 · 메서드 · 속성 · 접근자 · 익명 메서드 · 제어 · 익명 형식 · 초기화 · 람다)를 정하고 옵션표대로 올리거나 내린다. 앞 줄 끝에 주석이 있으면 올리지 않는다. 판단할 수 없는 블록은 그대로. 바꿀 것이 없으면 받은 글 그대로 |
| `ViewModels/HomeViewModel.cs` | 비교 규칙 칸에 [C# 스타일 끔 / C# AURORA / C# Allman]. 고르면 원본부터 다시 읽어 BASE · LOCAL · REMOTE 를 맞춘다(Git 두 칸 · 네 칸 · 파일 두 개 · mergetool). 편집 칸 글이 바뀌면 저장할 것이 있는 상태, 저장하면 그 스타일로 파일에 쓴다. **스타일만 바뀐 편집은 파일을 바꿀 때 묻지 않는다**(묻게 두면 파일마다 창이 뜬다). 상태바 요약에 스타일 |
| `Services/FolderComparer.cs` · `FolderViewModel.cs` | 스타일만 다른 C# 파일은 **같음**. 줄끝이 다르면 가리지 않는다 |
| `MergeHubSettings.cs` | [비교 옵션] "C# 코드 스타일 맞춤", 기본 끔 |
검사: StyleTest(새) **78건** - 두 스타일 사이 왕복, 다시 맞춰도 그대로, CRLF, 주석 · 문자열 · 전처리 속 중괄호, 갈래 판단 36가지, 옵션표, 1,600줄대 글 속도. FolderTest 57 → **62**. 검사 모음 22개 **948건 통과**.
### 확인한 것
만든 시연 재료(`demo\style\Allman.cs` 132줄, `Aurora.cs` 는 같은 코드를 AURORA 로 맞추고 값 한 줄 `RetryCount` 3 → 5).
| 확인 | 남은 차이 | 저장 |
|---|---|---|
| 스타일 끔 | **39줄** | 꺼짐 |
| C# AURORA | **1줄** (RetryCount) | 꺼짐 - 편집 칸이 이미 AURORA |
| C# Allman | 1줄 | **켜짐** - 편집 칸이 Allman 로 바뀜 |
| 다시 끔 | 39줄 | 꺼짐, **확인 창 안 뜸** |
시연 저장소(`D:\MergeHubDemo\repo`)를 MergeHub 로 병합 → `Line/Style.cs`(내 쪽 AURORA + 값 4, 상대 Allman + 값 6) 네 칸: 끔 **남은 충돌 1 · 차이 39줄** → AURORA **충돌 1 · 차이 1줄**.
설정 화면에 저장된 값은 `"CSharpBraceStyle": 1`. 확인 뒤 설정 파일은 원래 바이트로 되돌렸다.
### 남긴 것
- 들여쓰기 · 공백 · 그 밖의 서식은 맞추지 않는다. 중괄호 줄 위치만.
- git 병합(libgit2) 자체는 스타일을 모른다. 스타일만 다른 충돌도 git 은 충돌로 내고, 네 칸에서 맞춰 보여 줄 뿐 자동으로 풀지는 않는다.
## E-2. 발표 시연 재료 (2026-09-14, 추가 요청, 완료)
`demo\make-demo.ps1` 한 번이면 `demo\plain` · `style` · `mergetool` · `big` 과 병합 저장소 `D:\MergeHubDemo\repo` 를 새로 만든다(`tools\DemoMaker`, 지어낸 코드). 내용과 쓰는 법은 `demo\README.md`.
시연 저장소를 git 으로 병합하면 충돌 8개(같은 줄 · 머지 주석 줄 위 · 줄 끝 · 스타일 · 양쪽 새 파일 · 한쪽 삭제 · CP949 · XAML), `Gripper.cs` · `Heater.cs` 는 자동 병합. MergeHub 로 병합하면 머지 주석만 다른 2개를 스스로 풀어 6개.
처음 만든 생성기는 CP949 파일을 UTF-8 로 읽어 충돌이 안 났고, `Gripper.cs` 는 맞붙은 줄을 고쳐 충돌이 났다. 둘 다 고쳐 다시 만들었다. `make-demo.ps1` 은 PowerShell 5.1 이 한글을 읽도록 BOM 을 붙였다.
## B-7. 상태바가 병합 중에 믿을 수 없는 값을 보인다 (2026-09-14, 완료)
### 고치기 전에 다시 났다
새로 만든 저장소(충돌 23)를 병합하고 파일 비교 탭에서 충돌 파일을 열었다.
| 파일 | git 내 쪽(ours) 원본 | 상태바 인코딩 · 줄바꿈 |
|---|---|---|
| `Ui/Views/NewPanel.cs` (양쪽 새 파일) | BOM · CRLF 2/2줄 | **UTF-8 · 혼합** |
| `Io/LineEnding.cs` | BOM · CRLF 60/60줄 | **UTF-8 · 혼합** |
| `Io/Unit12.cs` | BOM · CRLF | UTF-8 (BOM) · CRLF (충돌 표시 줄이 CRLF 라 우연히 맞음) |
워킹트리 파일에는 git 이 넣은 충돌 표시 때문에 `NewPanel.cs` 에 홀로 LF 3개, `LineEnding.cs` 에 63개가 섞였고 BOM 도 안 보였다.
`Io/Unit12.cs` 에서 Take Theirs 로 충돌을 정하자 오른쪽 칸은 "충돌 정리 끝" 인데 상태바는 **"남은 차이 1줄"** 그대로. 남은 충돌 수는 상태바 어디에도 없었다.
### 무엇이 원인이었나
- 파일 비교 탭이 충돌 파일도 워킹트리(`DescribeWorkingFile`)로 인코딩 · 줄바꿈을 읽었다. 저장(해결로 저장)은 내 쪽 파일 형식을 따르는데(A-1) 상태바만 다른 것을 봤다.
- 상태바 요약은 "BASE 와 다른 줄 수" 였다. 네 칸에서는 정한 충돌도 BASE 와 달라 줄지 않는다.
- 확인하다 하나 더: 요약을 만든 **뒤에** 범례 수(남은 충돌 수 포함)를 셌다. 새 요약에 남은 충돌 수를 넣자 한 박자 늦어, 여섯 개를 다 정해도 "남은 충돌 1개" 로 남았다.
### 고친 것
| 파일 | 무엇 |
|---|---|
| `Services/GitService.cs` · `IGitService.cs` | `DescribeConflictFile` — 인덱스의 내 쪽(ours) blob, 없으면 상대 쪽 · 공통 조상 blob 으로 판별. 충돌이 아니면 워킹트리 |
| `ViewModels/HomeViewModel.cs` (`LoadFileAsync`) | 충돌 파일이면 `DescribeConflictFile` |
| `ViewModels/HomeViewModel.cs` (`UpdateSummaries`) | 네 칸이면 **"남은 충돌 N개 · 받을 변경 M줄"**(정한 충돌 · 이미 받은 줄 · 무시한 줄은 세지 않음), 둘 다 0 이면 "충돌 정리 끝". 두 칸은 "남은 차이 N줄"(0 이면 "차이 없음"). 범례 수를 요약보다 **먼저** 센다 |
### 확인한 것 (새로 만든 저장소, C# 스타일 맞춤은 확인 동안만 끔 - 설정 파일은 원래 바이트로 되돌림)
| 파일 | 상태바 | git 내 쪽 원본 |
|---|---|---|
| `Ui/Views/NewPanel.cs` | 남은 충돌 2개 · **UTF-8 (BOM) · CRLF** | BOM · CRLF 2/2 |
| `Io/LineEnding.cs` | 남은 충돌 1개 · **UTF-8 (BOM) · CRLF** | BOM · CRLF 60/60 |
| `Common/Util/한글설명.cs` | 남은 충돌 1개 · **CP949 · CRLF** | BOM 없음 · UTF-8 아님 · CRLF 10/10 |
| `Motion/Unit0.cs` | 남은 충돌 6개 · UTF-8 (BOM) · CRLF | BOM · CRLF 3,363/3,363 |
`Motion/Unit0.cs` 충돌 여섯 개를 Take Theirs 로 하나씩 정하며 읽었다.
| 정한 수 | 상태바 요약 | 오른쪽 칸 |
|---|---|---|
| 0 | 남은 충돌 6개 | 남은 충돌 6개 |
| 1 ~ 5 | 남은 충돌 5 · 4 · 3 · 2 · 1개 · 편집 N건 | 같음 |
| 6 | **충돌 정리 끝** · 편집 6건 | 충돌 정리 끝 |
| Undo 한 번 | 남은 충돌 1개 · 편집 5건 · 되돌리기 1건 | 남은 충돌 1개 |
(자동 조작은 파일 콤보 항목이 화면에 보이는 것만 만들어져 처음에 두 파일을 못 골랐다. 항목을 이름으로 불러와 고르게 고쳐 다시 쟀다.)
빌드 경고 0 · 오류 0.
## B-8. 병합 커밋 뒤 파일 비교 탭이 어정쩡하게 남는다 (2026-09-14, 다시 나지 않음 - 닫음)
지시서의 증상: 병합 커밋 뒤 목록이 비고, 3분할 틀에 병합과 상관없는 파일(`Common/Util/Unit103.cs`)이 BASE 칸에만 뜨고 언어는 "평문".
판정 기준: 병합 커밋이 끝나면 병합 전과 같은 보통 상태(선택 커밋 = 방금 만든 병합 커밋).
### 두 흐름으로 다시 해 봤다 (새로 만든 저장소, 코드는 고치지 않음)
**① Git 으로 모두 풀고 Git 탭에서 [병합 커밋].** 그 사이 파일 비교 탭은 `Io/Unit12.cs` 를 네 칸으로 열어 둔 채.
| 때 | 파일 목록 | 칸 | BASE 머리글 | 언어 · 요약 | 병합 진행 중 표시 |
|---|---|---|---|---|---|
| 병합 전 | 304 | 두 칸 | `d19ecb7 · Common/Util/Unit103.cs` | C# · 차이 없음 | 없음 |
| 병합 중 | 24(충돌만) | 네 칸 | `(merge base) Io/Unit12.cs` | C# · 남은 충돌 1개 | 있음 |
| **병합 커밋 뒤** | **304** | **두 칸** | **`3019433 · Io/Unit12.cs`** (방금 만든 병합 커밋) | **C# · 차이 없음** | **없음** |
화면에서도 수정 이력 맨 위(병합 커밋)가 골라져 있고 "Selected Commit 3019433 Merge branch 'feature/theirs' …".
**② C-1 때처럼 마지막 충돌들을 파일 비교 탭에서 푼 뒤 커밋.** Git 으로 두 파일만 남기고, 탭에서 `Recipe/ToBeDeletedByMe.cs` 는 [파일 지우기], `Io/Unit12.cs` 는 Take Theirs → [해결로 저장] → Git 탭 [병합 커밋].
| 때 | 파일 목록 | 칸 | BASE 머리글 | 언어 · 요약 | 병합 진행 중 표시 |
|---|---|---|---|---|---|
| 충돌 2개 남음 | 2 | 네 칸 | `(merge base) Io/Unit12.cs` | C# · 남은 충돌 1개 | 있음 |
| [파일 지우기] 뒤 | 1 | 네 칸 | `(merge base) Io/Unit12.cs` | C# · 남은 충돌 1개 | 있음 |
| [해결로 저장] 뒤(충돌 0 · MERGE_HEAD 있음) | 304 | 두 칸 | `1e6410d · Io/Unit12.cs` | C# · 남은 차이 1줄 | 있음 |
| **병합 커밋 뒤** | **304** | **두 칸** | **`3403732 · Io/Unit12.cs`** (방금 만든 병합 커밋) | **C# · 차이 없음** | **없음** |
### 판단
두 흐름 모두 판정 기준대로다. 5차 C-1 때 난 증상은 그 뒤 고친 것들로 없어진 것으로 본다.
- **B-1** — 새로고침 뒤 게이트가 풀릴 때까지 기다렸다 목록을 다시 세운다(예전에는 건너뛰어 목록이 빈 채로 남았다).
- **A-3** — 충돌이 아닌 파일을 열면 충돌 때문에 켠 네 칸을 접는다(예전에는 네 칸 틀이 남았다).
- **A-2 · B-2** — 목록을 다시 세울 때 빈 목록으로 갈아엎지 않고, 실린 파일을 기준으로 다룬다.
코드 변경 없음. 흐름 ② 는 D-1(처음부터 다시 끝까지)에서 한 번 더 본다.
## C-1. 로그와 목록 다듬기 (2026-09-14, 완료)
### 고치기 전에 다시 났다 (새로 만든 저장소를 MergeHub 로 병합)
| 확인 | 결과 |
|---|---|
| 병합 로그 | "충돌 26건 - 해결이 필요합니다." **두 번**. 두 번째는 머지 주석 자동 해결 줄들 **뒤에** 다시 나와 순서도 헷갈림 |
| 충돌 탭 목록 표시 | 24줄 중 표시가 붙은 것은 `Recipe/ToBeDeleted.cs · 상대쪽 삭제` 하나. `Ui/Views/NewPanel.cs`(양쪽 새 파일) · `Recipe/ToBeDeletedByMe.cs`(내가 삭제)는 표시 없음 |
| 푼 개수 | 어디에도 없음 |
| CP949 로그 이름 | `충돌 해결(직접 편집, CP949 · CRLF): Common/Util/한글설명.cs` — **이미 CP949**. A-1 에서 로그 문장을 `TextFile.Describe` 로 바꾸며 없어졌다. 이 항목은 확인만 |
### 무엇이 원인이었나
- `GitService` 가 결과 문장을 로그에 적고(`Report` → `Progress`), 병합 · revert · cherry-pick · rebase · pull 명령이 **같은 문장을 한 번 더** 적었다.
- 목록 템플릿에는 "이진" 과 "상대쪽 삭제" 두 표시만 있었다.
- 병합에서 처음 몇 개가 충돌했는지 남기는 곳이 없었다.
### 고친 것
| 파일 | 무엇 |
|---|---|
| `ViewModels/GitViewModel.cs` | 병합 · revert · cherry-pick · rebase 시작 · rebase 계속 · pull 에서 결과 문장을 다시 적지 않는다 |
| `Services/GitService.cs` | 추적 브랜치가 없어 pull 을 못 할 때도 로그에 적는다(이 길만 서비스가 안 적었다). 병합에서 충돌이 나면 처음 충돌 목록을 `.git/MERGEHUB_CONFLICTS`(첫 줄 MERGE_HEAD)에 적고, `GetMergeConflictTotal` 로 읽는다. MERGE_HEAD 가 다르면 다른 병합의 것이라 쓰지 않는다 |
| `Models/GitModels.cs` | `ConflictInfo.KindLabel` — 양쪽 수정 · 상대 삭제 · 내가 삭제 · 양쪽 새 파일(이진이면 " · 이진"), `IsDeletion` |
| `Views/GitView.xaml` | 목록 줄마다 종류(한쪽 삭제는 경고 색, 이진은 위험 색). 목록 위에 진행 한 줄 |
| `Services/GitWorkspace.cs` | `ConflictProgressText` — "남은 충돌 N개 · 푼 것 M개 (M/전체)", 다 풀면 "충돌 정리 끝 · 푼 것 M개 - 병합 커밋을 할 수 있습니다". 처음 수를 모르면(MergeHub 밖에서 시작한 병합) 남은 수만 |
처음에는 푼 개수를 git 인덱스의 해결 기록(REUC)으로 셌다. 확인해 보니 libgit2 가 **저절로 병합된 파일 7개**도 거기 남겨 병합 직후 "푼 것 9개 (9/33)" 로 틀리게 나왔다. 그래서 병합할 때 처음 목록을 적어 두는 쪽으로 바꿨다.
### 확인한 것
| 확인 | 결과 |
|---|---|
| 병합 로그 | "해결이 필요합니다" **한 번**. 그 뒤로 자동 해결 두 줄과 "자동으로 풀었습니다. 남은 충돌 24건." |
| 기록 파일 | 경로 26개 |
| 목록 종류 | 양쪽 수정 21 · 상대 삭제 1(`Recipe/ToBeDeleted.cs`) · 내가 삭제 1(`Recipe/ToBeDeletedByMe.cs`) · 양쪽 새 파일 1(`Ui/Views/NewPanel.cs`) |
| 병합 직후 진행 줄 | **남은 충돌 24개 · 푼 것 2개 (2/26)** — 머지 주석만 다른 2개가 스스로 풀림 |
| 같은 줄 충돌 15개 Accept Mine | 남은 충돌 9개 · 푼 것 17개 (17/26) |
| 앱을 다시 켠 뒤 | 남은 충돌 9개 · 푼 것 17개 (17/26) — 이어짐 |
| 남은 9개 Accept Mine | 충돌 정리 끝 · 푼 것 26개 - 병합 커밋을 할 수 있습니다 |
| 병합 전 | 진행 줄 없음 |
FolderTest 62 · ReportTest 31 · GitOpTest 36 건 통과(`GitModels.cs` 를 함께 컴파일). 빌드 경고 0 · 오류 0.