6차 B-6: 충돌 탭에서 여러 파일을 골라 Accept 를 한 번에, 다시 읽기는 끝에 한 번

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
김승준 2026-09-14 13:55:53 +09:00
parent 8dd9fa98a6
commit eb43385e14
6 changed files with 124 additions and 8 deletions

View File

@ -654,3 +654,40 @@ MERGED 는 git 이 충돌 표시를 넣어 둔 파일이라, 두 칸으로는
- 자동 풀기는 Git 탭 [Merge] 에만 걸었다. pull · rebase · cherry-pick 이 낸 충돌에는 아직 걸지 않았다.
- MERGED 칸에서 반영하며 새로 넣은 주석 줄은, 다시 정렬하면(타이핑 멈춤 · 규칙 변경) "무시" 색이 된다. 차이 수에는 원래도 세지 않던 줄이다.
## B-6. 충돌 탭에서 하나씩 누를 때마다 오래 기다린다 (2026-09-14, 완료)
### 고치기 전에 다시 났다
새로 만든 저장소를 병합(충돌 24)하고, 같은 줄 충돌 15개(`Vision/Unit10.cs` ~ `Motion/Unit24.cs`)를 충돌 탭에서 **하나씩 골라 Accept Mine**.
| 본 것 | 값 |
|---|---|
| 15개 합계 | **22.0초** |
| 한 개 평균 | **1.47초** (자동 조작의 고르기 0.3초 포함) |
| 여러 개 고르기 | 안 됨 - 목록이 한 개만 고른다 |
### 무엇이 원인이었나
- 목록이 `SelectionMode` 기본값(한 개)이었다.
- Accept 한 번이 작업 하나(`RunAndRefreshAsync`)라, 끝날 때마다 저장소 전체를 다시 읽었다.
### 고친 것
| 파일 | 무엇 |
|---|---|
| `Views/GitView.xaml` | 충돌 목록 `SelectionMode="Extended"`(Ctrl · Shift · Ctrl+A). 단추 옆에 "N개 고름 - Accept 는 고른 파일 모두에 한 번에 적용됩니다". 안내 글에 여러 개 고르기 |
| `Views/GitView.xaml.cs` | `SelectionChanged` 에서 고른 목록을 넘긴다(SelectedItems 는 바인딩이 안 된다) |
| `ViewModels/GitViewModel.cs` | `SetSelectedConflicts` · `ConflictTargets`. Accept Mine/Theirs/Both 가 고른 파일 모두를 **작업 하나**로 풀고 다시 읽기는 끝에 한 번. 한 파일이 실패해도 나머지는 풀고, 실패는 로그 · 알림. 로그 "충돌 N개를 Mine 로 한 번에 풀었습니다 (… ms)". Accept Both 확인 창은 "N개 파일 (첫 파일 외 N-1개)" |
### 확인한 것 (같은 저장소 · 같은 15개)
| 확인 | 결과 |
|---|---|
| 15개를 골라 Accept Mine 한 번 | "15개 고름" 표시. git 반영 **0.8초**, 목록이 9개로 다시 설 때까지 **1.2초** (고치기 전 22.0초) |
| 로그 | "충돌 15개를 Mine 로 한 번에 풀었습니다 (224ms · 저장소 다시 읽기는 끝에 한 번)" |
| 결과 | 충돌 24 → 9. 15개 모두 내 쪽(HEAD) 글과 같음 |
(자동 조작은 목록이 화면에 보이는 항목만 만들어 두는 탓에 처음에는 10개만 골랐다. 목록 항목을 모두 불러오게 고쳐 15개로 다시 쟀다.)
빌드 경고 0 · 오류 0.

View File

@ -8,6 +8,7 @@
| 날짜 | 항목 | 커밋 |
|---|---|---|
| 2026-09-14 | **6차 B-6** — 충돌 탭에서 여러 파일을 골라(Ctrl · Shift) Accept 를 한 번에. 작업 하나로 풀고 저장소 다시 읽기는 끝에 한 번. 같은 줄 충돌 15개 22.0초 → 1.2초 | 아래 |
| 2026-09-14 | **6차 B-5** — 머지 주석을 비교에서 무시(주석 줄은 거르고 줄 끝 주석은 떼고 견줌). 주석만 다른 git 충돌은 양쪽 주석을 남겨 자동으로 풂. 정렬이 주석 줄과 코드 줄을 짝지으면 떼어 놓음. 폴더 비교도. 설정 [머지 주석] 에서 끔 | 아래 |
| 2026-09-14 | **6차 B-4 확인 중 발견 두 건** — "마지막 차이" 로 멀리 건너뛰면 표시층 보정이 화면 밖 행으로 재져 네 칸이 9줄 위로 밀리던 것을 고침. `git mergetool` 이 넘긴 세 벌을 두 칸이 아닌 네 칸으로 열고 결과를 MERGED 파일에 쓴다 | 아래 |
| 2026-09-14 | **6차 B-4** — 3분할 MERGED 칸 줄 번호 자리가 20px 라 2949 가 "294" 로 잘리던 것을 고침. 네 칸 여백을 54 → 66px 로 맞춰 넓혀 다섯 자리까지 보인다 | 아래 |

View File

@ -109,7 +109,7 @@
| **완료** | B-4 MERGED 칸 줄 번호가 네 자리에서 잘린다 — 네 칸 여백을 66px 로, MERGED 번호 자리 20 → 36px(다섯 자리) |
| **완료** | B-4 확인 중 발견 두 건 — 멀리 건너뛰면 네 칸이 위로 밀려 아래가 빔 · `git mergetool` 세 벌이 두 칸으로 열림 |
| **완료** | B-5 머지 주석은 소스에 넣고, 병합 뒤에는 그 줄을 무시한다 — 비교(두 칸 · 네 칸 · 폴더)에서 가리고, 주석만 다른 git 충돌은 양쪽 주석을 남겨 자동으로 푼다. 설정에서 끔 |
| 대기 | B-6 충돌 탭에서 하나씩 누를 때마다 오래 기다린다 |
| **완료** | B-6 충돌 탭에서 하나씩 누를 때마다 오래 기다린다 — 여러 개 골라 Accept 한 번, 다시 읽기는 끝에 한 번. 15개 22.0초 → 1.2초 |
| 대기 | B-7 상태바가 병합 중에 믿을 수 없는 값을 보인다 |
| 대기 | B-8 병합 커밋 뒤 파일 비교 탭이 어정쩡하게 남는다 |

View File

@ -276,7 +276,29 @@ namespace MergeHub.ViewModels
});
});
private bool HasConflict() => SelectedConflict is not null;
/// <summary>
/// 충돌 목록에서 고른 파일들(6차 B-6). Ctrl · Shift 로 여러 개를 고를 수 있다.
/// ListBox.SelectedItems 는 바인딩이 안 돼 화면이 고를 때마다 넘겨준다.
/// </summary>
private IReadOnlyList<ConflictInfo> _selectedConflicts = Array.Empty<ConflictInfo>();
public void SetSelectedConflicts(IReadOnlyList<ConflictInfo> conflicts)
{
_selectedConflicts = conflicts ?? Array.Empty<ConflictInfo>();
OnPropertyChanged(nameof(ConflictSelectionText));
RaiseCanExecuteChanged();
}
/// <summary>Accept 가 적용될 파일들. 여러 개를 골랐으면 그것들, 아니면 고른 하나.</summary>
private IReadOnlyList<ConflictInfo> ConflictTargets =>
_selectedConflicts.Count > 0 ? _selectedConflicts
: SelectedConflict is { } one ? new[] { one } : Array.Empty<ConflictInfo>();
/// <summary>여러 개를 골랐을 때 단추 옆에 띄우는 한 줄.</summary>
public string ConflictSelectionText =>
ConflictTargets.Count > 1 ? $"{ConflictTargets.Count}개 고름 - Accept 는 고른 파일 모두에 한 번에 적용됩니다" : string.Empty;
private bool HasConflict() => ConflictTargets.Count > 0;
[RelayCommand(CanExecute = nameof(HasConflict))]
private Task AcceptMineAsync() => ResolveAsync(ConflictResolution.Mine);
@ -289,7 +311,10 @@ namespace MergeHub.ViewModels
{
// 파일 단위 "둘 다" 는 파일 두 벌을 통째로 이어 붙인다. 양쪽이 같은 이름으로 새로 만든 파일이면
// 같은 형식이 두 번 나와 빌드가 안 된다(5차 C-1 에서 그랬다). 누르기 전에 무엇이 일어나는지 알린다(6차 A-5).
var path = SelectedConflict?.Path ?? string.Empty;
var targets = ConflictTargets;
var path = targets.Count <= 1
? targets.FirstOrDefault()?.Path ?? string.Empty
: $"{targets.Count}개 파일 ({targets[0].Path} 외 {targets.Count - 1}개)";
var go = MessageUtil.ShowYesNo(
"파일 두 벌을 이어 붙입니다",
$"{path}\n\n내 것 다음에 상대 것을 한 파일로 이어 붙입니다. 내 파일의 인코딩과 줄끝으로 씁니다.\n\n" +
@ -317,9 +342,50 @@ namespace MergeHub.ViewModels
_workspace.RequestOpenConflict(conflict.Path);
}
private Task ResolveAsync(ConflictResolution resolution) =>
_workspace.RunAndRefreshAsync($"충돌 해결({resolution})",
() => _workspace.Git.ResolveConflict(SelectedConflict!.Path, resolution));
/// <summary>
/// 고른 충돌 파일들을 한 작업으로 푼다(6차 B-6).
///
/// 예전에는 파일 하나마다 작업 하나였고, 작업이 끝날 때마다 저장소 전체를 다시 읽었다(한 개 약 1.5초).
/// 이제는 고른 파일을 모두 풀고 <b>다시 읽기는 끝에 한 번</b>이다.
/// 한 파일이 실패해도 나머지는 계속 풀고, 실패한 것은 로그와 알림으로 알린다.
/// </summary>
private Task ResolveAsync(ConflictResolution resolution)
{
var targets = ConflictTargets.ToList();
if (targets.Count == 0) return Task.CompletedTask;
var label = targets.Count == 1 ? $"충돌 해결({resolution})" : $"충돌 해결({resolution}) {targets.Count}개";
return _workspace.RunAndRefreshAsync(label, () =>
{
var failed = new List<string>();
var watch = Stopwatch.StartNew();
foreach (var conflict in targets)
{
try
{
_workspace.Git.ResolveConflict(conflict.Path, resolution);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
failed.Add($"{conflict.Path}: {ex.Message}");
}
}
watch.Stop();
_workspace.OnUi(() =>
{
if (targets.Count > 1)
_workspace.AppendLog($"충돌 {targets.Count - failed.Count}개를 {resolution} 로 한 번에 풀었습니다 " +
$"({watch.ElapsedMilliseconds}ms · 저장소 다시 읽기는 끝에 한 번)");
foreach (var line in failed) _workspace.AppendLog("충돌 해결 실패: " + line);
if (failed.Count > 0) MessageUtil.ShowWarningToast($"{failed.Count}개는 풀지 못했습니다 - 로그를 보세요");
});
});
}
// ================= 이력 조작 =================

View File

@ -240,13 +240,15 @@
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Style="{StaticResource Muted}" Margin="0,0,0,6" TextWrapping="Wrap"
Text="파일 전체를 한쪽으로 채택하려면 아래 버튼을 쓰고, 줄마다 고르려면 목록을 두 번 누르거나 [3분할로 열기] 를 누르세요. 파일 비교 탭에서 세 칸으로 열립니다."/>
Text="파일 전체를 한쪽으로 채택하려면 아래 버튼을 쓰고, 줄마다 고르려면 목록을 두 번 누르거나 [3분할로 열기] 를 누르세요. 파일 비교 탭에서 세 칸으로 열립니다. Ctrl · Shift 로 여러 파일을 고르면 Accept 가 한 번에 적용됩니다(Ctrl+A 전부)."/>
<!-- 두 번 누르거나 Enter 로 파일 비교 탭의 3분할에서 연다(6차 B-1).
두 번 누르기는 MouseBinding 이 아니라 목록의 MouseDoubleClick 으로 받는다 -
항목이 마우스 누름을 먼저 처리해 버려 MouseBinding 은 항목 위에서 안 온다. -->
<!-- 여러 개를 골라 한 번에 Accept 한다(6차 B-6). SelectedItems 는 바인딩이 안 돼 SelectionChanged 로 넘긴다. -->
<ListBox Grid.Row="1" ItemsSource="{Binding Workspace.Conflicts}"
SelectedItem="{Binding SelectedConflict}" HorizontalContentAlignment="Stretch"
SelectionMode="Extended" SelectionChanged="Conflicts_SelectionChanged"
MouseDoubleClick="Conflicts_MouseDoubleClick"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.InputBindings>
@ -291,6 +293,8 @@
Background="{DynamicResource Ubisam.Brush.Warning}"
Foreground="{DynamicResource Ubisam.Brush.Accent.OnFill}"/>
<Button Content="병합 커밋" Command="{Binding CommitMergeCommand}" MinWidth="100"/>
<TextBlock Text="{Binding ConflictSelectionText}" Style="{StaticResource Muted}"
VerticalAlignment="Center" Margin="8,0,0,0"/>
</WrapPanel>
</Grid>
</TabItem>

View File

@ -1,4 +1,5 @@
using System.Windows;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using MergeHub.ViewModels;
@ -44,6 +45,13 @@ namespace MergeHub.Views
if (DataContext is MergeHub.ViewModels.GitViewModel vm && vm.OpenConflictInCompareCommand.CanExecute(null))
vm.OpenConflictInCompareCommand.Execute(null);
}
/// <summary>고른 충돌 파일들을 넘긴다(6차 B-6). ListBox.SelectedItems 는 바인딩할 수 없다.</summary>
private void Conflicts_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is not ListBox list || ViewModel is not { } vm) return;
vm.SetSelectedConflicts(list.SelectedItems.OfType<MergeHub.Models.ConflictInfo>().ToList());
}
private void Password_Changed(object sender, RoutedEventArgs e)
{
if (sender is PasswordBox box && ViewModel is { } vm) vm.Workspace.Password = box.Password;