diff --git a/doc/진행상황.md b/doc/진행상황.md
index 9e76cb0..e85e5b7 100644
--- a/doc/진행상황.md
+++ b/doc/진행상황.md
@@ -1,4 +1,4 @@
-# MergeHub 진행 상황
+# MergeHub 진행 상황
작업 하나가 끝날 때마다 이 표를 고치고 **로컬 커밋**한다.
푸시는 하지 않는다 — 사내 Gitea 저장소가 만들어지면 그때 한 번에 올린다.
@@ -71,14 +71,24 @@ CommunityToolkit 커맨드는 CommandManager 를 구독하지 않는데 이 둘
| 상태 | 항목 | 내용 | 확인 |
|---|---|---|---|
| **완료** | 명령줄 인자 수신 | `--merge/--diff --base/--local/--remote/--merged` 와 그냥 넘어온 경로. 이것이 다리의 나머지 반쪽이다 - `git config` 에 등록하는 쪽은 전부터 있었는데 받는 쪽이 없었다 | `ArgsTest` 26건 · 실제 실행 2건 |
-| **완료** | 탐색기 우클릭 | 파일·폴더·폴더 배경 세 자리. Git 탭 버튼 하나로 등록/해제하고 버튼 이름이 지금 상태를 말한다 | 레지스트리 3개 키 확인 |
+| **완료** | 탐색기에서 부르기 | 우클릭 → 보내기 → `MergeHub 로 비교`. Git 탭 버튼 하나로 등록/해제하고 버튼 이름이 지금 상태를 말한다 | 실제 메뉴에서 확인 |
-**HKEY_CURRENT_USER 에만 쓴다** - 관리자 권한이 필요 없고 지울 때도 이 사람 것만 지운다.
-사내 PC 에 권한 없이 깔리는 것이 전제라 이 선택이 중요하다. 셸 확장 DLL 을 만들지 않은 것도
-같은 이유다 - DLL 은 탐색기 프로세스 안에서 돌아 잘못되면 탐색기가 통째로 죽는다.
+**우클릭 메뉴에 바로 넣으려다 보내기로 돌아섰다.** 처음에는 레지스트리에 정적 verb 를 넣었다
+(`HKCU\Software\Classes\AllFilesystemObjects\shell\...`). 키는 제대로 들어갔고
+`HKEY_CLASSES_ROOT` 에도 병합돼 보였는데 탐색기 메뉴에는 뜨지 않았다.
+확인해 보니 이 PC 에서 우클릭 메뉴에 나오는 다른 프로그램(Notepad++ · Git Bash · WinMerge)은
+전부 HKLM(전체 사용자)에 등록돼 있었고, HKCU 에 넣은 것은 - 우리 것이든 실험용으로 넣은
+메모장 항목이든 - 하나도 그려지지 않았다. HKLM 은 관리자 권한이 필요해 쓸 수 없다.
-탐색기는 고른 것 하나만 준다. 그때는 왼쪽만 채우고 오른쪽은 비워 둔 채 화면을 연다 -
-사용자가 그 자리에서 상대를 고르면 되고, 아무것도 안 열리는 것보다 낫다.
+그래서 보내기(SendTo) 로 갔다. `%APPDATA%\Microsoft\Windows\SendTo` 에 바로가기
+파일 하나를 두는 것이 전부라 레지스트리도 권한도 필요 없고 윈도우 판을 타지 않는다.
+한 단계 더 들어가는 것이 유일한 단점인데, 대신 여러 개를 골라 한 번에 보낼 수 있다 -
+미뤄 두었던 "파일 두 개 골라 비교"가 덤으로 따라왔다.
+
+셸 확장 DLL 은 쓰지 않았다 - DLL 은 탐색기 프로세스 안에서 돌아 잘못되면 탐색기가 통째로 죽는다.
+
+고른 것이 하나뿐이면 왼쪽만 채우고 오른쪽은 비워 둔 채 화면을 연다 - 사용자가 그 자리에서
+상대를 고르면 되고, 아무것도 안 열리는 것보다 낫다.
---
@@ -88,8 +98,6 @@ CommunityToolkit 커맨드는 CommandManager 를 구독하지 않는데 이 둘
- 파일·폴더 드래그&드롭, 최근 파일 목록
- 로그인·권한
-- 탐색기에서 두 개를 한 번에 골라 비교(지금은 하나씩. 여러 개를 한 번에 받으려면
- 레지스트리 MultiSelectModel 이나 셸 확장이 필요하다)
- 비교 결과 보고서 내보내기
- 대용량 파일 성능 (수만 줄) — 실제 소스 크기를 확인한 뒤 판단
diff --git a/src/MergeHub/Services/ShellIntegration.cs b/src/MergeHub/Services/ShellIntegration.cs
index da23202..9beb116 100644
--- a/src/MergeHub/Services/ShellIntegration.cs
+++ b/src/MergeHub/Services/ShellIntegration.cs
@@ -5,29 +5,29 @@ using Microsoft.Win32;
namespace MergeHub.Services
{
///
- /// 탐색기 우클릭 메뉴 등록.
+ /// 탐색기에서 이 프로그램을 부를 수 있게 한다 - 보내기(SendTo) 메뉴에 바로가기를 둔다.
///
- /// HKEY_CURRENT_USER 에만 쓴다 - 관리자 권한이 필요 없고, 지울 때도 이 사람 것만 지운다.
- /// 사내 PC 에 관리자 권한 없이 깔리는 것이 전제라 이 선택이 중요하다.
+ /// 처음에는 우클릭 메뉴에 바로 넣으려고 레지스트리에 정적 verb 를 등록했다.
+ /// (HKCU\Software\Classes\AllFilesystemObjects\shell\...)
+ /// 이 PC 에서는 뜨지 않았다. 확인해 보니 우클릭 메뉴에 나오는 다른 프로그램
+ /// (Notepad++ · Git Bash · WinMerge)은 전부 HKLM(전체 사용자)에 등록돼 있었고,
+ /// HKCU 에 넣은 항목은 - 우리 것이든 실험용으로 넣은 것이든 - 하나도 그려지지 않았다.
+ /// HKLM 은 관리자 권한이 필요해 쓸 수 없다.
///
- /// 셸 확장 DLL 을 만들지 않은 이유 - DLL 을 등록하면 탐색기 프로세스 안으로 들어가
- /// 잘못되면 탐색기가 통째로 죽는다. 레지스트리 항목만 쓰는 이 방식은 최악의 경우에도
- /// "메뉴를 눌러도 프로그램이 안 뜬다"에서 끝난다.
+ /// 그래서 보내기로 간다. 이쪽은 레지스트리를 아예 쓰지 않고
+ /// %APPDATA%\Microsoft\Windows\SendTo 에 바로가기 파일 하나를 두는 것이 전부라
+ /// 권한도 필요 없고 윈도우 판을 타지 않는다. 덤이 하나 있는데, 여러 개를 골라
+ /// 한 번에 보낼 수 있다 - 파일 두 개를 골라 바로 비교하는 것이 그냥 된다.
+ ///
+ /// 단점은 한 단계 더 들어간다는 것뿐이다(우클릭 → 보내기 → MergeHub 로 비교).
///
public static class ShellIntegration
{
- /// 메뉴에 보일 이름.
+ /// 보내기 메뉴에 보일 이름.
public const string MenuLabel = "MergeHub 로 비교";
- private const string KeyName = "MergeHub.Compare";
-
- /// 파일 우클릭 · 폴더 우클릭 · 폴더 안 빈 곳 우클릭, 세 자리에 넣는다.
- private static readonly string[] Targets =
- {
- @"Software\Classes\*\shell\" + KeyName,
- @"Software\Classes\Directory\shell\" + KeyName,
- @"Software\Classes\Directory\Background\shell\" + KeyName,
- };
+ private static string ShortcutPath =>
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SendTo), MenuLabel + ".lnk");
public static bool IsRegistered
{
@@ -35,60 +35,82 @@ namespace MergeHub.Services
{
try
{
- using var key = Registry.CurrentUser.OpenSubKey(Targets[0]);
- return key is not null;
+ return File.Exists(ShortcutPath);
}
- catch (System.Security.SecurityException)
- {
- return false; // 정책으로 막힌 PC 면 "안 걸려 있다"로 본다
- }
- catch (UnauthorizedAccessException)
+ catch (IOException)
{
return false;
}
}
}
- ///
- /// 우클릭 메뉴를 넣는다. 이미 있으면 경로만 새로 쓴다(프로그램을 옮겼을 때를 위해).
- ///
+ /// 보내기 메뉴에 바로가기를 만든다. 이미 있으면 경로만 새로 쓴다(프로그램을 옮겼을 때).
public static void Register(string executablePath)
{
if (string.IsNullOrWhiteSpace(executablePath)) throw new ArgumentException("실행 파일 경로가 비었습니다.");
- var exe = executablePath;
- var icon = exe + ",0";
+ var shell = Type.GetTypeFromProgID("WScript.Shell")
+ ?? throw new InvalidOperationException("WScript.Shell 을 찾을 수 없습니다.");
- foreach (var path in Targets)
- {
- var background = path.Contains("Background");
+ var host = Activator.CreateInstance(shell)
+ ?? throw new InvalidOperationException("WScript.Shell 을 만들지 못했습니다.");
- using var key = Registry.CurrentUser.CreateSubKey(path)
- ?? throw new InvalidOperationException($"레지스트리 키를 만들지 못했습니다: {path}");
+ // 바로가기(.lnk)는 COM 으로만 만들 수 있다. 참조를 더하지 않으려고 늦은 바인딩으로 부른다.
+ var link = shell.InvokeMember("CreateShortcut", System.Reflection.BindingFlags.InvokeMethod,
+ null, host, new object[] { ShortcutPath })
+ ?? throw new InvalidOperationException("바로가기를 만들지 못했습니다.");
- key.SetValue(null, MenuLabel);
- key.SetValue("Icon", icon);
+ var type = link.GetType();
- using var command = key.CreateSubKey("command")
- ?? throw new InvalidOperationException("command 키를 만들지 못했습니다.");
+ Set(type, link, "TargetPath", executablePath);
+ Set(type, link, "WorkingDirectory", Path.GetDirectoryName(executablePath) ?? string.Empty);
+ Set(type, link, "Description", "고른 파일·폴더를 MergeHub 으로 비교합니다");
- // 폴더 안 빈 곳에서 부르면 %1 이 아니라 %V 가 그 폴더다.
- var target = background ? "%V" : "%1";
- command.SetValue(null, $"\"{exe}\" \"{target}\"");
- }
+ type.InvokeMember("Save", System.Reflection.BindingFlags.InvokeMethod, null, link, null);
+
+ // 예전 판이 레지스트리에 써 둔 것이 남아 있으면 같이 치운다.
+ RemoveLegacyRegistryEntries();
}
+ private static void Set(Type type, object link, string name, string value) =>
+ type.InvokeMember(name, System.Reflection.BindingFlags.SetProperty, null, link, new object[] { value });
+
public static void Unregister()
{
- foreach (var path in Targets)
+ try
+ {
+ if (File.Exists(ShortcutPath)) File.Delete(ShortcutPath);
+ }
+ catch (IOException)
+ {
+ // 지우지 못하면 그대로 둔다. 다시 누르면 된다.
+ }
+
+ RemoveLegacyRegistryEntries();
+ }
+
+ ///
+ /// 레지스트리로 등록하던 시절의 흔적. 이 PC 에서는 뜨지도 않던 것이라 남겨 둘 이유가 없다.
+ ///
+ private static void RemoveLegacyRegistryEntries()
+ {
+ string[] legacy =
+ {
+ @"Software\Classes\AllFilesystemObjects\shell\MergeHub.Compare",
+ @"Software\Classes\Directory\Background\shell\MergeHub.Compare",
+ @"Software\Classes\*\shell\MergeHub.Compare",
+ @"Software\Classes\Directory\shell\MergeHub.Compare",
+ };
+
+ foreach (var path in legacy)
{
try
{
Registry.CurrentUser.DeleteSubKeyTree(path, throwOnMissingSubKey: false);
}
- catch (ArgumentException)
+ catch (Exception e) when (e is ArgumentException || e is UnauthorizedAccessException)
{
- // 이미 없으면 그만이다.
+ // 이미 없거나 손댈 수 없으면 그만이다.
}
}
}
diff --git a/src/MergeHub/ViewModels/GitViewModel.cs b/src/MergeHub/ViewModels/GitViewModel.cs
index 3ace239..5b8f005 100644
--- a/src/MergeHub/ViewModels/GitViewModel.cs
+++ b/src/MergeHub/ViewModels/GitViewModel.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@@ -536,14 +536,14 @@ namespace MergeHub.ViewModels
private bool _isShellRegistered = ShellIntegration.IsRegistered;
public string ShellIntegrationLabel =>
- IsShellRegistered ? "탐색기 우클릭 해제" : "탐색기 우클릭 등록";
+ IsShellRegistered ? "탐색기 보내기 해제" : "탐색기 보내기 등록";
///
- /// 탐색기 우클릭 메뉴를 넣고 뺀다.
+ /// 탐색기 보내기 메뉴에 이 프로그램을 넣고 뺀다.
///
- /// HKEY_CURRENT_USER 에만 쓰므로 관리자 권한이 필요 없다 - 사내 PC 에 권한 없이
- /// 깔리는 것이 전제다. 셸 확장 DLL 을 만들지 않은 것도 같은 이유로,
- /// DLL 은 탐색기 프로세스 안에서 돌아 잘못되면 탐색기가 통째로 죽는다.
+ /// 우클릭 메뉴에 바로 넣는 레지스트리 방식은 이 PC 에서 뜨지 않았다
+ /// (자세한 이유는 ShellIntegration 설명 참고). 보내기는 바로가기 파일 하나라
+ /// 권한도 필요 없고, 여러 개를 골라 한 번에 보낼 수 있다.
///
[RelayCommand]
private void ToggleShellIntegration()
@@ -553,19 +553,19 @@ namespace MergeHub.ViewModels
if (IsShellRegistered)
{
ShellIntegration.Unregister();
- MessageUtil.ShowToast("탐색기 우클릭 메뉴를 뺐습니다");
+ MessageUtil.ShowToast("탐색기 보내기 메뉴에서 뺐습니다");
}
else
{
ShellIntegration.Register(ShellIntegration.CurrentExecutable());
- MessageUtil.ShowSuccessToast($"파일·폴더를 우클릭하면 \"{ShellIntegration.MenuLabel}\" 가 보입니다");
+ MessageUtil.ShowSuccessToast("우클릭 → 보내기 → " + ShellIntegration.MenuLabel + " 로 엽니다");
}
IsShellRegistered = ShellIntegration.IsRegistered;
}
catch (Exception ex)
{
- MessageUtil.ShowErrorToast($"탐색기 등록 실패: {ex.Message}");
+ MessageUtil.ShowErrorToast("보내기 등록 실패: " + ex.Message);
}
}
diff --git a/src/MergeHub/Views/GitView.xaml b/src/MergeHub/Views/GitView.xaml
index 3279c74..93f0ef0 100644
--- a/src/MergeHub/Views/GitView.xaml
+++ b/src/MergeHub/Views/GitView.xaml
@@ -134,7 +134,7 @@
+ ToolTip="탐색기에서 파일·폴더를 고르고 [우클릭 → 보내기] 로 이 프로그램에 넘길 수 있게 합니다. 여러 개를 한 번에 골라도 됩니다 (관리자 권한 필요 없음)"/>