3-Way 를 "끝낼 수 있게" 만들기 (자동 병합 · 충돌만 이동 · 완료 판정)

지금까지는 안 겹치는 변경까지 사람이 하나씩 눌러야 해서, 정작 판단이 필요한
서너 줄이 그 속에 묻혔다. git merge 가 자동으로 해 주는 일을 여기서도 해 준다.

- 자동 병합: 저쪽만 고친 줄(ThreeWayState.RemoteChanged)을 한 번에 가져온다.
  충돌과 내가 고친 줄은 건드리지 않는다. 이미 가져온 줄은 더 이상 RemoteChanged 가
  아니므로 여러 번 눌러도 안전하다.
- 충돌만 이동: Alt+Shift+↑↓. "다음 차이" 와 달리 아직 안 정한 충돌만 딛는다.
- 완료 판정: ACTIONS 에 "남은 충돌 N개"(빨강) → "충돌 정리 끝"(초록).

판단이 하나 필요했다 - 충돌 여부는 BASE·MINE·REMOTE 를 견주면 저절로 나오지만,
"내 것으로 간다"고 정한 줄은 내용이 그대로라 여전히 충돌로 보인다. 그래서
MergeLine.NeedsDecision(충돌이면서 반영·수정 표시가 없는 줄)을 따로 두었다.
그 결정은 글을 바꾸지 않아 편집 뒤 재정렬하면 사라지므로, 얼어 있는 BASE 줄 번호
(없으면 MINE 줄 번호)로 붙들어 되살린다.

곁들여 고친 것: Take Mine / Take Theirs 버튼이 줄을 옮겨도 켜짐·꺼짐이 그대로였다.
CommunityToolkit 커맨드는 CommandManager 를 구독하지 않는데 이 둘만 갱신 목록에서
빠져 있었다 - 키보드(Alt+←/→)로는 되고 버튼만 안 되던 이유다.

실행 확인(mergehub-demo, 3-Way = feature/cooling): 자동 병합으로 Theirs only 1→0,
다음 충돌+Take Theirs 를 반복해 남은 충돌 3→2→1→"충돌 정리 끝".
테스트 290 → 293건 전부 통과(단축키 3건 추가).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
김승준 2026-09-08 10:49:28 +09:00
parent 9956567bf8
commit 85c5e9e085
6 changed files with 191 additions and 8 deletions

View File

@ -24,13 +24,24 @@
| **완료** | blame (줄별 작성자) | `Blame` 탭 신설. 변경 목록에서 파일을 고르고 `blame` 을 누르면 경로가 넘어간다 | 확인: 31줄 전부 `커밋·날짜·작성자` 가 붙어 나옴 | | **완료** | blame (줄별 작성자) | `Blame` 탭 신설. 변경 목록에서 파일을 고르고 `blame` 을 누르면 경로가 넘어간다 | 확인: 31줄 전부 `커밋·날짜·작성자` 가 붙어 나옴 |
| **완료** | 변경분 파일 추출 | `GitService.ExportChangedFiles` 신설. 저장소 안 경로 구조를 그대로 만들고, <b>바이트 그대로</b> 옮긴다(텍스트로 다시 쓰면 줄바꿈·인코딩이 바뀐다). 지워진 파일은 뽑을 것이 없어 건너뛴다 | 확인: `2개 파일 → ...Temp (건너뜀 0개)` | | **완료** | 변경분 파일 추출 | `GitService.ExportChangedFiles` 신설. 저장소 안 경로 구조를 그대로 만들고, <b>바이트 그대로</b> 옮긴다(텍스트로 다시 쓰면 줄바꿈·인코딩이 바뀐다). 지워진 파일은 뽑을 것이 없어 건너뛴다 | 확인: `2개 파일 → ...Temp (건너뜀 0개)` |
### ③ 3-Way 병합 — "볼 수 있다"에서 "끝낼 수 있다"로 ### ③ 3-Way 병합 — **완료** (2026-09-08)
| 상태 | 항목 | 내용 | "볼 수 있다"에서 "끝낼 수 있다"로.
|---|---|---|
| 대기 | 자동 병합 | 한쪽만 고친 곳은 자동으로 결과에 넣는다. 사람은 충돌만 판단 | | 상태 | 항목 | 내용 | 확인 |
| 대기 | 충돌만 이동 | `다음 차이` 말고 `다음 충돌`. 자동 병합과 짝이다 | |---|---|---|---|
| 대기 | 병합 완료 판정 | 남은 충돌 0 이면 저장·커밋을 유도 | | **완료** | 자동 병합 | 저쪽만 고친 줄(`ThreeWayState.RemoteChanged`)을 한 번에 가져온다. 충돌과 내가 고친 줄은 안 건드린다. `git merge` 가 자동으로 해 주는 일을 여기서도 해 주는 것 | `Theirs only 1 → 0`, 자동 병합 버튼이 스스로 꺼짐 |
| **완료** | 충돌만 이동 | `이전 충돌` / `다음 충돌` (`Alt+Shift+↑↓`). <b>아직 안 정한 충돌만</b> 딛는다 | 3개를 차례로 딛고 마지막에 버튼이 꺼짐 |
| **완료** | 병합 완료 판정 | ACTIONS 위쪽에 `남은 충돌 N개`(빨강) → `충돌 정리 끝`(초록) | 3개 → 2 → 1 → 끝 |
**여기서 판단이 하나 필요했다** — 충돌인지 아닌지는 BASE·MINE·REMOTE 를 견주면 저절로 나오지만,
"내 것으로 간다"고 정한 줄은 <b>내용이 그대로라 여전히 충돌로 보인다.</b> 그래서 `NeedsDecision`
(충돌이면서 아직 반영·수정 표시가 없는 줄)을 따로 두었다. 그 결정은 글을 바꾸지 않으므로 편집 뒤
다시 정렬하면 사라지는데, 얼어 있는 <b>BASE 줄 번호</b>(없으면 MINE 줄 번호)로 붙들어 되살린다.
**곁들여 고친 것**: `Take Mine` · `Take Theirs` 버튼이 <b>줄을 옮겨도 켜짐/꺼짐이 그대로였다.</b>
CommunityToolkit 커맨드는 CommandManager 를 구독하지 않는데 이 둘만 갱신 목록에서 빠져 있었다.
키보드(`Alt+←/→`)로는 되고 버튼만 안 되던 이유가 이것이다.
### ① 폴더 비교 ### ① 폴더 비교
@ -69,4 +80,5 @@
| 날짜 | 항목 | 커밋 | | 날짜 | 항목 | 커밋 |
|---|---|---| |---|---|---|
| 2026-09-08 | 저장소 개설 (MergeHub · UbisamPlatform 각각) | `7719094` / `36ee902` | | 2026-09-08 | 저장소 개설 (MergeHub · UbisamPlatform 각각) | `7719094` / `36ee902` |
| 2026-09-08 | **④ Git 조작 5가지** — amend · stash 목록 · 커밋 간 비교 · blame · 변경분 추출 | 아래 | | 2026-09-08 | **④ Git 조작 5가지** — amend · stash 목록 · 커밋 간 비교 · blame · 변경분 추출 | `9956567` |
| 2026-09-08 | **③ 3-Way** — 자동 병합 · 충돌만 이동 · 병합 완료 판정 | 아래 |

View File

@ -130,6 +130,7 @@ namespace MergeHub.Models
[NotifyPropertyChangedFor(nameof(Highlight))] [NotifyPropertyChangedFor(nameof(Highlight))]
[NotifyPropertyChangedFor(nameof(ThreeWay))] [NotifyPropertyChangedFor(nameof(ThreeWay))]
[NotifyPropertyChangedFor(nameof(IsConflict))] [NotifyPropertyChangedFor(nameof(IsConflict))]
[NotifyPropertyChangedFor(nameof(NeedsDecision))]
[NotifyPropertyChangedFor(nameof(CanTakeRemote))] [NotifyPropertyChangedFor(nameof(CanTakeRemote))]
[NotifyPropertyChangedFor(nameof(CanTakeLocal))] [NotifyPropertyChangedFor(nameof(CanTakeLocal))]
private string _localText; private string _localText;
@ -141,11 +142,13 @@ namespace MergeHub.Models
/// <summary>BASE 내용을 가져와 반영한 줄인지. 되돌리면 풀린다.</summary> /// <summary>BASE 내용을 가져와 반영한 줄인지. 되돌리면 풀린다.</summary>
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(Highlight))] [NotifyPropertyChangedFor(nameof(Highlight))]
[NotifyPropertyChangedFor(nameof(NeedsDecision))]
private bool _isApplied; private bool _isApplied;
/// <summary>사용자가 직접 타이핑해 고친 줄인지. "머지 반영"과 구분해서 표시한다.</summary> /// <summary>사용자가 직접 타이핑해 고친 줄인지. "머지 반영"과 구분해서 표시한다.</summary>
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(Highlight))] [NotifyPropertyChangedFor(nameof(Highlight))]
[NotifyPropertyChangedFor(nameof(NeedsDecision))]
private bool _isManuallyEdited; private bool _isManuallyEdited;
/// <summary>반영하면서 줄 끝에 붙인 주석(툴팁으로 보여준다).</summary> /// <summary>반영하면서 줄 끝에 붙인 주석(툴팁으로 보여준다).</summary>
@ -281,6 +284,7 @@ namespace MergeHub.Models
[NotifyPropertyChangedFor(nameof(ThreeWay))] [NotifyPropertyChangedFor(nameof(ThreeWay))]
[NotifyPropertyChangedFor(nameof(Highlight))] [NotifyPropertyChangedFor(nameof(Highlight))]
[NotifyPropertyChangedFor(nameof(IsConflict))] [NotifyPropertyChangedFor(nameof(IsConflict))]
[NotifyPropertyChangedFor(nameof(NeedsDecision))]
[NotifyPropertyChangedFor(nameof(CanTakeRemote))] [NotifyPropertyChangedFor(nameof(CanTakeRemote))]
[NotifyPropertyChangedFor(nameof(CanTakeLocal))] [NotifyPropertyChangedFor(nameof(CanTakeLocal))]
private string _remoteText = string.Empty; private string _remoteText = string.Empty;
@ -363,6 +367,15 @@ namespace MergeHub.Models
public bool IsConflict => ThreeWay == ThreeWayState.Conflict; public bool IsConflict => ThreeWay == ThreeWayState.Conflict;
/// <summary>
/// 아직 사람이 골라야 하는 충돌 줄인지.
///
/// 충돌인지 아닌지는 BASE·MINE·REMOTE 를 견주면 저절로 나오지만, <b>"내 것으로 간다"고
/// 정한 줄은 내용이 그대로라 여전히 충돌로 보인다.</b> 그래서 "정했다"는 표시
/// (반영했거나 직접 고쳤거나)를 함께 본다 - 이 수가 0 이 되면 이 파일의 병합은 끝난 것이다.
/// </summary>
public bool NeedsDecision => IsConflict && !IsApplied && !IsManuallyEdited && !IsMergeNote;
/// <summary>REMOTE 내용을 가져올 수 있는 줄인지. 이미 같으면 가져올 것이 없다.</summary> /// <summary>REMOTE 내용을 가져올 수 있는 줄인지. 이미 같으면 가져올 것이 없다.</summary>
public bool CanTakeRemote => public bool CanTakeRemote =>
IsThreeWay && !IsMergeNote && ThreeWay is ThreeWayState.RemoteChanged or ThreeWayState.Conflict; IsThreeWay && !IsMergeNote && ThreeWay is ThreeWayState.RemoteChanged or ThreeWayState.Conflict;

View File

@ -21,6 +21,9 @@ namespace MergeHub.Services
GoToLine, GoToLine,
NextBookmark, NextBookmark,
ToggleBookmark, ToggleBookmark,
NextConflict,
PrevConflict,
AutoMerge,
OpenFolderCompare, OpenFolderCompare,
Find, Find,
FindNext, FindNext,
@ -109,6 +112,13 @@ namespace MergeHub.Services
new ShortcutEntry(ShortcutAction.CopyRightToLeft, Key.Left, ModifierKeys.Alt, "병합·편집", new ShortcutEntry(ShortcutAction.CopyRightToLeft, Key.Left, ModifierKeys.Alt, "병합·편집",
"오른쪽 → 왼쪽 반영 (2-Way: 이 줄 되돌리기 / 3-Way: Take Theirs)"), "오른쪽 → 왼쪽 반영 (2-Way: 이 줄 되돌리기 / 3-Way: Take Theirs)"),
new ShortcutEntry(ShortcutAction.NextConflict, Key.Down, ModifierKeys.Alt | ModifierKeys.Shift, "병합·편집",
"다음 충돌 (3-Way)", "차이가 아니라 <아직 안 정한 충돌>만 딛는다. 자동 병합과 짝이다."),
new ShortcutEntry(ShortcutAction.PrevConflict, Key.Up, ModifierKeys.Alt | ModifierKeys.Shift, "병합·편집",
"이전 충돌 (3-Way)"),
new ShortcutEntry(ShortcutAction.AutoMerge, Key.M, ModifierKeys.Alt, "병합·편집",
"자동 병합 - 저쪽만 고친 줄을 한 번에 가져온다 (3-Way)"),
new ShortcutEntry(ShortcutAction.Undo, Key.Z, ModifierKeys.Control, "병합·편집", "실행 취소", new ShortcutEntry(ShortcutAction.Undo, Key.Z, ModifierKeys.Control, "병합·편집", "실행 취소",
"머지 반영을 되돌린다. 되돌릴 머지 작업이 없으면 편집기의 글자 되돌리기가 그대로 동작한다."), "머지 반영을 되돌린다. 되돌릴 머지 작업이 없으면 편집기의 글자 되돌리기가 그대로 동작한다."),
new ShortcutEntry(ShortcutAction.Redo, Key.Y, ModifierKeys.Control, "병합·편집", "다시 실행"), new ShortcutEntry(ShortcutAction.Redo, Key.Y, ModifierKeys.Control, "병합·편집", "다시 실행"),

View File

@ -453,6 +453,11 @@ namespace MergeHub.ViewModels
UndoCommand.NotifyCanExecuteChanged(); UndoCommand.NotifyCanExecuteChanged();
RedoCommand.NotifyCanExecuteChanged(); RedoCommand.NotifyCanExecuteChanged();
AcceptAllCommand.NotifyCanExecuteChanged(); AcceptAllCommand.NotifyCanExecuteChanged();
// 3-Way 의 두 버튼. 빠져 있어서 줄을 옮겨도 켜짐/꺼짐이 그대로였다 -
// CommunityToolkit 의 커맨드는 CommandManager 를 구독하지 않으므로 직접 알려야 한다.
AcceptLocalCommand.NotifyCanExecuteChanged();
AcceptRemoteCommand.NotifyCanExecuteChanged();
FirstDiffCommand.NotifyCanExecuteChanged(); FirstDiffCommand.NotifyCanExecuteChanged();
PrevDiffCommand.NotifyCanExecuteChanged(); PrevDiffCommand.NotifyCanExecuteChanged();
NextDiffCommand.NotifyCanExecuteChanged(); NextDiffCommand.NotifyCanExecuteChanged();
@ -462,6 +467,10 @@ namespace MergeHub.ViewModels
ReloadCommand.NotifyCanExecuteChanged(); ReloadCommand.NotifyCanExecuteChanged();
SaveAndCommitCommand.NotifyCanExecuteChanged(); SaveAndCommitCommand.NotifyCanExecuteChanged();
SavePlainCommand.NotifyCanExecuteChanged(); SavePlainCommand.NotifyCanExecuteChanged();
AutoMergeCommand.NotifyCanExecuteChanged();
NextConflictCommand.NotifyCanExecuteChanged();
PrevConflictCommand.NotifyCanExecuteChanged();
} }
// ================= 로드 ================= // ================= 로드 =================
@ -621,6 +630,8 @@ namespace MergeHub.ViewModels
_baseTextSnapshot = baseText; _baseTextSnapshot = baseText;
_remoteTextSnapshot = threeWay ? remoteText : string.Empty; _remoteTextSnapshot = threeWay ? remoteText : string.Empty;
_mineTextSnapshot = threeWay ? localText : string.Empty; _mineTextSnapshot = threeWay ? localText : string.Empty;
_resolvedBaseNumbers.Clear(); // 파일이 바뀌면 앞 파일의 결정은 뜻이 없다
_resolvedMineNumbers.Clear();
_originalLocalText = BuildLocalText(); _originalLocalText = BuildLocalText();
SyncEditorFromLines(); SyncEditorFromLines();
@ -994,7 +1005,10 @@ namespace MergeHub.ViewModels
if (beforeHasLocal == afterHasLocal && string.Equals(before, after, StringComparison.Ordinal)) if (beforeHasLocal == afterHasLocal && string.Equals(before, after, StringComparison.Ordinal))
{ {
// 이미 내 원본과 같다. 충돌 줄이면 "내 것으로 정했다"는 표시만 남긴다. // 이미 내 원본과 같다. 충돌 줄이면 "내 것으로 정했다"는 표시만 남긴다.
// 내용이 안 바뀌므로 다시 정렬하면 이 표시가 사라진다 - BASE 줄 번호로 따로 붙들어 둔다.
line.IsApplied = true; line.IsApplied = true;
if (line.BaseNumber is int resolvedBase) _resolvedBaseNumbers.Add(resolvedBase);
else if (line.OriginalLocalNumber is int resolvedMine) _resolvedMineNumbers.Add(resolvedMine);
AfterEdit(); AfterEdit();
return; return;
} }
@ -1270,6 +1284,12 @@ namespace MergeHub.ViewModels
if (row.LocalNumber is int number && touched.Contains(number)) if (row.LocalNumber is int number && touched.Contains(number))
row.IsManuallyEdited = true; row.IsManuallyEdited = true;
// "내 것으로 간다"고 정했던 충돌은 내용이 그대로라 글만 봐서는 알 수 없다.
if (row.BaseNumber is int baseNumber
? _resolvedBaseNumbers.Contains(baseNumber)
: row.OriginalLocalNumber is int mineNumber && _resolvedMineNumbers.Contains(mineNumber))
row.IsApplied = true;
Lines.Add(row); Lines.Add(row);
} }
@ -1460,6 +1480,8 @@ namespace MergeHub.ViewModels
_baseTextSnapshot = baseText; _baseTextSnapshot = baseText;
_remoteTextSnapshot = string.Empty; // 파일 두 개 비교는 늘 2-Way 다 _remoteTextSnapshot = string.Empty; // 파일 두 개 비교는 늘 2-Way 다
_mineTextSnapshot = string.Empty; _mineTextSnapshot = string.Empty;
_resolvedBaseNumbers.Clear();
_resolvedMineNumbers.Clear();
RefreshLanguage(); RefreshLanguage();
var rows = BuildLines(baseText, localText); var rows = BuildLines(baseText, localText);
@ -2001,6 +2023,85 @@ namespace MergeHub.ViewModels
RefreshProblems(); RefreshProblems();
} }
// ================= 자동 병합 · 충돌만 보기 =================
/// <summary>
/// "내 것으로 간다"고 정한 충돌 줄의 BASE 줄 번호.
///
/// 그 결정은 <b>내용을 바꾸지 않으므로</b> 글만 봐서는 되살릴 수 없다. 편집 뒤 다시 정렬하면
/// 행이 새로 만들어지는데, 이걸 들고 있지 않으면 이미 정한 충돌이 되살아나
/// "남은 충돌"이 줄지 않는다. BASE 는 얼어 있으므로 그 줄 번호가 안전한 열쇠다.
/// </summary>
private readonly HashSet<int> _resolvedBaseNumbers = new HashSet<int>();
/// <summary>
/// BASE 에 없는 충돌(양쪽이 각자 새 줄을 넣은 자리)용. 이런 줄은 BASE 줄 번호가 없으므로
/// 역시 얼어 있는 MINE 줄 번호로 붙든다.
/// </summary>
private readonly HashSet<int> _resolvedMineNumbers = new HashSet<int>();
private bool CanAutoMerge() => IsThreeWay && Lines.Any(l => l.ThreeWay == ThreeWayState.RemoteChanged);
/// <summary>
/// 저쪽만 고친 줄을 한 번에 결과로 가져온다. 사람은 <b>충돌만</b> 판단하면 된다.
///
/// 안 겹치는 변경까지 하나씩 누르게 하면 100줄짜리 충돌 파일에서 정작 판단이 필요한
/// 서너 줄이 묻힌다 - `git merge` 가 자동으로 해 주는 일을 여기서도 해 준다.
/// 내가 고친 줄과 충돌 줄은 건드리지 않는다.
/// </summary>
[RelayCommand(CanExecute = nameof(CanAutoMerge))]
private void AutoMerge()
{
// 반영하면서 주석 줄이 끼어들어 Lines 가 바뀐다. 먼저 목록을 떠 놓고 돈다.
var targets = Lines.Where(l => l.ThreeWay == ThreeWayState.RemoteChanged && l.CanTakeRemote).ToList();
if (targets.Count == 0) return;
foreach (var line in targets) ApplyRemote(line, requery: false);
AfterEdit();
MessageUtil.ShowSuccessToast(UnresolvedConflictCount == 0
? $"{targets.Count}줄 자동 병합 · 남은 충돌 없음"
: $"{targets.Count}줄 자동 병합 · 남은 충돌 {UnresolvedConflictCount}개");
}
private bool CanGoConflict() => IsThreeWay && Lines.Any(l => l.NeedsDecision);
/// <summary>다음 충돌로. `다음 차이` 와 달리 <b>아직 안 정한 충돌만</b> 딛는다.</summary>
[RelayCommand(CanExecute = nameof(CanGoConflict))]
private void NextConflict()
{
for (var row = CaretRow + 1; row < Lines.Count; row++)
if (Lines[row].NeedsDecision) { GoToRow(row); return; }
// 끝까지 갔으면 처음으로 돈다 - 차이 이동과 같은 감각이다.
for (var row = 0; row < Lines.Count; row++)
if (Lines[row].NeedsDecision) { GoToRow(row); return; }
}
[RelayCommand(CanExecute = nameof(CanGoConflict))]
private void PrevConflict()
{
for (var row = CaretRow - 1; row >= 0; row--)
if (Lines[row].NeedsDecision) { GoToRow(row); return; }
for (var row = Lines.Count - 1; row >= 0; row--)
if (Lines[row].NeedsDecision) { GoToRow(row); return; }
}
/// <summary>아직 사람이 골라야 하는 충돌 줄 수. 0 이면 이 파일의 병합은 끝난 것이다.</summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(MergeStatusText))]
[NotifyPropertyChangedFor(nameof(IsMergeComplete))]
private int _unresolvedConflictCount;
/// <summary>3-Way 에서 지금 어디까지 왔는지 한 줄로.</summary>
public string MergeStatusText =>
!IsThreeWay ? string.Empty :
UnresolvedConflictCount == 0 ? "충돌 정리 끝" :
$"남은 충돌 {UnresolvedConflictCount}개";
public bool IsMergeComplete => IsThreeWay && UnresolvedConflictCount == 0;
// ================= 범례에 실시간으로 붙는 개수 ================= // ================= 범례에 실시간으로 붙는 개수 =================
[ObservableProperty] private int _changedCount; [ObservableProperty] private int _changedCount;
@ -2022,10 +2123,12 @@ namespace MergeHub.ViewModels
private void UpdateLegendCounts() private void UpdateLegendCounts()
{ {
int changed = 0, applied = 0, edited = 0, added = 0, removed = 0, moved = 0, ignored = 0, same = 0; int changed = 0, applied = 0, edited = 0, added = 0, removed = 0, moved = 0, ignored = 0, same = 0;
int remote = 0, conflict = 0; int remote = 0, conflict = 0, needsDecision = 0;
foreach (var line in Lines) foreach (var line in Lines)
{ {
if (line.NeedsDecision) needsDecision++;
switch (line.Highlight) switch (line.Highlight)
{ {
case LineHighlight.Applied: applied++; break; case LineHighlight.Applied: applied++; break;
@ -2051,6 +2154,7 @@ namespace MergeHub.ViewModels
SameCount = same; SameCount = same;
RemoteChangeCount = remote; RemoteChangeCount = remote;
ConflictCount = conflict; ConflictCount = conflict;
UnresolvedConflictCount = needsDecision;
} }
private string BuildLocalText() private string BuildLocalText()

View File

@ -1223,6 +1223,46 @@
Background="{DynamicResource Ubisam.Brush.Accent.Fill}" Background="{DynamicResource Ubisam.Brush.Accent.Fill}"
Foreground="{DynamicResource Ubisam.Brush.Accent.OnFill}" Foreground="{DynamicResource Ubisam.Brush.Accent.OnFill}"
ToolTip="선택한 줄을 REMOTE(Theirs) 내용으로"/> ToolTip="선택한 줄을 REMOTE(Theirs) 내용으로"/>
<!--
3-Way 흐름. "안 겹치는 변경은 자동으로, 사람은 충돌만" 이 순서다.
남은 충돌이 0 이 되면 그 자리에서 다 됐다고 알려 준다.
-->
<Border Margin="2,10,2,0" Padding="6,4" CornerRadius="3"
Visibility="{Binding IsThreeWay, Converter={StaticResource BoolToVis}}"
Background="{DynamicResource Ubisam.Brush.Input.Background}"
BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}" BorderThickness="1">
<TextBlock Text="{Binding MergeStatusText}" FontWeight="Bold" FontSize="11.5"
HorizontalAlignment="Center"
ToolTip="아직 사람이 골라야 하는 줄 수입니다">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Danger}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsMergeComplete}" Value="True">
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Success}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Border>
<Button Content="자동 병합" Command="{Binding AutoMergeCommand}"
MinHeight="30" Margin="2,4,2,0"
Visibility="{Binding IsThreeWay, Converter={StaticResource BoolToVis}}"
ToolTip="저쪽만 고친 줄을 한 번에 가져옵니다. 충돌과 내가 고친 줄은 건드리지 않습니다"/>
<UniformGrid Columns="2" Margin="0,4,0,0"
Visibility="{Binding IsThreeWay, Converter={StaticResource BoolToVis}}">
<Button Content="이전 충돌" Command="{Binding PrevConflictCommand}"
MinHeight="28" FontSize="11"
ToolTip="아직 안 정한 충돌만 딛습니다 (Alt+Shift+Up)"/>
<Button Content="다음 충돌" Command="{Binding NextConflictCommand}"
MinHeight="28" FontSize="11"
ToolTip="아직 안 정한 충돌만 딛습니다 (Alt+Shift+Down)"/>
</UniformGrid>
<Button Content="Revert" Command="{Binding RevertCommand}" MinHeight="30" Margin="2,4,2,0" <Button Content="Revert" Command="{Binding RevertCommand}" MinHeight="30" Margin="2,4,2,0"
Style="{StaticResource FilledButton}" Style="{StaticResource FilledButton}"
Background="{DynamicResource Ubisam.Brush.Warning}" Background="{DynamicResource Ubisam.Brush.Warning}"

View File

@ -709,6 +709,10 @@ namespace MergeHub.Views
GoToBox.SelectAll(); GoToBox.SelectAll();
return true; return true;
case ShortcutAction.NextConflict: return Run(vm.NextConflictCommand);
case ShortcutAction.PrevConflict: return Run(vm.PrevConflictCommand);
case ShortcutAction.AutoMerge: return Run(vm.AutoMergeCommand);
case ShortcutAction.NextBookmark: return Run(vm.NextBookmarkCommand); case ShortcutAction.NextBookmark: return Run(vm.NextBookmarkCommand);
case ShortcutAction.ToggleBookmark: case ShortcutAction.ToggleBookmark: