11차 F-1: 칸 이름 · 칸마다 어느 파일인지 · 머지 주석에 어디서 가져왔는지

- 칸 이름 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 <noreply@anthropic.com>
This commit is contained in:
김승준 2026-09-22 14:31:57 +09:00
parent 2f36f1bcb1
commit aafd7a5a6f
9 changed files with 338 additions and 40 deletions

View File

@ -159,6 +159,7 @@ A-1 에서 값으로 확인한 것 — 지운 파일은 "삭제되어 건너뜀
- 개요 "로컬 커밋" 숫자(버전 14 는 129). - 개요 "로컬 커밋" 숫자(버전 14 는 129).
- 이 뒤 11차 항목(F 등)에서 바뀌는 화면 · 설정 · 숫자. - 이 뒤 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, 완료) ## B-1. 배포 묶음을 다시 만들고 꼭 필요한 파일을 가림 (2026-09-22, 완료)
@ -500,6 +501,51 @@ libgit2 의 다듬기(`PrettifyMessages`)를 켜면 **손대지 않은 뒤 커
- **A4 쪽 높이**: 고치기 전 · 뒤 원문을 헤드리스 Edge 로 재니 8쪽 모두 1123px · 넘침 없음으로 같다. 기능 목록 쪽 그림으로 한 줄씩인 것 확인. - **A4 쪽 높이**: 고치기 전 · 뒤 원문을 헤드리스 Edge 로 재니 8쪽 모두 1123px · 넘침 없음으로 같다. 기능 목록 쪽 그림으로 한 줄씩인 것 확인.
- **잠금은 사용자가 했다**(`lock-plan.js`, 비밀번호는 화면에 안 보이게 받아 어디에도 남기지 않음 · 비교할 원본은 09-17 원문). 첫 "잠갔어" 때는 잠금 파일이 09-17 것 그대로여서 올리지 않고 멈췄다(PowerShell 문법을 CMD 에서 돌린 것으로 보임). 두 번째에 새 salt · IV 의 잠금 파일(13:12)이 생겨 올렸고, 발행본의 암호문이 그 파일과 같음을 되읽어 확인했다. 스크립트 출력은 다른 창이라 직접 보지 못했다. - **잠금은 사용자가 했다**(`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, 완료) ## F-12. C# 코드 스타일 맞춤의 기본값을 AURORA 로 (2026-09-18, 완료)
사용자: "기본값은 원본형태를 따르고 C#인 경우에 AURORA 스타일로 하자" → 물어서 정한 것은 **제품 기본값 AURORA · C# 파일만 · 저장 동작은 지금 그대로**(켜져 있으면 저장도 그 스타일로 쓴다). 사용자: "기본값은 원본형태를 따르고 C#인 경우에 AURORA 스타일로 하자" → 물어서 정한 것은 **제품 기본값 AURORA · C# 파일만 · 저장 동작은 지금 그대로**(켜져 있으면 저장도 그 스타일로 쓴다).

View File

@ -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차 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차 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 높이 그대로, 잠금은 사용자가 비밀번호로 | 아래 | | 2026-09-22 | **11차 E-2** — 웹 문서(버전 13)와 기획안(버전 17)을 11차에 맞춤. 두 발행본 모두 로컬 파일과 같음을 먼저 확인. 매뉴얼에 "커밋 메시지 고치기"([커밋 수정] 과 우클릭 [메시지 바꾸기...] 를 한 표로) · "변경분 추출"(새 폴더 · 안내 파일) 두 절과 한계 셋, 남은 일 · 만든 것에 11차, 숫자 1,247건 · 커밋 129. 단추 이름 91개를 소스에서 찾아 09-18 에 적은 "맞추지 않음" 을 실제 글자 "C# 스타일 끔" 으로 바로잡음. 기획안은 커밋 메시지 수정 완료 · 4단계 준비 중 · 개정 09-22, 8쪽 A4 높이 그대로, 잠금은 사용자가 비밀번호로 | 아래 |

View File

@ -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-2 메서드 단위로 골라 반영 · 머지 주석은 메서드 이름 위 한 줄 (지금은 줄 · 덩어리 · 전체) |
| 대기 | F-3 탐색기에서 폴더 두 개를 [보내기] 로 바로 비교 + 설치 파일 없이 되는지 확인 | | 대기 | F-3 탐색기에서 폴더 두 개를 [보내기] 로 바로 비교 + 설치 파일 없이 되는지 확인 |
| 대기 | F-4 파일 비교 탭 툴박스(ACTIONS) 감추는 단추 (지금은 접어도 폭 그대로) | | 대기 | F-4 파일 비교 탭 툴박스(ACTIONS) 감추는 단추 (지금은 접어도 폭 그대로) |

View File

@ -81,7 +81,7 @@ namespace MergeHub.Services
/// ///
/// 병합 충돌(또는 git mergetool 세 벌)이면 <c>Theirs(feature/theirs)</c> · <c>Mine(feature/mine)</c> 처럼 어느 쪽인지와 이름을 함께. /// 병합 충돌(또는 git mergetool 세 벌)이면 <c>Theirs(feature/theirs)</c> · <c>Mine(feature/mine)</c> 처럼 어느 쪽인지와 이름을 함께.
/// 예전에는 3-Way 콤보 값을 그대로 써서, 콤보를 안 건드리고 네 칸을 띄우는 충돌 파일에서 "(3-Way 안 씀) 반영" 이라고 적혔다. /// 예전에는 3-Way 콤보 값을 그대로 써서, 콤보를 안 건드리고 네 칸을 띄우는 충돌 파일에서 "(3-Way 안 씀) 반영" 이라고 적혔다.
/// 콤보로 고른 브랜치 3-Way 는 지금처럼 브랜치 이름만 쓴다. /// 콤보로 고른 브랜치 3-Way 는 브랜치 이름만 돌려준다 - 로그 · 주석은 그 앞에 REMOTE 를 붙여 쓴다(<see cref="TheirsSource"/>, 11차 F-1).
/// </summary> /// </summary>
public static string SideLabel(bool mine, string? name, bool mergeSides) public static string SideLabel(bool mine, string? name, bool mergeSides)
{ {
@ -92,13 +92,43 @@ namespace MergeHub.Services
return trimmed is null ? side : $"{side}({trimmed})"; return trimmed is null ? side : $"{side}({trimmed})";
} }
/// <summary>Take Theirs 로그. 상대 쪽에 없는 줄이면 지운 것으로 적는다.</summary> /// <summary>
/// Take Theirs 로그. 상대 쪽에 없는 줄이면 지운 것으로 적는다.
/// 쪽 이름은 머지 주석의 출처와 같은 말이다(<see cref="TheirsSource"/>, 11차 F-1).
/// </summary>
public static string TakeTheirsLog(string? number, string? theirs, bool hasLine, bool mergeSides) 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} 에 없음)"; return hasLine ? $"{number}행 {label} 반영" : $"{number}행 삭제({label} 에 없음)";
} }
/// <summary>두 칸에서 BASE 줄을 가져온 로그(11차 F-1). 주석 출처와 같은 말 <c>BASE(4eb9ece)</c>.</summary>
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 이 아니면 왼쪽 파일 이름
/// <summary>상대 쪽에서 가져온 줄의 출처.</summary>
public static string TheirsSource(string? name, bool mergeSides) =>
mergeSides ? SideLabel(mine: false, name, mergeSides: true) : Named("REMOTE", name);
/// <summary>내 쪽으로 정한 줄의 출처.</summary>
public static string MineSource(string? name) => SideLabel(mine: true, name, mergeSides: true);
/// <summary>두 칸에서 BASE 쪽을 가져온 줄의 출처.</summary>
public static string BaseSource(string? name) => Named("BASE", name);
private static string Named(string side, string? name) =>
string.IsNullOrWhiteSpace(name) ? side : $"{side}({name!.Trim()})";
/// <summary> /// <summary>
/// Take Mine 로그. 이미 내 것과 같으면 "정함" 이라고 적는다 - 예전에는 아무것도 안 적어 무엇을 골랐는지 로그에 없었다. /// Take Mine 로그. 이미 내 것과 같으면 "정함" 이라고 적는다 - 예전에는 아무것도 안 적어 무엇을 골랐는지 로그에 없었다.
/// 콤보로 고른 브랜치 3-Way 에서는 지금 문구("내 원본으로 되돌림")를 그대로 쓴다. /// 콤보로 고른 브랜치 3-Way 에서는 지금 문구("내 원본으로 되돌림")를 그대로 쓴다.

View File

@ -18,7 +18,7 @@ namespace MergeHub.Services
private const string Gap = " "; private const string Gap = " ";
/// <summary> /// <summary>
/// 주석 한 조각을 만든다. 구성은 딱 세 가지 - [사용자] 날짜 설명. /// 주석 한 조각을 만든다. 구성은 [사용자] 날짜 출처 설명 - 출처는 11차 F-1 에 더했다.
/// 설명이 비어 있어도 [사용자]와 날짜는 항상 남는다. /// 설명이 비어 있어도 [사용자]와 날짜는 항상 남는다.
/// </summary> /// </summary>
/// <summary> /// <summary>
@ -31,12 +31,25 @@ namespace MergeHub.Services
public static string AuthorOf(string? author) => public static string AuthorOf(string? author) =>
string.IsNullOrWhiteSpace(author) ? Environment.UserName : author!.Trim(); 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);
/// <summary>
/// 어디서 가져온 줄인지(출처)까지 적는다(11차 F-1). <c>[사람] 날짜 출처 설명</c> - 출처가 늘 같은 자리에 있게 설명보다 앞.
/// 출처는 Git 탭 로그와 같은 말이다(<see cref="MergeNames.TheirsSource"/> 등). 비어 있으면 옛 모양 그대로.
///
/// 알아보는 쪽(<see cref="NoteBody"/>)은 <c>[사람] 날짜</c> 뒤를 따지지 않으므로 옛 주석 · 새 주석을 똑같이 알아본다.
/// 그래서 무시 · 폴더 비교 · 충돌 자동 해결에서 출처만 다른 두 주석은 다름이 아니다.
/// </summary>
public static string Build(string? author, DateTime when, string? source, string? comment, string? path)
{ {
var who = AuthorOf(author); var who = AuthorOf(author);
var body = $"[{who}] {when:yyyy-MM-dd}"; var body = $"[{who}] {when:yyyy-MM-dd}";
var from = source?.Trim();
if (!string.IsNullOrEmpty(from)) body += $" {from}";
var note = comment?.Trim(); var note = comment?.Trim();
if (!string.IsNullOrEmpty(note)) body += $" {note}"; if (!string.IsNullOrEmpty(note)) body += $" {note}";

View File

@ -0,0 +1,51 @@
using System;
namespace MergeHub.Services
{
/// <summary>
/// 칸 머리 한 줄 - 그 칸이 <b>어느 쪽 · 어느 파일</b>인지(11차 F-1).
///
/// 칸 이름(BASE · LOCAL(Mine) · REMOTE(Theirs) · MERGED)은 큰 제목이 말하고, 그 아래 한 줄이 이것이다.
/// 예전에는 파일 경로가 BASE 칸에만 있었고 Mine · MERGED 칸은 브랜치 이름뿐이라, <c>git mergetool</c> 처럼
/// 네 칸이 서로 다른 파일을 볼 때 어느 칸이 어느 파일인지 화면에 없었다.
///
/// 한 줄을 넘기지 않는다. 좁으면 <see cref="Cut"/> 로 가운데를 줄이고 전체는 마우스를 올리면 보인다.
/// </summary>
public static class PaneHeaders
{
private const string Separator = " · ";
/// <summary>줄였다는 표시. 한 글자라 글자 수 셈이 쉽다.</summary>
public const string Ellipsis = "…";
/// <summary>쪽(브랜치 · 커밋 · 올릴 자리)과 파일. 한쪽을 모르면 아는 것만.</summary>
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;
}
/// <summary>저장하는 칸(네 칸의 MERGED · 두 칸의 LOCAL). 저장 단추가 쓰는 파일이 이것이라고 적는다.</summary>
public static string Saved(string? where, string? path) => Of(where, path) + " (저장할 파일)";
/// <summary>
/// 글자 <paramref name="keep"/> 개에 맞게 <b>가운데를</b> 줄인다. 들어가면 그대로.
///
/// 끝을 자르면(WPF 기본) 가장 중요한 파일 이름이 잘린다. 앞(브랜치)은 조금, 뒤(파일 이름)는 많이 남긴다.
/// </summary>
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);
}
}
}

View File

@ -224,8 +224,12 @@ namespace MergeHub.ViewModels
BaseHeader = string.IsNullOrEmpty(baseText) BaseHeader = string.IsNullOrEmpty(baseText)
? "(BASE 없음) 양쪽에서 새로 만든 파일입니다" ? "(BASE 없음) 양쪽에서 새로 만든 파일입니다"
: Path.GetFileName(request.Base); : Path.GetFileName(request.Base);
LocalHeader = Path.GetFileName(request.Merged); // 네 칸이 모두 다른 파일이다(--base · --local · --remote · --merged). 칸마다 제 파일 이름(11차 F-1).
RemoteHeader = $"REMOTE · {Path.GetFileName(request.Remote)}"; // 예전에는 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); _theirsSideName = Path.GetFileName(request.Remote);
_mineSideName = Path.GetFileName(request.Local); _mineSideName = Path.GetFileName(request.Local);
@ -481,7 +485,7 @@ namespace MergeHub.ViewModels
[ObservableProperty] private string? _selectedFile; [ObservableProperty] private string? _selectedFile;
[ObservableProperty] private MergeLine? _selectedLine; [ObservableProperty] private MergeLine? _selectedLine;
[ObservableProperty] private string _baseHeader = "BASE"; [ObservableProperty] private string _baseHeader = "BASE";
[ObservableProperty] private string _localHeader = "LOCAL (Mine)"; [ObservableProperty] private string _localHeader = "LOCAL";
[ObservableProperty] private string _diffSummary = "-"; [ObservableProperty] private string _diffSummary = "-";
[ObservableProperty] private string _editSummary = "-"; [ObservableProperty] private string _editSummary = "-";
[ObservableProperty] private string _commitMessage = string.Empty; [ObservableProperty] private string _commitMessage = string.Empty;
@ -620,7 +624,7 @@ namespace MergeHub.ViewModels
/// <summary>실제로 줄 끝에 붙을 주석을 미리 보여준다.</summary> /// <summary>실제로 줄 끝에 붙을 주석을 미리 보여준다.</summary>
public string MergeNotePreview => public string MergeNotePreview =>
_workspace.Settings.InsertMergeComment ? BuildMergeNote() : "(주석 남기기 꺼짐)"; _workspace.Settings.InsertMergeComment ? BuildMergeNote(PreviewSource) : "(주석 남기기 꺼짐)";
partial void OnSelectedLineChanged(MergeLine? value) => RaiseCanExecuteChanged(); partial void OnSelectedLineChanged(MergeLine? value) => RaiseCanExecuteChanged();
@ -1422,15 +1426,22 @@ namespace MergeHub.ViewModels
: _workspace.IsOperationInProgress && commit.ShortSha == _workspace.HeadSha : _workspace.IsOperationInProgress && commit.ShortSha == _workspace.HeadSha
? OperationLabels.BaseHeader(_workspace.OperationState.Kind, commit.ShortSha, path!) ? OperationLabels.BaseHeader(_workspace.OperationState.Kind, commit.ShortSha, path!)
: $"{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) ? OperationLabels.OursTitle(operation, _workspace.CurrentBranch)
: _workspace.CurrentBranch; : _workspace.CurrentBranch, path);
RemoteHeader = isConflict LocalHeader = threeWay
? $"REMOTE · {OperationLabels.TheirsTitle(operation)}" ? PaneHeaders.Saved(null, path)
: $"REMOTE · {branch}"; : PaneHeaders.Saved(_workspace.CurrentBranch, path);
RemoteHeader = PaneHeaders.Of(isConflict ? OperationLabels.TheirsTitle(operation) : branch, path);
if (deletion.IsDeletion) 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; _baseTextSnapshot = baseText;
_remoteTextSnapshot = threeWay ? remoteText : string.Empty; _remoteTextSnapshot = threeWay ? remoteText : string.Empty;
@ -1489,7 +1500,10 @@ namespace MergeHub.ViewModels
InvalidateDiffBlocks(); InvalidateDiffBlocks();
BaseHeader = "BASE"; BaseHeader = "BASE";
LocalHeader = "LOCAL (Mine)"; LocalHeader = "LOCAL";
MineHeader = "LOCAL(Mine)";
RemoteHeader = "REMOTE";
_baseSourceName = null;
DiffSummary = "-"; DiffSummary = "-";
EditSummary = "-"; EditSummary = "-";
HasChanges = false; HasChanges = false;
@ -1733,7 +1747,7 @@ namespace MergeHub.ViewModels
{ {
// 반영한 줄에는 누가·언제 반영했는지 주석을 남긴다. // 반영한 줄에는 누가·언제 반영했는지 주석을 남긴다.
// 주석은 줄 끝이 아니라 그 줄 "위"에 따로 들어간다 - 아래 InsertNoteAbove 참고. // 주석은 줄 끝이 아니라 그 줄 "위"에 따로 들어간다 - 아래 InsertNoteAbove 참고.
note = _workspace.Settings.InsertMergeComment ? BuildMergeNote() : null; note = _workspace.Settings.InsertMergeComment ? BuildMergeNote(MergeNames.BaseSource(_baseSourceName)) : null;
after = line.BaseText; after = line.BaseText;
afterHasLocal = true; afterHasLocal = true;
} }
@ -1746,9 +1760,8 @@ namespace MergeHub.ViewModels
if (beforeHasLocal == afterHasLocal && string.Equals(before, after, StringComparison.Ordinal)) if (beforeHasLocal == afterHasLocal && string.Equals(before, after, StringComparison.Ordinal))
return; return;
var description = line.HasBase var description = MergeNames.TakeBaseLog(
? $"{line.BaseNumber}행 BASE 반영" (line.HasBase ? line.BaseNumber : line.LocalNumber)?.ToString(), _baseSourceName, line.HasBase);
: $"{line.LocalNumber}행 삭제(BASE 에 없음)";
line.LocalText = after; line.LocalText = after;
line.HasLocal = afterHasLocal; line.HasLocal = afterHasLocal;
@ -1870,6 +1883,12 @@ namespace MergeHub.ViewModels
[ObservableProperty] private string _remoteHeader = "REMOTE"; [ObservableProperty] private string _remoteHeader = "REMOTE";
/// <summary>네 칸의 LOCAL(Mine) 칸 머리 - 내 쪽 · 파일(11차 F-1). 예전에는 MERGED 칸과 같은 값을 썼다.</summary>
[ObservableProperty] private string _mineHeader = "LOCAL(Mine)";
/// <summary>두 칸에서 BASE 를 가져올 때 주석 · 로그에 적는 이름(커밋 해시 · merge base · 왼쪽 파일 이름, 11차 F-1).</summary>
private string? _baseSourceName;
/// <summary>충돌 줄 수. 상태바와 요약에 쓴다.</summary> /// <summary>충돌 줄 수. 상태바와 요약에 쓴다.</summary>
[ObservableProperty] private int _conflictCount; [ObservableProperty] private int _conflictCount;
@ -1958,13 +1977,31 @@ namespace MergeHub.ViewModels
{ {
// 이미 내 원본과 같다. 충돌 줄이면 "내 것으로 정했다"는 표시만 남긴다. // 이미 내 원본과 같다. 충돌 줄이면 "내 것으로 정했다"는 표시만 남긴다.
// 내용이 안 바뀌므로 다시 정렬하면 이 표시가 사라진다 - BASE 줄 번호로 따로 붙들어 둔다. // 내용이 안 바뀌므로 다시 정렬하면 이 표시가 사라진다 - BASE 줄 번호로 따로 붙들어 둔다.
var first = !line.IsApplied;
line.IsApplied = true; line.IsApplied = true;
MarkApplied(line); MarkApplied(line);
// 병합의 두 쪽 중 내 것을 고른 것이다 - 내용은 안 바뀌어도 무엇을 골랐는지 로그에 남긴다(7차 A-2). // 병합의 두 쪽 중 내 것을 고른 것이다 - 내용은 안 바뀌어도 무엇을 골랐는지 로그에 남긴다(7차 A-2).
if (TakesMergeSides) 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(); AfterEdit();
return; return;
@ -1973,12 +2010,19 @@ namespace MergeHub.ViewModels
var description = MergeNames.TakeMineLog(line.LocalNumber?.ToString(), MineSideName, var description = MergeNames.TakeMineLog(line.LocalNumber?.ToString(), MineSideName,
afterHasLocal, alreadySame: false, TakesMergeSides); afterHasLocal, alreadySame: false, TakesMergeSides);
// 병합의 두 쪽에서 내 것으로 되돌리면 그 출처도 적는다(11차 F-1). 콤보로 고른 브랜치 3-Way 의
// "내 원본으로 되돌림" 은 되돌리기일 뿐이라 주석을 넣지 않는다(지금처럼).
var mineNote = TakesMergeSides && afterHasLocal ? MineNote() : null;
line.LocalText = after; line.LocalText = after;
line.HasLocal = afterHasLocal; line.HasLocal = afterHasLocal;
line.AppliedNote = null; line.AppliedNote = mineNote;
_undo.Push(new LineEdit(line, before, beforeHasLocal, after, afterHasLocal, _undo.Push(new LineEdit(line, before, beforeHasLocal, after, afterHasLocal,
EditKind.TakeBase, description)); EditKind.TakeBase, description)
{
InsertedNote = InsertNoteAbove(line, mineNote),
});
// 반영 표시는 되돌리기 스택 밖에도 남긴다 - 규칙을 바꾸면 스택이 비워지기 때문이다. // 반영 표시는 되돌리기 스택 밖에도 남긴다 - 규칙을 바꾸면 스택이 비워지기 때문이다.
MarkApplied(line); MarkApplied(line);
@ -2023,7 +2067,9 @@ namespace MergeHub.ViewModels
if (line.HasRemote) if (line.HasRemote)
{ {
note = _workspace.Settings.InsertMergeComment ? BuildMergeNote() : null; note = _workspace.Settings.InsertMergeComment
? BuildMergeNote(MergeNames.TheirsSource(TheirsSideName, TakesMergeSides))
: null;
after = line.RemoteText; after = line.RemoteText;
afterHasLocal = true; afterHasLocal = true;
} }
@ -2098,13 +2144,24 @@ namespace MergeHub.ViewModels
return row; return row;
} }
/// <summary>줄 끝에 붙일 주석 한 조각. [사용자] 날짜 설명 - 세 가지로 끝.</summary> /// <summary>반영한 줄 위에 넣을 주석 한 줄. [사용자] 날짜 출처 설명(출처는 11차 F-1).</summary>
private string BuildMergeNote() => MergeNote.Build( private string BuildMergeNote(string? source) => MergeNote.Build(
author: _workspace.AuthorName, author: _workspace.AuthorName,
when: DateTime.Now, when: DateTime.Now,
source: source,
comment: MergeComment, comment: MergeComment,
path: SelectedFile); path: SelectedFile);
/// <summary>내 쪽으로 정한 줄의 주석. 설정 "머지 주석 남기기" 가 꺼져 있으면 null.</summary>
private string? MineNote() => _workspace.Settings.InsertMergeComment
? BuildMergeNote(MergeNames.MineSource(MineSideName))
: null;
/// <summary>미리 보기에 쓸 출처. 네 칸 · 브랜치 3-Way 는 상대 쪽, 두 칸은 BASE - 가장 흔히 누르는 쪽이다.</summary>
private string PreviewSource => IsThreeWay
? MergeNames.TheirsSource(TheirsSideName, TakesMergeSides)
: MergeNames.BaseSource(_baseSourceName);
/// <summary>검사 목록에서 고른 줄로 화면을 옮긴다.</summary> /// <summary>검사 목록에서 고른 줄로 화면을 옮긴다.</summary>
[RelayCommand] [RelayCommand]
private void GoToProblem(CodeProblemItem? item) private void GoToProblem(CodeProblemItem? item)
@ -2711,6 +2768,9 @@ namespace MergeHub.ViewModels
var r = SplitLead(right); var r = SplitLead(right);
LocalHeaderLead = r.Lead; LocalHeaderLead = r.Lead;
LocalHeader = r.Tail; LocalHeader = r.Tail;
_baseSourceName = string.IsNullOrEmpty(LeftFile) ? null : Path.GetFileName(LeftFile);
OnPropertyChanged(nameof(MergeNotePreview));
} }
/// <summary> /// <summary>

View File

@ -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
{
/// <summary>
/// 칸 머리 한 줄을 <b>가운데를 줄여</b> 폭에 맞춘다(11차 F-1).
///
/// WPF 의 <c>TextTrimming</c> 은 끝만 자른다. 칸 머리는 "브랜치 · 경로/파일" 이라 끝을 자르면 가장 중요한
/// 파일 이름이 먼저 사라진다. 그래서 글자 수를 줄여 가며 폭에 맞는 가장 긴 모양(<see cref="PaneHeaders.Cut"/>)을 고른다.
/// 줄은 늘 한 줄이다. 전체 글은 툴팁이 보인다.
///
/// <see cref="Lead"/> 는 앞에 굵게 붙는 조각이다(Git 아닌 두 파일에서 갈리는 첫 칸, 4차 C-1). 줄이지 않는다.
/// </summary>
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);
/// <summary>한 번만 붙이려고 표시해 둔다.</summary>
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));
}
/// <summary>이 글이 이 TextBlock 의 글꼴로 차지할 너비.</summary>
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;
}
}
}

View File

@ -1016,8 +1016,12 @@
<StackPanel Grid.Row="0" Margin="8,6"> <StackPanel Grid.Row="0" Margin="8,6">
<TextBlock Text="LOCAL(Mine)" Style="{StaticResource PanelTitle}"/> <TextBlock Text="LOCAL(Mine)" Style="{StaticResource PanelTitle}"/>
<TextBlock Text="{Binding LocalHeader}" Style="{StaticResource Muted}" FontSize="11" <!-- 내 쪽 · 파일(11차 F-1). 예전에는 MERGED 칸과 같은 값(브랜치 이름)이라 git mergetool 에서 실제로 읽은 LOCAL 임시 파일과 달랐다.
TextTrimming="CharacterEllipsis"/> 네 칸 머리를 모두 같은 모양(읽으라고 쓰는 글씨 · 한 줄 · 가운데 줄이기 · 툴팁 전체)으로 둔다. -->
<TextBlock ctl:MiddleTrim.Text="{Binding MineHeader}" ToolTip="{Binding MineHeader}"
FontFamily="Consolas" FontSize="12" Margin="0,1,0,0"
TextTrimming="CharacterEllipsis"
Foreground="{DynamicResource Ubisam.Brush.ContentForeground}"/>
</StackPanel> </StackPanel>
<!-- 코드 칸을 제 자리 밖으로 못 나가게 자른다. 표시층의 미세 보정은 배치가 아니라 <!-- 코드 칸을 제 자리 밖으로 못 나가게 자른다. 표시층의 미세 보정은 배치가 아니라
@ -1106,9 +1110,8 @@
<TextBlock FontFamily="Consolas" FontSize="12" Margin="0,1,0,0" <TextBlock FontFamily="Consolas" FontSize="12" Margin="0,1,0,0"
TextTrimming="CharacterEllipsis" TextTrimming="CharacterEllipsis"
Foreground="{DynamicResource Ubisam.Brush.ContentForeground}" Foreground="{DynamicResource Ubisam.Brush.ContentForeground}"
ToolTip="{Binding BaseFileLocation}"> ToolTip="{Binding BaseFileLocation}"
<Run Text="{Binding BaseHeaderLead, Mode=OneWay}" FontWeight="Bold" ctl:MiddleTrim.Lead="{Binding BaseHeaderLead}" ctl:MiddleTrim.Text="{Binding BaseHeader}">
Foreground="{DynamicResource Ubisam.Brush.Accent}"/><Run Text="{Binding BaseHeader, Mode=OneWay}"/>
<TextBlock.ContextMenu> <TextBlock.ContextMenu>
<ContextMenu> <ContextMenu>
<MenuItem Header="전체 경로 복사" Click="CopyBasePath_Click"/> <MenuItem Header="전체 경로 복사" Click="CopyBasePath_Click"/>
@ -1200,9 +1203,8 @@
<TextBlock FontFamily="Consolas" FontSize="12" Margin="0,1,0,0" <TextBlock FontFamily="Consolas" FontSize="12" Margin="0,1,0,0"
TextTrimming="CharacterEllipsis" TextTrimming="CharacterEllipsis"
Foreground="{DynamicResource Ubisam.Brush.ContentForeground}" Foreground="{DynamicResource Ubisam.Brush.ContentForeground}"
ToolTip="{Binding SelectedFileFullPath}"> ToolTip="{Binding SelectedFileFullPath}"
<Run Text="{Binding LocalHeaderLead, Mode=OneWay}" FontWeight="Bold" ctl:MiddleTrim.Lead="{Binding LocalHeaderLead}" ctl:MiddleTrim.Text="{Binding LocalHeader}">
Foreground="{DynamicResource Ubisam.Brush.Accent}"/><Run Text="{Binding LocalHeader, Mode=OneWay}"/>
<TextBlock.ContextMenu> <TextBlock.ContextMenu>
<ContextMenu> <ContextMenu>
<MenuItem Header="전체 경로 복사" Click="CopyLocalPath_Click"/> <MenuItem Header="전체 경로 복사" Click="CopyLocalPath_Click"/>
@ -1355,8 +1357,11 @@
GridLength 자리에 넣으면 화면을 만들 때 그대로 터진다. --> GridLength 자리에 넣으면 화면을 만들 때 그대로 터진다. -->
<StackPanel Grid.Row="0" Margin="8,6"> <StackPanel Grid.Row="0" Margin="8,6">
<TextBlock Text="REMOTE(Theirs)" Style="{StaticResource PanelTitle}"/> <TextBlock Text="REMOTE(Theirs)" Style="{StaticResource PanelTitle}"/>
<TextBlock Text="{Binding RemoteHeader}" Style="{StaticResource Muted}" <!-- 상대 쪽 · 파일(11차 F-1). 큰 제목이 REMOTE 라고 말하므로 "REMOTE · " 를 되풀이하지 않는다. -->
FontSize="11" TextTrimming="CharacterEllipsis"/> <TextBlock ctl:MiddleTrim.Text="{Binding RemoteHeader}" ToolTip="{Binding RemoteHeader}"
FontFamily="Consolas" FontSize="12" Margin="0,1,0,0"
TextTrimming="CharacterEllipsis"
Foreground="{DynamicResource Ubisam.Brush.ContentForeground}"/>
<StackPanel Orientation="Horizontal" Margin="0,4,0,0"> <StackPanel Orientation="Horizontal" Margin="0,4,0,0">
<TextBlock Text="{Binding ConflictCount, StringFormat=Conflict {0}}" <TextBlock Text="{Binding ConflictCount, StringFormat=Conflict {0}}"