Ubisam-MergeTool/doc/history/2026-09-11-5차.md
ksj09406 238910634c 5차 A-2: Git 작업을 시작하는 자리에서 묻는다
커밋·체크아웃 뒤에 저장 안 한 편집이 말없이 사라지던 것을 막았다.

막는 자리를 옮겼다. 목록을 다시 세우는 자리(RefreshCommits →
LoadCommitAsync)가 아니라 Git 작업이 시작되는 문(GitWorkspace.
RunAndRefreshAsync)에서 묻는다. 그 순간에는 아직 아무것도 바뀌지
않았으므로 취소가 뜻을 가진다.

작업을 가려서 묻는 쪽은 접었다. fetch·태그·스테이징처럼 파일을 안
건드리는 열두 가지는 안 물으려 했는데, 해 보니 그런 작업 뒤에도
편집이 사라졌다. 파일이 그대로여도 저장소를 다시 읽으면서 본문을
새로 그리기 때문이다. 다시 그리지 않게 막는 쪽도 해 봤지만 고른
파일까지 바뀌는 경우가 있어 조건을 세울 수 없었다. 잃지 않는 쪽으로
갔다 - 확인 창은 고치던 것이 있을 때만 뜨고, 틀려도 한 번 더 묻는
것으로 끝난다. 알아낸 열두 이름은 KnownSafeCandidates 에 남겼다.

덤으로 4차 A-1 이 새던 자리 셋을 막았다. 목록을 다시 세우면 화면이
값을 되밀어 넣는데, 그것을 사람이 고른 것으로 보고 확인 창을 띄우고
있었다. OnSelectedNodeChanged(null·같은 커밋), OnRemoteBranchChanged
(_syncingBranches), Files.Clear()(_suspendGuard 안으로).

화면 시험 10건 통과(데모 저장소). GitOpTest 36건. 전체 623건 통과
(하네스 19벌). 빌드 경고 0 · 오류 0.

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

156 lines
8.2 KiB
Markdown

# 5차 — 실제 소스에 맞추기 (2026-09-11 ~)
지시서: [작업지시/5차.md](../작업지시/5차.md)
---
## A-1. CP949 파일 67개를 잘못 읽으면 한글이 깨진다 — **완료** (2026-09-11)
지시서가 **먼저 확인만 하고 틀리면 고치라**고 했다. 확인부터 했다.
### 잰 것 — 실제 저장소 2,091개 전부
`TextFile.Read` 로 실제 파일을 한 개씩 읽어 보는 도구를 만들어 돌렸다(원본은 읽기만 하고,
저장 왕복은 사본에만 했다).
| 실제 인코딩 | 개수 |
|---|---|
| UTF-8 (BOM) | 1,441 |
| UTF-8 (BOM 없음) | 353 |
| **CP949** | **67** |
| ASCII 만 | 230 |
| MergeHub 로 읽었을 때 | 결과 |
|---|---|
| 한글이 깨진 파일 | **0개** |
| 사본에 그대로 저장했을 때 바이트가 달라진 파일 (한글 파일 1,861개) | **0개** |
**판정 기준을 이미 만족하고 있었다.** 판별 규칙(BOM → 엄격한 UTF-8 → 실패하면 시스템 ANSI)이
실제 소스에서 맞게 돈다. 고칠 것이 없었다.
### 그래도 고친 것 둘
**① 판별 코드가 두 군데에 복사돼 있었다.**
`TextFile.Detect` 와 `GitService.DetectEncoding` 이 글자 하나까지 같은 코드였다.
`TextFile` 주석에는 "판별 규칙은 GitService 와 같다 - 같은 파일을 어느 경로로 열든 결과가 같아야 한다"
고 적혀 있었는데, **지키는 방법이 복사뿐이었다.** 한쪽만 고치면 조용히 어긋나고,
그러면 <b>같은 파일이 워킹트리에서는 멀쩡한데 커밋에서 열면 깨져</b> 보인다.
어느 쪽이 맞는지 알 방법도 없다. `TextFile.DetectEncoding` 하나로 모으고 `GitService` 는 그것을 부른다.
**② 같은 인코딩을 두 이름으로 부르고 있었다.**
상태바는 `KS_C_5601-1987`(코드페이지 949 의 공식 이름), 저장 형식 목록은 `CP949`.
실제 CP949 파일을 열어 보다가 눈에 띄었다 - "지금 이 파일이 그 CP949 라는 건가" 를 되묻게 된다.
`FriendlyName` 으로 949 는 언제나 `CP949` 라고 적는다. 두 자리 모두 고쳤다.
### 확인한 것
| 확인 | 결과 |
|---|---|
| 실제 CP949 파일(`AXA.cs` 사본)을 열었을 때 | `//========== 보드 및 모듈 정보 확인 함수 ...` 그대로 읽힌다 |
| 깨진 글자(U+FFFD) | 없다 |
| 상태바 인코딩 | `CP949` (전에는 `KS_C_5601-1987`) |
| 고친 뒤 실제 저장소 2,091개 재확인 | 깨짐 0개 · 왕복 어긋남 0개 (고치기 전과 같다) |
**검사를 새로 만들었다**(`EncTest` 16건). 실제 저장소가 없어도 도는 것이 요점이다 -
BOM · BOM 없는 UTF-8 · CP949 · ASCII 를 만들어 놓고 읽기 · 왕복 저장 · 형식 바꿔 저장까지 본다.
규칙을 여기 못박아 두면 다음에 누가 고쳐도 바로 걸린다.
전체 **587건 통과**(하네스 18벌) · 빌드 경고 0 · 오류 0.
**실제 저장소는 건드리지 않았다** - 작업 전후 모두 변경 1건(7월부터 있던 것)으로 같다.
---
## A-2. 커밋·체크아웃 뒤에 저장 안 한 편집이 조용히 사라진다 (2026-09-11, 완료)
### 어디서 사라지고 있었나
4차 A-1 에서 편집이 말없이 사라지던 길 여덟 군데를 막았지만, **Git 탭은 그대로 두었다.**
`RefreshCommits` → `LoadCommitAsync` 는 프로그램이 목록을 다시 세우는 한가운데라
거기서 확인 창을 띄우면 무엇에 답하는 창인지 알 수 없고, 취소해도 목록은 이미 새로 세워진 뒤였다.
그래서 **막는 자리를 옮겼다.** 목록을 다시 세우는 자리가 아니라 **Git 작업을 시작하는 자리**에서 묻는다.
그 순간에는 아직 아무것도 바뀌지 않았으므로 취소가 뜻을 가진다.
`GitWorkspace.RunAndRefreshAsync` 는 Git 탭의 작업 서른 가지가 모두 지나가는 문이다.
여기에 `ConfirmDiscardEdits` 갈고리를 달고, 파일 비교 화면이 그 갈고리를 채운다.
```csharp
public async Task<bool> RunAndRefreshAsync(string operation, Action action)
{
if (GitOperationKinds.TouchesFiles(operation) && ConfirmDiscardEdits is { } ask && !ask(operation)) return false;
var ok = await RunAsync(operation, action);
if (ok) await RefreshAsync();
return ok;
}
```
### 처음 고른 답이 틀렸다 — 가려서 묻기
처음에는 **작업을 가려서** 물으려 했다. fetch · 태그 생성 · 전체 스테이징처럼
**파일 내용을 안 건드리는 열두 가지**는 묻지 않는 쪽이 낫다고 봤다.
쓸데없이 뜨는 창만큼 사람을 지치게 하는 것이 없기 때문이다.
첫 시도는 호출 자리마다 `touchesFiles: false` 를 붙이는 것이었는데,
줄을 훑어 고치는 스크립트가 **엉뚱한 닫는 괄호**(예: `throw new InvalidOperationException(...)`)에 붙였다.
되돌리고, 판단을 **이름표 하나**(`GitOperationKinds`)로 모았다. 부르는 자리는 손대지 않는다.
그런데 **화면에서 해 보니 안 묻고 지나간 뒤에도 편집이 사라졌다.**
파일은 그대로인데도 저장소 상태를 다시 읽으면서 본문을 새로 그리기 때문이다.
다시 그리지 않게 막는 쪽으로도 해 봤다.
```csharp
if (HasChanges && !threeWay && string.Equals(localText, _originalLocalText, StringComparison.Ordinal))
return;
```
**이것도 안 들었다.** 다시 읽는 김에 고른 파일까지 바뀌는 경우가 있어
"안 바뀌었다" 를 정확히 가릴 조건을 세울 수가 없었다. 이 코드는 지웠다.
### 골라잡은 답 — 전부 묻는다
**잃지 않는 쪽으로 갔다.** `TouchesFiles` 는 이제 언제나 `true` 다.
- 확인 창은 **고치던 것이 있을 때만** 뜬다. 평소에는 한 번도 보이지 않는다.
- 틀리더라도 **한 번 더 묻는 것**으로 끝나지, 작업을 잃지는 않는다.
가려서 묻다 틀리면 사람이 고친 것을 잃는다. 두 실수의 값이 다르다.
다만 **무엇이 안전한지 알아낸 것은 버리지 않았다.** 열두 가지 이름을
`GitOperationKinds.KnownSafeCandidates` 에 남겨 두고, 왜 지금 쓰지 않는지를 클래스 주석에 적었다.
"다시 그려도 편집이 살아남는" 구조가 되면 거기서부터 다시 시작한다.
### 덤으로 잡은 것 — 4차 A-1 이 새던 자리 셋
전부 묻게 만들고 나니 **Git 탭을 그냥 열기만 해도 확인 창이 떴다.**
사람이 한 일이 아닌데 4차 A-1 의 확인 창이 불려 나오고 있었다. 셋을 잡았다.
| 새던 자리 | 왜 떴나 | 어떻게 막았나 |
|---|---|---|
| `OnSelectedNodeChanged` | 커밋 목록을 다시 세우면 잠깐 `null` 이 밀려 들어온다 | `value is not null && !sameCommit` 일 때만 묻는다 |
| `OnRemoteBranchChanged` | `RefreshRemoteBranches` 가 목록을 갈아 끼우며 값을 바꾼다 | `_syncingBranches` 동안은 묻지 않는다 |
| `OnSelectedFileChanged` | `Files.Clear()` 가 가드 밖에 있었다 | `_suspendGuard` 안으로 옮겼다 |
**목록을 다시 세우면 화면이 값을 되밀어 넣는다.** 그것은 사람이 고른 것이 아니다.
4차 A-1 을 넣을 때 놓친 것인데, A-2 를 화면에서 시험하다 드러났다.
### 확인한 것 (실제 화면, 데모 저장소)
| 확인 | 결과 |
|---|---|
| LOCAL 을 고친 줄이 들어간다 | OK |
| 전체 스테이징이 묻는다 | OK |
| 취소하면 스테이징도 안 한다 (`git diff --cached` 비었다) | OK |
| 취소하면 고친 줄이 그대로다 | OK |
| 체크아웃이 묻는다 | OK |
| 취소하면 브랜치가 그대로다 (`master`) | OK |
| 체크아웃을 취소해도 고친 줄이 그대로다 | OK |
| "저장하고 계속" 을 고르면 고친 내용이 파일에 남는다 | OK |
| 고친 것이 없으면 안 묻는다 | OK |
**열 가지 전부 통과.** 화면 시험은 UI Automation 으로 몰았다(`a2screen.ps1`).
확인 창이 **따로 뜨는 창이 아니라 본 창 안에 덮어 그리는 판**이라, 바깥 창 목록을 뒤지면 못 찾는다.
처음 두 번은 이것 때문에 "안 묻는다" 고 잘못 읽었다 — **시험 도구가 틀려서 제품을 의심했다.**
검사를 `GitOpTest` 36건으로 고쳤다. 전체 **623건 통과**(하네스 19벌) · 빌드 경고 0 · 오류 0.
**실제 저장소는 건드리지 않았다** — 데모 저장소(`mergehub-demo`) 사본으로만 했다.