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 @@