훑는 중인 목록을 GitWorkspace 에 두고 파일 비교 화면에서 이전/다음으로 딛는다. 화면에 보이는 순서를 따르고, 찾은 줄 목록도 같은 방식으로 훑는다. 먼저 묻고 자리를 옮긴다. Ctrl+PageUp/PageDown.
591 lines
26 KiB
C#
591 lines
26 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using MergeHub.Models;
|
|
using UbisamBase.Core.Logging;
|
|
using UbisamBase.Core.Messaging;
|
|
using UbisamBase.Core.SettingItems;
|
|
|
|
namespace MergeHub.Services
|
|
{
|
|
/// <summary>
|
|
/// 줄 단위로 견줄 파일 한 짝. 폴더 비교가 파일 비교로 넘기는 목록의 단위다.
|
|
/// </summary>
|
|
public sealed class ComparePair
|
|
{
|
|
public ComparePair(string left, string right, string name)
|
|
{
|
|
Left = left ?? string.Empty;
|
|
Right = right ?? string.Empty;
|
|
Name = name ?? string.Empty;
|
|
}
|
|
|
|
public string Left { get; }
|
|
public string Right { get; }
|
|
|
|
/// <summary>화면에 적을 이름. 대개 상대 경로다.</summary>
|
|
public string Name { get; }
|
|
}
|
|
/// <summary>
|
|
/// 홈(머지툴) 탭과 Git 탭이 함께 쓰는 저장소 상태.
|
|
/// IGitService 는 스레드 안전하지 않으므로 모든 호출을 여기서 한 줄로 직렬화한다.
|
|
/// DI 싱글턴으로 등록되어 두 화면이 같은 인스턴스를 본다.
|
|
/// </summary>
|
|
public sealed partial class GitWorkspace : ObservableObject
|
|
{
|
|
private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1);
|
|
private readonly SynchronizationContext? _uiContext;
|
|
private readonly Logger _logger = new Logger("MergeHub");
|
|
|
|
/// <summary>플랫폼에 등록된 사용자. 머지 주석에 남길 이름을 여기서 가져온다.</summary>
|
|
private readonly UbisamBase.Core.Settings.CurrentUserService? _currentUser;
|
|
|
|
public GitWorkspace(IGitService git, ISettingsService settingsService,
|
|
UbisamBase.Core.Settings.CurrentUserService currentUser)
|
|
{
|
|
Git = git;
|
|
_currentUser = currentUser;
|
|
_uiContext = SynchronizationContext.Current;
|
|
Settings = settingsService.RegisterSettings(nameof(MergeHubSettings), new MergeHubSettings());
|
|
|
|
RepositoryPath = Settings.DefaultRepositoryPath;
|
|
RemoteName = string.IsNullOrWhiteSpace(Settings.DefaultRemoteName) ? "origin" : Settings.DefaultRemoteName;
|
|
RemoteUrl = Settings.GiteaUrl;
|
|
UserName = Settings.GiteaUserName;
|
|
|
|
Git.IgnoreWhitespace = Settings.IgnoreWhitespace;
|
|
Git.WordLevelDiff = Settings.WordLevelDiff;
|
|
|
|
// 작성자 이름은 여기서 한 번 정해 둔다.
|
|
// 저장소를 읽을 때(ReadState)만 정하면 Git 없이 파일 두 개를 비교할 때 이름이 비고,
|
|
// 그러면 주석에 등록한 이름 대신 윈도우 계정이 찍힌다.
|
|
AuthorName = ResolveAuthorName();
|
|
|
|
// 최근 목록은 켤 때 한 번 읽고, 바뀔 때마다 바로 쓴다.
|
|
// 프로그램이 갑자기 꺼져도 목록이 남게 하려는 것이다 - 몇 줄짜리 글이라 쓰는 값이 싸다.
|
|
Recent.Load(RecentPath);
|
|
Recent.Changed += (_, _) => Recent.Save(RecentPath);
|
|
|
|
// `git mergetool` · 탐색기 우클릭이 넘긴 것을 화면으로 보낸다.
|
|
// 여기서 하는 이유 - 이 객체는 첫 화면이 만들어질 때 함께 만들어지므로
|
|
// 요청을 남겨 둘 자리로 가장 이르다.
|
|
RouteStartupArgs();
|
|
|
|
// 설정 화면에서 이름을 바꾸면 바로 따라간다.
|
|
if (_currentUser is not null)
|
|
{
|
|
_currentUser.PropertyChanged += (_, e) =>
|
|
{
|
|
if (e.PropertyName == nameof(_currentUser.UserName)) AuthorName = ResolveAuthorName();
|
|
};
|
|
}
|
|
|
|
Git.Progress += (_, message) =>
|
|
{
|
|
_logger.I(message);
|
|
OnUi(() => AppendLog(message));
|
|
};
|
|
}
|
|
|
|
public IGitService Git { get; }
|
|
|
|
/// <summary>
|
|
/// 최근에 견준 짝. 파일 비교와 폴더 비교가 함께 본다.
|
|
///
|
|
/// 여기에 두는 까닭 - 두 화면이 서로를 모르는 채로 같은 목록을 봐야 하고,
|
|
/// 이 객체가 이미 그 통로 노릇을 하고 있다.
|
|
/// </summary>
|
|
public RecentPairs Recent { get; } = new RecentPairs();
|
|
|
|
/// <summary>
|
|
/// 최근 목록을 두는 자리. 플랫폼 설정 폴더 아래에 둔다 - 장비마다 한 자리에 모인다.
|
|
/// <b>설정 화면에는 내놓지 않는다.</b> 사람이 손으로 고칠 값이 아니고, 설정 목록만 어지럽힌다.
|
|
/// </summary>
|
|
private static string RecentPath => Path.Combine(
|
|
UbisamBase.Core.Bootstrap.UbisamAppBuilder.ConfigRoot, "Settings", "MergeHub.Recent.txt");
|
|
|
|
public MergeHubSettings Settings { get; }
|
|
|
|
/// <summary>저장소 상태가 다시 읽힐 때마다 발생. 화면들이 자기 패널을 갱신한다.</summary>
|
|
public event Action? Refreshed;
|
|
|
|
/// <summary>
|
|
/// 작업이 끝나 커맨드의 실행 가능 여부가 달라졌을 수 있음을 알린다.
|
|
/// CommunityToolkit 의 RelayCommand 는 CommandManager 를 구독하지 않아
|
|
/// 각 ViewModel 이 직접 NotifyCanExecuteChanged 를 불러야 한다.
|
|
/// </summary>
|
|
public event Action? StateChanged;
|
|
|
|
// ================= 공유 목록 =================
|
|
|
|
public ObservableCollection<BranchInfo> Branches { get; } = new ObservableCollection<BranchInfo>();
|
|
public ObservableCollection<CommitInfo> Commits { get; } = new ObservableCollection<CommitInfo>();
|
|
|
|
/// <summary>커밋 + 브랜치 그래프 레인. 홈 화면의 커밋 트리가 이걸 바인딩한다.</summary>
|
|
public ObservableCollection<CommitNode> CommitNodes { get; } = new ObservableCollection<CommitNode>();
|
|
public ObservableCollection<FileStatusInfo> StatusFiles { get; } = new ObservableCollection<FileStatusInfo>();
|
|
public ObservableCollection<ConflictInfo> Conflicts { get; } = new ObservableCollection<ConflictInfo>();
|
|
public ObservableCollection<TagInfo> Tags { get; } = new ObservableCollection<TagInfo>();
|
|
public ObservableCollection<SubmoduleInfo> Submodules { get; } = new ObservableCollection<SubmoduleInfo>();
|
|
|
|
/// <summary>치워 둔 작업(stash) 목록. 새 것이 0번이다.</summary>
|
|
public ObservableCollection<StashInfo> Stashes { get; } = new ObservableCollection<StashInfo>();
|
|
|
|
// ================= 공유 상태 =================
|
|
|
|
[ObservableProperty] private string _repositoryPath = string.Empty;
|
|
[ObservableProperty] private string _remoteUrl = string.Empty;
|
|
[ObservableProperty] private string _remoteName = "origin";
|
|
[ObservableProperty] private string _userName = string.Empty;
|
|
[ObservableProperty] private string _password = string.Empty;
|
|
[ObservableProperty] private string _repositoryName = "-";
|
|
[ObservableProperty] private string _currentBranch = "-";
|
|
[ObservableProperty] private string _headSha = "-";
|
|
[ObservableProperty] private string _workingTreeSummary = "저장소 없음";
|
|
[ObservableProperty] private bool _isWorkingTreeClean = true;
|
|
[ObservableProperty] private bool _isRepositoryOpen;
|
|
[ObservableProperty] private bool _isRemoteConnected;
|
|
|
|
/// <summary>원격 등록 여부만 나타낸다 - 서버 접속 확인은 Fetch 를 눌러야 이뤄진다.</summary>
|
|
[ObservableProperty] private string _remoteStatusText = "원격 없음";
|
|
|
|
/// <summary>현재 원격의 주소(clone/push 대상).</summary>
|
|
[ObservableProperty] private string _remoteUrlText = "-";
|
|
|
|
/// <summary>추적 브랜치와 비교한 앞섬/뒤처짐. 마지막 fetch 시점 기준이다.</summary>
|
|
[ObservableProperty] private string _syncStatusText = "-";
|
|
|
|
/// <summary>추적 브랜치와 완전히 같은 상태인지.</summary>
|
|
[ObservableProperty] private bool _isUpToDate;
|
|
|
|
[ObservableProperty] private string _lastFetchText = "fetch 이력 없음";
|
|
|
|
/// <summary>머지 주석에 남길 작성자. 설정값 > git config user.name > 윈도우 계정 순.</summary>
|
|
[ObservableProperty] private string _authorName = Environment.UserName;
|
|
|
|
[ObservableProperty] private bool _isMergeInProgress;
|
|
[ObservableProperty] private bool _isRebaseInProgress;
|
|
[ObservableProperty] private string _pendingOperationText = string.Empty;
|
|
[ObservableProperty] private CommitInfo? _selectedCommit;
|
|
[ObservableProperty] private BranchInfo? _selectedBranch;
|
|
[ObservableProperty] private string _log = string.Empty;
|
|
|
|
// ================= 실행 =================
|
|
|
|
/// <summary>
|
|
/// Git 호출 하나를 백그라운드에서 직렬 실행한다.
|
|
/// 실패하면 로그 + Error 토스트를 띄우고 false 를 돌려준다.
|
|
/// </summary>
|
|
public async Task<bool> RunAsync(string operation, Action action)
|
|
{
|
|
// 이미 다른 Git 작업이 도는 중이면 겹쳐 실행하지 않는다.
|
|
// 조용히 사라지면 원인을 못 찾으니 반드시 로그를 남긴다.
|
|
if (!await _gate.WaitAsync(0))
|
|
{
|
|
_logger.W($"[{operation}] 다른 작업 진행 중이라 건너뜀");
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
await Task.Run(action);
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.E($"[{operation}] 실패", ex);
|
|
OnUi(() =>
|
|
{
|
|
AppendLog($"[{operation}] 실패: {ex.Message}");
|
|
MessageUtil.ShowErrorToast($"{operation} 실패: {ex.Message}");
|
|
});
|
|
return false;
|
|
}
|
|
finally
|
|
{
|
|
_gate.Release();
|
|
OnUi(() => StateChanged?.Invoke());
|
|
}
|
|
}
|
|
|
|
/// <summary>작업 하나를 실행하고 성공하면 저장소 상태를 다시 읽는다.</summary>
|
|
public async Task<bool> RunAndRefreshAsync(string operation, Action action)
|
|
{
|
|
var ok = await RunAsync(operation, action);
|
|
if (ok) await RefreshAsync();
|
|
return ok;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 기본 경로가 Git 저장소로 보이는지. 워크트리는 .git 이 파일이라 둘 다 본다.
|
|
///
|
|
/// 저장소가 아닌 폴더에 Open 을 걸면 실패 알림이 뜰 뿐 아니라, 그동안 작업 게이트를
|
|
/// 쥐고 있어서 뒤이어 시작하는 "파일 두 개 비교"까지 통째로 취소된다.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <b>고른 자리에서 위로 거슬러 올라가 찾는다.</b> <c>.git</c> 은 최상위에만 있는데
|
|
/// 사람은 지금 보고 있던 깊은 폴더를 고르기 마련이라, 그 자리만 보면
|
|
/// 멀쩡한 저장소를 두고 "저장소가 아니다"라고 하게 된다.
|
|
/// </remarks>
|
|
public bool LooksLikeRepository => PathUtil.FindRepositoryRoot(RepositoryPath).Length > 0;
|
|
|
|
/// <summary>설정에 기본 저장소 경로가 있으면 시작하자마자 연다.</summary>
|
|
public Task TryAutoOpenAsync()
|
|
{
|
|
if (IsRepositoryOpen || string.IsNullOrWhiteSpace(RepositoryPath))
|
|
return Task.CompletedTask;
|
|
|
|
var root = PathUtil.FindRepositoryRoot(RepositoryPath);
|
|
if (root.Length == 0) return Task.CompletedTask;
|
|
|
|
// 하위 폴더가 저장돼 있었으면 최상위로 바로잡아 둔다 - 화면의 경로 칸도 함께 맞는다.
|
|
RepositoryPath = root;
|
|
|
|
return RunAndRefreshAsync("저장소 자동 열기", () => Git.Open(root));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 상태를 다시 읽고, **게이트를 놓은 뒤에** Refreshed 를 발생시킨다.
|
|
/// 게이트를 쥔 채로 알리면 구독자가 거는 Git 호출이 전부 버려진다.
|
|
/// </summary>
|
|
public async Task RefreshAsync()
|
|
{
|
|
if (!await RunAsync("새로고침", ReadState)) return;
|
|
|
|
OnUi(() =>
|
|
{
|
|
Refreshed?.Invoke();
|
|
StateChanged?.Invoke();
|
|
});
|
|
}
|
|
|
|
private void ReadState()
|
|
{
|
|
if (!Git.IsOpen) return;
|
|
|
|
var info = Git.GetRepositoryInfo();
|
|
var branches = Git.GetBranches();
|
|
var commits = Git.GetCommits(maxCount: Settings.CommitPageSize <= 0 ? 200 : Settings.CommitPageSize);
|
|
var nodes = CommitGraphBuilder.Build(commits);
|
|
var status = Git.GetStatus();
|
|
var conflicts = Git.GetConflicts();
|
|
var tags = Git.GetTags();
|
|
var submodules = Git.GetSubmodules();
|
|
var stashes = Git.GetStashes();
|
|
var rebase = Git.GetRebaseStatus();
|
|
var remotes = Git.GetRemotes();
|
|
var mergeInProgress = Git.IsMergeInProgress;
|
|
|
|
var remote = remotes.FirstOrDefault(r => r.Name == RemoteName) ?? remotes.FirstOrDefault();
|
|
var head = branches.FirstOrDefault(b => b.IsCurrentHead);
|
|
var author = ResolveAuthorName();
|
|
|
|
OnUi(() =>
|
|
{
|
|
RepositoryPath = info.WorkingDirectory;
|
|
RepositoryName = GetRepositoryName(info.WorkingDirectory);
|
|
CurrentBranch = info.CurrentBranchName ?? "(detached)";
|
|
HeadSha = string.IsNullOrEmpty(info.HeadSha) ? "-" : info.HeadSha!.Substring(0, 7);
|
|
IsRepositoryOpen = true;
|
|
|
|
IsRemoteConnected = remotes.Count > 0;
|
|
RemoteStatusText = remotes.Count > 0 ? $"원격 {remotes.Count}개" : "원격 없음";
|
|
RemoteUrlText = remote?.Url ?? "-";
|
|
AuthorName = author;
|
|
|
|
(SyncStatusText, IsUpToDate) = DescribeSync(head);
|
|
|
|
IsMergeInProgress = mergeInProgress;
|
|
IsRebaseInProgress = rebase.IsInProgress;
|
|
PendingOperationText =
|
|
rebase.IsInProgress ? $"rebase {rebase.CurrentStep}/{rebase.TotalSteps}" :
|
|
mergeInProgress ? "병합 진행 중" : string.Empty;
|
|
|
|
IsWorkingTreeClean = status.IsClean;
|
|
WorkingTreeSummary = status.IsClean ? "Clean" : $"{status.Files.Count} files";
|
|
|
|
Replace(Branches, branches);
|
|
Replace(Commits, commits);
|
|
Replace(CommitNodes, nodes);
|
|
Replace(StatusFiles, status.Files);
|
|
Replace(Conflicts, conflicts);
|
|
Replace(Tags, tags);
|
|
Replace(Submodules, submodules);
|
|
Replace(Stashes, stashes);
|
|
|
|
if (SelectedCommit is null && Commits.Count > 0) SelectedCommit = Commits[0];
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 추적 브랜치와 몇 개 차이 나는지 문장으로 만든다.
|
|
/// 이 수치는 **마지막 fetch 시점** 기준이라, fetch 전에는 서버가 앞서 있어도 "최신"으로 보인다.
|
|
/// </summary>
|
|
private static (string Text, bool UpToDate) DescribeSync(BranchInfo? head)
|
|
{
|
|
if (head is null) return ("브랜치 없음", false);
|
|
if (head.TrackedBranch is null) return ("추적 브랜치 없음", false);
|
|
|
|
var ahead = head.AheadBy ?? 0;
|
|
var behind = head.BehindBy ?? 0;
|
|
|
|
if (ahead == 0 && behind == 0) return ($"최신 ({head.TrackedBranch})", true);
|
|
if (ahead > 0 && behind > 0) return ($"갈라짐 ↑{ahead} ↓{behind}", false);
|
|
if (behind > 0) return ($"뒤처짐 ↓{behind} - pull 필요", false);
|
|
return ($"앞섬 ↑{ahead} - push 필요", false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// MergeHub 설정 > 플랫폼에 등록된 사용자 > git config user.name > 윈도우 계정 순으로 고른다.
|
|
///
|
|
/// 플랫폼 사용자를 Git 설정보다 앞에 두는 이유 - 처음 켤 때 사람이 직접 적어 넣은 이름이고,
|
|
/// 장비를 여럿이 쓰는 현장에서는 그 이름이 실제로 작업한 사람이다.
|
|
/// MergeHub 설정을 더 앞에 두는 것은 "이 도구에서만 다르게 남기고 싶다"는 뜻이라 존중한다.
|
|
/// </summary>
|
|
private string ResolveAuthorName()
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(Settings.MergeCommentAuthor))
|
|
return Settings.MergeCommentAuthor.Trim();
|
|
|
|
if (_currentUser?.HasName == true) return _currentUser.UserName.Trim();
|
|
|
|
try
|
|
{
|
|
var configured = Git.GetConfig("user.name");
|
|
if (!string.IsNullOrWhiteSpace(configured)) return configured!.Trim();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// 설정을 못 읽는 건 치명적이지 않다 - 윈도우 계정으로 넘어간다.
|
|
}
|
|
|
|
return Environment.UserName;
|
|
}
|
|
|
|
/// <summary>fetch 가 실제로 성공한 시각을 기록한다. 동기화 표시가 언제 기준인지 알리기 위함.</summary>
|
|
public void MarkFetched()
|
|
{
|
|
var now = DateTime.Now;
|
|
OnUi(() => LastFetchText = $"마지막 fetch {now:HH:mm:ss}");
|
|
}
|
|
|
|
private static string GetRepositoryName(string workingDirectory)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(workingDirectory)) return "-";
|
|
var trimmed = workingDirectory.TrimEnd('\\', '/');
|
|
var slash = trimmed.LastIndexOfAny(new[] { '\\', '/' });
|
|
return slash >= 0 ? trimmed.Substring(slash + 1) : trimmed;
|
|
}
|
|
|
|
public void ApplyCredentials()
|
|
{
|
|
Git.Credentials = string.IsNullOrWhiteSpace(UserName)
|
|
? null
|
|
: new GitCredentials(UserName, Password);
|
|
}
|
|
|
|
public void AppendLog(string message) =>
|
|
Log += $"[{DateTime.Now:HH:mm:ss}] {message}{Environment.NewLine}";
|
|
|
|
/// <summary>
|
|
/// "이 두 파일을 줄 단위로 비교해 달라" - 폴더 비교 탭이 보내고 파일 비교 탭이 받는다.
|
|
///
|
|
/// 두 화면이 서로를 직접 알지 않도록 이미 둘 다 들고 있는 이 싱글턴을 통로로 쓴다.
|
|
/// </summary>
|
|
public event Action<string, string>? PlainCompareRequested;
|
|
|
|
/// <summary>아직 아무도 받지 않은 요청. 아래 설명 참고.</summary>
|
|
private (string Left, string Right)? _pendingPlainCompare;
|
|
|
|
public bool HasPendingPlainCompare => _pendingPlainCompare is not null;
|
|
|
|
/// <summary>
|
|
/// 요청을 남겨 두고 받을 사람이 있으면 바로 넘긴다.
|
|
///
|
|
/// 남겨 두는 것이 핵심이다. 탭 내용은 <b>그 탭을 처음 열 때</b> 만들어지므로,
|
|
/// 폴더 비교에서 파일을 처음 더블클릭하는 순간에는 파일 비교 화면이 아직 없어
|
|
/// 이벤트를 받을 사람이 없다. 그냥 발생시키면 요청이 허공에 사라지고,
|
|
/// 새로 만들어진 화면은 기본 파일을 연다 - "더블클릭이 안 먹는다"로 보이는 이유다.
|
|
/// </summary>
|
|
/// <param name="line">
|
|
/// 열자마자 짚어 줄 줄 번호. 0 이면 여느 때처럼 첫 차이로 간다.
|
|
/// 폴더 전체 검색에서 결과를 눌렀을 때, 파일만 열리고 맨 위에 서 있으면
|
|
/// 찾은 줄을 눈으로 다시 찾아야 한다.
|
|
/// </param>
|
|
/// <param name="lineIsLeft">그 번호가 왼쪽 파일 기준인지. 아니면 오른쪽 기준이다.</param>
|
|
public void RequestPlainCompare(string leftFile, string rightFile, int line = 0, bool lineIsLeft = true)
|
|
{
|
|
_pendingPlainCompare = (leftFile, rightFile);
|
|
RequestedLine = line;
|
|
RequestedLineIsLeft = lineIsLeft;
|
|
OnUi(DeliverPendingPlainCompare);
|
|
}
|
|
|
|
// ================= 폴더 비교에서 훑는 중인 파일 목록 =================
|
|
|
|
/// <summary>
|
|
/// 폴더 비교가 넘긴 "지금 훑는 중인 파일들". 파일 비교 화면의 이전/다음 파일이 이걸 딛는다.
|
|
///
|
|
/// <b>화면에 보이는 순서 그대로</b> 담는다 - 필터로 "코드가 다른 것만" 을 걸어 뒀으면
|
|
/// 그 안에서만 움직여야 한다. 목록에 없는 파일로 넘어가면 왜 거기로 갔는지 알 수 없다.
|
|
/// </summary>
|
|
public IReadOnlyList<ComparePair> Queue { get; private set; } = Array.Empty<ComparePair>();
|
|
|
|
/// <summary>지금 몇 번째를 보고 있는지. 목록이 없으면 -1.</summary>
|
|
public int QueueIndex { get; set; } = -1;
|
|
|
|
/// <summary>목록이나 자리가 바뀌었다.</summary>
|
|
public event EventHandler? QueueChanged;
|
|
|
|
public void SetQueue(IReadOnlyList<ComparePair> queue, int index)
|
|
{
|
|
Queue = queue ?? Array.Empty<ComparePair>();
|
|
QueueIndex = index;
|
|
OnUi(() => QueueChanged?.Invoke(this, EventArgs.Empty));
|
|
}
|
|
|
|
/// <summary>목록을 비운다. 폴더를 거치지 않고 연 파일은 딛고 갈 자리가 없다.</summary>
|
|
public void ClearQueue() => SetQueue(Array.Empty<ComparePair>(), -1);
|
|
|
|
public void RaiseQueueChanged() => OnUi(() => QueueChanged?.Invoke(this, EventArgs.Empty));
|
|
|
|
/// <summary>넘긴 파일에서 짚어 줄 자리. 없으면 0.</summary>
|
|
public int RequestedLine { get; private set; }
|
|
|
|
/// <summary>그 번호가 왼쪽 파일 기준인지.</summary>
|
|
public bool RequestedLineIsLeft { get; private set; }
|
|
|
|
/// <summary>
|
|
/// 짚어 줄 자리를 한 번 쓰고 지운다.
|
|
/// 지우지 않으면 그 다음에 연 파일까지 같은 줄로 끌려간다.
|
|
/// </summary>
|
|
public (int Line, bool IsLeft) TakeRequestedLine()
|
|
{
|
|
var one = (RequestedLine, RequestedLineIsLeft);
|
|
RequestedLine = 0;
|
|
return one;
|
|
}
|
|
|
|
/// <summary>남아 있는 요청을 넘긴다. 화면이 새로 만들어지면 스스로 한 번 불러 준다.</summary>
|
|
public void DeliverPendingPlainCompare()
|
|
{
|
|
if (_pendingPlainCompare is not { } pending) return;
|
|
if (PlainCompareRequested is null) return;
|
|
|
|
_pendingPlainCompare = null;
|
|
PlainCompareRequested.Invoke(pending.Left, pending.Right);
|
|
}
|
|
|
|
// ================= 켤 때 넘어온 인자 =================
|
|
|
|
/// <summary>폴더 비교 화면이 아직 없을 때 남겨 두는 요청. 파일 비교 쪽과 같은 방식이다.</summary>
|
|
private (string Left, string Right)? _pendingFolderCompare;
|
|
|
|
public void RequestFolderCompare(string left, string right)
|
|
{
|
|
_pendingFolderCompare = (left, right);
|
|
OnUi(DeliverPendingFolderCompare);
|
|
}
|
|
|
|
public void DeliverPendingFolderCompare()
|
|
{
|
|
if (_pendingFolderCompare is not { } pending) return;
|
|
if (FolderCompareRequested is null) return;
|
|
|
|
_pendingFolderCompare = null;
|
|
FolderCompareRequested.Invoke(pending.Left, pending.Right);
|
|
}
|
|
|
|
public event Action<string, string>? FolderCompareRequested;
|
|
|
|
/// <summary>
|
|
/// `git mergetool` · `git difftool` · 탐색기 우클릭이 넘긴 것을 화면으로 보낸다.
|
|
///
|
|
/// 창이 다 만들어진 뒤에 탭을 옮겨야 한다 - 그 전에는 옮길 곳이 아직 없어 요청이 사라진다.
|
|
/// 요청 자체는 남겨 두므로(pending) 늦게 만들어진 화면도 스스로 집어 간다.
|
|
/// </summary>
|
|
private void RouteStartupArgs()
|
|
{
|
|
var args = StartupArgs.Current;
|
|
|
|
if (args.WantsFolder)
|
|
{
|
|
RequestFolderCompare(args.LeftFolder, args.RightFolder);
|
|
ActivateWhenReady(MergeHub.ViewIds.Folder);
|
|
AppendLog($"탐색기에서 폴더 비교 요청: {args.LeftFolder}");
|
|
return;
|
|
}
|
|
|
|
if (!args.WantsFiles) return;
|
|
|
|
var (left, right) = args.DiffPair;
|
|
RequestPlainCompare(left, right);
|
|
ActivateWhenReady(MergeHub.ViewIds.Home);
|
|
|
|
AppendLog(args.Mode == StartupMode.Merge
|
|
? $"mergetool 로 호출됨: {args.Merged}"
|
|
: $"파일 비교 요청: {left} ↔ {right}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 탐색기에서 끌어다 놓은 것을 화면으로 보낸다.
|
|
///
|
|
/// 명령줄로 들어오는 길과 <b>같은 판단</b>을 쓴다 - 폴더면 폴더 비교, 파일이면 파일 비교.
|
|
/// 두 길이 다르게 굴면 "우클릭으로 열 때랑 끌어다 놓을 때가 다르다"가 되어 설명할 수 없다.
|
|
/// </summary>
|
|
public void RouteDroppedPaths(IReadOnlyList<string> paths)
|
|
{
|
|
if (paths is null || paths.Count == 0) return;
|
|
|
|
var args = StartupArgs.Parse(paths);
|
|
|
|
if (args.WantsFolder)
|
|
{
|
|
RequestFolderCompare(args.LeftFolder, args.RightFolder);
|
|
ActivateWhenReady(MergeHub.ViewIds.Folder);
|
|
AppendLog($"끌어다 놓기 - 폴더 비교: {args.LeftFolder}");
|
|
return;
|
|
}
|
|
|
|
if (!args.WantsFiles) return;
|
|
|
|
var (left, right) = args.DiffPair;
|
|
RequestPlainCompare(left, right);
|
|
ActivateWhenReady(MergeHub.ViewIds.Home);
|
|
AppendLog($"끌어다 놓기 - 파일 비교: {left} ↔ {right}");
|
|
}
|
|
|
|
private static void ActivateWhenReady(string viewId)
|
|
{
|
|
var app = System.Windows.Application.Current;
|
|
if (app is null) return;
|
|
|
|
app.Dispatcher.BeginInvoke(
|
|
new Action(() => UbisamBase.Core.Modules.TabManager.Activate(viewId)),
|
|
System.Windows.Threading.DispatcherPriority.ApplicationIdle);
|
|
}
|
|
|
|
/// <summary>ObservableCollection 변경은 UI 스레드에서만 허용된다.</summary>
|
|
public void OnUi(Action action)
|
|
{
|
|
if (_uiContext is null || SynchronizationContext.Current == _uiContext)
|
|
{
|
|
action();
|
|
return;
|
|
}
|
|
_uiContext.Send(_ => action(), null);
|
|
}
|
|
|
|
private static void Replace<T>(ObservableCollection<T> target, IEnumerable<T> items)
|
|
{
|
|
target.Clear();
|
|
foreach (var item in items) target.Add(item);
|
|
}
|
|
}
|
|
}
|