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>
This commit is contained in:
김승준 2026-09-14 11:47:45 +09:00
parent 9b597fc50f
commit c76e3d7aee
4 changed files with 90 additions and 10 deletions

View File

@ -371,3 +371,58 @@ A-2 · A-4 에서는 저장한 뒤에만 기다렸다 다시 세우게 했고,
화면 확인 **17건 통과**(Enter 1건은 처음에 목록 아래쪽 행을 골랐다가 자동 조작이 못 찾아 실패 - 가상화된 목록이라 화면 밖 행은 화면 확인 **17건 통과**(Enter 1건은 처음에 목록 아래쪽 행을 골랐다가 자동 조작이 못 찾아 실패 - 가상화된 목록이라 화면 밖 행은
안 만들어진다. 위쪽 행으로 다시 해 통과). 이번에 고친 파일은 검사 하네스가 쓰지 않아 앞의 785건이 그대로 유효하다. 안 만들어진다. 위쪽 행으로 다시 해 통과). 이번에 고친 파일은 검사 하네스가 쓰지 않아 앞의 785건이 그대로 유효하다.
빌드 경고 0 · 오류 0. 빌드 경고 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.

View File

@ -8,6 +8,7 @@
| 날짜 | 항목 | 커밋 | | 날짜 | 항목 | 커밋 |
|---|---|---| |---|---|---|
| 2026-09-14 | **6차 B-2** — 파일을 바꿀 때 확인 창이 고친 파일 이름을 대고, [저장하고 계속] 이 엉뚱한 파일(새로 고른 파일)에 쓰던 것을 막음. [취소] 뒤 콤보가 제자리로. 저장소를 먼저 열면 목록이 0개이던 B-1 빈틈도 | 아래 |
| 2026-09-14 | **6차 B-1** — 새로고침 뒤 파일 목록을 게이트가 풀릴 때까지 기다렸다 세움(병합 뒤 304개 → 23개). 충돌 목록 두 번 누르기·Enter·[3분할로 열기] 로 파일 비교 탭 네 칸에서 연다. 없는 '홈 탭' 글을 고침 | 아래 | | 2026-09-14 | **6차 B-1** — 새로고침 뒤 파일 목록을 게이트가 풀릴 때까지 기다렸다 세움(병합 뒤 304개 → 23개). 충돌 목록 두 번 누르기·Enter·[3분할로 열기] 로 파일 비교 탭 네 칸에서 연다. 없는 '홈 탭' 글을 고침 | 아래 |
| 2026-09-14 | **6차 A-5** — 파일 단위 Accept Both 가 두 벌을 글로 풀어 이어 붙이고 내 쪽 형식으로 쓴다(가운데 BOM 사라짐). 누르기 전에 무엇이 일어나는지 묻는다. A 묶음 끝 | 아래 | | 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-4** — 한쪽이 지운 충돌(UD·DU)은 줄 단위 3분할 대신 남은 파일을 두 칸으로 보이고 [파일 남기기] · [파일 지우기] 로 푼다. 거꾸로 세던 범례(Theirs only 89)가 없어짐 | 아래 |

View File

@ -104,7 +104,7 @@
| 상태 | 항목 | | 상태 | 항목 |
|---|---| |---|---|
| **완료** | B-1 충돌 목록에서 3분할 화면까지 가는 길이 끊겨 있다 — 병합 뒤 목록이 바로 서고, 두 번 누르기·Enter·[3분할로 열기] | | **완료** | B-1 충돌 목록에서 3분할 화면까지 가는 길이 끊겨 있다 — 병합 뒤 목록이 바로 서고, 두 번 누르기·Enter·[3분할로 열기] |
| 대기 | B-2 확인 창이 엉뚱한 파일 이름을 댄다 | | **완료** | B-2 확인 창이 엉뚱한 파일 이름을 댄다 — 이름·[저장하고 계속] 모두 실린 파일 기준, [취소] 뒤 콤보도 돌아온다 |
| 대기 | B-3 커밋 메시지가 엉뚱하게 남는다 | | 대기 | B-3 커밋 메시지가 엉뚱하게 남는다 |
| 대기 | B-4 MERGED 칸 줄 번호가 네 자리에서 잘린다 | | 대기 | B-4 MERGED 칸 줄 번호가 네 자리에서 잘린다 |
| 대기 | B-5 머지 주석은 소스에 넣고, 병합 뒤에는 그 줄을 무시한다 | | 대기 | B-5 머지 주석은 소스에 넣고, 병합 뒤에는 그 줄을 무시한다 |

View File

@ -69,6 +69,15 @@ namespace MergeHub.ViewModels
return; return;
} }
// Git 탭에서 저장소를 이미 연 뒤에 이 화면이 처음 만들어진 경우(6차 B-2 를 확인하다 찾은 B-1 의 빈틈).
// 아래 자동 열기는 이미 열린 저장소면 아무것도 안 하고, 새로고침 신호는 이 화면이 생기기 전에 지나갔다.
// 그래서 파일 목록이 끝내 0개로 남았다. 지금 저장소로 목록을 세운다.
if (_workspace.IsRepositoryOpen)
{
OnWorkspaceRefreshed();
return;
}
// 설정에 기본 저장소가 있으면 바로 열어서 첫 화면부터 내용이 보이게 한다. // 설정에 기본 저장소가 있으면 바로 열어서 첫 화면부터 내용이 보이게 한다.
// 저장소가 아니면 설정에 적어 둔 두 파일로 비교 화면을 만든다 - Git 없이도 쓰는 // 저장소가 아니면 설정에 적어 둔 두 파일로 비교 화면을 만든다 - Git 없이도 쓰는
// 도구라는 것을 첫 화면에서 바로 보여 준다. 둘을 동시에 걸면 안 된다. // 도구라는 것을 첫 화면에서 바로 보여 준다. 둘을 동시에 걸면 안 된다.
@ -355,10 +364,16 @@ namespace MergeHub.ViewModels
if (!ConfirmDiscard($"다른 파일을 엽니다: {value}")) if (!ConfirmDiscard($"다른 파일을 엽니다: {value}"))
{ {
// 되돌리기는 <b>콤보의 선택 처리가 끝난 다음 차례</b>에 한다(6차 B-2).
// 이 자리는 콤보가 고르는 도중이라, 여기서 바로 값을 되돌리면 콤보가 그 알림을 흘려 버린다.
// [취소] 를 눌렀는데 콤보는 새 파일(Io/Unit20.cs)을, 화면은 고치던 파일(Io/Unit12.cs)을 보이며 서로 달랐다.
var back = _lastLoadedFile; var back = _lastLoadedFile;
_revertingFile = true; Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() =>
try { SelectedFile = back; } {
finally { _revertingFile = false; } _revertingFile = true;
try { SelectedFile = back; }
finally { _revertingFile = false; }
}), DispatcherPriority.Background);
return; return;
} }
@ -2349,9 +2364,13 @@ namespace MergeHub.ViewModels
if (_suspendGuard) return true; if (_suspendGuard) return true;
if (!HasChanges) return true; if (!HasChanges) return true;
var name = IsPlainMode // <b>고친 파일</b>의 이름을 댄다(6차 B-2). 파일을 바꿀 때는 이 창이 뜨는 순간 SelectedFile 이 이미 새 파일이라,
? (string.IsNullOrWhiteSpace(RightFile) ? "지금 파일" : Path.GetFileName(RightFile)) // 예전에는 그 새 파일 이름을 댔다 - Motion/Unit0.cs 를 고쳤는데 "ToBeDeleted.cs 에 저장하지 않은 편집" 이라고 했다.
: (string.IsNullOrWhiteSpace(SelectedFile) ? "지금 파일" : Path.GetFileName(SelectedFile)); // Git 모드는 저장소 안 경로로 적는다. 다른 폴더에 같은 이름 파일이 흔하다.
var editing = IsPlainMode ? RightFile : (_lastLoadedFile ?? SelectedFile);
var name = string.IsNullOrWhiteSpace(editing)
? "지금 파일"
: IsPlainMode ? Path.GetFileName(editing) : editing;
var choice = MessageUtil.ShowChoice( var choice = MessageUtil.ShowChoice(
"저장하지 않은 편집", "저장하지 않은 편집",
@ -2377,12 +2396,17 @@ namespace MergeHub.ViewModels
/// </summary> /// </summary>
private bool SaveWorkingCopy() private bool SaveWorkingCopy()
{ {
// <b>지금 화면에 실려 있는 파일</b>에 쓴다(6차 B-2). 파일을 바꾸다 이 확인 창이 떴을 때는 SelectedFile 이
// 이미 새 파일이다. 예전에는 그 새 파일에 썼다 - Io/Unit20.cs 를 고치고 Io/Unit12.cs 로 옮기며
// "저장하고 계속" 을 누르자 <b>Unit20 의 글이 Unit12 에 써지고 Unit12 가 해결된 것으로 스테이징됐다.</b>
var loaded = IsPlainMode ? null : (_lastLoadedFile ?? SelectedFile);
var target = IsPlainMode var target = IsPlainMode
? RightFile ? RightFile
: string.IsNullOrEmpty(SelectedFile) : string.IsNullOrEmpty(loaded)
? string.Empty ? string.Empty
: Path.Combine(_workspace.RepositoryPath, : Path.Combine(_workspace.RepositoryPath,
SelectedFile!.Replace('/', Path.DirectorySeparatorChar)); loaded!.Replace('/', Path.DirectorySeparatorChar));
if (string.IsNullOrWhiteSpace(target)) if (string.IsNullOrWhiteSpace(target))
{ {
@ -2396,7 +2420,7 @@ namespace MergeHub.ViewModels
// 병합 중 충돌 파일을 워킹트리에만 쓰면 git 은 여전히 충돌로 본다. 충돌 표시는 사라졌는데 // 병합 중 충돌 파일을 워킹트리에만 쓰면 git 은 여전히 충돌로 본다. 충돌 표시는 사라졌는데
// 인덱스는 풀리지 않은 어정쩡한 상태가 된다. [해결로 저장] 과 같은 길로 보낸다(6차 A-2). // 인덱스는 풀리지 않은 어정쩡한 상태가 된다. [해결로 저장] 과 같은 길로 보낸다(6차 A-2).
var conflictPath = IsPlainMode ? null : SelectedFile; var conflictPath = loaded;
if (conflictPath is not null && _workspace.IsMergeInProgress && if (conflictPath is not null && _workspace.IsMergeInProgress &&
_workspace.Git.GetConflicts().Any(c => c.Path == conflictPath)) _workspace.Git.GetConflicts().Any(c => c.Path == conflictPath))
{ {