Ubisam-MergeTool/doc/history/2026-09-14-6차.md
ksj09406 04b8ef8a60 6차 A-5: Accept Both 는 글로 이어 붙이고 누르기 전에 묻는다
양쪽이 같은 이름으로 새로 만든 파일에 Accept Both 를 누르면 묻지 않고
두 blob 을 바이트째 이어 붙였다. 결과 파일 한가운데에 BOM 이 하나 더
들어갔고 같은 class 가 두 번 나와 빌드가 안 됐다.

- BothJoin: 두 벌을 각자 제 인코딩으로 글로 풀어 이어 붙인 뒤 내 쪽
  형식(인코딩, BOM, 줄끝)으로 다시 쓴다. 규칙은 A-1 의 TextFile.Encode
- ResolveConflict(Both): 바이트 이어 붙이기 대신 BothJoin
- Git 탭 Accept Both: 먼저 묻는다(무엇이 일어나는지, 줄 단위로 둘 다
  넣으려면 3분할에서). 단추 도움말

Both 는 없애지 않았다. 이어 붙이는 게 맞는 파일도 있다.

화면 확인 14건 통과(묻기, 아니오면 그대로, 예면 BOM 1개와 CRLF 한
가지, 보통 충돌도 같음). EncTest 10건 추가. 21벌 785건 통과
(새로 빌드한 실행 파일). 경고 0, 오류 0. A 묶음 끝.

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

23 KiB
Raw Blame History

6차 — 병합을 끝까지 믿고 쓸 수 있게

지시서: ../작업지시/6차.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 으로 넘긴 것 - 새로고침 도중 게이트를 쥔 채 목록 다시 세우기가 불려 건너뛰는 뿌리 - 는 아직 그대로다(지금은 목록이 비지 않고 옛 목록이 남는다).