diff --git a/doc/history/2026-09-18-11차.md b/doc/history/2026-09-18-11차.md
index d5f65db..4f74515 100644
--- a/doc/history/2026-09-18-11차.md
+++ b/doc/history/2026-09-18-11차.md
@@ -159,6 +159,7 @@ A-1 에서 값으로 확인한 것 — 지운 파일은 "삭제되어 건너뜀
- 개요 "로컬 커밋" 숫자(버전 14 는 129).
- 이 뒤 11차 항목(F 등)에서 바뀌는 화면 · 설정 · 숫자.
+- F-1: 칸 이름 `LOCAL(Mine)` · `REMOTE(Theirs)`, 칸 머리 "어느 쪽 · 어느 파일"(MERGED · 두 칸 LOCAL 은 "(저장할 파일)", mergetool 은 임시 파일 이름), 긴 이름은 가운데 줄임 · 툴팁. 머지 주석 모양 `[사람] 날짜 출처 설명`(출처 = 로그의 말 — Theirs(…) · Mine(…) · REMOTE(…) · BASE(…)), **Take Mine 도 주석**, 두 칸 로그 `BASE(해시) 반영` · 브랜치 3-Way 로그 `REMOTE(브랜치) 반영`. 매뉴얼의 주석 예시 · 칸 설명 · 해결 기록이 남는 곳 표. 숫자 1,312건.
## B-1. 배포 묶음을 다시 만들고 꼭 필요한 파일을 가림 (2026-09-22, 완료)
@@ -500,6 +501,51 @@ libgit2 의 다듬기(`PrettifyMessages`)를 켜면 **손대지 않은 뒤 커
- **A4 쪽 높이**: 고치기 전 · 뒤 원문을 헤드리스 Edge 로 재니 8쪽 모두 1123px · 넘침 없음으로 같다. 기능 목록 쪽 그림으로 한 줄씩인 것 확인.
- **잠금은 사용자가 했다**(`lock-plan.js`, 비밀번호는 화면에 안 보이게 받아 어디에도 남기지 않음 · 비교할 원본은 09-17 원문). 첫 "잠갔어" 때는 잠금 파일이 09-17 것 그대로여서 올리지 않고 멈췄다(PowerShell 문법을 CMD 에서 돌린 것으로 보임). 두 번째에 새 salt · IV 의 잠금 파일(13:12)이 생겨 올렸고, 발행본의 암호문이 그 파일과 같음을 되읽어 확인했다. 스크립트 출력은 다른 창이라 직접 보지 못했다.
+## F-1. 칸 이름을 맞추고 칸마다 어느 파일인지 · 머지 주석에 어디서 가져왔는지 (2026-09-22, 완료)
+
+사용자 결정(2026-09-22): 칸 이름은 **표준 용어 + 영문 쪽 이름**(`LOCAL(Mine)` · `REMOTE(Theirs)` — 4차 · 6차의 "표준 용어 그대로" 를 이어 가고 로그의 Mine/Theirs 와 맞춤), 주석 출처는 **쪽 이름 + 브랜치/해시**, 설명이 있으면 **출처 먼저, 설명 뒤**.
+
+### 고치기 전 — 화면에서 (`f1-probe.ps1`)
+
+| 길 | 칸 머리에 보인 것 |
+|---|---|
+| 병합 네 칸 | BASE `(merge base) Weld/Unit1.cs` · LOCAL (Mine) `feature/mine` · MERGED `feature/mine` · REMOTE (Theirs) `REMOTE · feature/theirs` — **파일 이름은 BASE 칸에만**, REMOTE 칸은 제목의 말을 되풀이 |
+| 두 칸 | BASE `9b064cc · Weld/Unit1.cs` · LOCAL `feature/mine` — 저장하는 칸에 파일이 없음 |
+| git mergetool · `--merge` (코드로) | **Mine 칸이 MERGED 칸과 같은 값**(저장할 파일 이름)을 써서, 실제로 읽은 `--local` 임시 파일과 다른 이름이 보였다 |
+
+머지 주석은 `// [김승준] 2026-09-22` 뿐이고 Take Mine 은 주석을 남기지 않았다(로그에만 "정함").
+
+### 고친 것
+
+- **칸 머리** — 칸마다 한 줄 "어느 쪽 · 어느 파일"(`PaneHeaders`). 네 칸: `feature/mine · Weld/Unit1.cs` · `(merge base) Weld/Unit1.cs` · `Weld/Unit1.cs (저장할 파일)` · `feature/theirs · Weld/Unit1.cs`. 두 칸: `3a12077 · Weld/Unit1.cs` · `feature/mine · Weld/Unit1.cs (저장할 파일)`. mergetool · `--merge`: 네 칸이 제 임시 파일 이름(Mine 칸은 새 `MineHeader`). rebase 는 `올릴 자리 …`, cherry-pick · revert 는 `가져오는 커밋 …` · `되돌리는 커밋 …` 에 파일이 붙는다.
+- 제목 `LOCAL (Mine)` · `REMOTE (Theirs)` → `LOCAL(Mine)` · `REMOTE(Theirs)`. 네 칸 머리 글씨를 모두 BASE 와 같은 모양(읽으라고 쓰는 글씨)으로.
+- **한 줄 · 가운데 줄이기** — WPF 끝 자르기는 파일 이름부터 잘라서, 폭에 맞게 가운데를 `…` 로 줄이는 `MiddleTrim`(앞 조각 굵게는 그대로). 전체는 툴팁.
+- **머지 주석 출처** — `[사람] 날짜 출처 설명`(`MergeNote.Build` 에 출처). 출처는 로그와 같은 말(`MergeNames.TheirsSource` · `MineSource` · `BaseSource`): 네 칸 `Theirs(feature/theirs)` · `Mine(feature/mine)`, rebase · cherry-pick · revert `Theirs(커밋 42e68b2)`, 브랜치 3-Way `REMOTE(develop)`, 두 칸 `BASE(3a12077)`(Git 아니면 왼쪽 파일 이름).
+- 로그도 같은 말로 — 두 칸 `9행 BASE 반영` → `9행 BASE(af3925e) 반영`, 브랜치 3-Way `7행 develop 반영` → `7행 REMOTE(develop) 반영`. 네 칸 로그는 원래 같은 말이라 그대로.
+- **Take Mine 도 주석을 남긴다**(지시서 판정 기준) — 병합의 두 쪽에서 내 것으로 정하면 `Mine(…)` 주석 한 줄. 같은 줄을 또 눌러도 쌓이지 않고(처음 한 번), 되돌리기가 주석 줄까지 뺀다. 브랜치 3-Way 의 "내 원본으로 되돌림" 은 되돌리기라 주석 없음(그대로).
+- **무시 조건** — 알아보는 규칙(`[사람] 날짜` 뒤는 따지지 않음)이 원래 출처를 받아들이는 모양이라 `NoteBody` · `LooksLikeNote` · `TailStart` · `CompareRules` · `FolderComparer` · `NoteConflict` · `NoteRows` 는 코드를 바꾸지 않았다. 대신 옛 모양 · 새 모양 · 출처만 다른 모양을 하네스로 굳힘.
+
+### 확인한 것
+
+- 하네스: NoteTest(출처 자리 · 옛 모양 바이트 같음 · XAML 닫는 기호 · 줄 끝 조각 · 무시 키 · 출처만 다른 충돌 자동 해결 두 주석 다 남김 · 코드가 다르면 안 풂) · RulesTest(무시 · 규칙 끄면 차이) · FolderTest(출처만 다름 · 옛↔새 → **같음(머지 주석만 다름)**, 규칙 끄면 다름(코드는 같음)) · MergeSaveTest(출처 = 로그의 말 · 칸 머리 · 가운데 줄이기). 22벌 **1,312건** 통과(1,260 → +52) · 빌드 경고 0 · 오류 0.
+- 화면(`f1-verify.ps1` · `f1-trim.ps1`, UIA 만 — 마우스 안 씀):
+
+| 길 | 칸 머리 | 누른 것 → 로그 | 파일에 남은 주석 |
+|---|---|---|---|
+| 병합 네 칸 | `feature/mine · Weld/Unit1.cs` · `(merge base) Weld/Unit1.cs` · `Weld/Unit1.cs (저장할 파일)` · `feature/theirs · Weld/Unit1.cs` | `101행 Theirs(feature/theirs) 반영` · `802행 Mine(feature/mine) 으로 정함` (Take Mine 한 번 더 → 단추 꺼짐) | `// [김승준] 2026-09-22 Theirs(feature/theirs)` · `… Mine(feature/mine)` — 범례 무시 2 |
+| 두 칸 | `af3925e · Weld/Unit1.cs` · `feature/mine · Weld/Unit1.cs (저장할 파일)` | `9행 BASE(af3925e) 반영` | 화면의 주석 줄 `// [김승준] 2026-09-22 BASE(af3925e)`(두 칸은 Save & Commit 뿐이라 커밋하지 않고 화면에서 읽음) |
+| `--merge` | `Unit1_LOCAL_4321.cs` · `Unit1_BASE_4321.cs` · `Unit1.cs (저장할 파일)` · `Unit1_REMOTE_4321.cs` | `Theirs(Unit1_REMOTE_4321.cs) 반영` · `Mine(Unit1_LOCAL_4321.cs) 으로 정함` | 같은 말 두 줄 |
+| git mergetool (`git -c` 로만 도구 설정 — 설정 파일에 안 남음 확인) | `Unit1_LOCAL_290.cs` · `Unit1_BASE_290.cs` · `Unit1.cs (저장할 파일)` · `Unit1_REMOTE_290.cs` | `Theirs(Unit1_REMOTE_290.cs) 반영` | 같은 말 |
+
+- 칸 머리는 모두 한 줄(높이 21). 117자 파일 이름으로 `--merge`: `WeldSequenceController_Laser…ipeParameterValidation_Unit1_LOCAL_4321.cs` 처럼 가운데가 줄고 끝(쪽 · 파일 이름)이 남음, 툴팁에 전체. 창 폭을 UIA 로 줄이는 것은 이 창에서 먹지 않아 긴 이름으로 대신 봄.
+- 회귀: 11차 E-1 병합 길(`e11-e1-merge.ps1 -Way merge`, 마우스 확인 B-1 · 스크린숏만 끔) — 누른 Take 18 = 로그 18, 병합 커밋 본문 26개, 줄끝 · BOM 바뀐 파일 0 · 충돌 표식 0 · class 검사 0, 옛 모양 주석만 다른 2건 자동 해결 그대로. 결과 파일의 Take Mine 줄 위에 `Mine(feature/mine)` 주석이 새로 들어간다(이번 변경).
+ - 첫 번째 회차는 1) 파일 여러 개 골라 Accept Theirs 가 한 개만 골라져(24 → 23, 20초) 줄 단위로 처음 가 보는 파일들에서 Take 가 꺼져 있었다. 그 파일들을 새로 열어 보면 Take 둘 다 켜짐(`f1-take-probe.ps1`), 두 번째 회차는 1) 이 24 → 9(1초)로 예전과 같아 끝까지 — 시험 도구의 여러 개 고르기가 한 번 빗나간 것으로 본다.
+
+### 알아 둘 것
+
+- Take Mine 주석이 새로 들어가므로 같은 순서로 풀어도 결과 파일이 전보다 주석 줄 수만큼 길다(무시 규칙이 켜져 있으면 차이로 세지 않음). 코드 줄 · 줄끝 · BOM 은 그대로.
+- 파일 단위 Accept(충돌 탭)는 여전히 소스에 주석을 남기지 않는다 — 병합 커밋 본문(8차 F-1)이 그 기록이다.
+
## F-12. C# 코드 스타일 맞춤의 기본값을 AURORA 로 (2026-09-18, 완료)
사용자: "기본값은 원본형태를 따르고 C#인 경우에 AURORA 스타일로 하자" → 물어서 정한 것은 **제품 기본값 AURORA · C# 파일만 · 저장 동작은 지금 그대로**(켜져 있으면 저장도 그 스타일로 쓴다).
diff --git a/doc/history/changelog.md b/doc/history/changelog.md
index 5f45e8d..9e75491 100644
--- a/doc/history/changelog.md
+++ b/doc/history/changelog.md
@@ -8,6 +8,7 @@
| 날짜 | 항목 | 커밋 |
|---|---|---|
+| 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 기본 기능이라 배포 묶음 파일은 그대로 | 아래 |
| 2026-09-22 | **11차 D-1** — 시험 도구 둘(제품 아님). 줄끝 검사 스크립트가 부모 하나인 커밋(rebase · cherry-pick · revert)에서 없는 둘째 부모로 `git merge-base` 를 불러 오류를 찍던 것 → 첫 부모와만 견준다고 적고 부르지 않음. 파일 비교 탭 요약을 읽는 함수가 제품의 "차이 없음" 을 못 읽어 커밋 뒤 요약이 빈칸이던 것 → 읽음. rebase 한 길을 다시 돌려 오류 줄 0 · 요약 "차이 없음". 제품 코드 변경 없음 | 아래 |
| 2026-09-22 | **11차 E-2** — 웹 문서(버전 13)와 기획안(버전 17)을 11차에 맞춤. 두 발행본 모두 로컬 파일과 같음을 먼저 확인. 매뉴얼에 "커밋 메시지 고치기"([커밋 수정] 과 우클릭 [메시지 바꾸기...] 를 한 표로) · "변경분 추출"(새 폴더 · 안내 파일) 두 절과 한계 셋, 남은 일 · 만든 것에 11차, 숫자 1,247건 · 커밋 129. 단추 이름 91개를 소스에서 찾아 09-18 에 적은 "맞추지 않음" 을 실제 글자 "C# 스타일 끔" 으로 바로잡음. 기획안은 커밋 메시지 수정 완료 · 4단계 준비 중 · 개정 09-22, 8쪽 A4 높이 그대로, 잠금은 사용자가 비밀번호로 | 아래 |
diff --git a/doc/진행상황.md b/doc/진행상황.md
index 4078fc0..a11c4a1 100644
--- a/doc/진행상황.md
+++ b/doc/진행상황.md
@@ -370,7 +370,7 @@
| 상태 | 항목 |
|---|---|
-| 대기 | F-1 LOCAL · REMOTE · MERGED 이름 맞추고 칸마다 어느 파일인지 보이게 (지금은 경로가 맨 위 한 줄에만) · **덧**: 머지 주석에도 어디서 가져왔는지(REMOTE/BASE · 브랜치/해시) 남기고 그에 맞춰 무시 조건(`MergeNote` · `CompareRules` · `FolderComparer` · `NoteConflict` · `NoteRows`) 수정 — 옛 주석도 그대로 무시 |
+| **완료** | F-1 칸 이름 `LOCAL(Mine)` · `REMOTE(Theirs)` 로 맞추고 칸마다 "어느 쪽 · 어느 파일"(MERGED · 두 칸 LOCAL 은 "저장할 파일", mergetool 은 임시 파일 이름 — Mine 칸이 MERGED 이름을 보이던 것 고침), 한 줄 · 가운데 줄이기 · 툴팁 전체 · **덧**: 머지 주석 `[사람] 날짜 출처 설명`, 출처는 로그와 같은 말(Theirs(…) · Mine(…) · REMOTE(…) · BASE(해시)), Take Mine 도 주석. 무시 조건은 원래 출처를 받는 모양이라 코드 그대로, 옛 · 새 · 출처만 다른 주석을 하네스로 굳힘. 네 길 화면 확인 · E-1 병합 길 회귀 없음 · 1,312건 |
| 대기 | F-2 메서드 단위로 골라 반영 · 머지 주석은 메서드 이름 위 한 줄 (지금은 줄 · 덩어리 · 전체) |
| 대기 | F-3 탐색기에서 폴더 두 개를 [보내기] 로 바로 비교 + 설치 파일 없이 되는지 확인 |
| 대기 | F-4 파일 비교 탭 툴박스(ACTIONS) 감추는 단추 (지금은 접어도 폭 그대로) |
diff --git a/src/MergeHub/Services/MergeNames.cs b/src/MergeHub/Services/MergeNames.cs
index 7d32da2..f49d669 100644
--- a/src/MergeHub/Services/MergeNames.cs
+++ b/src/MergeHub/Services/MergeNames.cs
@@ -81,7 +81,7 @@ namespace MergeHub.Services
///
/// 병합 충돌(또는 git mergetool 세 벌)이면 Theirs(feature/theirs) · Mine(feature/mine) 처럼 어느 쪽인지와 이름을 함께.
/// 예전에는 3-Way 콤보 값을 그대로 써서, 콤보를 안 건드리고 네 칸을 띄우는 충돌 파일에서 "(3-Way 안 씀) 반영" 이라고 적혔다.
- /// 콤보로 고른 브랜치 3-Way 는 지금처럼 브랜치 이름만 쓴다.
+ /// 콤보로 고른 브랜치 3-Way 는 브랜치 이름만 돌려준다 - 로그 · 주석은 그 앞에 REMOTE 를 붙여 쓴다(, 11차 F-1).
///
public static string SideLabel(bool mine, string? name, bool mergeSides)
{
@@ -92,13 +92,43 @@ namespace MergeHub.Services
return trimmed is null ? side : $"{side}({trimmed})";
}
- /// Take Theirs 로그. 상대 쪽에 없는 줄이면 지운 것으로 적는다.
+ ///
+ /// Take Theirs 로그. 상대 쪽에 없는 줄이면 지운 것으로 적는다.
+ /// 쪽 이름은 머지 주석의 출처와 같은 말이다(, 11차 F-1).
+ ///
public static string TakeTheirsLog(string? number, string? theirs, bool hasLine, bool mergeSides)
{
- var label = SideLabel(mine: false, theirs, mergeSides);
+ var label = TheirsSource(theirs, mergeSides);
return hasLine ? $"{number}행 {label} 반영" : $"{number}행 삭제({label} 에 없음)";
}
+ /// 두 칸에서 BASE 줄을 가져온 로그(11차 F-1). 주석 출처와 같은 말 BASE(4eb9ece).
+ public static string TakeBaseLog(string? number, string? baseName, bool hasLine)
+ {
+ var label = BaseSource(baseName);
+ return hasLine ? $"{number}행 {label} 반영" : $"{number}행 삭제({label} 에 없음)";
+ }
+
+ // ---- 머지 주석의 출처(11차 F-1) ----
+ // 소스에 남는 주석에 "어디서 가져온 줄인지" 를 적는다. 로그와 같은 말을 써야 로그를 끈 뒤에도 소스만 보고 맞춰 볼 수 있다.
+ // 네 칸(병합 · pull) Theirs(feature/theirs) · Mine(feature/mine)
+ // rebase · cherry-pick · revert Theirs(커밋 4eb9ece) - 이름은 OperationLabels.TheirsLogName 이 준다
+ // 브랜치 3-Way(콤보) REMOTE(develop) - 병합의 두 쪽이 아니라 고른 브랜치다
+ // 두 칸 BASE(4eb9ece) - 견준 커밋. Git 이 아니면 왼쪽 파일 이름
+
+ /// 상대 쪽에서 가져온 줄의 출처.
+ public static string TheirsSource(string? name, bool mergeSides) =>
+ mergeSides ? SideLabel(mine: false, name, mergeSides: true) : Named("REMOTE", name);
+
+ /// 내 쪽으로 정한 줄의 출처.
+ public static string MineSource(string? name) => SideLabel(mine: true, name, mergeSides: true);
+
+ /// 두 칸에서 BASE 쪽을 가져온 줄의 출처.
+ public static string BaseSource(string? name) => Named("BASE", name);
+
+ private static string Named(string side, string? name) =>
+ string.IsNullOrWhiteSpace(name) ? side : $"{side}({name!.Trim()})";
+
///
/// Take Mine 로그. 이미 내 것과 같으면 "정함" 이라고 적는다 - 예전에는 아무것도 안 적어 무엇을 골랐는지 로그에 없었다.
/// 콤보로 고른 브랜치 3-Way 에서는 지금 문구("내 원본으로 되돌림")를 그대로 쓴다.
diff --git a/src/MergeHub/Services/MergeNote.cs b/src/MergeHub/Services/MergeNote.cs
index 47574c0..236bdf7 100644
--- a/src/MergeHub/Services/MergeNote.cs
+++ b/src/MergeHub/Services/MergeNote.cs
@@ -18,7 +18,7 @@ namespace MergeHub.Services
private const string Gap = " ";
///
- /// 주석 한 조각을 만든다. 구성은 딱 세 가지 - [사용자] 날짜 설명.
+ /// 주석 한 조각을 만든다. 구성은 [사용자] 날짜 출처 설명 - 출처는 11차 F-1 에 더했다.
/// 설명이 비어 있어도 [사용자]와 날짜는 항상 남는다.
///
///
@@ -31,12 +31,25 @@ namespace MergeHub.Services
public static string AuthorOf(string? author) =>
string.IsNullOrWhiteSpace(author) ? Environment.UserName : author!.Trim();
- public static string Build(string? author, DateTime when, string? comment, string? path)
+ public static string Build(string? author, DateTime when, string? comment, string? path) =>
+ Build(author, when, source: null, comment, path);
+
+ ///
+ /// 어디서 가져온 줄인지(출처)까지 적는다(11차 F-1). [사람] 날짜 출처 설명 - 출처가 늘 같은 자리에 있게 설명보다 앞.
+ /// 출처는 Git 탭 로그와 같은 말이다( 등). 비어 있으면 옛 모양 그대로.
+ ///
+ /// 알아보는 쪽()은 [사람] 날짜 뒤를 따지지 않으므로 옛 주석 · 새 주석을 똑같이 알아본다.
+ /// 그래서 무시 · 폴더 비교 · 충돌 자동 해결에서 출처만 다른 두 주석은 다름이 아니다.
+ ///
+ public static string Build(string? author, DateTime when, string? source, string? comment, string? path)
{
var who = AuthorOf(author);
var body = $"[{who}] {when:yyyy-MM-dd}";
+ var from = source?.Trim();
+ if (!string.IsNullOrEmpty(from)) body += $" {from}";
+
var note = comment?.Trim();
if (!string.IsNullOrEmpty(note)) body += $" {note}";
diff --git a/src/MergeHub/Services/PaneHeaders.cs b/src/MergeHub/Services/PaneHeaders.cs
new file mode 100644
index 0000000..8dd8049
--- /dev/null
+++ b/src/MergeHub/Services/PaneHeaders.cs
@@ -0,0 +1,51 @@
+using System;
+
+namespace MergeHub.Services
+{
+ ///
+ /// 칸 머리 한 줄 - 그 칸이 어느 쪽 · 어느 파일인지(11차 F-1).
+ ///
+ /// 칸 이름(BASE · LOCAL(Mine) · REMOTE(Theirs) · MERGED)은 큰 제목이 말하고, 그 아래 한 줄이 이것이다.
+ /// 예전에는 파일 경로가 BASE 칸에만 있었고 Mine · MERGED 칸은 브랜치 이름뿐이라, git mergetool 처럼
+ /// 네 칸이 서로 다른 파일을 볼 때 어느 칸이 어느 파일인지 화면에 없었다.
+ ///
+ /// 한 줄을 넘기지 않는다. 좁으면 로 가운데를 줄이고 전체는 마우스를 올리면 보인다.
+ ///
+ public static class PaneHeaders
+ {
+ private const string Separator = " · ";
+
+ /// 줄였다는 표시. 한 글자라 글자 수 셈이 쉽다.
+ public const string Ellipsis = "…";
+
+ /// 쪽(브랜치 · 커밋 · 올릴 자리)과 파일. 한쪽을 모르면 아는 것만.
+ public static string Of(string? where, string? path)
+ {
+ var w = where?.Trim() ?? string.Empty;
+ var p = path?.Trim() ?? string.Empty;
+ if (w.Length == 0) return p;
+ if (p.Length == 0) return w;
+ return w + Separator + p;
+ }
+
+ /// 저장하는 칸(네 칸의 MERGED · 두 칸의 LOCAL). 저장 단추가 쓰는 파일이 이것이라고 적는다.
+ public static string Saved(string? where, string? path) => Of(where, path) + " (저장할 파일)";
+
+ ///
+ /// 글자 개에 맞게 가운데를 줄인다. 들어가면 그대로.
+ ///
+ /// 끝을 자르면(WPF 기본) 가장 중요한 파일 이름이 잘린다. 앞(브랜치)은 조금, 뒤(파일 이름)는 많이 남긴다.
+ ///
+ public static string Cut(string? text, int keep)
+ {
+ if (string.IsNullOrEmpty(text) || keep <= 0) return string.Empty;
+ if (text!.Length <= keep) return text;
+ if (keep == 1) return Ellipsis;
+
+ var room = keep - Ellipsis.Length;
+ var head = room * 2 / 5;
+ var tail = room - head;
+ return text.Substring(0, head) + Ellipsis + text.Substring(text.Length - tail);
+ }
+ }
+}
diff --git a/src/MergeHub/ViewModels/HomeViewModel.cs b/src/MergeHub/ViewModels/HomeViewModel.cs
index 9429481..6af8b1b 100644
--- a/src/MergeHub/ViewModels/HomeViewModel.cs
+++ b/src/MergeHub/ViewModels/HomeViewModel.cs
@@ -224,8 +224,12 @@ namespace MergeHub.ViewModels
BaseHeader = string.IsNullOrEmpty(baseText)
? "(BASE 없음) 양쪽에서 새로 만든 파일입니다"
: Path.GetFileName(request.Base);
- LocalHeader = Path.GetFileName(request.Merged);
- RemoteHeader = $"REMOTE · {Path.GetFileName(request.Remote)}";
+ // 네 칸이 모두 다른 파일이다(--base · --local · --remote · --merged). 칸마다 제 파일 이름(11차 F-1).
+ // 예전에는 Mine 칸도 LocalHeader(저장할 파일 이름)를 같이 써서 실제로 읽은 --local 과 달랐다.
+ MineHeader = Path.GetFileName(request.Local);
+ LocalHeader = PaneHeaders.Saved(null, Path.GetFileName(request.Merged));
+ RemoteHeader = Path.GetFileName(request.Remote);
+ _baseSourceName = Path.GetFileName(request.Base);
_theirsSideName = Path.GetFileName(request.Remote);
_mineSideName = Path.GetFileName(request.Local);
@@ -481,7 +485,7 @@ namespace MergeHub.ViewModels
[ObservableProperty] private string? _selectedFile;
[ObservableProperty] private MergeLine? _selectedLine;
[ObservableProperty] private string _baseHeader = "BASE";
- [ObservableProperty] private string _localHeader = "LOCAL (Mine)";
+ [ObservableProperty] private string _localHeader = "LOCAL";
[ObservableProperty] private string _diffSummary = "-";
[ObservableProperty] private string _editSummary = "-";
[ObservableProperty] private string _commitMessage = string.Empty;
@@ -620,7 +624,7 @@ namespace MergeHub.ViewModels
/// 실제로 줄 끝에 붙을 주석을 미리 보여준다.
public string MergeNotePreview =>
- _workspace.Settings.InsertMergeComment ? BuildMergeNote() : "(주석 남기기 꺼짐)";
+ _workspace.Settings.InsertMergeComment ? BuildMergeNote(PreviewSource) : "(주석 남기기 꺼짐)";
partial void OnSelectedLineChanged(MergeLine? value) => RaiseCanExecuteChanged();
@@ -1422,15 +1426,22 @@ namespace MergeHub.ViewModels
: _workspace.IsOperationInProgress && commit.ShortSha == _workspace.HeadSha
? OperationLabels.BaseHeader(_workspace.OperationState.Kind, commit.ShortSha, path!)
: $"{commit.ShortSha} · {path}";
- LocalHeader = isConflict
+ // 칸마다 "어느 쪽 · 어느 파일"(11차 F-1). 칸 이름(LOCAL(Mine) · REMOTE(Theirs) · MERGED)은 큰 제목이 말한다.
+ // 예전에는 Mine · MERGED 칸이 브랜치 이름뿐이고 REMOTE 칸은 "REMOTE · " 를 한 번 더 적었다.
+ MineHeader = PaneHeaders.Of(isConflict
? OperationLabels.OursTitle(operation, _workspace.CurrentBranch)
- : _workspace.CurrentBranch;
- RemoteHeader = isConflict
- ? $"REMOTE · {OperationLabels.TheirsTitle(operation)}"
- : $"REMOTE · {branch}";
+ : _workspace.CurrentBranch, path);
+ LocalHeader = threeWay
+ ? PaneHeaders.Saved(null, path)
+ : PaneHeaders.Saved(_workspace.CurrentBranch, path);
+ RemoteHeader = PaneHeaders.Of(isConflict ? OperationLabels.TheirsTitle(operation) : branch, path);
if (deletion.IsDeletion)
- LocalHeader = deletion.SurvivingLabel(_workspace.CurrentBranch, theirsName ?? "상대");
+ LocalHeader = PaneHeaders.Of(deletion.SurvivingLabel(_workspace.CurrentBranch, theirsName ?? "상대"), path);
+
+ // 두 칸에서 BASE 줄을 가져오면 주석 · 로그에 적을 이름 - 머리에 보인 그 커밋(11차 F-1).
+ _baseSourceName = threeWay || isConflict ? baseOrigin : commit.ShortSha;
+ OnPropertyChanged(nameof(MergeNotePreview));
_baseTextSnapshot = baseText;
_remoteTextSnapshot = threeWay ? remoteText : string.Empty;
@@ -1489,7 +1500,10 @@ namespace MergeHub.ViewModels
InvalidateDiffBlocks();
BaseHeader = "BASE";
- LocalHeader = "LOCAL (Mine)";
+ LocalHeader = "LOCAL";
+ MineHeader = "LOCAL(Mine)";
+ RemoteHeader = "REMOTE";
+ _baseSourceName = null;
DiffSummary = "-";
EditSummary = "-";
HasChanges = false;
@@ -1733,7 +1747,7 @@ namespace MergeHub.ViewModels
{
// 반영한 줄에는 누가·언제 반영했는지 주석을 남긴다.
// 주석은 줄 끝이 아니라 그 줄 "위"에 따로 들어간다 - 아래 InsertNoteAbove 참고.
- note = _workspace.Settings.InsertMergeComment ? BuildMergeNote() : null;
+ note = _workspace.Settings.InsertMergeComment ? BuildMergeNote(MergeNames.BaseSource(_baseSourceName)) : null;
after = line.BaseText;
afterHasLocal = true;
}
@@ -1746,9 +1760,8 @@ namespace MergeHub.ViewModels
if (beforeHasLocal == afterHasLocal && string.Equals(before, after, StringComparison.Ordinal))
return;
- var description = line.HasBase
- ? $"{line.BaseNumber}행 BASE 반영"
- : $"{line.LocalNumber}행 삭제(BASE 에 없음)";
+ var description = MergeNames.TakeBaseLog(
+ (line.HasBase ? line.BaseNumber : line.LocalNumber)?.ToString(), _baseSourceName, line.HasBase);
line.LocalText = after;
line.HasLocal = afterHasLocal;
@@ -1870,6 +1883,12 @@ namespace MergeHub.ViewModels
[ObservableProperty] private string _remoteHeader = "REMOTE";
+ /// 네 칸의 LOCAL(Mine) 칸 머리 - 내 쪽 · 파일(11차 F-1). 예전에는 MERGED 칸과 같은 값을 썼다.
+ [ObservableProperty] private string _mineHeader = "LOCAL(Mine)";
+
+ /// 두 칸에서 BASE 를 가져올 때 주석 · 로그에 적는 이름(커밋 해시 · merge base · 왼쪽 파일 이름, 11차 F-1).
+ private string? _baseSourceName;
+
/// 충돌 줄 수. 상태바와 요약에 쓴다.
[ObservableProperty] private int _conflictCount;
@@ -1958,13 +1977,31 @@ namespace MergeHub.ViewModels
{
// 이미 내 원본과 같다. 충돌 줄이면 "내 것으로 정했다"는 표시만 남긴다.
// 내용이 안 바뀌므로 다시 정렬하면 이 표시가 사라진다 - BASE 줄 번호로 따로 붙들어 둔다.
+ var first = !line.IsApplied;
line.IsApplied = true;
MarkApplied(line);
// 병합의 두 쪽 중 내 것을 고른 것이다 - 내용은 안 바뀌어도 무엇을 골랐는지 로그에 남긴다(7차 A-2).
if (TakesMergeSides)
- _workspace.AppendLog(MergeNames.TakeMineLog((line.LocalNumber ?? line.BaseNumber)?.ToString(),
- MineSideName, afterHasLocal, alreadySame: true, mergeSides: true));
+ {
+ var chosen = MergeNames.TakeMineLog((line.LocalNumber ?? line.BaseNumber)?.ToString(),
+ MineSideName, afterHasLocal, alreadySame: true, mergeSides: true);
+
+ // 소스에도 남긴다(11차 F-1) - 로그는 앱을 끄면 사라진다. 같은 줄을 또 눌러 주석이 쌓이지 않게 처음 한 번만.
+ var chosenNote = first && afterHasLocal ? MineNote() : null;
+ if (chosenNote is not null)
+ {
+ line.AppliedNote = chosenNote;
+ _undo.Push(new LineEdit(line, before, beforeHasLocal, after, afterHasLocal,
+ EditKind.TakeBase, chosen)
+ {
+ InsertedNote = InsertNoteAbove(line, chosenNote),
+ });
+ _redo.Clear();
+ }
+
+ _workspace.AppendLog(chosen);
+ }
AfterEdit();
return;
@@ -1973,12 +2010,19 @@ namespace MergeHub.ViewModels
var description = MergeNames.TakeMineLog(line.LocalNumber?.ToString(), MineSideName,
afterHasLocal, alreadySame: false, TakesMergeSides);
+ // 병합의 두 쪽에서 내 것으로 되돌리면 그 출처도 적는다(11차 F-1). 콤보로 고른 브랜치 3-Way 의
+ // "내 원본으로 되돌림" 은 되돌리기일 뿐이라 주석을 넣지 않는다(지금처럼).
+ var mineNote = TakesMergeSides && afterHasLocal ? MineNote() : null;
+
line.LocalText = after;
line.HasLocal = afterHasLocal;
- line.AppliedNote = null;
+ line.AppliedNote = mineNote;
_undo.Push(new LineEdit(line, before, beforeHasLocal, after, afterHasLocal,
- EditKind.TakeBase, description));
+ EditKind.TakeBase, description)
+ {
+ InsertedNote = InsertNoteAbove(line, mineNote),
+ });
// 반영 표시는 되돌리기 스택 밖에도 남긴다 - 규칙을 바꾸면 스택이 비워지기 때문이다.
MarkApplied(line);
@@ -2023,7 +2067,9 @@ namespace MergeHub.ViewModels
if (line.HasRemote)
{
- note = _workspace.Settings.InsertMergeComment ? BuildMergeNote() : null;
+ note = _workspace.Settings.InsertMergeComment
+ ? BuildMergeNote(MergeNames.TheirsSource(TheirsSideName, TakesMergeSides))
+ : null;
after = line.RemoteText;
afterHasLocal = true;
}
@@ -2098,13 +2144,24 @@ namespace MergeHub.ViewModels
return row;
}
- /// 줄 끝에 붙일 주석 한 조각. [사용자] 날짜 설명 - 세 가지로 끝.
- private string BuildMergeNote() => MergeNote.Build(
+ /// 반영한 줄 위에 넣을 주석 한 줄. [사용자] 날짜 출처 설명(출처는 11차 F-1).
+ private string BuildMergeNote(string? source) => MergeNote.Build(
author: _workspace.AuthorName,
when: DateTime.Now,
+ source: source,
comment: MergeComment,
path: SelectedFile);
+ /// 내 쪽으로 정한 줄의 주석. 설정 "머지 주석 남기기" 가 꺼져 있으면 null.
+ private string? MineNote() => _workspace.Settings.InsertMergeComment
+ ? BuildMergeNote(MergeNames.MineSource(MineSideName))
+ : null;
+
+ /// 미리 보기에 쓸 출처. 네 칸 · 브랜치 3-Way 는 상대 쪽, 두 칸은 BASE - 가장 흔히 누르는 쪽이다.
+ private string PreviewSource => IsThreeWay
+ ? MergeNames.TheirsSource(TheirsSideName, TakesMergeSides)
+ : MergeNames.BaseSource(_baseSourceName);
+
/// 검사 목록에서 고른 줄로 화면을 옮긴다.
[RelayCommand]
private void GoToProblem(CodeProblemItem? item)
@@ -2711,6 +2768,9 @@ namespace MergeHub.ViewModels
var r = SplitLead(right);
LocalHeaderLead = r.Lead;
LocalHeader = r.Tail;
+
+ _baseSourceName = string.IsNullOrEmpty(LeftFile) ? null : Path.GetFileName(LeftFile);
+ OnPropertyChanged(nameof(MergeNotePreview));
}
///
diff --git a/src/MergeHub/Views/Controls/MiddleTrim.cs b/src/MergeHub/Views/Controls/MiddleTrim.cs
new file mode 100644
index 0000000..628e5df
--- /dev/null
+++ b/src/MergeHub/Views/Controls/MiddleTrim.cs
@@ -0,0 +1,92 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Media;
+using MergeHub.Services;
+
+namespace MergeHub.Views.Controls
+{
+ ///
+ /// 칸 머리 한 줄을 가운데를 줄여 폭에 맞춘다(11차 F-1).
+ ///
+ /// WPF 의 TextTrimming 은 끝만 자른다. 칸 머리는 "브랜치 · 경로/파일" 이라 끝을 자르면 가장 중요한
+ /// 파일 이름이 먼저 사라진다. 그래서 글자 수를 줄여 가며 폭에 맞는 가장 긴 모양()을 고른다.
+ /// 줄은 늘 한 줄이다. 전체 글은 툴팁이 보인다.
+ ///
+ /// 는 앞에 굵게 붙는 조각이다(Git 아닌 두 파일에서 갈리는 첫 칸, 4차 C-1). 줄이지 않는다.
+ ///
+ public static class MiddleTrim
+ {
+ public static readonly DependencyProperty TextProperty = DependencyProperty.RegisterAttached(
+ "Text", typeof(string), typeof(MiddleTrim), new PropertyMetadata(null, OnChanged));
+
+ public static readonly DependencyProperty LeadProperty = DependencyProperty.RegisterAttached(
+ "Lead", typeof(string), typeof(MiddleTrim), new PropertyMetadata(null, OnChanged));
+
+ public static string? GetText(DependencyObject d) => (string?)d.GetValue(TextProperty);
+ public static void SetText(DependencyObject d, string? value) => d.SetValue(TextProperty, value);
+ public static string? GetLead(DependencyObject d) => (string?)d.GetValue(LeadProperty);
+ public static void SetLead(DependencyObject d, string? value) => d.SetValue(LeadProperty, value);
+
+ /// 한 번만 붙이려고 표시해 둔다.
+ private static readonly DependencyProperty HookedProperty = DependencyProperty.RegisterAttached(
+ "Hooked", typeof(bool), typeof(MiddleTrim), new PropertyMetadata(false));
+
+ private static void OnChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is not TextBlock block) return;
+
+ if (!(bool)block.GetValue(HookedProperty))
+ {
+ block.SetValue(HookedProperty, true);
+ block.SizeChanged += (_, args) => { if (args.WidthChanged) Fit(block); };
+ block.Loaded += (_, _) => Fit(block);
+ }
+
+ Fit(block);
+ }
+
+ private static void Fit(TextBlock block)
+ {
+ var text = GetText(block) ?? string.Empty;
+ var lead = GetLead(block) ?? string.Empty;
+ var room = block.ActualWidth - block.Padding.Left - block.Padding.Right - Width(block, lead, FontWeights.Bold);
+
+ var shown = text;
+ if (room > 0 && Width(block, text, block.FontWeight) > room)
+ {
+ // 폭에 드는 가장 긴 글자 수를 반으로 나눠 찾는다. 너비는 글자 수에 따라 늘기만 한다.
+ int low = 0, high = text.Length - 1;
+ while (low < high)
+ {
+ var mid = (low + high + 1) / 2;
+ if (Width(block, PaneHeaders.Cut(text, mid), block.FontWeight) <= room) low = mid;
+ else high = mid - 1;
+ }
+
+ shown = PaneHeaders.Cut(text, low);
+ }
+
+ block.Inlines.Clear();
+ if (lead.Length > 0)
+ {
+ var head = new Run(lead) { FontWeight = FontWeights.Bold };
+ head.SetResourceReference(TextElement.ForegroundProperty, "Ubisam.Brush.Accent");
+ block.Inlines.Add(head);
+ }
+
+ block.Inlines.Add(new Run(shown));
+ }
+
+ /// 이 글이 이 TextBlock 의 글꼴로 차지할 너비.
+ private static double Width(TextBlock block, string text, FontWeight weight)
+ {
+ if (text.Length == 0) return 0;
+ var typeface = new Typeface(block.FontFamily, block.FontStyle, weight, block.FontStretch);
+ var dip = VisualTreeHelper.GetDpi(block).PixelsPerDip;
+ var formatted = new FormattedText(text, System.Globalization.CultureInfo.CurrentUICulture,
+ block.FlowDirection, typeface, block.FontSize, Brushes.Black, dip);
+ return formatted.WidthIncludingTrailingWhitespace;
+ }
+ }
+}
diff --git a/src/MergeHub/Views/HomeView.xaml b/src/MergeHub/Views/HomeView.xaml
index dfd5257..6770c6a 100644
--- a/src/MergeHub/Views/HomeView.xaml
+++ b/src/MergeHub/Views/HomeView.xaml
@@ -1015,9 +1015,13 @@
-
-
+
+
+
-
-
+
+
+