11차 C-2: 이력 줄 우클릭으로 마지막이 아닌 커밋의 메시지를 바꾼다
기획안 1차 범위 "커밋 우클릭 -> 이름 변경". 사용자 결정대로 좁은 범위에서만 한다. HEAD 가 아닌 커밋의 메시지를 바꾸면 그 커밋부터 HEAD 까지가 새 해시로 다시 써진다. 고른 커밋의 메시지만 갈고 뒤 커밋은 트리 · 작성자 · 작성 시각 · 부모 관계를 그대로 두어(LibGit2Sharp Refs.RewriteHistory) 해시만 바뀐다. 바꾸기 전 자리는 refs/original/날짜-시각/ 에 남긴다. 막는 자리(메뉴는 켜 두고 눌렀을 때 까닭과 푸는 길을 알림으로): 작업 중 · 커밋 안 한 변경 · 지금 브랜치 조상 아님 · 원격에 올라감 · 다른 로컬 브랜치도 품음 · 구간에 태그 · 사이에 병합 커밋. 파일 비교 탭의 저장 안 한 편집은 우클릭해 줄을 고를 때 기존 창으로 묻는다. - CommitReword (새): 막을 까닭 · 확인 글 · 완료 알림 · 새 메시지 다듬기 - GitService.InspectReword / RewordCommit, RewordInfo - RewordWindow (새): 여러 줄 입력, [바꾸기] 는 기본 단추가 아님 - GitView: MergeHub 첫 우클릭 메뉴, 우클릭한 줄을 먼저 고른다 - 선택이 고친 커밋의 새 해시를 따라간다 - 화면에서 찾아 고침: 새 메시지가 끝 줄바꿈 없이 저장됐다 -> git -m 처럼 다듬는다(뒤 커밋까지 다듬지 않게 새 메시지 하나만) 하네스 MergeSaveTest +40 (272건, 22벌 1,247건) · 빌드 경고 0 · 오류 0. 화면 확인: 되는 자리 1(트리 · 작성자 · 작성 시각 같음, 되돌리기 확인) · 막는 자리 8 모두 막힘. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8f9ac13872
commit
588f3d75e9
@ -193,8 +193,74 @@ A-1 에서 값으로 확인한 것 — 지운 파일은 "삭제되어 건너뜀
|
||||
|
||||
### 남은 것
|
||||
|
||||
- **8. 여러 줄 커밋 메시지** — 칸이 한 줄이라 본문을 칠 수 없다. 이번 범위 밖으로 적어 둔다(병합 커밋 본문은 앱이 저절로 붙인다).
|
||||
- C-2(마지막이 아닌 커밋의 메시지 수정)는 C-1 을 쓰면서 더 필요한지 보고 정한다.
|
||||
- **8. 여러 줄 커밋 메시지** — 칸이 한 줄이라 본문을 칠 수 없다. 이번 범위 밖으로 적어 둔다(병합 커밋 본문은 앱이 저절로 붙인다). C-2 의 [메시지 바꾸기] 창은 여러 줄을 받는다.
|
||||
- C-2 는 아래(2026-09-22 완료).
|
||||
|
||||
## C-2. 마지막이 아닌 커밋의 메시지 바꾸기 (2026-09-18 ~ 09-22, 완료 — 좁은 범위)
|
||||
|
||||
사용자 결정(2026-09-18): **한다 — 좁은 범위로.** 화면은 기획안 문구대로 **이력 줄 우클릭 메뉴**.
|
||||
|
||||
### 고치기 전
|
||||
|
||||
- 이력 줄을 우클릭해도 **메뉴가 없고**, 이력 탭 단추는 Revert · Cherry-pick · Reset · 여기부터 · 해제 · 변경분 추출뿐이었다(`e11-c2-before.ps1`). 고른 커밋의 메시지를 바꿀 길이 없었다.
|
||||
- 메시지는 커밋 해시에 들어가므로, HEAD 가 아닌 커밋을 바꾸면 **그 커밋부터 HEAD 까지가 새 해시로 다시 써진다**. 아직 안 올린 커밋이면 브랜치가 새 줄기로 옮겨 갈 뿐이지만, 올린 뒤라면 남의 이력과 갈라진다(사용자에게 설명함).
|
||||
|
||||
### 만든 것
|
||||
|
||||
| 자리 | 한 일 |
|
||||
|---|---|
|
||||
| `Services/CommitReword.cs` (새) | 막을 까닭 — 고른 커밋 없음 · 작업 중 · 커밋 안 한 변경 · 지금 브랜치 조상 아님 · 원격에 올라감 · 다른 로컬 브랜치도 품음 · 구간에 태그 · 사이에 병합 커밋. 먼저 풀어야 할 것부터 말하고 **푸는 길**까지 적는다. 확인 글 · 완료 알림 · 메뉴 설명. `Clean` — 새 메시지를 git `-m` 처럼 다듬는다(아래) |
|
||||
| `Services/GitService.cs` · `IGitService.cs` · `Models/GitModels.cs` | `InspectReword` — 저장소에서 사실만 읽는다(다시 쓸 수 · 병합 수 · 올라간 원격 브랜치 · 다른 브랜치 · 태그). `RewordCommit` — LibGit2Sharp `Refs.RewriteHistory` 로 **고른 커밋의 메시지만** 갈고 트리 · 작성자 · 작성 시각 · 부모 관계는 그대로. 바꾸기 전 자리는 `refs/original/날짜-시각/` 에 남긴다(같은 자리에 두 번 쓰면 libgit2 가 막아서 시각을 붙임). 고친 커밋의 새 해시도 돌려준다 |
|
||||
| `Views/RewordWindow.xaml(.cs)` (새 창) | 새 메시지를 받는 창. 고친 커밋 한 줄(해시 · 작성자 · 날짜) · 몇 개가 다시 써지는지를 늘 보이고, **여러 줄**을 받는다. 지금 메시지를 미리 넣어 전체를 골라 둔다. [바꾸기] 는 기본 단추가 아니다(Enter 는 줄바꿈) |
|
||||
| `Views/GitView.xaml(.cs)` | 이력 목록의 우클릭 메뉴 **[메시지 바꾸기...]** — MergeHub 의 첫 우클릭 메뉴. 목록은 오른쪽 누름으로 선택이 안 바뀌므로 **우클릭한 줄을 먼저 고른다**(10차 B-1 처럼 저장 안 한 편집이 있으면 먼저 묻고 [취소] 면 메뉴도 안 연다) |
|
||||
| `ViewModels/GitViewModel.cs` | 막힐 자리면 메뉴를 끄지 않고 **눌렀을 때 까닭을 알림으로** — 까닭은 저장소를 읽어 봐야 알고, 꺼 두면 왜 안 되는지 알 길이 없다. 되면 창 → 확인 창 → 실행 → 고른 커밋이 **새 해시**를 따라간다 |
|
||||
| 하네스 `MergeSaveTest` | **+40건(232 → 272)** — 막는 여덟 가지와 보는 순서 · 확인 글 · 완료 알림 · 빈 메시지 · 안 바뀐 메시지 · 다듬기 아홉 |
|
||||
|
||||
### 화면에서 찾아 고친 것 — 끝 줄바꿈
|
||||
|
||||
첫 회차에 새 메시지가 **줄바꿈 없이** 저장됐다(`셋` 으로 끝남). git 이 만든 커밋은 늘 `\n` 으로 끝나고, MergeHub 의 [커밋] · [커밋 수정]도 libgit2 가 다듬는다. 칸을 어떻게 고쳤느냐에 따라 끝 줄바꿈이 있다 없다 했다.
|
||||
libgit2 의 다듬기(`PrettifyMessages`)를 켜면 **손대지 않은 뒤 커밋들 메시지까지** 다듬어지므로, 새 메시지 하나만 `CommitReword.Clean` 으로 다듬는다(줄 끝 공백 · 앞뒤 빈 줄 없앰, 빈 줄 여럿은 하나, 끝은 줄바꿈 하나, `#` 줄 · 들여쓰기는 둠). 검사 9건을 먼저 넣어 실패를 보고 고쳤다.
|
||||
|
||||
같은 회차의 다른 둘은 **스크립트 탓**으로 가렸다(`e11-c2-probe.ps1`): 첫 우클릭이 안 먹은 것은 창을 앞으로 부르는 도우미가 다른 프로그램 창을 앞으로 가져와 클릭이 앱에 안 닿은 것, 한글이 한 글자만 들어간 것은 8ms 간격으로 몰아 친 것. 150ms 간격으로 치면 그대로 들어갔다.
|
||||
|
||||
### 고친 뒤 화면에서 (`e11-c2-setup.sh` · `e11-c2a.ps1` · `e11-c2b.ps1` · `e11-c2c.ps1`)
|
||||
|
||||
시험 저장소: `reword` 브랜치에 커밋 넷(작성자 박지훈 · 이수진, 셋째는 일부러 **"셋제 커밋"** 오타). 막힐 자리로 `pushed`(bare 원격에 올림) · `merged`(사이에 병합) · `shared` + `shared-copy` · `tagged`(v9.9).
|
||||
|
||||
**되는 자리** — "셋제 커밋" 우클릭 → [메시지 바꾸기...]
|
||||
|
||||
| 무엇 | 결과 |
|
||||
|---|---|
|
||||
| 우클릭 | 그 줄이 먼저 골라지고 메뉴 `메시지 바꾸기...` |
|
||||
| 창 | `83ed0b2 · 박지훈 · 2026-09-16 11:00` · "이 커밋과 그 뒤 1개, 모두 2개가 새 해시로 다시 써집니다" · 칸에 `셋제 커밋` (`e11-c2-window.png`) |
|
||||
| [취소] | HEAD `4c0263e` 그대로 |
|
||||
| "셋째 커밋" 치고 [바꾸기] | 확인 창 "83ed0b2 "셋제 커밋" 의 메시지를 바꿉니다. 이 커밋과 그 뒤 1개, 모두 2개가 새 해시로 다시 써집니다. 내용 · 작성자 · 작성 시각은 그대로입니다. 바꾸기 전 HEAD 4c0263e 는 로그에 남겨…" (`e11-c2-confirm.png`) |
|
||||
| [예] | 알림 `메시지 바꿈 완료 (옛 HEAD 4c0263e → 새 HEAD 1b11c03) · 커밋 2개 다시 씀` · 로그 `… · 되돌릴 자리 refs/original/20260922-102604/` |
|
||||
| git 대조 | 넷째 · 셋째: **트리 해시 · 작성자 · 작성 시각 같음**, 해시만 새것. 제목은 셋째만 바뀜. 둘째 아래는 해시까지 그대로 · 커밋 수 같음 · 워킹트리 깨끗 |
|
||||
| 메시지 끝 | `셋째 커밋\n` — 손 안 댄 넷째(`넷째 커밋\n`)와 같은 꼴 |
|
||||
| 고른 커밋 | 새 해시 `b2d9880` 을 따라감 |
|
||||
| 되돌리기 | `git reset --hard refs/original/20260922-102604/heads/reword` → `4c0263e … 셋제 커밋` 이 그대로 돌아옴 |
|
||||
|
||||
**막히는 자리** — 모두 창이 안 뜨고 HEAD 그대로, 알림에 까닭
|
||||
|
||||
| 자리 | 알림 |
|
||||
|---|---|
|
||||
| 올린 커밋 | "이 커밋은 origin2/pushed 에 이미 올라가 있습니다. 올린 커밋을 다시 쓰면 남의 이력과 갈라져 막습니다." |
|
||||
| 사이에 병합 | "고른 커밋과 HEAD 사이에 병합 커밋이 1개 있습니다. 병합 이력은 다시 쓰지 않습니다." |
|
||||
| 다른 브랜치도 품음 | "이 커밋을 다른 브랜치도 품고 있습니다(shared-copy). 그 브랜치까지 다시 써야 해서 막습니다." |
|
||||
| 구간에 태그 | "다시 쓸 구간에 태그가 있습니다(v9.9). 태그가 가리키는 커밋이 사라져 막습니다." |
|
||||
| 작업 중(병합 충돌 26개) | "병합 · rebase · cherry-pick · revert 를 끝낸 뒤에 쓸 수 있습니다." |
|
||||
| 커밋 안 한 변경 | "커밋하지 않은 변경이 있습니다. 먼저 커밋하거나 stash 한 뒤에 바꾸세요." |
|
||||
| 다른 브랜치에만([모든 브랜치]) | "지금 브랜치에서 닿지 않는 커밋입니다. 그 커밋이 있는 브랜치로 체크아웃한 뒤에 바꾸세요." (`e11-c2-other.png`) |
|
||||
| 파일 비교 탭에 저장 안 한 편집 | 우클릭하자 `저장하지 않은 편집` 창("Common/Util/Unit103.cs 에 저장하지 않은 편집이 있습니다. 다른 커밋을 봅니다.") → [취소] 면 메뉴도 안 열리고 선택 그대로 (`e11-c2-unsaved.png`) |
|
||||
|
||||
- 하네스 22벌 **1,247건** 통과(1,207 → +40) · 빌드 경고 0 · 오류 0 · 설정 파일 해시 같음.
|
||||
|
||||
### 알아 둘 것
|
||||
|
||||
- 판단은 **마지막 fetch 시점** 기준이다. fetch 전에 남이 올린 것까지는 모른다(C-1 과 같음).
|
||||
- 되돌릴 자리 `refs/original/…` 은 이력 목록에 안 보인다(목록은 브랜치만 훑음). 지우는 단추는 두지 않았다 — 되돌릴 일이 없으면 그대로 두어도 된다.
|
||||
- HEAD 커밋도 이 메뉴로 바꿀 수 있다. [커밋 수정] 과 달리 **스테이징한 파일이 들어가지 않는** 메시지만 고치는 길이다.
|
||||
|
||||
## F-12. C# 코드 스타일 맞춤의 기본값을 AURORA 로 (2026-09-18, 완료)
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
| 날짜 | 항목 | 커밋 |
|
||||
|---|---|---|
|
||||
| 2026-09-22 | **11차 C-2** — 마지막이 아닌 커밋의 메시지를 바꾼다(기획안 "커밋 우클릭 → 이름 변경", 사용자 결정 좁은 범위). 이력 줄 우클릭 [메시지 바꾸기...] — MergeHub 첫 우클릭 메뉴이고, 우클릭한 줄을 먼저 고른다. 여러 줄 창과 확인 창("이 커밋과 그 뒤 N개가 새 해시로 다시 써집니다")을 거쳐, 고른 커밋 메시지만 갈고 뒤 커밋은 트리 · 작성자 · 작성 시각 그대로 다시 쓴다. 바꾸기 전 자리를 `refs/original/…` 에 남기고 알림 · 로그에 옛 → 새 HEAD. 올린 커밋 · 사이 병합 · 다른 브랜치도 품음 · 태그 · 작업 중 · 커밋 안 한 변경 · 다른 브랜치에만 있는 커밋이면 까닭을 알림으로 말하고 막는다. 화면에서 새 메시지가 끝 줄바꿈 없이 저장되는 것을 찾아 git `-m` 처럼 다듬게 함. 하네스 MergeSaveTest +40(272건) | 아래 |
|
||||
| 2026-09-18 | **11차 C-1 (마무리)** — [커밋 수정] 을 누르기 전에 **먼저 묻는다**(사용자 결정): 스테이징해 둔 파일이 있으면 "파일 N개도 이 커밋에 들어갑니다", 이미 올린 커밋이면 "origin/… 에 이미 올라가 있습니다 · 고치면 다음 Push 가 거부됩니다". 둘 다면 두 줄을 한 창에. [아니오] 면 커밋도 메시지 칸도 그대로 둔다. 막지 않는 까닭은 앞선 커밋 수가 마지막 fetch 시점 기준이라 확실치 않아서다. 하네스 MergeSaveTest +7(232건) | 아래 |
|
||||
| 2026-09-18 | **11차 C-1 (일부)** — [커밋 수정]의 여덟 가지를 화면에서 재현한 뒤 넷을 고침: [마지막 메시지]가 이력 맨 위(딴 브랜치일 수 있음) 대신 **HEAD 커밋** 메시지를 읽고, 고쳐 써도 **원래 작성자 · 작성 시각을 지키며**(git 과 같게), 병합 · rebase · cherry-pick · revert **진행 중에는 단추를 끄고**(예전엔 눌러서 실패), 알림에 **"옛 해시 → 새 해시 · 파일 N개 포함"**. 하네스 MergeSaveTest +15(225건). 스테이징 파일 처리(2)와 이미 올린 커밋(6)은 사용자 결정 대기 | 아래 |
|
||||
| 2026-09-18 | **11차 F-1 에 덧 (추가 요청)** — 지시서의 F-1(칸 이름 · 어느 파일인지)에 "머지 주석에도 어디서 가져왔는지(REMOTE/BASE · 브랜치/해시)를 남기고, 그에 맞춰 무시 조건도 고친다" 를 적음. 고칠 곳(`MergeNote.NoteBody` · `LooksLikeNote` · `TailStart` · `CompareRules` · `FolderComparer` · `NoteConflict` · `NoteRows`)과 옛 주석도 그대로 무시되어야 한다는 기준까지. 코드 변경 없음 | 아래 |
|
||||
|
||||
@ -351,7 +351,7 @@
|
||||
| 상태 | 항목 |
|
||||
|---|---|
|
||||
| **완료** | C-1 있는 [커밋 수정] 바로잡기 — 여덟 가지를 화면에서 재현한 뒤 **1 · 3 · 4 · 7 고침**([마지막 메시지]가 HEAD 것 · 작성자와 작성 시각 보존 · 작업 중 단추 끔 · 알림에 "옛 → 새 해시 · 파일 N개 포함"), 이어서 **2 · 6 은 "먼저 묻기"**(사용자 결정) — 스테이징해 둔 파일이 있거나 이미 올린 커밋이면 확인 창을 띄우고 [아니오] 면 아무것도 안 바뀐다. 하네스 MergeSaveTest +22(232건). 8(여러 줄 메시지 못 침)만 범위 밖. 옛 항목: — [마지막 메시지] 가 목록 맨 위(다른 브랜치일 수 있음) · 스테이징 파일이 말없이 들어감 · 원래 작성자 · 시각이 바뀜 · 작업 중에도 켜짐 · 병합 커밋 · 올린 커밋 · 옛 해시 · `#` 줄 |
|
||||
| 대기 | C-2 마지막 커밋이 아닌 커밋의 메시지 수정 (기획안 "커밋 우클릭 → 이름 변경") — 할지 먼저 정함 |
|
||||
| **완료** | C-2 마지막 커밋이 아닌 커밋의 메시지 수정 (기획안 "커밋 우클릭 → 이름 변경") — 사용자 결정 **좁은 범위**. 이력 줄 우클릭 [메시지 바꾸기...] → 여러 줄 창 → 확인 창 → 고른 커밋 메시지만 갈고 그 뒤를 같은 트리 · 작성자 · 작성 시각으로 다시 씀(`RewriteHistory`), 되돌릴 자리 `refs/original/…`, 선택이 새 해시를 따라감. 올린 커밋 · 사이에 병합 · 다른 브랜치도 품음 · 태그 · 작업 중 · 커밋 안 한 변경 · 다른 브랜치에만 · 저장 안 한 편집 여덟 자리가 화면에서 막힘. 새 메시지 끝 줄바꿈을 git 처럼 다듬음(화면에서 찾음). 하네스 MergeSaveTest +40(272건) |
|
||||
|
||||
### D. 시험 도구 정리
|
||||
|
||||
|
||||
@ -56,6 +56,26 @@ namespace MergeHub.Models
|
||||
public bool IsStaged => StagedState is not (WorkingFileState.Unmodified or WorkingFileState.Untracked or WorkingFileState.Ignored);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 고른 커밋의 메시지를 바꿔도 되는지 재 본 것(11차 C-2).
|
||||
/// 여기 담기는 것은 <b>저장소에서 읽은 사실</b>뿐이고, 되는지 안 되는지와 그 문장은 <c>CommitReword</c> 가 정한다.
|
||||
/// </summary>
|
||||
/// <param name="Exists">그런 커밋이 있는가.</param>
|
||||
/// <param name="IsAncestorOfHead">지금 HEAD 에서 닿는가(HEAD 자신 포함).</param>
|
||||
/// <param name="RewriteCount">다시 써야 할 커밋 수(고른 커밋 포함).</param>
|
||||
/// <param name="MergeCommits">그 가운데 병합 커밋 수.</param>
|
||||
/// <param name="PushedTo">이미 올라가 있는 원격 브랜치 이름. 없으면 null.</param>
|
||||
/// <param name="OtherBranches">그 커밋을 함께 품은 다른 로컬 브랜치들.</param>
|
||||
/// <param name="TagsInRange">다시 쓸 구간에 걸린 태그들.</param>
|
||||
public sealed record RewordInfo(
|
||||
bool Exists,
|
||||
bool IsAncestorOfHead,
|
||||
int RewriteCount,
|
||||
int MergeCommits,
|
||||
string? PushedTo,
|
||||
IReadOnlyList<string> OtherBranches,
|
||||
IReadOnlyList<string> TagsInRange);
|
||||
|
||||
/// <summary>저장소 전체 상태 요약. (상단 Working Tree 배지용)</summary>
|
||||
public sealed record RepositoryStatusInfo(
|
||||
bool IsClean,
|
||||
|
||||
144
src/MergeHub/Services/CommitReword.cs
Normal file
144
src/MergeHub/Services/CommitReword.cs
Normal file
@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MergeHub.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 마지막이 아닌 커밋의 메시지를 바꿀 수 있는가, 바꾸기 전에 무엇을 보여야 하는가(11차 C-2).
|
||||
///
|
||||
/// <b>왜 따로 정하나</b> - 메시지는 커밋 해시에 들어간다. 고른 커밋의 메시지를 바꾸면 그 커밋부터
|
||||
/// HEAD 까지가 <b>모두 새 해시로 다시 써진다</b>(트리 · 작성자 · 작성 시각은 그대로인 다른 커밋이 된다).
|
||||
/// 그 커밋이 이미 원격에 올라갔거나 다른 브랜치도 품고 있으면 남의 이력과 갈라진다.
|
||||
/// 그래서 2026-09-18 사용자 결정대로 <b>좁은 범위</b>에서만 켠다 - 지금 브랜치의 조상이고,
|
||||
/// 원격에 아직 없고, 사이에 병합 커밋 · 태그가 없고, 작업 중이 아니고, 워킹트리가 깨끗할 때.
|
||||
///
|
||||
/// 막지 않고 묻기만 하는 [커밋 수정](C-1)과 기준이 다른 까닭 - amend 는 한 칸만 바꾸지만
|
||||
/// 이것은 뒤따르는 커밋까지 모두 갈아 치운다. 되돌릴 자리를 남겨도(refs/original · reflog)
|
||||
/// 남이 이미 받아 간 뒤에는 되돌릴 수 없다.
|
||||
/// </summary>
|
||||
public static class CommitReword
|
||||
{
|
||||
/// <summary>
|
||||
/// 막을 까닭. 바꿔도 되면 null.
|
||||
/// 화면에서 그대로 보여 줄 문장이므로 <b>무엇을 하면 풀리는지</b>까지 적는다.
|
||||
/// </summary>
|
||||
/// <param name="hasCommit">이력에서 커밋을 골랐는가.</param>
|
||||
/// <param name="operationInProgress">병합 · rebase · cherry-pick · revert 가 도는 중인가.</param>
|
||||
/// <param name="workingTreeDirty">커밋하지 않은 변경이 있는가.</param>
|
||||
/// <param name="isAncestorOfHead">고른 커밋이 지금 HEAD 에서 닿는가(HEAD 자신 포함).</param>
|
||||
/// <param name="pushedTo">이미 올라간 원격 브랜치 이름. 안 올라갔으면 null.</param>
|
||||
/// <param name="otherBranches">그 커밋을 함께 품은 <b>다른 로컬 브랜치</b> 이름들.</param>
|
||||
/// <param name="tagsInRange">다시 쓸 구간에 걸린 태그 이름들.</param>
|
||||
/// <param name="mergeCommits">다시 쓸 구간의 병합 커밋 수(고른 커밋 포함).</param>
|
||||
public static string? BlockReason(bool hasCommit, bool operationInProgress, bool workingTreeDirty,
|
||||
bool isAncestorOfHead, string? pushedTo,
|
||||
IReadOnlyList<string>? otherBranches, IReadOnlyList<string>? tagsInRange,
|
||||
int mergeCommits)
|
||||
{
|
||||
if (!hasCommit) return "이력에서 커밋을 먼저 고르세요.";
|
||||
|
||||
if (operationInProgress)
|
||||
return "병합 · rebase · cherry-pick · revert 를 끝낸 뒤에 쓸 수 있습니다.";
|
||||
|
||||
if (workingTreeDirty)
|
||||
return "커밋하지 않은 변경이 있습니다. 먼저 커밋하거나 stash 한 뒤에 바꾸세요.";
|
||||
|
||||
if (!isAncestorOfHead)
|
||||
return "지금 브랜치에서 닿지 않는 커밋입니다. 그 커밋이 있는 브랜치로 체크아웃한 뒤에 바꾸세요.";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(pushedTo))
|
||||
return $"이 커밋은 {pushedTo} 에 이미 올라가 있습니다. 올린 커밋을 다시 쓰면 남의 이력과 갈라져 막습니다.";
|
||||
|
||||
if (otherBranches is { Count: > 0 })
|
||||
return $"이 커밋을 다른 브랜치도 품고 있습니다({Join(otherBranches)}). 그 브랜치까지 다시 써야 해서 막습니다.";
|
||||
|
||||
if (tagsInRange is { Count: > 0 })
|
||||
return $"다시 쓸 구간에 태그가 있습니다({Join(tagsInRange)}). 태그가 가리키는 커밋이 사라져 막습니다.";
|
||||
|
||||
if (mergeCommits > 0)
|
||||
return $"고른 커밋과 HEAD 사이에 병합 커밋이 {mergeCommits}개 있습니다. 병합 이력은 다시 쓰지 않습니다.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>바꾸기 전에 띄울 확인 글. 무엇이 몇 개 다시 써지는지와 되돌릴 자리를 함께 적는다.</summary>
|
||||
/// <param name="shortSha">고른 커밋의 짧은 해시.</param>
|
||||
/// <param name="subject">고른 커밋의 지금 제목.</param>
|
||||
/// <param name="rewriteCount">다시 쓸 커밋 수(고른 커밋 포함).</param>
|
||||
/// <param name="headShortSha">지금 HEAD 짧은 해시 - 되돌릴 자리.</param>
|
||||
public static string Confirm(string shortSha, string subject, int rewriteCount, string headShortSha)
|
||||
{
|
||||
var tail = rewriteCount <= 1
|
||||
? "이 커밋이 새 해시로 다시 써집니다"
|
||||
: $"이 커밋과 그 뒤 {rewriteCount - 1}개, 모두 {rewriteCount}개가 새 해시로 다시 써집니다";
|
||||
|
||||
return $"{shortSha} \"{Trim(subject)}\" 의 메시지를 바꿉니다.\n\n" +
|
||||
$"{tail}. 내용 · 작성자 · 작성 시각은 그대로입니다.\n" +
|
||||
$"바꾸기 전 HEAD {headShortSha} 는 로그에 남겨 되돌릴 수 있게 합니다.\n\n" +
|
||||
"계속할까요?";
|
||||
}
|
||||
|
||||
/// <summary>바꾼 뒤 알릴 글.</summary>
|
||||
public static string Done(string? oldHead, string? newHead, int rewriteCount) =>
|
||||
$"메시지 바꿈 완료 (옛 HEAD {Short(oldHead)} → 새 HEAD {Short(newHead)}) · 커밋 {rewriteCount}개 다시 씀";
|
||||
|
||||
/// <summary>
|
||||
/// 새로 친 메시지를 git 이 <c>-m</c> 으로 받을 때처럼 다듬는다(cleanup=whitespace).
|
||||
/// 줄 끝 공백 · 앞뒤 빈 줄을 없애고, 빈 줄 여럿은 하나로, 끝은 줄바꿈 하나. <c>#</c> 줄과 들여쓰기는 둔다.
|
||||
///
|
||||
/// <b>왜</b> - 다듬지 않고 쓰니 칸을 어떻게 고쳤느냐에 따라 끝 줄바꿈이 있다 없다 했다
|
||||
/// (2026-09-22 화면: "셋" 으로 끝난 커밋. git 이 만든 커밋은 모두 줄바꿈으로 끝난다).
|
||||
/// libgit2 의 다듬기(<c>PrettifyMessages</c>)를 켜면 <b>손대지 않은 뒤 커밋들 메시지까지</b> 다듬어지므로,
|
||||
/// 새 메시지 하나만 여기서 다듬는다.
|
||||
/// </summary>
|
||||
/// <returns>다듬은 글. 남는 것이 없으면 빈 글.</returns>
|
||||
public static string Clean(string? message)
|
||||
{
|
||||
var lines = (message ?? string.Empty).Replace("\r\n", "\n").Replace('\r', '\n').Split('\n');
|
||||
var kept = new List<string>(lines.Length);
|
||||
var blank = false;
|
||||
|
||||
foreach (var raw in lines)
|
||||
{
|
||||
var line = raw.TrimEnd();
|
||||
if (line.Length == 0)
|
||||
{
|
||||
blank = kept.Count > 0; // 앞 빈 줄은 버리고, 사이 빈 줄은 하나만 남긴다
|
||||
continue;
|
||||
}
|
||||
|
||||
if (blank) kept.Add(string.Empty);
|
||||
blank = false;
|
||||
kept.Add(line);
|
||||
}
|
||||
|
||||
return kept.Count == 0 ? string.Empty : string.Join("\n", kept) + "\n";
|
||||
}
|
||||
|
||||
/// <summary>메시지가 비면 바꾸지 않는다 - 빈 메시지 커밋은 만들지 않는다.</summary>
|
||||
public static bool IsUsableMessage(string? message) => !string.IsNullOrWhiteSpace(message);
|
||||
|
||||
/// <summary>바뀐 것이 없으면 다시 쓸 까닭도 없다(해시만 바뀌는 헛일).</summary>
|
||||
public static bool Changed(string? oldMessage, string? newMessage) =>
|
||||
Normalize(oldMessage) != Normalize(newMessage);
|
||||
|
||||
/// <summary>우클릭 메뉴에 걸어 둘 설명 - 왜 꺼졌는지는 눌러 보면 알려 준다.</summary>
|
||||
public static string MenuHint =>
|
||||
"고른 커밋의 메시지를 바꿉니다. 그 커밋부터 HEAD 까지 새 해시로 다시 써집니다 (원격에 올리기 전에만)";
|
||||
|
||||
private static string Normalize(string? text) =>
|
||||
(text ?? string.Empty).Replace("\r\n", "\n").Trim();
|
||||
|
||||
private static string Join(IReadOnlyList<string> names) =>
|
||||
names.Count <= 3 ? string.Join(" · ", names) : string.Join(" · ", names[0], names[1], names[2]) + $" 외 {names.Count - 3}개";
|
||||
|
||||
private static string Trim(string? subject)
|
||||
{
|
||||
var one = Normalize(subject).Replace("\n", " ");
|
||||
return one.Length > 40 ? one.Substring(0, 40) + "…" : one;
|
||||
}
|
||||
|
||||
private static string Short(string? sha) =>
|
||||
string.IsNullOrEmpty(sha) ? "-" : (sha!.Length > 7 ? sha.Substring(0, 7) : sha);
|
||||
}
|
||||
}
|
||||
@ -495,6 +495,109 @@ namespace MergeHub.Services
|
||||
return commit.Sha;
|
||||
}
|
||||
|
||||
// ================= 마지막이 아닌 커밋의 메시지 바꾸기 (11차 C-2) =================
|
||||
|
||||
/// <summary>
|
||||
/// 고른 커밋의 메시지를 바꿔도 되는지 <b>재 본다</b>(바꾸지는 않는다).
|
||||
/// 판단 문장은 <see cref="CommitReword"/> 가 만든다 - 여기서는 저장소에서 읽을 수 있는 사실만 모은다.
|
||||
/// </summary>
|
||||
public RewordInfo InspectReword(string sha)
|
||||
{
|
||||
var target = ResolveCommit(sha);
|
||||
var head = Repo.Head?.Tip;
|
||||
if (target is null || head is null)
|
||||
return new RewordInfo(false, false, 0, 0, null, Array.Empty<string>(), Array.Empty<string>());
|
||||
|
||||
// HEAD 에서 닿는가 - 합류점이 자기 자신이면 조상이다(자기 자신도 포함).
|
||||
var isAncestor = string.Equals(Repo.ObjectDatabase.FindMergeBase(target, head)?.Sha, target.Sha, StringComparison.Ordinal);
|
||||
if (!isAncestor)
|
||||
return new RewordInfo(true, false, 0, 0, null, Array.Empty<string>(), Array.Empty<string>());
|
||||
|
||||
// 다시 쓸 커밋 = 고른 커밋 + HEAD 에서 닿지만 고른 커밋에서는 닿지 않는 것들.
|
||||
var after = Repo.Commits.QueryBy(new CommitFilter
|
||||
{
|
||||
IncludeReachableFrom = head,
|
||||
ExcludeReachableFrom = target,
|
||||
SortBy = CommitSortStrategies.Topological
|
||||
}).ToList();
|
||||
|
||||
var range = new List<Commit>(after.Count + 1) { target };
|
||||
range.AddRange(after);
|
||||
|
||||
var merges = range.Count(c => c.Parents.Count() > 1);
|
||||
|
||||
// 이미 올라간 커밋인가 - 원격 추적 브랜치 아무 곳에나 들어 있으면 올라간 것으로 본다.
|
||||
string? pushedTo = null;
|
||||
foreach (var remote in Repo.Branches.Where(b => b.IsRemote))
|
||||
{
|
||||
if (remote.Tip is null) continue;
|
||||
if (!string.Equals(Repo.ObjectDatabase.FindMergeBase(target, remote.Tip)?.Sha, target.Sha, StringComparison.Ordinal)) continue;
|
||||
pushedTo = remote.FriendlyName;
|
||||
break;
|
||||
}
|
||||
|
||||
// 다른 로컬 브랜치도 품고 있으면 그 브랜치까지 다시 써야 한다 - 막는다.
|
||||
var current = Repo.Head?.FriendlyName;
|
||||
var others = Repo.Branches
|
||||
.Where(b => !b.IsRemote && b.Tip is not null && !string.Equals(b.FriendlyName, current, StringComparison.Ordinal))
|
||||
.Where(b => string.Equals(Repo.ObjectDatabase.FindMergeBase(target, b.Tip)?.Sha, target.Sha, StringComparison.Ordinal))
|
||||
.Select(b => b.FriendlyName)
|
||||
.ToList();
|
||||
|
||||
var shas = new HashSet<string>(range.Select(c => c.Sha), StringComparer.Ordinal);
|
||||
var tags = Repo.Tags
|
||||
.Where(t => t.Target?.Sha is { } s && shas.Contains(s))
|
||||
.Select(t => t.FriendlyName)
|
||||
.ToList();
|
||||
|
||||
return new RewordInfo(true, true, range.Count, merges, pushedTo, others, tags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 고른 커밋의 메시지를 바꾸고 그 뒤 커밋을 <b>같은 내용으로 다시 쓴다</b>(`git rebase -i` 의 reword).
|
||||
///
|
||||
/// 트리 · 작성자 · 작성 시각 · 부모 관계는 그대로 두고 메시지만 갈아 끼우므로, 남는 것은 해시뿐이다.
|
||||
/// 바꾸기 전 자리는 <c>refs/original/…</c> 아래에 남겨 되돌릴 수 있게 한다 - 같은 자리에 두 번 쓰면
|
||||
/// libgit2 가 "이미 있다" 로 막으므로 시각을 붙여 새 자리에 남긴다.
|
||||
/// </summary>
|
||||
/// <returns>바꾸기 전 HEAD · 바꾼 뒤 HEAD · 다시 쓴 커밋 수 · 되돌릴 자리(백업 ref 이름) · 고친 커밋의 새 해시.</returns>
|
||||
public (string OldHead, string NewHead, int Rewritten, string BackupRef, string NewTarget) RewordCommit(string sha, string message)
|
||||
{
|
||||
// 새 메시지는 git 이 -m 으로 받을 때처럼 다듬는다(끝 줄바꿈 하나 등) - CommitReword.Clean 참고.
|
||||
var cleaned = CommitReword.Clean(message);
|
||||
if (cleaned.Length == 0)
|
||||
throw new ArgumentException("커밋 메시지가 비어 있습니다.", nameof(message));
|
||||
|
||||
var target = ResolveCommit(sha) ?? throw new ArgumentException($"커밋을 찾을 수 없습니다: {sha}", nameof(sha));
|
||||
var oldHead = Repo.Head.Tip?.Sha ?? string.Empty;
|
||||
var info = InspectReword(target.Sha);
|
||||
var backup = $"refs/original/{DateTime.Now:yyyyMMdd-HHmmss}/";
|
||||
|
||||
Repo.Refs.RewriteHistory(new RewriteHistoryOptions
|
||||
{
|
||||
BackupRefsNamespace = backup,
|
||||
// 켜면 <b>손대지 않은 뒤 커밋들 메시지까지</b> 다듬어진다. 새 메시지만 위에서 다듬었다.
|
||||
PrettifyMessages = false,
|
||||
CommitHeaderRewriter = c => string.Equals(c.Sha, target.Sha, StringComparison.Ordinal)
|
||||
? CommitRewriteInfo.From(c, message: cleaned)
|
||||
: CommitRewriteInfo.From(c)
|
||||
}, new[] { target });
|
||||
|
||||
var newHead = Repo.Head.Tip?.Sha ?? string.Empty;
|
||||
|
||||
// 고친 커밋의 새 자리 - 구간에 병합 커밋이 없는 것을 이미 막아 두었으므로 첫 부모를 따라
|
||||
// 다시 쓴 개수만큼 내려가면 그 커밋이다. 두 탭의 고른 커밋이 새 해시를 따라가는 데 쓴다.
|
||||
var newTarget = Repo.Head.Tip;
|
||||
for (var i = 1; i < info.RewriteCount && newTarget is not null; i++)
|
||||
newTarget = newTarget.Parents.FirstOrDefault();
|
||||
|
||||
Report($"커밋 메시지 바꿈: {Short(oldHead)} → {Short(newHead)} · 커밋 {info.RewriteCount}개 다시 씀 · 되돌릴 자리 {backup}");
|
||||
return (oldHead, newHead, info.RewriteCount, backup, newTarget?.Sha ?? newHead);
|
||||
}
|
||||
|
||||
private static string Short(string? sha) =>
|
||||
string.IsNullOrEmpty(sha) ? "-" : (sha!.Length > 7 ? sha.Substring(0, 7) : sha);
|
||||
|
||||
// ================= 병합 =================
|
||||
|
||||
public bool IsMergeInProgress =>
|
||||
|
||||
@ -66,6 +66,10 @@ namespace MergeHub.Services
|
||||
void Unstage(params string[] paths);
|
||||
string Commit(string message, bool amend = false);
|
||||
|
||||
// ---- 마지막이 아닌 커밋의 메시지 바꾸기 (11차 C-2) ----------------
|
||||
RewordInfo InspectReword(string sha);
|
||||
(string OldHead, string NewHead, int Rewritten, string BackupRef, string NewTarget) RewordCommit(string sha, string message);
|
||||
|
||||
// ---- 병합 ---------------------------------------------------------
|
||||
/// <summary>
|
||||
/// 브랜치를 현재 HEAD 로 병합. 기획안 6p 비목표("사람 확인 없는 자동 커밋 금지")에 따라
|
||||
|
||||
@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using MergeHub.Models;
|
||||
using MergeHub.Services;
|
||||
using MergeHub.Views;
|
||||
using UbisamBase.Core.Messaging;
|
||||
|
||||
namespace MergeHub.ViewModels
|
||||
@ -576,6 +577,73 @@ namespace MergeHub.ViewModels
|
||||
private Task ResetAsync() => _workspace.RunAndRefreshAsync($"reset --{SelectedResetKind}",
|
||||
() => _workspace.Git.Reset(SelectedResetKind, SelectedCommit!.Sha));
|
||||
|
||||
/// <summary>우클릭 메뉴 [메시지 바꾸기...] 에 걸어 둘 설명.</summary>
|
||||
public string RewordHint => CommitReword.MenuHint;
|
||||
|
||||
/// <summary>
|
||||
/// 이력에서 고른 커밋의 메시지를 바꾼다(11차 C-2, `git rebase -i` 의 reword).
|
||||
///
|
||||
/// HEAD 가 아니면 <b>그 커밋부터 HEAD 까지가 새 해시로 다시 써진다</b>. 그래서 좁은 범위에서만 한다 -
|
||||
/// 막을 까닭은 <see cref="CommitReword.BlockReason"/> 가 정하고, 여기서는 눌렀을 때 <b>왜 안 되는지 말해 준다</b>.
|
||||
/// 메뉴를 꺼 두지 않는 까닭: 꺼 두면 왜 안 되는지 알 길이 없고, 까닭은 저장소를 읽어 봐야 안다.
|
||||
///
|
||||
/// 저장 안 한 편집은 <see cref="GitWorkspace.RunAndRefreshAsync"/> 가 지금까지의 Git 작업과 같은 창으로 묻는다.
|
||||
/// </summary>
|
||||
[RelayCommand(CanExecute = nameof(HasCommit))]
|
||||
private async Task RewordAsync()
|
||||
{
|
||||
var commit = SelectedCommit!;
|
||||
var info = _workspace.Git.InspectReword(commit.Sha);
|
||||
|
||||
var blocked = CommitReword.BlockReason(
|
||||
info.Exists, AmendBlockedByOperation, !_workspace.IsWorkingTreeClean,
|
||||
info.IsAncestorOfHead, info.PushedTo, info.OtherBranches, info.TagsInRange, info.MergeCommits);
|
||||
|
||||
if (blocked is not null)
|
||||
{
|
||||
MessageUtil.ShowWarningToast($"메시지를 바꿀 수 없습니다 - {blocked}");
|
||||
return;
|
||||
}
|
||||
|
||||
var target = $"{commit.ShortSha} · {commit.AuthorName} · {commit.When.LocalDateTime:yyyy-MM-dd HH:mm}";
|
||||
var typed = RewordWindow.Ask(target, commit.Message,
|
||||
info.RewriteCount <= 1
|
||||
? "이 커밋이 새 해시로 다시 써집니다"
|
||||
: $"이 커밋과 그 뒤 {info.RewriteCount - 1}개, 모두 {info.RewriteCount}개가 새 해시로 다시 써집니다");
|
||||
|
||||
if (typed is null) return;
|
||||
|
||||
if (!CommitReword.IsUsableMessage(typed))
|
||||
{
|
||||
MessageUtil.ShowWarningToast("메시지가 비어 있어 바꾸지 않았습니다");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CommitReword.Changed(commit.Message, typed))
|
||||
{
|
||||
MessageUtil.ShowToast("메시지가 그대로라 다시 쓰지 않았습니다");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MessageUtil.ShowYesNo("커밋 메시지 바꾸기",
|
||||
CommitReword.Confirm(commit.ShortSha, commit.MessageShort, info.RewriteCount, _workspace.HeadSha)))
|
||||
return;
|
||||
|
||||
var newTarget = string.Empty;
|
||||
var done = await _workspace.RunAndRefreshAsync("커밋 메시지 바꾸기", () =>
|
||||
{
|
||||
var result = _workspace.Git.RewordCommit(commit.Sha, typed);
|
||||
newTarget = result.NewTarget;
|
||||
_workspace.OnUi(() =>
|
||||
MessageUtil.ShowSuccessToast(CommitReword.Done(result.OldHead, result.NewHead, result.Rewritten)));
|
||||
});
|
||||
|
||||
// 고른 커밋을 <b>새 해시로</b> 따라간다. 그냥 두면 옛 해시가 사라져 선택이 HEAD 로 떨어진다(9차 A-1 · A-2).
|
||||
if (!done || newTarget.Length == 0) return;
|
||||
if (_workspace.Commits.FirstOrDefault(c => string.Equals(c.Sha, newTarget, StringComparison.Ordinal)) is { } moved)
|
||||
SelectedCommit = moved;
|
||||
}
|
||||
|
||||
private bool CanRebase() =>
|
||||
SelectedBranch is not null && !SelectedBranch.IsCurrentHead && !_workspace.IsRebaseInProgress;
|
||||
|
||||
@ -1009,6 +1077,7 @@ namespace MergeHub.ViewModels
|
||||
RevertCommand.NotifyCanExecuteChanged();
|
||||
CherryPickCommand.NotifyCanExecuteChanged();
|
||||
ResetCommand.NotifyCanExecuteChanged();
|
||||
RewordCommand.NotifyCanExecuteChanged();
|
||||
RebaseCommand.NotifyCanExecuteChanged();
|
||||
RebaseContinueCommand.NotifyCanExecuteChanged();
|
||||
RebaseAbortCommand.NotifyCanExecuteChanged();
|
||||
|
||||
@ -415,6 +415,14 @@
|
||||
<ListBox x:Name="HistoryList" Grid.Row="0" Grid.Column="0" ItemsSource="{Binding Workspace.Commits}"
|
||||
SelectedItem="{Binding SelectedCommit}" HorizontalContentAlignment="Stretch"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
|
||||
<!-- 기획안의 "커밋 우클릭 → 이름 변경"(11차 C-2). 메뉴는 시각 트리 밖이라
|
||||
목록(PlacementTarget)의 DataContext 를 가져와야 명령이 걸린다. -->
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
||||
<MenuItem Header="메시지 바꾸기..." Command="{Binding RewordCommand}"
|
||||
ToolTip="{Binding RewordHint}"/>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Margin="0,2">
|
||||
|
||||
@ -30,6 +30,18 @@ namespace MergeHub.Views
|
||||
e.Handled = true; // [취소] 거나, 창이 누름을 끊어 목록이 스스로 못 고른다
|
||||
if (answer == true) ChooseHistoryRow(vm, HistoryList.Items.IndexOf(target));
|
||||
};
|
||||
// 우클릭한 줄을 먼저 고른다(11차 C-2). 목록은 오른쪽 누름으로는 선택을 바꾸지 않아서, 그대로 두면
|
||||
// <b>가리킨 줄이 아닌 다른 커밋</b>의 메시지를 바꾸게 된다 - 다시 쓴 이력은 되돌리기 번거롭다.
|
||||
HistoryList.PreviewMouseRightButtonDown += (_, e) =>
|
||||
{
|
||||
if (ViewModel is not { } vm) return;
|
||||
if (ItemsControl.ContainerFromElement(HistoryList, e.OriginalSource as DependencyObject) is not ListBoxItem item) return;
|
||||
if (item.DataContext is not MergeHub.Models.CommitInfo target) return;
|
||||
|
||||
var answer = vm.ConfirmChooseCommit(target);
|
||||
if (answer == false) { e.Handled = true; return; } // [취소] - 메뉴도 열지 않는다
|
||||
ChooseHistoryRow(vm, HistoryList.Items.IndexOf(target));
|
||||
};
|
||||
HistoryList.PreviewKeyDown += (_, e) =>
|
||||
{
|
||||
if (ViewModel is not { } vm) return;
|
||||
|
||||
65
src/MergeHub/Views/RewordWindow.xaml
Normal file
65
src/MergeHub/Views/RewordWindow.xaml
Normal file
@ -0,0 +1,65 @@
|
||||
<Window x:Class="MergeHub.Views.RewordWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Width="560" SizeToContent="Height"
|
||||
WindowStyle="None"
|
||||
ResizeMode="NoResize"
|
||||
ShowInTaskbar="False"
|
||||
Background="{DynamicResource Ubisam.Brush.ContentBackground}">
|
||||
|
||||
<!-- 별도 창이라 셸의 리소스를 자동 상속받지 못한다 - ShortcutHelpWindow · 플랫폼 대화상자와 같은 패턴. -->
|
||||
<Window.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/UbisamBase.Core;component/Themes/ShellStyles.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Window.Resources>
|
||||
|
||||
<Border BorderBrush="{DynamicResource Ubisam.Brush.SubTabBar.Border}" BorderThickness="1">
|
||||
<DockPanel TextElement.Foreground="{DynamicResource Ubisam.Brush.ContentForeground}">
|
||||
|
||||
<Border x:Name="TitleBar" DockPanel.Dock="Top"
|
||||
Background="{DynamicResource Ubisam.Brush.Sidebar.Background}"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.SubTabBar.Border}"
|
||||
BorderThickness="0,0,0,1" Padding="16,10"
|
||||
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Content="✕" Click="Cancel_Click"
|
||||
Background="Transparent" BorderThickness="0" Padding="8,0"
|
||||
MinHeight="20" FontSize="13"
|
||||
Foreground="{DynamicResource Ubisam.Brush.Muted}"/>
|
||||
<TextBlock Text="커밋 메시지 바꾸기" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource Ubisam.Brush.ContentForeground}"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<Border DockPanel.Dock="Bottom" Padding="16,12"
|
||||
BorderBrush="{DynamicResource Ubisam.Brush.SubTabBar.Border}" BorderThickness="0,1,0,0">
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Content="바꾸기" MinWidth="84" Padding="14,7"
|
||||
Margin="8,0,0,0"
|
||||
Style="{StaticResource Ubisam.Style.Button.Primary}"
|
||||
IsDefault="False" Click="Ok_Click"/>
|
||||
<Button DockPanel.Dock="Right" Content="취소" MinWidth="84" Padding="14,7"
|
||||
IsCancel="True" Click="Cancel_Click"/>
|
||||
<TextBlock x:Name="CountText" VerticalAlignment="Center" FontSize="11.5"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource Ubisam.Brush.Muted}"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<StackPanel Margin="16,14">
|
||||
<!-- 어느 커밋을 고쳤는지 창 안에서도 보여야 한다 - 목록에서 고른 줄과 다른 커밋을 고치면 되돌리기 어렵다. -->
|
||||
<TextBlock x:Name="TargetText" FontFamily="Consolas" FontSize="11.5"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Foreground="{DynamicResource Ubisam.Brush.Warning}"/>
|
||||
<TextBlock Text="새 메시지" Margin="0,10,0,4" FontSize="12"
|
||||
Foreground="{DynamicResource Ubisam.Brush.Muted}"/>
|
||||
<!-- 여러 줄을 받는다. 커밋 메시지 칸(한 줄)과 달리 본문까지 칠 수 있다. -->
|
||||
<TextBox x:Name="MessageBox" Height="120" AcceptsReturn="True" TextWrapping="Wrap"
|
||||
VerticalScrollBarVisibility="Auto" FontFamily="Consolas"/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Window>
|
||||
58
src/MergeHub/Views/RewordWindow.xaml.cs
Normal file
58
src/MergeHub/Views/RewordWindow.xaml.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace MergeHub.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// 고른 커밋의 새 메시지를 받는 창(11차 C-2).
|
||||
///
|
||||
/// 커밋 메시지 칸(Git 탭 아래)은 한 줄이라 본문을 칠 수 없다(11차 C-1 의 8번). 여기서는 여러 줄을 받는다 -
|
||||
/// 메시지를 고치러 들어온 자리에서까지 제목만 칠 수 있으면 고치는 뜻이 없다.
|
||||
///
|
||||
/// [바꾸기] 는 기본 단추로 두지 않는다. Enter 는 줄바꿈이어야 하고, 실수로 눌러 이력을 다시 쓰면 안 된다.
|
||||
/// </summary>
|
||||
public partial class RewordWindow : Window
|
||||
{
|
||||
private RewordWindow(string target, string message, string countText)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
TargetText.Text = target;
|
||||
CountText.Text = countText;
|
||||
MessageBox.Text = message;
|
||||
|
||||
if (Application.Current?.MainWindow is { IsLoaded: true } main && !ReferenceEquals(main, this))
|
||||
{
|
||||
Owner = main;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
}
|
||||
else
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
}
|
||||
|
||||
// 고칠 글을 미리 넣어 두고 모두 골라 둔다 - 그대로 지우고 치거나, 한 글자만 고치거나 둘 다 편하게.
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
MessageBox.Focus();
|
||||
MessageBox.SelectAll();
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>새 메시지를 받아 온다. [취소] 하거나 창을 닫으면 null.</summary>
|
||||
/// <param name="target">고친 커밋을 알아볼 한 줄(해시 · 작성자 · 날짜).</param>
|
||||
/// <param name="currentMessage">지금 메시지 - 칸에 미리 넣어 둔다.</param>
|
||||
/// <param name="countText">몇 개가 다시 써지는지 아래에 늘 보이는 말.</param>
|
||||
public static string? Ask(string target, string currentMessage, string countText)
|
||||
{
|
||||
var window = new RewordWindow(target, currentMessage, countText);
|
||||
return window.ShowDialog() == true ? window.MessageBox.Text : null;
|
||||
}
|
||||
|
||||
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) => DragMove();
|
||||
|
||||
private void Ok_Click(object sender, RoutedEventArgs e) => DialogResult = true;
|
||||
|
||||
private void Cancel_Click(object sender, RoutedEventArgs e) => DialogResult = false;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user