ksj09406 13988d1f0a 12차 - 폴더 비교 읽기 쉽게 · 양방향 머지 · 칸 요약 · 정보 창 · 문서 탭
새 플랫폼(UbisamBase.Launcher) 위로 옮기면서 12차 A~E 를 한 번에 담는다.
항목별 값과 판정은 doc/history/2026-09-23-12차.md, 요약은 doc/history/changelog.md.

A. 폴더 비교를 읽기 쉽게
- A-1 폴더 줄의 크기가 늘 0B 이던 것 - 안에 든 파일 크기 합계를 아래에서 위로 더해 적는다
  (걸러 낸 파일은 합계에서도 빠진다)
- A-2 좌우로 나눠 보이기 - 왼쪽 · 오른쪽 목록을 나란히 두고 같은 줄에 맞춘다.
  한쪽에만 있는 항목은 반대편을 비운다

B. 파일 비교 화면
- B-1 2-Way 에서 양쪽으로 머지 - 칸마다 반영 · 저장. 왼쪽 기준점을 따로 세고,
  되돌리기 · 저장 백업도 양쪽 모두
- B-2 프로그램 정보 창 - 버전 · 빌드 날짜 · 만든 사람 · 사내 문의처 · 플랫폼 버전 · 로그 폴더
- B-3 칸 아래에 무엇이 달라졌는지 - 칸마다 줄 수 · 추가 · 삭제 · 수정, 가운데에 진행

C. 플랫폼 (플랫폼 저장소에서 함께 나감)
- C-2 하단 탭 줄을 한 단계 작게 - 74 -> 61 DIP. 실측 133 -> 109px 이고
  그만큼 코드 칸이 늘어 파일 비교가 47 -> 48줄

E. 저장하지 않아도 남는 편집
- E-2 여러 짝을 동시에(문서 탭). 지금까지는 한 짝만 들어서 짝을 바꾸면
  저장하지 않은 편집을 묻고 버리면 사라졌다(재현: 16줄 -> 15줄).
  이제 떠 있던 탭이 그 글을 맡아 두므로 묻지 않는다.
  - 탭마다 HomeViewModel 을 따로 만들지 않았다 - 이 화면은 GitWorkspace 의 여러 사건과
    한 자리짜리 약속(ConfirmDiscardEdits · ConfirmLeaveCommit)에 물려 있다.
    화면은 하나로 두고 탭이 파일에 없는 것만 맡는다(고친 글 · 왼쪽 글 · 되돌리기 이력 ·
    두 기준점 · 캐럿 · 스크롤 · 저장 형식)
  - 탭 이름은 파일 이름, 다르면 왼 <-> 오. 양쪽이 같은 부분은 한 번만 적는다
    (WeldController_기준 <-> 내작업.cs, 421 -> 288px). 같은 이름이 겹치면
    갈릴 때까지 위 폴더를 한 겹씩 덧붙인다
  - 저장하지 않은 탭에 가운뎃점. 사람이 손댄 것만 센다 - 중괄호 맞춤으로만 바뀐 것은
    묻는 자리와 같은 기준으로 뺀다(안 그러면 .cs 를 여는 것만으로 늘 점이 붙는다)
  - 탭이 사라져도 고친 글은 보관함에 남아 다시 열면 되살아난다(짝 50개).
    E-1(껐다 켜도 남는 편집)은 이 보관함을 파일로 옮겨 적으면 된다
  - 이전 · 다음 파일은 지금 탭의 짝만 바꾸고, 폴더 비교에서 두 번 누르면 새 탭(사용자 결정)
  - Ctrl+Tab · Ctrl+Shift+Tab · Ctrl+W · 가운데 단추 닫기. 닫을 때도 묻지 않는다
  - Git 탭 · 병합 중 네 칸은 탭이 맡지 않는다 - 그쪽은 예전처럼 묻는다

하네스 27벌 1,787건 통과 · 실패 0(DocTabTest 106건 새로 만듦).
빌드 경고 0 · 오류 0. 화면으로 E-2 34건 · 회귀 19건 확인.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 13:21:54 +09:00

620 lines
43 KiB
XML

<UserControl x:Class="MergeHub.Views.GitView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MergeHub.Views"
xmlns:shell="clr-namespace:UbisamBase.Core.Shell;assembly=UbisamBase.Core"
shell:PlatformChrome.FullBleed="True"
mc:Ignorable="d">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/MergeHub;component/Views/Controls/AppStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
<Style TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Margin" Value="2"/>
<Setter Property="MinWidth" Value="76"/>
</Style>
<Style x:Key="Muted" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Muted}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style x:Key="GroupTitle" TargetType="TextBlock">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Accent}"/>
<Setter Property="Margin" Value="0,0,0,4"/>
</Style>
<Style x:Key="Code" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="FontFamily" Value="Consolas"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="AcceptsReturn" Value="True"/>
<Setter Property="TextWrapping" Value="NoWrap"/>
<Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="VerticalContentAlignment" Value="Top"/>
</Style>
</ResourceDictionary>
</UserControl.Resources>
<Grid Margin="8" TextElement.Foreground="{DynamicResource Ubisam.Brush.ContentForeground}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="120"/>
</Grid.RowDefinitions>
<!-- ===== 저장소 연결 ===== -->
<Border Grid.Row="0" Padding="10,8" Margin="0,0,0,6"
Background="{DynamicResource Ubisam.Brush.Input.Background}"
BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}" BorderThickness="1">
<StackPanel>
<!--
저장소 연결(4차 C-3).
<b>로컬 경로 한 줄만 늘 보인다.</b> 이 도구는 로컬에서 쓰는 머지툴이 우선이고
아직 원격 저장소가 없다. 그런데 예전에는 원격 URL · 계정 · Fetch/Pull/Push 가
화면 위쪽 4분의 1을 차지한 채 전부 비활성이었다.
<b>지우지 않고 접었다</b> - 사내 Gitea 가 생기면 다시 필요하다.
-->
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
<TextBlock Text="저장소 연결" Style="{StaticResource GroupTitle}" Margin="0,0,10,0"/>
<!--
접힌 것이 있다는 사실 자체가 보여야 한다. 처음에는 11px 회색 단추였는데
<b>사용자가 못 찾았다</b> - 글자를 키우고 강조색을 줘서 "누르는 것"으로 읽히게 했다.
-->
<Button Content="{Binding RemoteToggleLabel}" Command="{Binding ToggleRemoteCommand}"
MinWidth="190" MinHeight="26" Padding="10,2" FontSize="12" FontWeight="Bold"
Margin="0,0,0,4" Cursor="Hand"
Foreground="{DynamicResource Ubisam.Brush.Accent}"
ToolTip="원격 저장소·계정 칸을 접거나 폅니다. 접힌 상태는 다음에 켤 때도 그대로입니다"/>
</StackPanel>
<Grid Margin="0,0,0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="로컬 경로" Style="{StaticResource Muted}" Margin="0,0,6,0"/>
<TextBox Grid.Column="1" Text="{Binding Workspace.RepositoryPath, UpdateSourceTrigger=PropertyChanged}"/>
<Button Grid.Column="2" Content="폴더..." Click="BrowseFolder_Click"/>
<Button Grid.Column="3" Content="열기" Command="{Binding OpenCommand}"/>
<!-- 새로고침은 로컬 작업이다. 원격 줄에 있다가 접히면 함께 사라져 여기로 옮겼다. -->
<Button Grid.Column="4" Content="새로고침" Command="{Binding RefreshCommand}"/>
</Grid>
<Grid Visibility="{Binding IsRemoteOpen, Converter={StaticResource BoolToVis}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="110"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="원격 URL" Style="{StaticResource Muted}" Margin="0,0,6,0"/>
<TextBox Grid.Column="1" Text="{Binding Workspace.RemoteUrl, UpdateSourceTrigger=PropertyChanged}"
ToolTip="예) https://gitea.내부도메인/team/repo.git"/>
<TextBlock Grid.Column="2" Text="이름" Style="{StaticResource Muted}" Margin="8,0,6,0"/>
<TextBox Grid.Column="3" Text="{Binding Workspace.RemoteName, UpdateSourceTrigger=PropertyChanged}"/>
<Button Grid.Column="4" Content="클론" Command="{Binding CloneCommand}"/>
<Button Grid.Column="5" Content="원격 추가" Command="{Binding AddRemoteCommand}"
ToolTip="다른 저장소를 원격으로 등록하고 바로 fetch"/>
<Button Grid.Column="6" Content="원격 삭제" Command="{Binding RemoveRemoteCommand}"/>
</Grid>
<Grid Margin="0,4,0,0"
Visibility="{Binding IsRemoteOpen, Converter={StaticResource BoolToVis}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Gitea 계정" Style="{StaticResource Muted}" Margin="0,0,6,0"/>
<TextBox Grid.Column="1" Text="{Binding Workspace.UserName, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Grid.Column="2" Text="토큰/암호" Style="{StaticResource Muted}" Margin="8,0,6,0"/>
<PasswordBox x:Name="TokenBox" Grid.Column="3" PasswordChanged="Password_Changed"
Background="{DynamicResource Ubisam.Brush.Input.Background}"
Foreground="{DynamicResource Ubisam.Brush.ContentForeground}"
BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}" Padding="4,3"/>
<StackPanel Grid.Column="4" Orientation="Horizontal" HorizontalAlignment="Right">
<!-- 11차 F-11: 토큰은 윈도 자격 증명 관리자에 맡긴다. 맡겨 둔 것이 있을 때만 보인다. -->
<TextBlock Text="저장됨" Style="{StaticResource Muted}" FontSize="11" VerticalAlignment="Center" Margin="0,0,6,0"
ToolTip="이 서버의 토큰을 윈도 자격 증명 관리자에 맡겨 두었습니다. 설정 파일에는 토큰이 남지 않습니다"
Visibility="{Binding Workspace.HasSavedToken, Converter={StaticResource BoolToVis}}"/>
<Button Content="토큰 지우기" Command="{Binding ForgetTokenCommand}" Margin="0,0,8,0"
ToolTip="맡겨 둔 토큰을 지우고 토큰 칸을 비웁니다"/>
<Button Content="Fetch" Command="{Binding FetchCommand}"/>
<Button Content="Pull" Command="{Binding PullCommand}"/>
<Button Content="Push" Command="{Binding PushCommand}"/>
</StackPanel>
</Grid>
</StackPanel>
</Border>
<!-- ===== 상태 배지 ===== -->
<!-- ItemHeight 를 24 로 묶어 두면 그보다 큰 버튼이 잘린다("mergetool 등록"이 반쯤 잘려 보였다).
줄 높이는 내용이 정하게 두고, 글자만 세로 가운데로 맞춘다. -->
<WrapPanel Grid.Row="1" Margin="2,0,0,6">
<TextBlock Text="HEAD" Style="{StaticResource Muted}" Margin="0,0,6,0" VerticalAlignment="Center"/>
<TextBlock Text="{Binding Workspace.CurrentBranch}" FontWeight="Bold" Margin="0,0,6,0"
Foreground="{DynamicResource Ubisam.Brush.Accent}" VerticalAlignment="Center"/>
<TextBlock Text="{Binding Workspace.HeadSha}" FontFamily="Consolas" Margin="0,0,16,0"
Foreground="{DynamicResource Ubisam.Brush.Warning}" VerticalAlignment="Center"/>
<TextBlock Text="Working Tree" Style="{StaticResource Muted}" Margin="0,0,6,0" VerticalAlignment="Center"/>
<TextBlock Text="{Binding Workspace.WorkingTreeSummary}" Margin="0,0,16,0" VerticalAlignment="Center"/>
<TextBlock Text="{Binding Workspace.PendingOperationText}" FontWeight="Bold" Margin="0,0,16,0"
Foreground="{DynamicResource Ubisam.Brush.Danger}" VerticalAlignment="Center"/>
<Button Content="작업 취소" Command="{Binding AbortCommand}"
ToolTip="진행 중인 merge / cherry-pick / revert / rebase 를 취소"/>
<Button Content="mergetool 등록" Command="{Binding RegisterMergeToolCommand}" MinWidth="110"
ToolTip="git config 의 merge.tool 로 이 프로그램을 등록 (전역 .gitconfig 수정)"/>
<!-- HKCU 에만 쓰므로 관리자 권한이 필요 없다. 버튼 이름이 지금 상태를 그대로 말한다. -->
<Button Content="{Binding ShellIntegrationLabel}" Command="{Binding ToggleShellIntegrationCommand}"
MinWidth="140"
ToolTip="탐색기에서 파일·폴더를 고르고 [우클릭 → 보내기] 로 이 프로그램에 넘길 수 있게 합니다. 여러 개를 한 번에 골라도 됩니다 (관리자 권한 필요 없음)"/>
</WrapPanel>
<!-- ===== 작업 영역 ===== -->
<TabControl Grid.Row="2" Background="Transparent"
BorderBrush="{DynamicResource Ubisam.Brush.Input.Border}">
<!-- 브랜치 / 병합 -->
<TabItem Header="브랜치 · 병합">
<Grid Margin="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="260"/>
</Grid.ColumnDefinitions>
<ListBox Grid.Column="0" ItemsSource="{Binding Workspace.Branches}"
SelectedItem="{Binding SelectedBranch}" HorizontalContentAlignment="Stretch"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="0,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="★ " FontSize="11"
Foreground="{DynamicResource Ubisam.Brush.Accent}"
Visibility="{Binding IsCurrentHead, Converter={StaticResource BoolToVis}}"/>
<StackPanel Grid.Column="1">
<TextBlock Text="{Binding FriendlyName}"/>
<TextBlock Text="{Binding TipMessageShort}" FontSize="11"
Foreground="{DynamicResource Ubisam.Brush.Muted}"
TextTrimming="CharacterEllipsis"/>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal">
<TextBlock Text="↑" FontSize="11" Foreground="{DynamicResource Ubisam.Brush.Muted}"/>
<TextBlock Text="{Binding AheadBy}" FontSize="11" Margin="0,0,6,0"
Foreground="{DynamicResource Ubisam.Brush.Muted}"/>
<TextBlock Text="↓" FontSize="11" Foreground="{DynamicResource Ubisam.Brush.Muted}"/>
<TextBlock Text="{Binding BehindBy}" FontSize="11"
Foreground="{DynamicResource Ubisam.Brush.Muted}"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Column="1" Margin="8,0,0,0">
<TextBlock Text="선택 브랜치" Style="{StaticResource GroupTitle}"/>
<Button Content="체크아웃" Command="{Binding CheckoutCommand}" HorizontalAlignment="Stretch"/>
<Button Content="Merge → 현재 브랜치로" Command="{Binding MergeCommand}"
HorizontalAlignment="Stretch" MinHeight="32"
Style="{StaticResource FilledButton}"
Background="{DynamicResource Ubisam.Brush.Danger.Fill}"
Foreground="{DynamicResource Ubisam.Brush.Accent.OnFill}"/>
<Button Content="브랜치 삭제" Command="{Binding DeleteBranchCommand}" HorizontalAlignment="Stretch"/>
<TextBlock Text="새 브랜치" Style="{StaticResource GroupTitle}" Margin="0,12,0,4"/>
<TextBox Text="{Binding NewBranchName, UpdateSourceTrigger=PropertyChanged}"
ToolTip="선택 커밋에서 분기. 선택이 없으면 HEAD"/>
<Button Content="생성 + 체크아웃" Command="{Binding CreateBranchCommand}"
HorizontalAlignment="Stretch" Margin="2,4,2,0"/>
<TextBlock Text="rebase" Style="{StaticResource GroupTitle}" Margin="0,12,0,4"/>
<Button Content="선택 브랜치 위로 rebase" Command="{Binding RebaseCommand}" HorizontalAlignment="Stretch"/>
<UniformGrid Columns="2">
<Button Content="계속" Command="{Binding RebaseContinueCommand}"/>
<Button Content="중단" Command="{Binding RebaseAbortCommand}"/>
</UniformGrid>
</StackPanel>
</Grid>
</TabItem>
<!-- 충돌 -->
<TabItem Header="충돌">
<Grid Margin="6">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 진행 한 줄(6차 C-1): "남은 충돌 N개 · 푼 것 M개 (M/전체)". 병합이 아니면 비어 접힌다. -->
<StackPanel Grid.Row="0" Margin="0,0,0,6">
<TextBlock Text="{Binding Workspace.ConflictProgressText}" FontWeight="Bold" Margin="0,0,0,2"
Visibility="{Binding Workspace.IsOperationInProgress, Converter={StaticResource BoolToVis}}"/>
<TextBlock Style="{StaticResource Muted}" TextWrapping="Wrap"
Text="파일 전체를 한쪽으로 채택하려면 아래 버튼을 쓰고, 줄마다 고르려면 목록을 두 번 누르거나 [3분할로 열기] 를 누르세요. 파일 비교 탭에서 세 칸으로 열립니다. Ctrl · Shift 로 여러 파일을 고르면 Accept 가 한 번에 적용됩니다(Ctrl+A 전부)."/>
</StackPanel>
<!-- 두 번 누르거나 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>
<KeyBinding Key="Enter" Command="{Binding OpenConflictInCompareCommand}"/>
</ListBox.InputBindings>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path}" TextTrimming="CharacterEllipsis"/>
<!-- 충돌 종류(6차 C-1): 양쪽 수정 · 상대 삭제 · 내가 삭제 · 양쪽 새 파일(+ 이진).
한쪽이 지운 것은 정할 것이 "남길까 지울까" 라 경고 색으로 띄운다. -->
<TextBlock Text="{Binding KindLabel, StringFormat={} · {0}}" FontSize="11" VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Muted}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsDeletion}" Value="True">
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Warning}"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsBinary}" Value="True">
<Setter Property="Foreground" Value="{DynamicResource Ubisam.Brush.Danger}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<WrapPanel Grid.Row="2" Margin="0,6,0,0">
<Button Content="3분할로 열기" Command="{Binding OpenConflictInCompareCommand}" MinWidth="100"
ToolTip="고른 파일을 파일 비교 탭에서 세 칸으로 엽니다. 목록을 두 번 누르거나 Enter 를 눌러도 됩니다"/>
<Button Content="Accept Mine" Command="{Binding AcceptMineCommand}" MinWidth="100" Style="{StaticResource FilledButton}"
Background="{DynamicResource Ubisam.Brush.Success}"
Foreground="{DynamicResource Ubisam.Brush.Accent.OnFill}"/>
<Button Content="Accept Theirs" Command="{Binding AcceptTheirsCommand}" MinWidth="100" Style="{StaticResource FilledButton}"
Background="{DynamicResource Ubisam.Brush.Accent.Fill}"
Foreground="{DynamicResource Ubisam.Brush.Accent.OnFill}"/>
<Button Content="Accept Both" Command="{Binding AcceptBothCommand}" MinWidth="100" Style="{StaticResource FilledButton}"
ToolTip="내 파일 다음에 상대 파일을 통째로 이어 붙입니다. 누르면 먼저 묻습니다"
Background="{DynamicResource Ubisam.Brush.Warning}"
Foreground="{DynamicResource Ubisam.Brush.Accent.OnFill}"/>
<!-- 작업마다 끝내는 법이 다르다 — 병합 커밋 · 계속(rebase) · 커밋(cherry-pick · revert)(7차 C-2). -->
<Button Content="{Binding FinishOperationLabel}" Command="{Binding FinishOperationCommand}" MinWidth="100"
ToolTip="충돌을 다 푼 작업을 끝냅니다. 병합은 병합 커밋, rebase 는 다음 단계로, cherry-pick · revert 는 원래 메시지로 커밋"/>
<TextBlock Text="{Binding ConflictSelectionText}" Style="{StaticResource Muted}"
VerticalAlignment="Center" Margin="8,0,0,0"/>
</WrapPanel>
<!-- 이번 병합에서 푼 파일(11차 F-8). [Save](해결로 저장)는 다음 충돌로 넘어가 편집기 Undo 로 돌아올 수 없다 - 여기서 다시 충돌로 되돌린다.
병합 중이고 푼 파일이 있을 때만 보인다. -->
<Grid Grid.Row="3" Margin="0,10,0,0">
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<DataTrigger Binding="{Binding Workspace.ResolvedConflicts.Count}" Value="0">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.ColumnSpan="2" Style="{StaticResource Muted}" Margin="0,0,0,4"
Text="{Binding Workspace.ResolvedConflicts.Count, StringFormat=이번 병합에서 푼 파일 {0}개 - 잘못 풀었으면 골라 [다시 충돌로]}"/>
<ListBox Grid.Row="1" ItemsSource="{Binding Workspace.ResolvedConflicts}" MaxHeight="120"
SelectedItem="{Binding SelectedResolvedConflict}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
AutomationProperties.Name="푼 파일"/>
<Button Grid.Row="1" Grid.Column="1" Content="다시 충돌로" Command="{Binding ReopenConflictCommand}"
MinWidth="100" Margin="6,0,0,0" VerticalAlignment="Top"
ToolTip="고른 파일을 푼 것을 되돌려 다시 충돌로 만듭니다. 저장해 둔 해결 내용은 충돌 표시 글로 바뀝니다"/>
</Grid>
</Grid>
</TabItem>
<!-- 변경 / 커밋 -->
<TabItem Header="변경 · 커밋">
<Grid Margin="6">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" ItemsSource="{Binding Workspace.StatusFiles}"
SelectedItem="{Binding SelectedStatusFile}" HorizontalContentAlignment="Stretch"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding StagedState}" Width="80" FontSize="11"
Foreground="{DynamicResource Ubisam.Brush.Success}"/>
<TextBlock Text="{Binding WorkingDirState}" Width="80" FontSize="11"
Foreground="{DynamicResource Ubisam.Brush.Warning}"/>
<TextBlock Text="{Binding Path}" FontSize="11" TextTrimming="CharacterEllipsis"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="1" Margin="0,6,0,0">
<WrapPanel>
<Button Content="스테이징" Command="{Binding StageSelectedCommand}"/>
<Button Content="언스테이징" Command="{Binding UnstageSelectedCommand}"/>
<Button Content="변경 취소" Command="{Binding RestoreSelectedCommand}"
ToolTip="git checkout -- &lt;file&gt;"/>
<Button Content="전체 스테이징" Command="{Binding StageAllCommand}" MinWidth="96"/>
<Button Content="stash 저장" Command="{Binding StashSaveCommand}"/>
<Button Content="blame" Command="{Binding BlameSelectedFileCommand}"
ToolTip="고른 파일을 'Blame' 탭으로 보냅니다"/>
</WrapPanel>
<!-- 치워 둔 작업(stash). 목록이 없으면 맨 위 것만 꺼낼 수 있어
"어제 치워 둔 것"에 손이 닿지 않는다. -->
<Grid Margin="0,6,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="stash" Style="{StaticResource Muted}"
VerticalAlignment="Center" Margin="0,0,6,0"/>
<ComboBox Grid.Column="1" ItemsSource="{Binding Workspace.Stashes}"
SelectedItem="{Binding SelectedStash}"
ToolTip="치워 둔 작업 목록. 새 것이 위에 옵니다">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Index}" FontFamily="Consolas" FontSize="11"
Foreground="{DynamicResource Ubisam.Brush.Warning}"
Margin="0,0,8,0"/>
<TextBlock Text="{Binding Message}" FontSize="11"
TextTrimming="CharacterEllipsis"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Grid.Column="2" Content="적용" Command="{Binding StashApplySelectedCommand}"
ToolTip="고른 stash 를 꺼내 적용합니다 (목록에서 사라집니다)"/>
<Button Grid.Column="3" Content="삭제" Command="{Binding StashDropSelectedCommand}"
ToolTip="고른 stash 를 버립니다"/>
</Grid>
<Grid Margin="0,6,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="커밋 메시지" Style="{StaticResource Muted}" Margin="0,0,6,0"/>
<TextBox Grid.Column="1" Text="{Binding CommitMessage, UpdateSourceTrigger=PropertyChanged}"/>
<Button Grid.Column="2" Content="마지막 메시지" Command="{Binding LoadLastMessageCommand}"
ToolTip="고쳐 쓰려는 메시지를 처음부터 다시 치지 않게, 마지막 커밋의 메시지를 가져옵니다"/>
<Button Grid.Column="3" Content="커밋" Command="{Binding CommitCommand}"/>
<!-- 설명은 상황에 따라 바뀐다 - 작업 중이면 왜 꺼졌는지 말한다(11차 C-1) -->
<Button Grid.Column="4" Content="커밋 수정" Command="{Binding AmendCommand}"
ToolTip="{Binding AmendHint}"/>
</Grid>
</StackPanel>
</Grid>
</TabItem>
<!-- 이력 -->
<TabItem Header="이력">
<Grid Margin="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListBox x:Name="HistoryList" Grid.Row="0" Grid.Column="0" ItemsSource="{Binding Workspace.Commits}"
SelectedItem="{Binding SelectedCommit}" HorizontalContentAlignment="Stretch"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<!-- 기획안의 "커밋 우클릭 → 이름 변경"(11차 C-2). 메뉴는 시각 트리 밖이라
목록(PlacementTarget)의 DataContext 를 가져와야 명령이 걸린다. -->
<ListBox.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="메시지 바꾸기..." Command="{Binding RewordCommand}"
ToolTip="{Binding RewordHint}"/>
</ContextMenu>
</ListBox.ContextMenu>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,2">
<TextBlock Text="{Binding MessageShort}" TextTrimming="CharacterEllipsis"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding ShortSha}" FontFamily="Consolas" FontSize="11"
Foreground="{DynamicResource Ubisam.Brush.Warning}"/>
<TextBlock Text=" · " FontSize="11" Foreground="{DynamicResource Ubisam.Brush.Muted}"/>
<TextBlock Text="{Binding AuthorName}" FontSize="11"
Foreground="{DynamicResource Ubisam.Brush.Muted}"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBox Grid.Row="0" Grid.Column="1" Style="{StaticResource Code}" Margin="6,0,0,0"
Text="{Binding DiffText, Mode=OneWay}"/>
<WrapPanel Grid.Row="1" Grid.ColumnSpan="2" Margin="0,6,0,0">
<!-- 끄면 지금 브랜치에서 닿는 커밋만. 켜면 다른 브랜치 커밋도 골라 cherry-pick 할 수 있다(7차 C-2). -->
<CheckBox Content="모든 브랜치" IsChecked="{Binding Workspace.ShowAllBranchesInHistory}"
VerticalAlignment="Center" Margin="0,0,14,0"
ToolTip="다른 브랜치의 커밋까지 이력에 보입니다. cherry-pick 할 커밋을 고를 때 켭니다"/>
<TextBlock Text="선택 커밋 →" Style="{StaticResource Muted}" Margin="0,0,6,0"/>
<Button Content="Revert" Command="{Binding RevertCommand}"
ToolTip="선택 커밋의 변경을 되돌리는 커밋 생성"/>
<Button Content="Cherry-pick" Command="{Binding CherryPickCommand}" MinWidth="90"
ToolTip="선택 커밋만 현재 브랜치로 가져오기"/>
<ComboBox ItemsSource="{Binding ResetKinds}" SelectedItem="{Binding SelectedResetKind}"
Width="80" Margin="8,2,2,2"/>
<Button Content="Reset" Command="{Binding ResetCommand}"
ToolTip="선택 커밋으로 HEAD 이동"/>
<!-- 두 커밋 "사이"를 보려면 시작점을 따로 잡아야 한다.
잡지 않으면 지금까지처럼 부모와 견준다. -->
<Border Width="1" Margin="10,2" Background="{DynamicResource Ubisam.Brush.Input.Border}"/>
<TextBlock Text="{Binding DiffRangeText}" Style="{StaticResource Muted}"
VerticalAlignment="Center" Margin="0,0,6,0"
ToolTip="지금 무엇과 무엇을 견주고 있는지"/>
<Button Content="여기부터" Command="{Binding SetDiffFromCommand}"
ToolTip="고른 커밋을 비교의 시작점으로 잡습니다"/>
<Button Content="해제" Command="{Binding ClearDiffFromCommand}"
ToolTip="다시 '부모와 비교'로 돌아갑니다"/>
<Button Content="변경분 추출" Command="{Binding ExportChangesCommand}" MinWidth="96"
ToolTip="이 범위에서 바뀐 파일만 골라 폴더로 뽑습니다"/>
</WrapPanel>
</Grid>
</TabItem>
<!--
Blame - 줄마다 "누가 언제 이 줄을 만들었나".
머지 중에 "이 줄 왜 이렇게 됐지"를 묻게 되는데, 이력을 뒤지는 것보다 빠르다.
-->
<TabItem Header="Blame">
<Grid Margin="6">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0,0,0,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="파일 경로" Style="{StaticResource Muted}"
VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBox Grid.Column="1" Text="{Binding BlamePath, UpdateSourceTrigger=PropertyChanged}"
ToolTip="저장소 안의 상대 경로 (예: src/MergeHub/AppSetup.cs). '변경 · 커밋' 탭에서 파일을 고르고 [blame] 을 누르면 자동으로 채워집니다"/>
<Button Grid.Column="2" Content="실행" Command="{Binding BlameCommand}"
ToolTip="이력에서 커밋을 골라 두면 그 시점 기준으로 봅니다"/>
</Grid>
<TextBox Grid.Row="1" Style="{StaticResource Code}"
Text="{Binding BlameText, Mode=OneWay}"/>
</Grid>
</TabItem>
<!-- 태그 / 서브모듈 -->
<TabItem Header="태그 · 서브모듈">
<Grid Margin="6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="태그" Style="{StaticResource GroupTitle}"/>
<ListBox Grid.Row="1" Grid.Column="0" Margin="0,0,6,0"
ItemsSource="{Binding Workspace.Tags}" SelectedItem="{Binding SelectedTag}" HorizontalContentAlignment="Stretch"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" FontWeight="Bold"/>
<TextBlock Text=" · " Foreground="{DynamicResource Ubisam.Brush.Muted}"/>
<TextBlock Text="{Binding Message}" FontSize="11"
Foreground="{DynamicResource Ubisam.Brush.Muted}"
TextTrimming="CharacterEllipsis"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="2" Grid.Column="0" Margin="0,6,6,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding TagName, UpdateSourceTrigger=PropertyChanged}"
ToolTip="예) v1.0.0"/>
<TextBox Grid.Column="1" Margin="4,0,0,0"
Text="{Binding TagMessage, UpdateSourceTrigger=PropertyChanged}"
ToolTip="메시지를 넣으면 annotated tag"/>
<Button Grid.Column="2" Content="생성" Command="{Binding CreateTagCommand}"/>
</Grid>
<Button Content="선택 태그 삭제" Command="{Binding DeleteTagCommand}" HorizontalAlignment="Left" Margin="2,4,2,0"/>
</StackPanel>
<TextBlock Grid.Row="0" Grid.Column="1" Text="서브모듈" Style="{StaticResource GroupTitle}"/>
<ListBox Grid.Row="1" Grid.Column="1" ItemsSource="{Binding Workspace.Submodules}"
HorizontalContentAlignment="Stretch"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path}" TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding Status}" FontSize="11" TextTrimming="CharacterEllipsis"
Foreground="{DynamicResource Ubisam.Brush.Muted}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Grid.Row="2" Grid.Column="1" Content="서브모듈 업데이트 (--init)"
Command="{Binding UpdateSubmodulesCommand}" HorizontalAlignment="Left" Margin="2,6,2,0"
MinWidth="180"/>
</Grid>
</TabItem>
</TabControl>
<!-- ===== 로그 ===== -->
<TextBox Grid.Row="3" Style="{StaticResource Code}" Margin="0,6,0,0"
Text="{Binding Workspace.Log, Mode=OneWay}" TextChanged="Log_TextChanged"/>
</Grid>
</UserControl>