From aafd7a5a6fface3439056db28d8e857c4f202bac Mon Sep 17 00:00:00 2001 From: ksj09406 Date: Tue, 22 Sep 2026 14:31:57 +0900 Subject: [PATCH] =?UTF-8?q?11=EC=B0=A8=20F-1:=20=EC=B9=B8=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=C2=B7=20=EC=B9=B8=EB=A7=88=EB=8B=A4=20=EC=96=B4?= =?UTF-8?q?=EB=8A=90=20=ED=8C=8C=EC=9D=BC=EC=9D=B8=EC=A7=80=20=C2=B7=20?= =?UTF-8?q?=EB=A8=B8=EC=A7=80=20=EC=A3=BC=EC=84=9D=EC=97=90=20=EC=96=B4?= =?UTF-8?q?=EB=94=94=EC=84=9C=20=EA=B0=80=EC=A0=B8=EC=99=94=EB=8A=94?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 칸 이름 LOCAL(Mine) · REMOTE(Theirs)(사용자 결정: 표준 용어 + 영문 쪽 이름) - 칸 머리마다 한 줄 "어느 쪽 · 어느 파일"(PaneHeaders). MERGED · 두 칸 LOCAL 은 "(저장할 파일)", mergetool · --merge 는 제 임시 파일 이름 - Mine 칸이 MERGED 이름을 보이던 것(MineHeader) 고침. 좁으면 가운데를 줄이고 툴팁에 전체(MiddleTrim) - 머지 주석 [사람] 날짜 출처 설명(사용자 결정: 쪽 이름 + 브랜치/해시, 출처 먼저). 출처는 로그와 같은 말 - 두 칸 로그 BASE(해시) 반영, 브랜치 3-Way 로그 REMOTE(브랜치) 반영 - Take Mine 도 주석(처음 한 번, 되돌리면 함께 빠짐) - 무시 조건은 [사람] 날짜 뒤를 따지지 않아 코드 그대로 - 옛 · 새 · 출처만 다른 주석을 하네스로 굳힘(NoteTest · RulesTest · FolderTest · MergeSaveTest, 1,312건) - 네 길 화면 확인 · E-1 병합 길 회귀 없음 Co-Authored-By: Claude Opus 5 --- doc/history/2026-09-18-11차.md | 46 ++++++++++ doc/history/changelog.md | 1 + doc/진행상황.md | 2 +- src/MergeHub/Services/MergeNames.cs | 36 +++++++- src/MergeHub/Services/MergeNote.cs | 17 +++- src/MergeHub/Services/PaneHeaders.cs | 51 +++++++++++ src/MergeHub/ViewModels/HomeViewModel.cs | 104 +++++++++++++++++----- src/MergeHub/Views/Controls/MiddleTrim.cs | 92 +++++++++++++++++++ src/MergeHub/Views/HomeView.xaml | 29 +++--- 9 files changed, 338 insertions(+), 40 deletions(-) create mode 100644 src/MergeHub/Services/PaneHeaders.cs create mode 100644 src/MergeHub/Views/Controls/MiddleTrim.cs 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 @@ - - + + + - - + + +