6차 A-2: 병합 중에는 [해결로 저장] 을 따로 둔다
Save & Commit 이 쓰기, 스테이징, 커밋을 한 덩어리로 돌려서 남은 충돌은 마지막 커밋 단계에서야 막혔다. 화면은 실패라는데 파일은 이미 써지고 스테이징됐고(충돌 22 -> 21), 편집 상태도 안 치워서 다른 파일을 고를 때마다 확인 창이 다시 떴다. - MergeSavePlan: 지금 커밋해도 되는지 판단만 따로 뺐다 - Save & Commit: 병합 중 다른 충돌이 남으면 아무것도 쓰기 전에 멈춘다 - [해결로 저장]: 병합 중에만 보인다. 이 파일만 쓰고 스테이징한다 - Ctrl+S: 병합 중이면 해결로 저장 - 확인 창의 "저장하고 계속": 병합 중 충돌 파일이면 충돌까지 푼다 화면에서 해 보다 둘이 더 나왔다. - 게이트에 막혀 건너뛴 목록 불러오기가 빈 결과로 목록을 비웠다. 건너뛰면 그대로 두고, 해결로 저장 뒤에는 게이트가 풀릴 때까지 기다렸다 다시 세운다(WhenIdleAsync). 남은 충돌 첫 파일이 열린다. - 충돌을 전부 Take Mine 으로 정하면 글이 안 바뀌어 저장할 수 없었다. 3분할에서 충돌을 다 정했으면 고친 글이 없어도 저장할 수 있다. 화면 확인 25건 통과. MergeSaveTest 18건 추가, 21벌 716건 통과. 경고 0, 오류 0. 줄끝(A-1)은 아직 그대로다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
99a5d10775
commit
0485c60745
82
doc/history/2026-09-14-6차.md
Normal file
82
doc/history/2026-09-14-6차.md
Normal file
@ -0,0 +1,82 @@
|
||||
# 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 에서 본다.
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
| 날짜 | 항목 | 커밋 |
|
||||
|---|---|---|
|
||||
| 2026-09-14 | **6차 A-2** — 병합 중 [해결로 저장] 을 따로 둠(쓰고 스테이징만). Save & Commit 은 남은 충돌이 있으면 쓰기 전에 막는다. 건너뛴 목록 불러오기가 목록을 비우던 것도 막음 | 아래 |
|
||||
| 2026-09-14 | **지시서 6차 B-5 정함** — 머지 주석은 소스에 넣고, 비교·병합에서는 그 줄을 무시한다. git 은 모르므로 주석 차이뿐인 충돌은 MergeHub 가 푼다 | 아래 |
|
||||
| 2026-09-14 | **지시서 6차** — 5차 C-1 에서 걸린 23가지를 A(망가뜨리는 것 5) · B(헷갈리는 것 8) · C(사소 1) · D(다시 끝까지)로 묶음 | 아래 |
|
||||
| 2026-09-14 | **5차 C-1** — 만든 저장소(303개 · 충돌 22)로 병합을 MergeHub 만으로 커밋까지 해 봄. 고치지 않고 걸린 것 23가지를 적음. 가장 큰 것은 3분할 저장이 CRLF 를 LF 로 바꾸는 것 | 아래 |
|
||||
@ -47,6 +48,7 @@
|
||||
|
||||
| 날짜 | 문서 | 내용 |
|
||||
|---|---|---|
|
||||
| 2026-09-14 | [2026-09-14-6차.md](2026-09-14-6차.md) | 6차 지시서 진행 중 |
|
||||
| 2026-09-11 | [2026-09-11-5차.md](2026-09-11-5차.md) | 5차 지시서 진행 중 |
|
||||
| 2026-09-10 | [2026-09-10-4차.md](2026-09-10-4차.md) | 4차 지시서 열세 항목. 전부 끝 |
|
||||
| 2026-09-09 | [2026-09-09-3차.md](2026-09-09-3차.md) | 3차 지시서 A~E 스물세 항목. 항목마다 원인과 판단을 적었다 |
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
|
||||
## 6차 — 병합을 끝까지 믿고 쓸 수 있게 (2026-09-14 ~)
|
||||
|
||||
진행 기록: [history/2026-09-14-6차.md](history/2026-09-14-6차.md)
|
||||
지시서: [작업지시/6차.md](작업지시/6차.md). 5차 C-1 에서 만든 저장소로 병합을 끝까지 해 보고 걸린 23가지다.
|
||||
확인은 같은 저장소(`C1Repo`)로 한다. 실제 소스는 열지 않는다.
|
||||
|
||||
@ -93,7 +94,7 @@
|
||||
| 상태 | 항목 |
|
||||
|---|---|
|
||||
| 대기 | A-1 3분할로 푼 파일을 저장하면 CRLF 가 LF 로 바뀐다 |
|
||||
| 대기 | A-2 Save & Commit 이 "실패" 라는데 파일은 이미 써지고 스테이징됐다 |
|
||||
| **완료** | A-2 Save & Commit 이 "실패" 라는데 파일은 이미 써지고 스테이징됐다 — [해결로 저장] 을 따로 둠 |
|
||||
| 대기 | A-3 충돌 파일을 고르면 상대(Theirs) 칸이 안 나온다 |
|
||||
| 대기 | A-4 상대가 지운 파일을 3분할이 다룰 줄 모른다 |
|
||||
| 대기 | A-5 양쪽 새 파일에 Accept Both 를 누르면 빌드가 깨진다 |
|
||||
|
||||
@ -186,6 +186,19 @@ namespace MergeHub.Services
|
||||
|
||||
// ================= 실행 =================
|
||||
|
||||
/// <summary>
|
||||
/// 지금 도는 Git 작업이 끝날 때까지 기다린다. 아무 일도 하지 않는다.
|
||||
///
|
||||
/// <see cref="RunAsync"/> 는 게이트가 막혀 있으면 기다리지 않고 건너뛴다(겹쳐 돌면 안 되니까).
|
||||
/// 그런데 새로고침 뒤처럼 "이건 꼭 해야 하는" 일이 그 순간 걸리면 조용히 빠진다 -
|
||||
/// 병합 중 [해결로 저장] 뒤 파일 목록이 안 바뀐 것이 그랬다(6차 A-2). 그럴 때 이걸로 기다렸다 다시 한다.
|
||||
/// </summary>
|
||||
public async Task WhenIdleAsync()
|
||||
{
|
||||
await _gate.WaitAsync();
|
||||
_gate.Release();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Git 호출 하나를 백그라운드에서 직렬 실행한다.
|
||||
/// 실패하면 로그 + Error 토스트를 띄우고 false 를 돌려준다.
|
||||
|
||||
60
src/MergeHub/Services/MergeSavePlan.cs
Normal file
60
src/MergeHub/Services/MergeSavePlan.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MergeHub.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 병합 중에 "저장" 을 누르면 무엇을 해도 되는지 가른다.
|
||||
///
|
||||
/// <b>따로 뺀 까닭</b> - 예전에는 Save & Commit 이 파일을 쓰고 스테이징한 <b>뒤에</b> 커밋에서 막혔다.
|
||||
/// 화면은 "실패" 라고 하는데 충돌은 이미 하나 줄어 있었다(6차 A-2). 되돌릴 수 없는 일을 하기 전에
|
||||
/// 먼저 물어봐야 한다. 그 판단을 화면 코드 한가운데 두면 또 순서가 뒤바뀌기 쉬워서 여기로 모았다.
|
||||
/// </summary>
|
||||
public static class MergeSavePlan
|
||||
{
|
||||
/// <summary>
|
||||
/// 지금 커밋해도 되는지. 된다면 null, 안 되면 사람에게 보여 줄 까닭.
|
||||
///
|
||||
/// 지금 저장하려는 파일은 셈에서 뺀다 - 이 파일은 저장하면서 풀리기 때문이다.
|
||||
/// 그 파일 말고 <b>하나라도</b> 남아 있으면 커밋할 수 없다.
|
||||
/// </summary>
|
||||
/// <param name="conflictPaths">git 이 아직 충돌로 보는 파일들. 경로는 저장소 기준 '/' 구분.</param>
|
||||
/// <param name="savingPath">지금 저장하려는 파일. 없으면 null.</param>
|
||||
public static string? CommitBlocker(IEnumerable<string> conflictPaths, string? savingPath)
|
||||
{
|
||||
if (conflictPaths is null) return null;
|
||||
|
||||
var saving = Normalize(savingPath);
|
||||
var others = conflictPaths
|
||||
.Select(Normalize)
|
||||
.Where(p => p.Length > 0 && !string.Equals(p, saving, StringComparison.Ordinal))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
if (others.Count == 0) return null;
|
||||
|
||||
return $"아직 풀지 않은 충돌이 {others.Count}건 있어 커밋하지 않았습니다. "
|
||||
+ "파일은 바꾸지 않았습니다. [해결로 저장] 으로 이 파일만 저장해 두고, 나머지를 다 푼 뒤 커밋하세요.";
|
||||
}
|
||||
|
||||
/// <summary>남은 충돌 수. 지금 막 저장한 파일은 뺀다. 저장 뒤 알림에 쓴다.</summary>
|
||||
public static int RemainingAfterSave(IEnumerable<string> conflictPaths, string? savedPath)
|
||||
{
|
||||
if (conflictPaths is null) return 0;
|
||||
var saved = Normalize(savedPath);
|
||||
return conflictPaths
|
||||
.Select(Normalize)
|
||||
.Where(p => p.Length > 0 && !string.Equals(p, saved, StringComparison.Ordinal))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Count();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// git 은 경로를 '/' 로, 대소문자를 그대로 본다. 윈도우 경로 구분자로 들어와도 같게 맞춘다.
|
||||
/// 대소문자는 맞추지 않는다 - git 이 서로 다른 파일로 볼 수 있다.
|
||||
/// </summary>
|
||||
private static string Normalize(string? path) =>
|
||||
string.IsNullOrWhiteSpace(path) ? string.Empty : path!.Trim().Replace('\\', '/');
|
||||
}
|
||||
}
|
||||
@ -596,9 +596,19 @@ namespace MergeHub.ViewModels
|
||||
var encoding = SaveEncoding;
|
||||
var newLine = SaveNewLine;
|
||||
string? sha = null;
|
||||
string? blocked = null;
|
||||
|
||||
var ok = await _workspace.RunAsync("Save & Commit", () =>
|
||||
{
|
||||
// 병합 중에 다른 충돌이 남아 있으면 <b>아무것도 쓰기 전에</b> 멈춘다(6차 A-2).
|
||||
// 예전에는 파일을 쓰고 스테이징한 뒤에 커밋에서 막혔다. 화면은 "실패" 라는데
|
||||
// 충돌은 이미 하나 줄어 있었고, 편집기는 여전히 저장 안 한 편집으로 알았다.
|
||||
if (_workspace.Git.IsMergeInProgress)
|
||||
{
|
||||
blocked = MergeSavePlan.CommitBlocker(_workspace.Git.GetConflicts().Select(c => c.Path), path);
|
||||
if (blocked is not null) return;
|
||||
}
|
||||
|
||||
var isConflict = _workspace.Git.GetConflicts().Any(c => c.Path == path);
|
||||
if (isConflict)
|
||||
{
|
||||
@ -622,8 +632,15 @@ namespace MergeHub.ViewModels
|
||||
|
||||
if (!ok) return;
|
||||
|
||||
if (blocked is not null)
|
||||
{
|
||||
_workspace.OnUi(() => MessageUtil.ShowWarningToast(blocked));
|
||||
return;
|
||||
}
|
||||
|
||||
_workspace.OnUi(() =>
|
||||
{
|
||||
_originalLocalText = content;
|
||||
_undo.Clear();
|
||||
_redo.Clear();
|
||||
HasChanges = false;
|
||||
@ -635,6 +652,88 @@ namespace MergeHub.ViewModels
|
||||
await _workspace.RefreshAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지금 열린 파일이 <b>git 충돌로</b> 열렸는지. 해결로 저장을 켤지 가를 때 쓴다.
|
||||
/// </summary>
|
||||
private bool _loadedAsConflict;
|
||||
|
||||
/// <summary>
|
||||
/// 고친 글이 있으면 저장할 수 있다. 고친 글이 없어도 <b>충돌로 열린 파일의 충돌을 다 정했으면</b> 저장할 수 있다.
|
||||
///
|
||||
/// 뒤쪽이 필요한 까닭 - Take Mine 은 글을 바꾸지 않는다(이미 내 것이다). 그래서 충돌을 전부 내 것으로
|
||||
/// 정하면 "고친 것 없음" 이 되어 저장 단추가 꺼지고, git 은 계속 충돌로 본다. 풀 길이 없었다(6차 A-2).
|
||||
/// 3분할이 아닐 때는 충돌 수를 셀 수 없으므로 켜지 않는다 - 정하지도 않은 파일을 내 것으로 덮으면 안 된다.
|
||||
/// </summary>
|
||||
private bool CanSaveResolved() =>
|
||||
IsGitMode && _workspace.IsMergeInProgress && !string.IsNullOrEmpty(SelectedFile) &&
|
||||
(HasChanges || (_loadedAsConflict && IsThreeWay && UnresolvedConflictCount == 0));
|
||||
|
||||
/// <summary>
|
||||
/// 병합 중에 <b>이 파일만</b> 풀어서 저장한다. 쓰고 스테이징까지 하고 커밋은 하지 않는다(6차 A-2).
|
||||
///
|
||||
/// 예전에는 Git 모드의 저장이 Save & Commit 하나뿐이었다. 충돌이 스물두 개면 스물두 개를
|
||||
/// 한 번에 다 풀어야만 저장할 수 있었고, 도중에 누르면 반쯤 해 놓고 "실패" 라고 했다.
|
||||
/// 커밋은 충돌을 다 푼 뒤에 Git 탭의 [병합 커밋] 이나 Save & Commit 으로 따로 한다.
|
||||
/// </summary>
|
||||
[RelayCommand(CanExecute = nameof(CanSaveResolved))]
|
||||
private async Task SaveResolvedAsync()
|
||||
{
|
||||
var path = SelectedFile;
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
|
||||
var content = BuildLocalText();
|
||||
var encoding = SaveEncoding;
|
||||
var newLine = SaveNewLine;
|
||||
var remaining = 0;
|
||||
|
||||
var ok = await _workspace.RunAsync("해결로 저장", () =>
|
||||
{
|
||||
var isConflict = _workspace.Git.GetConflicts().Any(c => c.Path == path);
|
||||
if (isConflict)
|
||||
{
|
||||
_workspace.Git.ResolveConflictWithContent(path!, content);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 충돌은 아니지만 병합 중에 손본 파일 - 저장 형식을 따라 쓰고 스테이징한다.
|
||||
var full = Path.Combine(_workspace.RepositoryPath,
|
||||
path!.Replace('/', Path.DirectorySeparatorChar));
|
||||
TextFile.Write(full, content, encoding, newLine);
|
||||
_workspace.Git.Stage(path!);
|
||||
}
|
||||
|
||||
remaining = MergeSavePlan.RemainingAfterSave(_workspace.Git.GetConflicts().Select(c => c.Path), path);
|
||||
});
|
||||
|
||||
if (!ok) return;
|
||||
|
||||
_workspace.OnUi(() =>
|
||||
{
|
||||
// 저장한 내용이 이제 "원본" 이다. 이걸 안 하면 저장한 뒤에도 편집이 남은 것으로 보여
|
||||
// 다른 파일을 고를 때마다 확인 창이 다시 뜬다(5차 C-1 에서 겪었다).
|
||||
_originalLocalText = content;
|
||||
_undo.Clear();
|
||||
_redo.Clear();
|
||||
HasChanges = false;
|
||||
RaiseCanExecuteChanged();
|
||||
|
||||
MessageUtil.ShowSuccessToast(remaining == 0
|
||||
? $"해결로 저장 · {Path.GetFileName(path)} · 남은 충돌 없음 - 이제 커밋할 수 있습니다"
|
||||
: $"해결로 저장 · {Path.GetFileName(path)} · 남은 충돌 {remaining}건");
|
||||
});
|
||||
|
||||
await _workspace.RefreshAsync();
|
||||
|
||||
// 새로고침이 알아서 목록을 다시 세우지만, 그 신호가 게이트를 쥔 채로 오면 건너뛴다.
|
||||
// 새로고침 직후에도 다른 화면(Git 탭)이 게이트를 잠깐 쥐는 일이 있어, 곧바로 불러도 또 건너뛰었다.
|
||||
// 게이트가 풀릴 때까지 기다렸다가 세운다. 방금 푼 파일은 충돌 목록에서 빠지므로 남은 충돌의 첫 파일이 열린다.
|
||||
for (var attempt = 0; attempt < 5; attempt++)
|
||||
{
|
||||
await _workspace.WhenIdleAsync();
|
||||
if (await LoadCommitAsync()) break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>CommunityToolkit 의 커맨드는 CommandManager 를 구독하지 않아 직접 알려야 한다.</summary>
|
||||
private void RaiseCanExecuteChanged()
|
||||
{
|
||||
@ -657,6 +756,7 @@ namespace MergeHub.ViewModels
|
||||
RevertAllCommand.NotifyCanExecuteChanged();
|
||||
ReloadCommand.NotifyCanExecuteChanged();
|
||||
SaveAndCommitCommand.NotifyCanExecuteChanged();
|
||||
SaveResolvedCommand.NotifyCanExecuteChanged();
|
||||
SavePlainCommand.NotifyCanExecuteChanged();
|
||||
ExportReportCommand.NotifyCanExecuteChanged();
|
||||
|
||||
@ -689,16 +789,24 @@ namespace MergeHub.ViewModels
|
||||
}
|
||||
|
||||
/// <summary>선택 커밋이 담고 있는 파일 목록을 채우고, 가능하면 이전 선택을 유지한다.</summary>
|
||||
private async Task LoadCommitAsync()
|
||||
/// <returns>
|
||||
/// 목록을 다시 세웠거나 세울 것이 없으면 true. <b>게이트에 막혀 건너뛰었으면 false</b> -
|
||||
/// 꼭 다시 세워야 하는 쪽(해결로 저장)이 기다렸다가 한 번 더 부를 수 있게 한다.
|
||||
/// </returns>
|
||||
private async Task<bool> LoadCommitAsync()
|
||||
{
|
||||
var commit = SelectedCommit;
|
||||
if (commit is null || !_workspace.IsRepositoryOpen) return;
|
||||
if (commit is null || !_workspace.IsRepositoryOpen) return true;
|
||||
|
||||
var previous = SelectedFile;
|
||||
IReadOnlyList<string> files = Array.Empty<string>();
|
||||
var changed = new List<string>();
|
||||
|
||||
await _workspace.RunAsync("파일 목록", () =>
|
||||
// 다른 작업이 게이트를 쥐고 있으면 RunAsync 는 아무것도 안 하고 false 를 준다.
|
||||
// 그대로 내려가면 <b>빈 목록으로 파일 목록을 갈아엎는다</b> - 병합 중에 [해결로 저장] 을 누르자
|
||||
// 파일 콤보가 0개가 되고 화면이 반쯤 비었다(6차 A-2, 로그 "[파일 목록] 건너뜀").
|
||||
// 본문 로드는 이미 이렇게 막고 있었는데 목록만 빠져 있었다. 건너뛰면 지금 목록을 그대로 둔다.
|
||||
var listed = await _workspace.RunAsync("파일 목록", () =>
|
||||
{
|
||||
// 충돌 중이면 충돌 파일만 보여준다 - 지금 해결해야 할 대상이다.
|
||||
var conflicts = _workspace.Git.GetConflicts().Select(c => c.Path).ToList();
|
||||
@ -717,6 +825,8 @@ namespace MergeHub.ViewModels
|
||||
.Select(f => f.Path));
|
||||
});
|
||||
|
||||
if (!listed) return false;
|
||||
|
||||
_workspace.OnUi(() =>
|
||||
{
|
||||
// 목록을 프로그램이 다시 세우는 중이다. 사용자가 고른 것이 아니므로 묻지 않는다 -
|
||||
@ -745,6 +855,8 @@ namespace MergeHub.ViewModels
|
||||
_suspendGuard = false;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>선택 파일의 BASE(선택 커밋 시점)와 LOCAL(현재 워킹트리)을 줄 단위로 맞춰 싣는다.</summary>
|
||||
@ -846,6 +958,7 @@ namespace MergeHub.ViewModels
|
||||
_baseTextSnapshot = baseText;
|
||||
_remoteTextSnapshot = threeWay ? remoteText : string.Empty;
|
||||
_mineTextSnapshot = threeWay ? localText : string.Empty;
|
||||
_loadedAsConflict = isConflict;
|
||||
_appliedBaseNumbers.Clear(); // 파일이 바뀌면 앞 파일의 결정은 뜻이 없다
|
||||
_appliedMineNumbers.Clear();
|
||||
_originalLocalText = BuildLocalText();
|
||||
@ -889,6 +1002,7 @@ namespace MergeHub.ViewModels
|
||||
DiffSummary = "-";
|
||||
EditSummary = "-";
|
||||
HasChanges = false;
|
||||
_loadedAsConflict = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -2079,7 +2193,19 @@ namespace MergeHub.ViewModels
|
||||
try
|
||||
{
|
||||
var content = BuildLocalText();
|
||||
TextFile.Write(target, content, SaveEncoding, SaveNewLine);
|
||||
|
||||
// 병합 중 충돌 파일을 워킹트리에만 쓰면 git 은 여전히 충돌로 본다. 충돌 표시는 사라졌는데
|
||||
// 인덱스는 풀리지 않은 어정쩡한 상태가 된다. [해결로 저장] 과 같은 길로 보낸다(6차 A-2).
|
||||
var conflictPath = IsPlainMode ? null : SelectedFile;
|
||||
if (conflictPath is not null && _workspace.IsMergeInProgress &&
|
||||
_workspace.Git.GetConflicts().Any(c => c.Path == conflictPath))
|
||||
{
|
||||
_workspace.Git.ResolveConflictWithContent(conflictPath, content);
|
||||
}
|
||||
else
|
||||
{
|
||||
TextFile.Write(target, content, SaveEncoding, SaveNewLine);
|
||||
}
|
||||
|
||||
_originalLocalText = content;
|
||||
_undo.Clear();
|
||||
@ -2935,6 +3061,7 @@ namespace MergeHub.ViewModels
|
||||
RemoteChangeCount = remote;
|
||||
ConflictCount = conflict;
|
||||
UnresolvedConflictCount = needsDecision;
|
||||
SaveResolvedCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
private string BuildLocalText()
|
||||
|
||||
@ -1408,6 +1408,18 @@
|
||||
FontWeight="Bold"
|
||||
ToolTip="LOCAL 내용을 파일에 쓰고 스테이징 후 커밋"/>
|
||||
|
||||
<!-- 병합 중에만 보인다. 이 파일만 풀어서 저장하고 커밋은 하지 않는다(6차 A-2).
|
||||
Git 모드가 아니면 병합도 없으므로 바깥에서 한 번 더 가린다. -->
|
||||
<StackPanel DockPanel.Dock="Bottom"
|
||||
Visibility="{Binding IsGitMode, Converter={StaticResource BoolToVis}}">
|
||||
<Button Content="해결로 저장" MinHeight="34" Margin="0,8,0,0"
|
||||
Command="{Binding SaveResolvedCommand}"
|
||||
Visibility="{Binding Workspace.IsMergeInProgress, Converter={StaticResource BoolToVis}}"
|
||||
Style="{StaticResource FilledButton}"
|
||||
FontWeight="Bold"
|
||||
ToolTip="이 파일만 풀어서 저장하고 스테이징합니다. 커밋은 하지 않습니다 (Ctrl+S)"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 창이 낮거나 커밋 메시지가 길어지면 아래가 잘리므로 스크롤을 둔다. -->
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel>
|
||||
|
||||
@ -861,8 +861,12 @@ namespace MergeHub.Views
|
||||
case ShortcutAction.Undo: return Run(vm.UndoCommand);
|
||||
case ShortcutAction.Redo: return Run(vm.RedoCommand);
|
||||
|
||||
// 병합 중의 Ctrl+S 는 "이 파일 해결로 저장" 이다. 예전에는 Save & Commit 이라
|
||||
// 파일 하나를 저장하려다 커밋까지 가려 했다(6차 A-2). 병합이 아니면 지금까지와 같다.
|
||||
case ShortcutAction.Save:
|
||||
return Run(vm.IsPlainMode ? (ICommand)vm.SavePlainCommand : vm.SaveAndCommitCommand);
|
||||
return Run(vm.IsPlainMode ? (ICommand)vm.SavePlainCommand
|
||||
: vm.Workspace.IsMergeInProgress ? vm.SaveResolvedCommand
|
||||
: vm.SaveAndCommitCommand);
|
||||
|
||||
case ShortcutAction.Recompare: return Run(vm.ReloadCommand);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user