ksj09406 588f3d75e9 11차 C-2: 이력 줄 우클릭으로 마지막이 아닌 커밋의 메시지를 바꾼다
기획안 1차 범위 "커밋 우클릭 -> 이름 변경". 사용자 결정대로 좁은 범위에서만 한다.

HEAD 가 아닌 커밋의 메시지를 바꾸면 그 커밋부터 HEAD 까지가 새 해시로 다시
써진다. 고른 커밋의 메시지만 갈고 뒤 커밋은 트리 · 작성자 · 작성 시각 ·
부모 관계를 그대로 두어(LibGit2Sharp Refs.RewriteHistory) 해시만 바뀐다.
바꾸기 전 자리는 refs/original/날짜-시각/ 에 남긴다.

막는 자리(메뉴는 켜 두고 눌렀을 때 까닭과 푸는 길을 알림으로):
작업 중 · 커밋 안 한 변경 · 지금 브랜치 조상 아님 · 원격에 올라감 ·
다른 로컬 브랜치도 품음 · 구간에 태그 · 사이에 병합 커밋.
파일 비교 탭의 저장 안 한 편집은 우클릭해 줄을 고를 때 기존 창으로 묻는다.

- CommitReword (새): 막을 까닭 · 확인 글 · 완료 알림 · 새 메시지 다듬기
- GitService.InspectReword / RewordCommit, RewordInfo
- RewordWindow (새): 여러 줄 입력, [바꾸기] 는 기본 단추가 아님
- GitView: MergeHub 첫 우클릭 메뉴, 우클릭한 줄을 먼저 고른다
- 선택이 고친 커밋의 새 해시를 따라간다
- 화면에서 찾아 고침: 새 메시지가 끝 줄바꿈 없이 저장됐다 -> git -m 처럼
  다듬는다(뒤 커밋까지 다듬지 않게 새 메시지 하나만)

하네스 MergeSaveTest +40 (272건, 22벌 1,247건) · 빌드 경고 0 · 오류 0.
화면 확인: 되는 자리 1(트리 · 작성자 · 작성 시각 같음, 되돌리기 확인) ·
막는 자리 8 모두 막힘.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 10:34:58 +09:00

126 lines
6.8 KiB
C#

using System.Linq;
using System.Windows;
using System.Windows.Controls;
using MergeHub.ViewModels;
namespace MergeHub.Views
{
/// <summary>
/// Git 탭. DataContext(GitViewModel)는 플랫폼 TabManager 가 DI 로 만들어 붙여준다.
/// </summary>
public partial class GitView : UserControl
{
public GitView()
{
InitializeComponent();
// 다른 탭에 있는 동안 파일 비교 탭에서 고른 커밋의 내용을, 이 탭이 다시 보일 때 그린다(10차 B-1).
IsVisibleChanged += (_, e) => { if (e.NewValue is true) ViewModel?.ShowPendingDiff(); };
// 이력에서 다른 커밋을 누르면 파일 비교 탭도 따라간다. 거기 저장 안 한 편집이 있으면 선택이 바뀌기 전에 묻고,
// [취소] 면 누름을 막아 선택이 그대로다(10차 B-1).
HistoryList.PreviewMouseLeftButtonDown += (_, e) =>
{
if (ViewModel is not { } vm) return;
if (ItemsControl.ContainerFromElement(HistoryList, e.OriginalSource as DependencyObject) is not ListBoxItem item) return;
if (item.DataContext is not MergeHub.Models.CommitInfo target) return;
var answer = vm.ConfirmChooseCommit(target);
if (answer is null) return; // 물을 것 없음 - 목록이 여느 때처럼 고른다
e.Handled = true; // [취소] 거나, 창이 누름을 끊어 목록이 스스로 못 고른다
if (answer == true) ChooseHistoryRow(vm, HistoryList.Items.IndexOf(target));
};
// 우클릭한 줄을 먼저 고른다(11차 C-2). 목록은 오른쪽 누름으로는 선택을 바꾸지 않아서, 그대로 두면
// <b>가리킨 줄이 아닌 다른 커밋</b>의 메시지를 바꾸게 된다 - 다시 쓴 이력은 되돌리기 번거롭다.
HistoryList.PreviewMouseRightButtonDown += (_, e) =>
{
if (ViewModel is not { } vm) return;
if (ItemsControl.ContainerFromElement(HistoryList, e.OriginalSource as DependencyObject) is not ListBoxItem item) return;
if (item.DataContext is not MergeHub.Models.CommitInfo target) return;
var answer = vm.ConfirmChooseCommit(target);
if (answer == false) { e.Handled = true; return; } // [취소] - 메뉴도 열지 않는다
ChooseHistoryRow(vm, HistoryList.Items.IndexOf(target));
};
HistoryList.PreviewKeyDown += (_, e) =>
{
if (ViewModel is not { } vm) return;
var at = HistoryList.SelectedIndex;
var to = e.Key switch
{
System.Windows.Input.Key.Up => at - 1,
System.Windows.Input.Key.Down => at + 1,
System.Windows.Input.Key.Home => 0,
System.Windows.Input.Key.End => HistoryList.Items.Count - 1,
System.Windows.Input.Key.PageUp => at - 10,
System.Windows.Input.Key.PageDown => at + 10,
_ => at
};
to = System.Math.Max(0, System.Math.Min(HistoryList.Items.Count - 1, to));
if (to == at) return;
var answer = vm.ConfirmChooseCommit(HistoryList.Items[to] as MergeHub.Models.CommitInfo);
if (answer is null) return;
e.Handled = true;
if (answer == true) ChooseHistoryRow(vm, to);
};
}
/// <summary>물은 뒤 [계속] 이면 그 커밋을 직접 고른다. 창이 누름을 끊어 목록이 스스로 고르지 못한다(10차 B-1).</summary>
private void ChooseHistoryRow(GitViewModel vm, int index)
{
if (index < 0 || index >= HistoryList.Items.Count) return;
vm.SelectedCommit = HistoryList.Items[index] as MergeHub.Models.CommitInfo;
HistoryList.ScrollIntoView(HistoryList.Items[index]);
(HistoryList.ItemContainerGenerator.ContainerFromIndex(index) as ListBoxItem)?.Focus();
}
private GitViewModel? ViewModel => DataContext as GitViewModel;
private void BrowseFolder_Click(object sender, RoutedEventArgs e)
{
var vm = ViewModel;
if (vm is null) return;
// 예전에는 WinForms FolderBrowserDialog 를 썼다. net48 에서는 옛날 트리 창이라
// 주소 칸이 없고 경로를 붙여넣을 수도 없어서, 깊은 폴더가 기억돼 있으면
// 저장소 최상위까지 거슬러 올라가기가 번거로웠다. FolderPicker.cs 참고.
var picked = MergeHub.Services.FolderPicker.Pick("Git 저장소 폴더 고르기", vm.Workspace.RepositoryPath);
if (picked is null) return;
// .git 은 최상위에만 있다. 하위 폴더를 골랐어도 위로 거슬러 올라가 최상위를 연다 -
// 사람은 지금 보고 있던 깊은 폴더를 고르기 마련이다.
var root = MergeHub.Services.PathUtil.FindRepositoryRoot(picked);
vm.Workspace.RepositoryPath = root.Length > 0 ? root : picked;
if (vm.OpenCommand.CanExecute(null)) vm.OpenCommand.Execute(null);
}
// PasswordBox 는 보안상 Password 를 바인딩할 수 없어 코드 비하인드로 전달한다.
/// <summary>충돌 목록을 두 번 누르면 파일 비교 탭의 3분할에서 연다(6차 B-1). 항목 위가 아니면 무시한다.</summary>
private void Conflicts_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (e.OriginalSource is not System.Windows.DependencyObject hit) return;
if (System.Windows.Controls.ItemsControl.ContainerFromElement((System.Windows.Controls.ItemsControl)sender, hit) is null) return;
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;
}
private void Log_TextChanged(object sender, TextChangedEventArgs e)
{
if (sender is TextBox box) box.ScrollToEnd();
}
}
}