diff --git a/doc/history/2026-09-18-11차.md b/doc/history/2026-09-18-11차.md
index b0704ed..8e0f136 100644
--- a/doc/history/2026-09-18-11차.md
+++ b/doc/history/2026-09-18-11차.md
@@ -161,6 +161,7 @@ A-1 에서 값으로 확인한 것 — 지운 파일은 "삭제되어 건너뜀
- 이 뒤 11차 항목(F 등)에서 바뀌는 화면 · 설정 · 숫자.
- F-1: 칸 이름 `LOCAL(Mine)` · `REMOTE(Theirs)`, 칸 머리 "어느 쪽 · 어느 파일"(MERGED · 두 칸 LOCAL 은 "(저장할 파일)", mergetool 은 임시 파일 이름), 긴 이름은 가운데 줄임 · 툴팁. 머지 주석 모양 `[사람] 날짜 출처 설명`(출처 = 로그의 말 — Theirs(…) · Mine(…) · REMOTE(…) · BASE(…)), **Take Mine 도 주석**, 두 칸 로그 `BASE(해시) 반영` · 브랜치 3-Way 로그 `REMOTE(브랜치) 반영`. 매뉴얼의 주석 예시 · 칸 설명 · 해결 기록이 남는 곳 표. 숫자 1,312건.
- F-2: [이 메서드 반영](C# · C/C++, 클래스 멤버 통째, 단추 아래 한 줄에 잡은 범위 · 못 잡은 까닭), 덩어리 · 메서드 반영은 주석 한 줄 · 로그 한 줄 · Undo 한 번. 매뉴얼의 반영 단추 설명에. 숫자 23벌 1,395건.
+- F-3: 매뉴얼 "탐색기에서 열기" — 여러 개를 골라 보내면 앞의 둘(3개 이상은 알림), 폴더 둘은 바로 폴더 비교, 260자 넘는 경로는 열지 못함(알림), 배포 묶음을 옮겨도 켜면 보내기 바로가기가 따라옴, 설치 파일 필요 없음. 알아 둘 한계에 "떠 있어도 새 창". 숫자 23벌 1,411건.
## B-1. 배포 묶음을 다시 만들고 꼭 필요한 파일을 가림 (2026-09-22, 완료)
@@ -587,6 +588,59 @@ libgit2 의 다듬기(`PrettifyMessages`)를 켜면 **손대지 않은 뒤 커
- 범위는 반영해 올 쪽(BASE · REMOTE) 글로 잡는다. 그쪽에 없는 메서드(내 쪽에서만 새로 만든 메서드)는 잡히지 않아 단추가 꺼진다.
- XAML · 그 밖의 언어는 단추가 꺼지고 까닭을 말한다(사용자 결정: C# + C/C++).
+## F-3. 탐색기에서 폴더 두 개를 [보내기] 로 바로 비교 · 설치 없이 되는지 (2026-09-22, 완료)
+
+### 어떻게 쟀나 — 사용자의 보내기 메뉴는 건드리지 않고
+
+탐색기 [보내기] 는 SendTo 폴더의 바로가기에 고른 항목들을 **떨어뜨리는 것**(바로가기의 IDropTarget::Drop)이다. 같은 셸 길을 부르는 도구(`SendToSim` — 고른 항목의 데이터 개체를 임시 폴더의 바로가기에 Drop)와 받은 명령줄을 적기만 하는 `ArgsEcho` 로 쟀다. 바로가기는 [탐색기 보내기 등록] 과 같은 모양(대상 · 작업 폴더).
+
+| 경우 | 탐색기가 넘긴 것 | 고치기 전 MergeHub |
+|---|---|---|
+| 파일 2개 | **프로세스 1개에 인자 2개** | 파일 비교로 열림 |
+| 폴더 2개 | 프로세스 1개에 인자 2개 | **창 하나에 왼쪽 · 오른쪽 폴더, 폴더 비교까지**(`전체 1개 · 같음 1`) |
+| 폴더 1개 | 인자 1개 | 폴더 비교, 왼쪽만 채움(오른쪽은 사용자가 고름) |
+| 3개 이상 | 프로세스 1개에 인자 3개 | 앞의 둘만 비교, 셋째는 **말없이 버림** |
+| 경로에 공백 | 그대로(`left side`) | 폴더 비교까지 |
+| 한글 | 그대로(`왼쪽 폴더`) | 폴더 비교까지 |
+| 260자 넘는 경로 | **8.3 짧은 이름**(123자, `ME0359~1\긴폴더~1\…\RIGHTL~1`) | **아무것도 안 열림** — 로그엔 폴더인데 "파일 비교 요청" |
+
+- 탐색기는 여러 개를 **한 번, 프로세스 하나**로 넘긴다 — "두 번 뜬 프로세스를 하나로 모으는 길" 은 필요 없다.
+- 긴 경로: net48 에서 짧은 이름을 `Path.GetFullPath` 가 긴 이름(277자)으로 펼친 뒤 못 찾는다(`Directory.Exists` 거짓, `\\?\` 를 붙이면 "잘못된 문자" — `LongPathProbe`). 열려면 플랫폼 실행 파일 설정(긴 경로 스위치)을 바꿔야 한다.
+- MergeHub 가 **이미 떠 있을 때** 보내면 창이 하나 더 뜬다(프로세스 2개).
+
+### 정한 것 (2026-09-22 사용자)
+
+- 긴 경로: **열 수 없다고 알린다**(지원은 플랫폼 설정을 바꿔야 해서 하지 않음).
+- 3개 이상: **앞의 둘 + 알림**.
+- 배포 묶음을 옮겨 바로가기가 옛 자리를 가리키면: **켜질 때 저절로 고친다**.
+- 이미 떠 있을 때: **지금처럼 새 창**(하나로 모으려면 플랫폼에 한 번만 실행 · 창 사이 전달이 필요 — 남은 것).
+
+### 고친 것
+
+- `StartupArgs` — 셋째부터는 `Extra`, 앞의 둘 가운데 파일도 폴더도 아닌 것은 `Missing`(있으면 **아무것도 열지 않는다**). 알림 한 줄 `Notice`: `3개를 골랐습니다 - 앞의 둘만 비교하고 1개(Third)는 두었습니다` · `LeftLong 외 1개 - 경로가 277자라 열 수 없습니다(260자가 넘는 경로는 아직 열지 못합니다)` · 그 밖은 `… - 찾을 수 없습니다`. 이름표 붙은 경로(`git difftool` 의 지워진 쪽)는 따지지 않는다.
+- `GitWorkspace` — 켤 때 넘어온 것 · 끌어다 놓은 것 모두 같은 말로 로그 한 줄 + 경고 알림. 켤 때는 사용자 이름 창(모달)이 먼저 떠서 그 뒤로 알림이 지나가 버렸다 — **본 창이 활성이 된 뒤에** 띄운다(이름 창을 닫고 0.4초 뒤에 보임).
+- `ShellIntegration.RepairIfMoved` — 등록해 둔 사람만, 바로가기 대상이 지금 실행 파일과 다르면 **바로가기 파일만** 다시 쓰고 로그 한 줄(`보내기 바로가기가 옛 자리(…)를 가리켜 지금 실행 파일로 고쳤습니다`). 레지스트리는 손대지 않는다. 이미 맞으면 파일을 건드리지 않는다.
+
+### 설치 파일 없이 되는가
+
+**된다.** 보내기는 `%APPDATA%\Microsoft\Windows\SendTo` 에 바로가기 하나를 두는 것이 전부다(관리자 권한 · HKLM · 설치 관리자 없음). 배포 묶음을 옮기면 켤 때 바로가기가 따라온다. 레지스트리에 닿는 것은 [탐색기 보내기 등록] 을 누를 때 **예전 판이 HKCU 에 남긴 항목이 있으면 지우는 것** 하나뿐이다(새로 쓰지 않음).
+
+### 확인한 것
+
+- 하네스 ArgsTest +16(52건): 3개 → 앞의 둘 · 남은 것 · 알림 글, 2개 · 1개 → 알림 없음, 없는 경로 → 안 열고 그 경로 · "찾을 수 없습니다", 260자 넘는 경로 → 안 열고 "260자", 이름표 붙은 difftool 경로는 안 따짐, 빈 인자, 바로가기 대상 판정(같은 곳 · 대소문자 · 빈 대상 · 지금 실행 파일 모름). 23벌 **1,411건** · 빌드 경고 0 · 오류 0.
+- 화면(`f3-sendto.ps1` · `f3-e2e.ps1` · `f3-e2e2.ps1` · `f3-toast.ps1` · `f3-second.ps1`, 바로가기 대상을 실제 MergeHub Debug 로):
+ - 폴더 2개 · 공백 · 한글 → 창 하나에서 폴더 비교까지(로그 `폴더 비교: 왼쪽 ↔ 오른쪽 · 전체 1개 · 같음 1`). 파일 2개 → 파일 비교.
+ - 3개 → 폴더 비교(앞의 둘) + 로그 · 알림 `3개를 골랐습니다 - …1개(Third)는 두었습니다`, 알림은 이름 창을 닫은 직후.
+ - 긴 경로 → 아무것도 열지 않고 로그 · 알림 `LeftLong 외 1개 - 경로가 277자라 열 수 없습니다…`.
+ - 바로가기 저절로 고침: 사용자의 실제 바로가기를 바이트 그대로 떠 두고 대상을 `D:\옛자리\bin\…` 로 바꿈 → 켜자 지금 실행 파일 · 작업 폴더로 고쳐지고 로그 한 줄, 한 번 더 켜도 파일 그대로 → 원래 파일 **바이트 그대로 되돌림**(원래 대상이 이미 Debug 실행 파일이라 평소 시험으로는 바뀌지 않는다).
+- 시험 재료(`%TEMP%\mergehub-f3`, 긴 경로 포함)는 지웠다.
+
+### 남은 것
+
+- 이미 떠 있는 창으로 보내기(한 번만 실행 · 창 사이 전달) — 플랫폼 일이라 이번 범위 밖.
+- 260자 넘는 경로를 여는 것 — 플랫폼 실행 파일의 긴 경로 설정과 전체 시험이 필요.
+- 여러 개를 골랐을 때 **어느 것이 왼쪽인지**는 탐색기가 넘기는 순서를 따른다(도구는 고른 순서대로 넘겼다). 실제 탐색기 창에서 누른 순서와의 관계는 재지 않았다.
+
## F-12. C# 코드 스타일 맞춤의 기본값을 AURORA 로 (2026-09-18, 완료)
사용자: "기본값은 원본형태를 따르고 C#인 경우에 AURORA 스타일로 하자" → 물어서 정한 것은 **제품 기본값 AURORA · C# 파일만 · 저장 동작은 지금 그대로**(켜져 있으면 저장도 그 스타일로 쓴다).
diff --git a/doc/history/changelog.md b/doc/history/changelog.md
index 8f0af71..934bc67 100644
--- a/doc/history/changelog.md
+++ b/doc/history/changelog.md
@@ -8,6 +8,7 @@
| 날짜 | 항목 | 커밋 |
|---|---|---|
+| 2026-09-22 | **11차 F-3 (추가 요청)** — 탐색기 [보내기] 가 여러 개를 어떻게 넘기는지 셸의 같은 길(바로가기에 Drop)로 일곱 경우를 잼: **프로세스 하나에 인자로 모두**, 폴더 둘은 창 하나에서 폴더 비교까지 됨. 고친 것(사용자 결정): 3개 이상은 앞의 둘 + 알림(예전엔 말없이 버림), 260자 넘는 경로(탐색기가 8.3 이름으로 넘기고 net48 이 긴 이름으로 펼쳐 못 찾음 — 예전엔 아무것도 안 열림)는 열지 않고 까닭 알림, 알림은 이름 창 뒤로 지나가지 않게 본 창이 활성이 된 뒤에. 배포 묶음을 옮기면 켤 때 보내기 바로가기를 지금 실행 파일로 고침(등록한 사람만 · 바로가기 파일만 · 레지스트리 안 씀). 설치 파일 필요 없음 확인. 떠 있을 때 새 창은 그대로(플랫폼 일). ArgsTest +16(1,411건) | 아래 |
| 2026-09-22 | **11차 F-2 (추가 요청)** — **[이 메서드 반영]**: 캐럿이 놓인 클래스 멤버(메서드 · 생성자 · 속성 · 이벤트 · 연산자 · 필드 · enum, 로컬 함수 안이면 감싸는 멤버)를 통째로 반영(사용자 결정: C# + C/C++, 멤버 전부). Roslyn 없이 `MemberRanges` 가 주석 · 문자열(축자 · 보간 · raw · C++ raw · 숫자 구분자)을 가리고 `#if` 갈래마다 다시 읽어 중괄호 짝으로 범위를 잡는다 — 못 잡으면 단추를 끄고 단추 아래 한 줄에 까닭. 덩어리 · 메서드 반영은 머지 주석 한 줄(메서드 이름 위 · 덩어리 첫 줄 위) · 로그 한 줄 · Undo 한 번(묶음 번호) — [이 덩어리 반영] 이 줄마다 주석을 달던 것도 고침(사용자 결정). 메서드 반영 저장본 = 같은 줄 줄 단위 반영 저장본(바이트). 하네스 MemberTest 83건(23벌 1,395건) | 아래 |
| 2026-09-22 | **11차 F-1 (추가 요청)** — 칸 이름을 `LOCAL(Mine)` · `REMOTE(Theirs)` 로(사용자 결정: 표준 용어 + 영문 쪽 이름), 칸 머리마다 한 줄 "어느 쪽 · 어느 파일" — 파일 이름이 BASE 칸에만 있었고, mergetool 에서 Mine 칸이 MERGED 이름을 보였다. 좁으면 가운데를 줄이고 툴팁에 전체(`MiddleTrim`). 머지 주석에 출처(사용자 결정: 쪽 이름 + 브랜치/해시, 출처 먼저 설명 뒤) `[사람] 날짜 Theirs(feature/theirs)` — 로그와 같은 말이 되게 두 칸 로그 `BASE(해시) 반영` · 브랜치 3-Way 로그 `REMOTE(브랜치) 반영` 으로. Take Mine 도 주석을 남긴다(처음 한 번, 되돌리면 함께 빠짐). 무시 조건은 `[사람] 날짜` 뒤를 따지지 않아 코드 그대로 — 옛 · 새 · 출처만 다른 주석을 하네스로 굳힘. 네 길(병합 네 칸 · 두 칸 · `--merge` · git mergetool) 화면 확인, E-1 병합 길 회귀 없음. 하네스 +52(1,312건) | 아래 |
| 2026-09-22 | **11차 A-4** — 변경분을 폴더에 더해 **ZIP 으로도** 뽑는다(사용자 결정). 뽑은 폴더 옆에 같은 이름 `.zip`, 안에 폴더와 안내 파일을 나란히(풀면 폴더로 뽑은 것과 같은 모양). 바이트 · 경로 그대로, 한글 이름은 UTF-8 표시, 반쯤 된 ZIP 은 남기지 않음, 못 만들어도 추출은 성공. 같은 이름 ZIP 이 남아 있으면 새 이름을 고름. 아홉 경우를 다시 뽑아 ZIP 8/8 을 Windows tar 로 풀어 폴더와 경로 · 해시가 같음. 하네스 ReportTest +13(70건). .NET Framework 기본 기능이라 배포 묶음 파일은 그대로 | 아래 |
diff --git a/doc/진행상황.md b/doc/진행상황.md
index 1da8d90..aa8cb91 100644
--- a/doc/진행상황.md
+++ b/doc/진행상황.md
@@ -372,7 +372,7 @@
|---|---|
| **완료** | F-1 칸 이름 `LOCAL(Mine)` · `REMOTE(Theirs)` 로 맞추고 칸마다 "어느 쪽 · 어느 파일"(MERGED · 두 칸 LOCAL 은 "저장할 파일", mergetool 은 임시 파일 이름 — Mine 칸이 MERGED 이름을 보이던 것 고침), 한 줄 · 가운데 줄이기 · 툴팁 전체 · **덧**: 머지 주석 `[사람] 날짜 출처 설명`, 출처는 로그와 같은 말(Theirs(…) · Mine(…) · REMOTE(…) · BASE(해시)), Take Mine 도 주석. 무시 조건은 원래 출처를 받는 모양이라 코드 그대로, 옛 · 새 · 출처만 다른 주석을 하네스로 굳힘. 네 길 화면 확인 · E-1 병합 길 회귀 없음 · 1,312건 |
| **완료** | F-2 메서드 단위로 골라 반영 — [이 메서드 반영](C# + C/C++, 클래스 멤버 전부 · 사용자 결정), 캐럿이 놓인 멤버를 반영해 올 쪽에서 중괄호 짝으로 잡음(`MemberRanges` — 문자열 · 주석 · raw · `#if` 갈래에 안 속음, 못 잡으면 단추 끄고 까닭 한 줄). 덩어리 · 메서드 반영은 주석 한 줄(메서드 이름 위) · 로그 한 줄 · Undo 한 번 — 덩어리가 줄마다 주석을 달던 것도 고침. 메서드 반영 = 같은 줄 줄 단위 반영 바이트 같음. MemberTest 83건(C# 29 · C++ 13 멤버) · 1,395건 |
-| 대기 | F-3 탐색기에서 폴더 두 개를 [보내기] 로 바로 비교 + 설치 파일 없이 되는지 확인 |
+| **완료** | F-3 탐색기 [보내기] 로 폴더 두 개 바로 비교 — 탐색기는 여러 개를 프로세스 하나에 인자로 모두 넘김(일곱 경우 값으로 잼), 폴더 둘은 창 하나에서 폴더 비교까지. 사용자 결정대로 3개 이상은 앞의 둘 + 알림, 260자 넘는 경로(탐색기가 8.3 이름으로 넘김 · net48 이 못 찾음)는 열지 않고 알림, 옮긴 배포 묶음은 켤 때 보내기 바로가기를 저절로 고침(바로가기 파일만 · 레지스트리 안 씀), 떠 있을 때는 새 창 그대로. 설치 파일 필요 없음. ArgsTest +16 · 1,411건 |
| 대기 | F-4 파일 비교 탭 툴박스(ACTIONS) 감추는 단추 (지금은 접어도 폭 그대로) |
| 대기 | F-5 글자 크기 조절 (지금은 Consolas 12 고정) |
| 대기 | F-6 글자를 되도록 많이 보이게 — 넓게 보기 한 단추, 늘어난 줄 수로 잼 |
diff --git a/src/MergeHub/Services/GitWorkspace.cs b/src/MergeHub/Services/GitWorkspace.cs
index 7fa0b59..f1410ca 100644
--- a/src/MergeHub/Services/GitWorkspace.cs
+++ b/src/MergeHub/Services/GitWorkspace.cs
@@ -77,6 +77,17 @@ namespace MergeHub.Services
// 요청을 남겨 둘 자리로 가장 이르다.
RouteStartupArgs();
+ // 보내기 바로가기가 옛 자리를 가리키면 지금 실행 파일로 고친다(11차 F-3). 등록한 사람만, 레지스트리는 안 건드린다.
+ try
+ {
+ if (ShellIntegration.RepairIfMoved(ShellIntegration.CurrentExecutable()) is { } old)
+ AppendLog($"보내기 바로가기가 옛 자리({old})를 가리켜 지금 실행 파일로 고쳤습니다");
+ }
+ catch (Exception ex)
+ {
+ _logger.W($"보내기 바로가기 확인 실패: {ex.Message}");
+ }
+
// 설정 화면에서 이름을 바꾸면 바로 따라간다.
if (_currentUser is not null)
{
@@ -712,6 +723,7 @@ namespace MergeHub.Services
private void RouteStartupArgs()
{
var args = StartupArgs.Current;
+ if (!ReportArgs(args, "탐색기에서")) return;
if (args.WantsFolder)
{
@@ -752,6 +764,7 @@ namespace MergeHub.Services
if (paths is null || paths.Count == 0) return;
var args = StartupArgs.Parse(paths);
+ if (!ReportArgs(args, "끌어다 놓기")) return;
if (args.WantsFolder)
{
@@ -769,6 +782,37 @@ namespace MergeHub.Services
AppendLog($"끌어다 놓기 - 파일 비교: {left} ↔ {right}");
}
+ ///
+ /// 넘어온 경로에 알릴 것(열 수 없는 경로 · 셋째부터)이 있으면 로그와 알림에 같은 말로 남긴다(11차 F-3).
+ /// 열 수 없는 경로가 있으면 false - 아무것도 열지 않는다.
+ ///
+ private bool ReportArgs(StartupArgs args, string from)
+ {
+ if (args.Notice is not { } notice) return true;
+
+ AppendLog($"{from}: {notice}");
+ // 켤 때는 사용자 이름 창(모달)이 먼저 뜬다. 그 뒤에서 알림이 지나가 버리지 않게, 본 창이 활성이 된 뒤에 띄운다.
+ var app = System.Windows.Application.Current;
+ app?.Dispatcher.BeginInvoke(new Action(() =>
+ {
+ var window = app.MainWindow;
+ if (window is null || window.IsActive)
+ {
+ MessageUtil.ShowWarningToast(notice);
+ return;
+ }
+
+ EventHandler? once = null;
+ once = (_, _) =>
+ {
+ window.Activated -= once;
+ MessageUtil.ShowWarningToast(notice);
+ };
+ window.Activated += once;
+ }), System.Windows.Threading.DispatcherPriority.ApplicationIdle);
+ return args.Missing.Count == 0;
+ }
+
private static void ActivateWhenReady(string viewId)
{
var app = System.Windows.Application.Current;
diff --git a/src/MergeHub/Services/ShellIntegration.cs b/src/MergeHub/Services/ShellIntegration.cs
index 9beb116..a2df5e3 100644
--- a/src/MergeHub/Services/ShellIntegration.cs
+++ b/src/MergeHub/Services/ShellIntegration.cs
@@ -72,6 +72,54 @@ namespace MergeHub.Services
RemoveLegacyRegistryEntries();
}
+ ///
+ /// 등록해 둔 바로가기가 옛 자리를 가리키면 지금 실행 파일로 고친다(11차 F-3, 사용자 결정 "켜질 때 저절로").
+ /// 배포 묶음을 다른 폴더로 옮기면 바로가기가 없는 파일을 가리켜 보내기가 동작하지 않았다.
+ ///
+ /// 등록하지 않은 사람은 건드리지 않는다. 바로가기 파일만 다시 쓰고 레지스트리는 손대지 않는다
+ /// ( 의 옛 흔적 치우기도 부르지 않는다).
+ ///
+ /// 고쳤으면 옛 대상, 아니면 null.
+ public static string? RepairIfMoved(string executablePath)
+ {
+ if (!IsRegistered || string.IsNullOrWhiteSpace(executablePath)) return null;
+
+ var shell = Type.GetTypeFromProgID("WScript.Shell");
+ if (shell is null) return null;
+ var host = Activator.CreateInstance(shell);
+ if (host is null) return null;
+
+ var link = shell.InvokeMember("CreateShortcut", System.Reflection.BindingFlags.InvokeMethod,
+ null, host, new object[] { ShortcutPath });
+ if (link is null) return null;
+
+ var type = link.GetType();
+ var old = type.InvokeMember("TargetPath", System.Reflection.BindingFlags.GetProperty, null, link, null) as string ?? string.Empty;
+ if (!PointsElsewhere(old, executablePath)) return null;
+
+ Set(type, link, "TargetPath", executablePath);
+ Set(type, link, "WorkingDirectory", Path.GetDirectoryName(executablePath) ?? string.Empty);
+ type.InvokeMember("Save", System.Reflection.BindingFlags.InvokeMethod, null, link, null);
+ return old;
+ }
+
+ /// 바로가기 대상이 지금 실행 파일과 다른 곳인지(대소문자 · 경로 모양은 가리지 않음).
+ public static bool PointsElsewhere(string? linkTarget, string? currentExecutable)
+ {
+ if (string.IsNullOrWhiteSpace(currentExecutable)) return false;
+ if (string.IsNullOrWhiteSpace(linkTarget)) return true;
+
+ try
+ {
+ return !string.Equals(Path.GetFullPath(linkTarget!.Trim()), Path.GetFullPath(currentExecutable!.Trim()),
+ StringComparison.OrdinalIgnoreCase);
+ }
+ catch (Exception e) when (e is ArgumentException || e is NotSupportedException || e is PathTooLongException)
+ {
+ return true;
+ }
+ }
+
private static void Set(Type type, object link, string name, string value) =>
type.InvokeMember(name, System.Reflection.BindingFlags.SetProperty, null, link, new object[] { value });
diff --git a/src/MergeHub/Services/StartupArgs.cs b/src/MergeHub/Services/StartupArgs.cs
index cf42535..45df91f 100644
--- a/src/MergeHub/Services/StartupArgs.cs
+++ b/src/MergeHub/Services/StartupArgs.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
namespace MergeHub.Services
{
@@ -51,6 +52,57 @@ namespace MergeHub.Services
public string LooseLeft { get; private set; } = string.Empty;
public string LooseRight { get; private set; } = string.Empty;
+ ///
+ /// 셋째부터 넘어온 경로(11차 F-3). 탐색기 [보내기] 는 고른 것을 모두 한 번에 넘기는데 비교는 둘이라,
+ /// 앞의 둘만 열고 나머지는 알린다(사용자 결정). 예전에는 말없이 버렸다.
+ ///
+ public IReadOnlyList Extra { get; private set; } = Array.Empty();
+
+ ///
+ /// 앞의 둘 가운데 파일도 폴더도 아닌 경로(11차 F-3). 있으면 아무것도 열지 않고 알린다.
+ /// 260자를 넘는 경로는 탐색기가 8.3 짧은 이름으로 넘기는데, .NET Framework 가 긴 이름으로 펼친 뒤
+ /// 찾지 못한다 - 예전에는 폴더인데도 "파일 비교 요청" 으로 적히고 아무것도 안 열렸다.
+ /// 이름표 붙은 경로(git difftool 의 지워진 쪽 등)는 따지지 않는다.
+ ///
+ public IReadOnlyList Missing { get; private set; } = Array.Empty();
+
+ /// 사용자에게 알릴 한 줄(없으면 null). 로그와 알림에 같은 말을 쓴다.
+ public string? Notice
+ {
+ get
+ {
+ if (Missing.Count > 0)
+ {
+ var first = Missing[0];
+ var why = FullLength(first) >= 260
+ ? $"경로가 {FullLength(first)}자라 열 수 없습니다(260자가 넘는 경로는 아직 열지 못합니다)"
+ : "찾을 수 없습니다";
+ var more = Missing.Count > 1 ? $" 외 {Missing.Count - 1}개" : string.Empty;
+ return $"{Path.GetFileName(first.TrimEnd('\\', '/'))}{more} - {why}";
+ }
+
+ if (Extra.Count > 0)
+ {
+ var names = string.Join(", ", Extra.Select(p => Path.GetFileName(p.TrimEnd('\\', '/'))));
+ return $"{Extra.Count + 2}개를 골랐습니다 - 앞의 둘만 비교하고 {Extra.Count}개({names})는 두었습니다";
+ }
+
+ return null;
+ }
+ }
+
+ private static int FullLength(string path)
+ {
+ try
+ {
+ return Path.GetFullPath(path).Length;
+ }
+ catch (Exception e) when (e is ArgumentException || e is NotSupportedException || e is PathTooLongException)
+ {
+ return path.Length;
+ }
+ }
+
/// 이번 실행에 넘어온 인자. 한 번만 읽는다.
public static StartupArgs Current { get; } = Parse(SafeArgs());
@@ -111,6 +163,17 @@ namespace MergeHub.Services
{
if (loose.Count == 0) return;
+ if (loose.Count > 2) result.Extra = loose.GetRange(2, loose.Count - 2);
+
+ var missing = loose.Take(2).Where(p => !Exists(p)).ToList();
+ if (missing.Count > 0)
+ {
+ // 없는 것을 열려 하면 엉뚱한 갈래(폴더인데 파일 비교)로 가서 빈 화면이 된다. 아무것도 열지 않는다.
+ result.Missing = missing;
+ result.Mode = StartupMode.None;
+ return;
+ }
+
var first = loose[0];
var second = loose.Count > 1 ? loose[1] : string.Empty;
@@ -129,6 +192,18 @@ namespace MergeHub.Services
result.LooseRight = second;
}
+ private static bool Exists(string path)
+ {
+ try
+ {
+ return File.Exists(path) || Directory.Exists(path);
+ }
+ catch (ArgumentException)
+ {
+ return false;
+ }
+ }
+
private static bool IsFolder(string path)
{
try