Ubisam-MergeTool/doc/history/2026-09-14-6차.md
ksj09406 c76e3d7aee 6차 B-2: 파일을 바꿀 때 확인 창과 저장이 고친 파일을 가리킨다
Io/Unit12.cs 를 고치고 콤보에서 Io/Unit20.cs 를 고르면 확인 창이
"Unit20.cs 에 저장하지 않은 편집" 이라고 했다. [취소] 뒤에는 콤보가
Io/Unit20.cs 를, 화면은 Io/Unit12.cs 를 보여 서로 달랐다.

확인하다 더 큰 것을 찾았다. [저장하고 계속] 이 새로 고른 파일에 썼다.
Io/Unit20.cs 를 고치고 Io/Unit12.cs 로 옮기며 누르자 Io/Unit12.cs 안에
class Unit20 이 들어가고 해결된 것으로 스테이징됐다. 병합 결과가
망가지는 버그다.

원인은 하나다. 콤보에서 고르면 SelectedFile 이 먼저 새 파일로 바뀐
뒤에 변경 처리가 불리는데, 확인 창의 이름과 저장할 곳을 둘 다
SelectedFile 에서 가져왔다.

- ConfirmDiscard: 이름을 화면에 실린 파일(_lastLoadedFile)에서.
  Git 모드는 저장소 안 경로로
- SaveWorkingCopy: 쓸 곳도 실린 파일로. 충돌이면 그 파일의 충돌을 푼다
- 취소 뒤 되돌리기를 콤보의 선택 처리가 끝난 다음 차례에
- 저장소가 이미 열려 있으면 생성자에서 목록을 세운다(B-1 빈틈 -
  저장소를 먼저 열고 파일 비교 탭을 나중에 열면 목록이 0개였다)

화면 확인 18건 통과(목록 23개, 이름, 취소, 저장하지 않고 계속,
저장하고 계속 뒤 Unit20 은 풀리고 Unit12 는 그대로). 경고 0, 오류 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:47:45 +09:00

429 lines
31 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.