밖에서 부르기: 명령줄 인자 수신 · 탐색기 우클릭 등록
git config 에 mergetool/difftool 로 등록하는 쪽은 전부터 있었는데 정작 그 인자를 받는 쪽이 없어 다리가 반만 놓여 있었다. 나머지 반쪽을 놓는다. - Services/StartupArgs: 켤 때 넘어온 인자를 읽는다. 이름표 붙은 것(--merge --base B --local L --remote R --merged M)과 그냥 넘어온 경로 둘 다 받는다. 탐색기는 "%1" 하나만 주므로 후자가 그 자리다. 경로가 폴더면 폴더 비교로, 파일이면 파일 비교로 보낸다. difftool 의 LOCAL/REMOTE 와 탐색기의 "먼저 고른 것/나중 것" 은 뜻이 달라 따로 담는다 - 이 도구의 왼쪽은 늘 "기준"이라 difftool 에서는 REMOTE 가 왼쪽이 된다. - Services/ShellIntegration: 탐색기 우클릭 메뉴를 파일·폴더·폴더 배경 세 자리에 넣는다. HKEY_CURRENT_USER 에만 쓰므로 관리자 권한이 필요 없고, 지울 때도 이 사람 것만 지운다 - 사내 PC 에 권한 없이 깔리는 것이 전제다. 셸 확장 DLL 을 만들지 않은 것도 같은 이유로, DLL 은 탐색기 프로세스 안에서 돌아 잘못되면 탐색기가 통째로 죽는다. 폴더 배경에서는 %1 이 아니라 %V 가 그 폴더다. - GitWorkspace.RouteStartupArgs 가 요청을 화면으로 보낸다. 창이 다 만들어진 뒤에 탭을 옮겨야 해서(그 전에는 옮길 곳이 없다) ApplicationIdle 로 미루고, 요청 자체는 남겨 두어 늦게 만들어진 화면이 스스로 집어 가게 했다(폴더 비교 쪽도 파일 비교와 같은 방식으로 추가). - 곁들여: 파일 비교가 "양쪽 경로가 모두 있을 때만" 열리고 있었다. 탐색기는 하나만 주므로 한쪽만 있어도 열도록 고쳤다 - 없는 쪽은 빈 내용이 되어 전부 추가로 보인다. ArgsTest 26건 신설. 전체 356건 통과. 실행 확인: 폴더 경로로 켜면 폴더 비교 탭에 그 폴더가 왼쪽에, 파일 두 개로 켜면 파일 비교 탭에 나란히. 등록 버튼을 누르면 레지스트리 3개 키가 생기고 버튼 이름이 "탐색기 우클릭 해제" 로 바뀐다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
55d9deb418
commit
49a05a0628
23
doc/진행상황.md
23
doc/진행상황.md
@ -66,14 +66,30 @@ CommunityToolkit 커맨드는 CommandManager 를 구독하지 않는데 이 둘
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### ⑤ 밖에서 부르기 — **완료** (2026-09-08)
|
||||||
|
|
||||||
|
| 상태 | 항목 | 내용 | 확인 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **완료** | 명령줄 인자 수신 | `--merge/--diff --base/--local/--remote/--merged` 와 그냥 넘어온 경로. <b>이것이 다리의 나머지 반쪽</b>이다 - `git config` 에 등록하는 쪽은 전부터 있었는데 받는 쪽이 없었다 | `ArgsTest` 26건 · 실제 실행 2건 |
|
||||||
|
| **완료** | 탐색기 우클릭 | 파일·폴더·폴더 배경 세 자리. Git 탭 버튼 하나로 등록/해제하고 버튼 이름이 지금 상태를 말한다 | 레지스트리 3개 키 확인 |
|
||||||
|
|
||||||
|
**HKEY_CURRENT_USER 에만 쓴다** - 관리자 권한이 필요 없고 지울 때도 이 사람 것만 지운다.
|
||||||
|
사내 PC 에 권한 없이 깔리는 것이 전제라 이 선택이 중요하다. 셸 확장 DLL 을 만들지 않은 것도
|
||||||
|
같은 이유다 - DLL 은 탐색기 프로세스 안에서 돌아 잘못되면 탐색기가 통째로 죽는다.
|
||||||
|
|
||||||
|
탐색기는 고른 것 <b>하나만</b> 준다. 그때는 왼쪽만 채우고 오른쪽은 비워 둔 채 화면을 연다 -
|
||||||
|
사용자가 그 자리에서 상대를 고르면 되고, 아무것도 안 열리는 것보다 낫다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 3~4주차로 미룬 것
|
## 3~4주차로 미룬 것
|
||||||
|
|
||||||
디테일이라 지금 붙잡을 일이 아니다.
|
디테일이라 지금 붙잡을 일이 아니다.
|
||||||
|
|
||||||
- 파일·폴더 드래그&드롭, 최근 파일 목록
|
- 파일·폴더 드래그&드롭, 최근 파일 목록
|
||||||
- 로그인·권한
|
- 로그인·권한
|
||||||
- 탐색기 우클릭 등록, `git mergetool` 인자 수신
|
- 탐색기에서 <b>두 개를 한 번에</b> 골라 비교(지금은 하나씩. 여러 개를 한 번에 받으려면
|
||||||
(※ `git config` 에 등록하는 쪽은 이미 있는데 **받는 쪽이 없다** — 다리를 반만 놓은 상태다)
|
레지스트리 MultiSelectModel 이나 셸 확장이 필요하다)
|
||||||
- 비교 결과 보고서 내보내기
|
- 비교 결과 보고서 내보내기
|
||||||
- 대용량 파일 성능 (수만 줄) — 실제 소스 크기를 확인한 뒤 판단
|
- 대용량 파일 성능 (수만 줄) — 실제 소스 크기를 확인한 뒤 판단
|
||||||
|
|
||||||
@ -87,4 +103,5 @@ CommunityToolkit 커맨드는 CommandManager 를 구독하지 않는데 이 둘
|
|||||||
| 2026-09-08 | **④ Git 조작 5가지** — amend · stash 목록 · 커밋 간 비교 · blame · 변경분 추출 | `9956567` |
|
| 2026-09-08 | **④ Git 조작 5가지** — amend · stash 목록 · 커밋 간 비교 · blame · 변경분 추출 | `9956567` |
|
||||||
| 2026-09-08 | **③ 3-Way** — 자동 병합 · 충돌만 이동 · 병합 완료 판정 | `85c5e9e` |
|
| 2026-09-08 | **③ 3-Way** — 자동 병합 · 충돌만 이동 · 병합 완료 판정 | `85c5e9e` |
|
||||||
| 2026-09-08 | **① 폴더 비교** — 폴더째 복사·삭제 · 보기 필터 · 비교 취소 | `524a6a7` |
|
| 2026-09-08 | **① 폴더 비교** — 폴더째 복사·삭제 · 보기 필터 · 비교 취소 | `524a6a7` |
|
||||||
| 2026-09-08 | **② 파일 비교** — 바꾸기 · 덩어리 반영 · 저장 형식 · 이진/그림 비교 | `de1a43c` + 아래 |
|
| 2026-09-08 | **② 파일 비교** — 바꾸기 · 덩어리 반영 · 저장 형식 · 이진/그림 비교 | `de1a43c` `55d9deb` |
|
||||||
|
| 2026-09-08 | **⑤ 밖에서 부르기** — 명령줄 인자 · 탐색기 우클릭 | 아래 |
|
||||||
|
|||||||
@ -48,6 +48,11 @@ namespace MergeHub.Services
|
|||||||
// 그러면 주석에 등록한 이름 대신 윈도우 계정이 찍힌다.
|
// 그러면 주석에 등록한 이름 대신 윈도우 계정이 찍힌다.
|
||||||
AuthorName = ResolveAuthorName();
|
AuthorName = ResolveAuthorName();
|
||||||
|
|
||||||
|
// `git mergetool` · 탐색기 우클릭이 넘긴 것을 화면으로 보낸다.
|
||||||
|
// 여기서 하는 이유 - 이 객체는 첫 화면이 만들어질 때 함께 만들어지므로
|
||||||
|
// 요청을 남겨 둘 자리로 가장 이르다.
|
||||||
|
RouteStartupArgs();
|
||||||
|
|
||||||
// 설정 화면에서 이름을 바꾸면 바로 따라간다.
|
// 설정 화면에서 이름을 바꾸면 바로 따라간다.
|
||||||
if (_currentUser is not null)
|
if (_currentUser is not null)
|
||||||
{
|
{
|
||||||
@ -379,6 +384,67 @@ namespace MergeHub.Services
|
|||||||
PlainCompareRequested.Invoke(pending.Left, pending.Right);
|
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}");
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
/// <summary>ObservableCollection 변경은 UI 스레드에서만 허용된다.</summary>
|
||||||
public void OnUi(Action action)
|
public void OnUi(Action action)
|
||||||
{
|
{
|
||||||
|
|||||||
105
src/MergeHub/Services/ShellIntegration.cs
Normal file
105
src/MergeHub/Services/ShellIntegration.cs
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace MergeHub.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 탐색기 우클릭 메뉴 등록.
|
||||||
|
///
|
||||||
|
/// <b>HKEY_CURRENT_USER 에만 쓴다</b> - 관리자 권한이 필요 없고, 지울 때도 이 사람 것만 지운다.
|
||||||
|
/// 사내 PC 에 관리자 권한 없이 깔리는 것이 전제라 이 선택이 중요하다.
|
||||||
|
///
|
||||||
|
/// 셸 확장 DLL 을 만들지 않은 이유 - DLL 을 등록하면 탐색기 프로세스 안으로 들어가
|
||||||
|
/// 잘못되면 탐색기가 통째로 죽는다. 레지스트리 항목만 쓰는 이 방식은 최악의 경우에도
|
||||||
|
/// "메뉴를 눌러도 프로그램이 안 뜬다"에서 끝난다.
|
||||||
|
/// </summary>
|
||||||
|
public static class ShellIntegration
|
||||||
|
{
|
||||||
|
/// <summary>메뉴에 보일 이름.</summary>
|
||||||
|
public const string MenuLabel = "MergeHub 로 비교";
|
||||||
|
|
||||||
|
private const string KeyName = "MergeHub.Compare";
|
||||||
|
|
||||||
|
/// <summary>파일 우클릭 · 폴더 우클릭 · 폴더 안 빈 곳 우클릭, 세 자리에 넣는다.</summary>
|
||||||
|
private static readonly string[] Targets =
|
||||||
|
{
|
||||||
|
@"Software\Classes\*\shell\" + KeyName,
|
||||||
|
@"Software\Classes\Directory\shell\" + KeyName,
|
||||||
|
@"Software\Classes\Directory\Background\shell\" + KeyName,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bool IsRegistered
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var key = Registry.CurrentUser.OpenSubKey(Targets[0]);
|
||||||
|
return key is not null;
|
||||||
|
}
|
||||||
|
catch (System.Security.SecurityException)
|
||||||
|
{
|
||||||
|
return false; // 정책으로 막힌 PC 면 "안 걸려 있다"로 본다
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 우클릭 메뉴를 넣는다. 이미 있으면 경로만 새로 쓴다(프로그램을 옮겼을 때를 위해).
|
||||||
|
/// </summary>
|
||||||
|
public static void Register(string executablePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(executablePath)) throw new ArgumentException("실행 파일 경로가 비었습니다.");
|
||||||
|
|
||||||
|
var exe = executablePath;
|
||||||
|
var icon = exe + ",0";
|
||||||
|
|
||||||
|
foreach (var path in Targets)
|
||||||
|
{
|
||||||
|
var background = path.Contains("Background");
|
||||||
|
|
||||||
|
using var key = Registry.CurrentUser.CreateSubKey(path)
|
||||||
|
?? throw new InvalidOperationException($"레지스트리 키를 만들지 못했습니다: {path}");
|
||||||
|
|
||||||
|
key.SetValue(null, MenuLabel);
|
||||||
|
key.SetValue("Icon", icon);
|
||||||
|
|
||||||
|
using var command = key.CreateSubKey("command")
|
||||||
|
?? throw new InvalidOperationException("command 키를 만들지 못했습니다.");
|
||||||
|
|
||||||
|
// 폴더 안 빈 곳에서 부르면 %1 이 아니라 %V 가 그 폴더다.
|
||||||
|
var target = background ? "%V" : "%1";
|
||||||
|
command.SetValue(null, $"\"{exe}\" \"{target}\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Unregister()
|
||||||
|
{
|
||||||
|
foreach (var path in Targets)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Registry.CurrentUser.DeleteSubKeyTree(path, throwOnMissingSubKey: false);
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
// 이미 없으면 그만이다.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>지금 돌고 있는 실행 파일. net48 에는 Environment.ProcessPath 가 없다.</summary>
|
||||||
|
public static string CurrentExecutable()
|
||||||
|
{
|
||||||
|
var path = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
|
||||||
|
return string.IsNullOrEmpty(path)
|
||||||
|
? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UbisamBase.Shell.exe")
|
||||||
|
: path!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
167
src/MergeHub/Services/StartupArgs.cs
Normal file
167
src/MergeHub/Services/StartupArgs.cs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace MergeHub.Services
|
||||||
|
{
|
||||||
|
public enum StartupMode
|
||||||
|
{
|
||||||
|
/// <summary>그냥 켠 것. 지금까지처럼 설정의 기본 파일을 연다.</summary>
|
||||||
|
None,
|
||||||
|
|
||||||
|
/// <summary>파일 두 개를 나란히(`git difftool`, 탐색기 우클릭).</summary>
|
||||||
|
Diff,
|
||||||
|
|
||||||
|
/// <summary>충돌 해결(`git mergetool`). BASE·LOCAL·REMOTE 를 받고 MERGED 에 쓴다.</summary>
|
||||||
|
Merge,
|
||||||
|
|
||||||
|
/// <summary>폴더 비교.</summary>
|
||||||
|
Folder,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 프로그램을 켤 때 넘어온 인자.
|
||||||
|
///
|
||||||
|
/// 이것이 있어야 <b>`git mergetool` 과 탐색기 우클릭이 이 프로그램을 부를 수 있다.</b>
|
||||||
|
/// (git config 에 등록하는 쪽은 전부터 있었는데 받는 쪽이 없어 다리가 반만 놓여 있었다.)
|
||||||
|
///
|
||||||
|
/// 받는 모양은 두 가지다.
|
||||||
|
/// - 이름표 붙은 것: <c>--merge --base B --local L --remote R --merged M</c>
|
||||||
|
/// - 그냥 경로: 탐색기가 <c>"%1"</c> 하나만 주는 경우. 파일이면 비교, 폴더면 폴더 비교로 본다.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StartupArgs
|
||||||
|
{
|
||||||
|
public StartupMode Mode { get; private set; } = StartupMode.None;
|
||||||
|
|
||||||
|
public string Base { get; private set; } = string.Empty;
|
||||||
|
public string Local { get; private set; } = string.Empty;
|
||||||
|
public string Remote { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>결과를 써 넣을 파일. `git mergetool` 이 이 파일을 다시 읽어 간다.</summary>
|
||||||
|
public string Merged { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
public string LeftFolder { get; private set; } = string.Empty;
|
||||||
|
public string RightFolder { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이름표 없이 그냥 넘어온 파일 경로. 탐색기가 <c>"%1"</c> 만 줄 때가 이 자리다.
|
||||||
|
/// 이름표가 붙은 <see cref="Local"/>·<see cref="Remote"/> 와 뜻이 달라 따로 둔다 -
|
||||||
|
/// difftool 은 "내 것/상대 것"이지만 탐색기는 그냥 "먼저 고른 것/나중 것"이다.
|
||||||
|
/// </summary>
|
||||||
|
public string LooseLeft { get; private set; } = string.Empty;
|
||||||
|
public string LooseRight { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>이번 실행에 넘어온 인자. 한 번만 읽는다.</summary>
|
||||||
|
public static StartupArgs Current { get; } = Parse(SafeArgs());
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> SafeArgs()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var all = Environment.GetCommandLineArgs();
|
||||||
|
return all.Length > 1 ? new List<string>(all).GetRange(1, all.Length - 1) : Array.Empty<string>();
|
||||||
|
}
|
||||||
|
catch (NotSupportedException)
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StartupArgs Parse(IReadOnlyList<string> args)
|
||||||
|
{
|
||||||
|
var result = new StartupArgs();
|
||||||
|
if (args is null || args.Count == 0) return result;
|
||||||
|
|
||||||
|
var loose = new List<string>();
|
||||||
|
|
||||||
|
for (var i = 0; i < args.Count; i++)
|
||||||
|
{
|
||||||
|
var arg = args[i] ?? string.Empty;
|
||||||
|
|
||||||
|
switch (arg.ToLowerInvariant())
|
||||||
|
{
|
||||||
|
case "--merge": result.Mode = StartupMode.Merge; continue;
|
||||||
|
case "--diff": result.Mode = StartupMode.Diff; continue;
|
||||||
|
case "--folder": result.Mode = StartupMode.Folder; continue;
|
||||||
|
|
||||||
|
case "--base": result.Base = Next(args, ref i); continue;
|
||||||
|
case "--local": result.Local = Next(args, ref i); continue;
|
||||||
|
case "--remote": result.Remote = Next(args, ref i); continue;
|
||||||
|
case "--merged": result.Merged = Next(args, ref i); continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이름표가 없는 것은 경로로 본다. 셸에서 넘어오는 "%1" 이 이 자리다.
|
||||||
|
if (!arg.StartsWith("-", StringComparison.Ordinal) && arg.Length > 0) loose.Add(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Fill(result, loose);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Next(IReadOnlyList<string> args, ref int i) =>
|
||||||
|
i + 1 < args.Count ? args[++i] : string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이름표 없이 넘어온 경로를 자리에 맞춘다.
|
||||||
|
///
|
||||||
|
/// 탐색기 우클릭은 고른 것 하나만 준다. 그때는 <b>왼쪽만 채우고 오른쪽은 비워 둔다</b> -
|
||||||
|
/// 사용자가 그 자리에서 상대를 고르면 되고, 아무것도 안 열리는 것보다 훨씬 낫다.
|
||||||
|
/// </summary>
|
||||||
|
private static void Fill(StartupArgs result, List<string> loose)
|
||||||
|
{
|
||||||
|
if (loose.Count == 0) return;
|
||||||
|
|
||||||
|
var first = loose[0];
|
||||||
|
var second = loose.Count > 1 ? loose[1] : string.Empty;
|
||||||
|
|
||||||
|
// 이름표로 이미 모드를 정했으면 남은 경로만 채운다.
|
||||||
|
if (result.Mode == StartupMode.Folder || (result.Mode == StartupMode.None && IsFolder(first)))
|
||||||
|
{
|
||||||
|
result.Mode = StartupMode.Folder;
|
||||||
|
if (result.LeftFolder.Length == 0) result.LeftFolder = first;
|
||||||
|
if (result.RightFolder.Length == 0) result.RightFolder = second;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Mode == StartupMode.None) result.Mode = StartupMode.Diff;
|
||||||
|
|
||||||
|
result.LooseLeft = first;
|
||||||
|
result.LooseRight = second;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsFolder(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Directory.Exists(path);
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 파일 비교 화면에 넣을 두 파일.
|
||||||
|
/// `git difftool` 은 LOCAL 이 내 것, REMOTE 가 상대 것이라 <b>왼쪽이 REMOTE</b> 가 된다 -
|
||||||
|
/// 이 도구의 왼쪽은 늘 "기준"이기 때문이다.
|
||||||
|
/// </summary>
|
||||||
|
public (string Left, string Right) DiffPair
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// 그냥 넘어온 경로가 있으면 그 순서가 곧 왼쪽·오른쪽이다.
|
||||||
|
if (LooseLeft.Length > 0 || LooseRight.Length > 0) return (LooseLeft, LooseRight);
|
||||||
|
|
||||||
|
return Mode == StartupMode.Merge
|
||||||
|
? (Base, Merged.Length > 0 ? Merged : Local)
|
||||||
|
: (Remote.Length > 0 ? Remote : Base, Local);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool WantsFolder => Mode == StartupMode.Folder && LeftFolder.Length > 0;
|
||||||
|
|
||||||
|
public bool WantsFiles =>
|
||||||
|
Mode is StartupMode.Diff or StartupMode.Merge && (DiffPair.Left.Length > 0 || DiffPair.Right.Length > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -111,6 +111,10 @@ namespace MergeHub.ViewModels
|
|||||||
{
|
{
|
||||||
_workspace = workspace;
|
_workspace = workspace;
|
||||||
|
|
||||||
|
// 탐색기 우클릭으로 켰으면 그 폴더가 왼쪽에 들어온다.
|
||||||
|
_workspace.FolderCompareRequested += OnFolderCompareRequested;
|
||||||
|
_workspace.DeliverPendingFolderCompare();
|
||||||
|
|
||||||
// 저장소를 열어 뒀으면 왼쪽 기본값으로 삼는다 - 대개 그 폴더를 견주게 된다.
|
// 저장소를 열어 뒀으면 왼쪽 기본값으로 삼는다 - 대개 그 폴더를 견주게 된다.
|
||||||
if (!string.IsNullOrWhiteSpace(_workspace.RepositoryPath) &&
|
if (!string.IsNullOrWhiteSpace(_workspace.RepositoryPath) &&
|
||||||
Directory.Exists(_workspace.RepositoryPath))
|
Directory.Exists(_workspace.RepositoryPath))
|
||||||
@ -119,6 +123,21 @@ namespace MergeHub.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 탐색기 우클릭·명령줄이 넘긴 폴더를 받는다.
|
||||||
|
///
|
||||||
|
/// 오른쪽이 비어 있으면 <b>왼쪽만 채우고 기다린다</b> - 탐색기는 고른 것 하나만 주므로
|
||||||
|
/// 대개 이쪽이다. 아무 일도 안 일어나는 것보다 반쯤 채워 놓는 편이 낫다.
|
||||||
|
/// </summary>
|
||||||
|
private void OnFolderCompareRequested(string left, string right)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(left)) LeftFolder = left;
|
||||||
|
if (!string.IsNullOrWhiteSpace(right)) RightFolder = right;
|
||||||
|
|
||||||
|
if (Directory.Exists(LeftFolder) && Directory.Exists(RightFolder)) _ = CompareAsync();
|
||||||
|
else Summary = "오른쪽 폴더를 고르고 비교를 누르세요.";
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>홈 탭에서 이 두 파일을 비교해 달라고 알린다.</summary>
|
/// <summary>홈 탭에서 이 두 파일을 비교해 달라고 알린다.</summary>
|
||||||
public event EventHandler<(string Left, string Right)>? OpenInDiffRequested;
|
public event EventHandler<(string Left, string Right)>? OpenInDiffRequested;
|
||||||
|
|
||||||
|
|||||||
@ -528,6 +528,47 @@ namespace MergeHub.ViewModels
|
|||||||
_workspace.OnUi(() => MessageUtil.ShowSuccessToast("git config 에 mergehub 등록 완료"));
|
_workspace.OnUi(() => MessageUtil.ShowSuccessToast("git config 에 mergehub 등록 완료"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ================= 탐색기 우클릭 =================
|
||||||
|
|
||||||
|
/// <summary>지금 우클릭 메뉴에 걸려 있는지. 버튼 이름을 이 값으로 바꾼다.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShellIntegrationLabel))]
|
||||||
|
private bool _isShellRegistered = ShellIntegration.IsRegistered;
|
||||||
|
|
||||||
|
public string ShellIntegrationLabel =>
|
||||||
|
IsShellRegistered ? "탐색기 우클릭 해제" : "탐색기 우클릭 등록";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 탐색기 우클릭 메뉴를 넣고 뺀다.
|
||||||
|
///
|
||||||
|
/// HKEY_CURRENT_USER 에만 쓰므로 <b>관리자 권한이 필요 없다</b> - 사내 PC 에 권한 없이
|
||||||
|
/// 깔리는 것이 전제다. 셸 확장 DLL 을 만들지 않은 것도 같은 이유로,
|
||||||
|
/// DLL 은 탐색기 프로세스 안에서 돌아 잘못되면 탐색기가 통째로 죽는다.
|
||||||
|
/// </summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void ToggleShellIntegration()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (IsShellRegistered)
|
||||||
|
{
|
||||||
|
ShellIntegration.Unregister();
|
||||||
|
MessageUtil.ShowToast("탐색기 우클릭 메뉴를 뺐습니다");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ShellIntegration.Register(ShellIntegration.CurrentExecutable());
|
||||||
|
MessageUtil.ShowSuccessToast($"파일·폴더를 우클릭하면 \"{ShellIntegration.MenuLabel}\" 가 보입니다");
|
||||||
|
}
|
||||||
|
|
||||||
|
IsShellRegistered = ShellIntegration.IsRegistered;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageUtil.ShowErrorToast($"탐색기 등록 실패: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>CommunityToolkit 의 커맨드는 CommandManager 를 구독하지 않아 직접 알려야 한다.</summary>
|
/// <summary>CommunityToolkit 의 커맨드는 CommandManager 를 구독하지 않아 직접 알려야 한다.</summary>
|
||||||
private void RaiseCanExecuteChanged()
|
private void RaiseCanExecuteChanged()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1443,7 +1443,9 @@ namespace MergeHub.ViewModels
|
|||||||
/// <summary>고른 두 파일을 읽어 비교 화면을 만든다.</summary>
|
/// <summary>고른 두 파일을 읽어 비교 화면을 만든다.</summary>
|
||||||
private async Task LoadPlainFilesAsync()
|
private async Task LoadPlainFilesAsync()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(LeftFile) || string.IsNullOrWhiteSpace(RightFile)) return;
|
// 한쪽만 있어도 연다 - 탐색기 우클릭은 고른 파일 하나만 준다.
|
||||||
|
// 없는 쪽은 빈 내용이 되어 전부 추가(또는 삭제)로 보인다.
|
||||||
|
if (string.IsNullOrWhiteSpace(LeftFile) && string.IsNullOrWhiteSpace(RightFile)) return;
|
||||||
|
|
||||||
// 이진 파일은 줄 단위로 견줄 것이 없다. 깨진 글자로 화면을 채우느니 그렇다고 알린다.
|
// 이진 파일은 줄 단위로 견줄 것이 없다. 깨진 글자로 화면을 채우느니 그렇다고 알린다.
|
||||||
if (TextFile.IsBinary(LeftFile) || TextFile.IsBinary(RightFile))
|
if (TextFile.IsBinary(LeftFile) || TextFile.IsBinary(RightFile))
|
||||||
|
|||||||
@ -130,6 +130,11 @@
|
|||||||
ToolTip="진행 중인 merge / cherry-pick / revert / rebase 를 취소"/>
|
ToolTip="진행 중인 merge / cherry-pick / revert / rebase 를 취소"/>
|
||||||
<Button Content="mergetool 등록" Command="{Binding RegisterMergeToolCommand}" MinWidth="110"
|
<Button Content="mergetool 등록" Command="{Binding RegisterMergeToolCommand}" MinWidth="110"
|
||||||
ToolTip="git config 의 merge.tool 로 이 프로그램을 등록 (전역 .gitconfig 수정)"/>
|
ToolTip="git config 의 merge.tool 로 이 프로그램을 등록 (전역 .gitconfig 수정)"/>
|
||||||
|
|
||||||
|
<!-- HKCU 에만 쓰므로 관리자 권한이 필요 없다. 버튼 이름이 지금 상태를 그대로 말한다. -->
|
||||||
|
<Button Content="{Binding ShellIntegrationLabel}" Command="{Binding ToggleShellIntegrationCommand}"
|
||||||
|
MinWidth="140"
|
||||||
|
ToolTip="탐색기에서 파일·폴더를 우클릭하면 이 프로그램으로 열 수 있게 합니다 (관리자 권한 필요 없음)"/>
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
|
|
||||||
<!-- ===== 작업 영역 ===== -->
|
<!-- ===== 작업 영역 ===== -->
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user