Ubisam-MergeTool/doc/history/2026-09-14-6차.md

587 lines
42 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) + [<]/[>] 20 | 30px |
| MERGED | 북마크 10 + 번호 24 (띄움 4) + [>] 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` 온전. 확대해 보니 [>] 단추와도 안 겹친다. 위 세 칸도 온전 |
| 만든 저장소(`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.