From 04b8ef8a605063070a51a73535fcaa18af9424ee Mon Sep 17 00:00:00 2001 From: ksj09406 Date: Mon, 14 Sep 2026 11:06:14 +0900 Subject: [PATCH] =?UTF-8?q?6=EC=B0=A8=20A-5:=20Accept=20Both=20=EB=8A=94?= =?UTF-8?q?=20=EA=B8=80=EB=A1=9C=20=EC=9D=B4=EC=96=B4=20=EB=B6=99=EC=9D=B4?= =?UTF-8?q?=EA=B3=A0=20=EB=88=84=EB=A5=B4=EA=B8=B0=20=EC=A0=84=EC=97=90=20?= =?UTF-8?q?=EB=AC=BB=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 양쪽이 같은 이름으로 새로 만든 파일에 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 --- doc/history/2026-09-14-6차.md | 50 +++++++++++++++++++++++++ doc/history/changelog.md | 1 + doc/진행상황.md | 2 +- src/MergeHub/Services/BothJoin.cs | 37 ++++++++++++++++++ src/MergeHub/Services/GitService.cs | 13 ++----- src/MergeHub/ViewModels/GitViewModel.cs | 13 ++++++- src/MergeHub/Views/GitView.xaml | 1 + 7 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 src/MergeHub/Services/BothJoin.cs diff --git a/doc/history/2026-09-14-6차.md b/doc/history/2026-09-14-6차.md index 386f2ca..a082d74 100644 --- a/doc/history/2026-09-14-6차.md +++ b/doc/history/2026-09-14-6차.md @@ -267,3 +267,53 @@ A-2 에서 "건너뛴 목록 불러오기가 목록을 비우지 않게" 막았 다시 만들려는데 생성기가 `다른 프로세스가 사용 중` 으로 멈추고 저장소가 **빈 폴더**로 남았다. 앱을 끄는 명령과 생성기를 동시에 돌렸고, 그 뒤에도 셸이 이 폴더를 작업 폴더로 잡고 있어 **뿌리 폴더를 지울 수 없었다.** 생성기가 뿌리는 두고 **안만 비우게** 고쳤다. 제품 코드와는 상관없다. + + +--- + +## 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 으로 넘긴 것 - **새로고침 도중 게이트를 쥔 채 목록 다시 세우기가 불려 건너뛰는 뿌리** - +는 아직 그대로다(지금은 목록이 비지 않고 옛 목록이 남는다). diff --git a/doc/history/changelog.md b/doc/history/changelog.md index 42bdc57..faf0ff0 100644 --- a/doc/history/changelog.md +++ b/doc/history/changelog.md @@ -8,6 +8,7 @@ | 날짜 | 항목 | 커밋 | |---|---|---| +| 2026-09-14 | **6차 A-5** — 파일 단위 Accept Both 가 두 벌을 글로 풀어 이어 붙이고 내 쪽 형식으로 쓴다(가운데 BOM 사라짐). 누르기 전에 무엇이 일어나는지 묻는다. A 묶음 끝 | 아래 | | 2026-09-14 | **6차 A-4** — 한쪽이 지운 충돌(UD·DU)은 줄 단위 3분할 대신 남은 파일을 두 칸으로 보이고 [파일 남기기] · [파일 지우기] 로 푼다. 거꾸로 세던 범례(Theirs only 89)가 없어짐 | 아래 | | 2026-09-14 | **6차 A-3** — 병합 중 충돌 파일을 열면 콤보를 안 건드려도 네 칸이 뜬다. REMOTE 제목에 상대 브랜치 이름(MERGE_HEAD → 병합 메시지 → 짧은 해시) | 아래 | | 2026-09-14 | **6차 A-1** — 충돌 해결 저장이 내 쪽(ours) 파일의 줄끝·인코딩을 따른다(3,363줄 파일 +3,367/−3,363 → +8/−4). 검사 모음이 옛 실행 파일로 돌던 것을 찾아 세 벌을 고침 | 아래 | diff --git a/doc/진행상황.md b/doc/진행상황.md index a510ac4..f21e52e 100644 --- a/doc/진행상황.md +++ b/doc/진행상황.md @@ -97,7 +97,7 @@ | **완료** | A-2 Save & Commit 이 "실패" 라는데 파일은 이미 써지고 스테이징됐다 — [해결로 저장] 을 따로 둠 | | **완료** | A-3 충돌 파일을 고르면 상대(Theirs) 칸이 안 나온다 — 콤보를 안 건드려도 네 칸, REMOTE 제목에 상대 브랜치 | | **완료** | A-4 상대가 지운 파일을 3분할이 다룰 줄 모른다 — 남은 파일을 두 칸으로, [파일 남기기] · [파일 지우기] 띠 | -| 대기 | A-5 양쪽 새 파일에 Accept Both 를 누르면 빌드가 깨진다 | +| **완료** | A-5 양쪽 새 파일에 Accept Both 를 누르면 빌드가 깨진다 — 누르기 전에 묻고, 글로 이어 붙여 BOM 한 번 · 줄끝 한 가지 | ### B. 헷갈리거나 막히는 것 diff --git a/src/MergeHub/Services/BothJoin.cs b/src/MergeHub/Services/BothJoin.cs new file mode 100644 index 0000000..5699e78 --- /dev/null +++ b/src/MergeHub/Services/BothJoin.cs @@ -0,0 +1,37 @@ +using System; + +namespace MergeHub.Services +{ + /// + /// 충돌 파일 두 벌을 "둘 다" 로 이어 붙인다(파일 단위 Accept Both). + /// + /// 예전에는 바이트째 이어 붙였다. 그러면 양쪽이 다 BOM 으로 시작할 때 결과 파일 한가운데에 BOM 이 하나 더 + /// 들어가고(컴파일러는 그 자리를 이상한 글자로 본다), 양쪽 줄끝이 다르면 한 파일 안에 CRLF 와 LF 가 섞였다(6차 A-5). + /// + /// 이제는 두 벌을 각자 제 인코딩으로 글로 풀어 이어 붙인 뒤, 내 쪽 파일의 형식(인코딩 · BOM · 줄끝)으로 + /// 다시 쓴다. 규칙은 저장과 같은 를 쓴다 - 형식 판단이 두 군데로 갈리면 또 어긋난다. + /// + /// 한쪽이 비어 있으면 다른 쪽을 손대지 않고 그대로 돌려준다(이어 붙일 것이 없다). + /// + public static class BothJoin + { + public static byte[] Join(byte[]? ours, byte[]? theirs) + { + ours ??= Array.Empty(); + theirs ??= Array.Empty(); + + if (ours.Length == 0) return theirs; + if (theirs.Length == 0) return ours; + + var text = Decode(ours).Replace("\r\n", "\n"); + if (!text.EndsWith("\n", StringComparison.Ordinal)) text += "\n"; + text += Decode(theirs).Replace("\r\n", "\n"); + + return TextFile.Encode(text, encodingName: null, newLineName: null, reference: ours); + } + + /// 제 인코딩으로 푼다. 글자로 딸려 온 BOM(U+FEFF)은 뗀다 - 다시 쓸 때 맨 앞에 한 번만 붙는다. + private static string Decode(byte[] bytes) => + TextFile.DetectEncoding(bytes).GetString(bytes).TrimStart('\uFEFF'); + } +} diff --git a/src/MergeHub/Services/GitService.cs b/src/MergeHub/Services/GitService.cs index 11dc782..deb5378 100644 --- a/src/MergeHub/Services/GitService.cs +++ b/src/MergeHub/Services/GitService.cs @@ -880,20 +880,15 @@ namespace MergeHub.Services if (resolution == ConflictResolution.Both) { - // 인코딩 변환 없이 바이트 단위로 이어붙인다 - CP949 등 원본 인코딩이 그대로 보존된다. + // 양쪽 파일을 읽는다. 한쪽이 없으면 빈 것으로 본다. var oursBlob = LookupBlob(conflict.Ours); var theirsBlob = LookupBlob(conflict.Theirs); var ours = oursBlob is null ? Array.Empty() : ReadBlobBytes(oursBlob, normalized); var theirs = theirsBlob is null ? Array.Empty() : ReadBlobBytes(theirsBlob, normalized); - using var output = File.Create(fullPath); - output.Write(ours, 0, ours.Length); - if (ours.Length > 0 && ours[ours.Length - 1] != Lf) - { - var newLine = ContainsCrLf(ours) ? new[] { Cr, Lf } : new[] { Lf }; - output.Write(newLine, 0, newLine.Length); - } - output.Write(theirs, 0, theirs.Length); + // 바이트째 이어 붙이면 결과 한가운데에 BOM 이 하나 더 들어가고 줄끝이 섞였다(6차 A-5). + // 두 벌을 글로 풀어 이어 붙인 뒤 내 쪽 형식(인코딩 · BOM · 줄끝)으로 다시 쓴다. + File.WriteAllBytes(fullPath, BothJoin.Join(ours, theirs)); } else if (chosen is null) { diff --git a/src/MergeHub/ViewModels/GitViewModel.cs b/src/MergeHub/ViewModels/GitViewModel.cs index 602638c..32fd4d2 100644 --- a/src/MergeHub/ViewModels/GitViewModel.cs +++ b/src/MergeHub/ViewModels/GitViewModel.cs @@ -272,7 +272,18 @@ namespace MergeHub.ViewModels private Task AcceptTheirsAsync() => ResolveAsync(ConflictResolution.Theirs); [RelayCommand(CanExecute = nameof(HasConflict))] - private Task AcceptBothAsync() => ResolveAsync(ConflictResolution.Both); + private Task AcceptBothAsync() + { + // 파일 단위 "둘 다" 는 파일 두 벌을 통째로 이어 붙인다. 양쪽이 같은 이름으로 새로 만든 파일이면 + // 같은 형식이 두 번 나와 빌드가 안 된다(5차 C-1 에서 그랬다). 누르기 전에 무엇이 일어나는지 알린다(6차 A-5). + var path = SelectedConflict?.Path ?? string.Empty; + var go = MessageUtil.ShowYesNo( + "파일 두 벌을 이어 붙입니다", + $"{path}\n\n내 것 다음에 상대 것을 한 파일로 이어 붙입니다. 내 파일의 인코딩과 줄끝으로 씁니다.\n\n" + + "양쪽이 같은 이름으로 새로 만든 파일이면 같은 형식이 두 번 나와 빌드가 안 될 수 있습니다.\n" + + "줄마다 골라 둘 다 넣으려면 파일 비교 탭의 3분할에서 하세요.\n\n이어 붙일까요?"); + return go ? ResolveAsync(ConflictResolution.Both) : Task.CompletedTask; + } private Task ResolveAsync(ConflictResolution resolution) => _workspace.RunAndRefreshAsync($"충돌 해결({resolution})", diff --git a/src/MergeHub/Views/GitView.xaml b/src/MergeHub/Views/GitView.xaml index 450f506..6ef0d54 100644 --- a/src/MergeHub/Views/GitView.xaml +++ b/src/MergeHub/Views/GitView.xaml @@ -278,6 +278,7 @@ Background="{DynamicResource Ubisam.Brush.Accent.Fill}" Foreground="{DynamicResource Ubisam.Brush.Accent.OnFill}"/>