diff --git a/doc/history/2026-09-18-11차.md b/doc/history/2026-09-18-11차.md
index e6ba3ea..f79c6f8 100644
--- a/doc/history/2026-09-18-11차.md
+++ b/doc/history/2026-09-18-11차.md
@@ -127,6 +127,52 @@ A-1 에서 값으로 확인한 것 — 지운 파일은 "삭제되어 건너뜀
- A-4(ZIP)는 아직 정하지 않았다.
- 매뉴얼 · 웹 문서의 변경분 추출 설명(새 폴더 · 안내 파일)은 E-2 에서 함께 고친다.
+## C-1. 있는 [커밋 수정] 을 다시 내 보고 바로잡음 (2026-09-18, 일부 완료 — 2 · 6 은 결정 대기)
+
+### 고치기 전에 여덟 가지를 화면에서 다시 냈다
+
+시험 저장소: `C1Repo` 위에 `amend` 브랜치를 만들고 **작성자 박지훈인 커밋**을 HEAD 로, 다른 브랜치(`feature/revert`)에는 **더 새 커밋**을, 그리고 bare 저장소를 원격으로 걸어 upstream 을 두었다(`e11-c1-setup.sh`).
+
+| # | 고치기 전에 본 것 |
+|---|---|
+| 1 | [모든 브랜치] 켠 채 [마지막 메시지] → 칸에 `딴 브랜치 최신 작업`(HEAD 아님). 그대로 [커밋 수정] 하니 **HEAD 제목이 그 메시지로 바뀌었다** |
+| 2 | 앱에서 [전체 스테이징] 한 파일이 **말없이 커밋에 들어갔다**(남은 변경 없음). 알림은 "커밋 수정" 뿐 |
+| 3 | 작성자 `박지훈` · 작성 `2026-09-15 10:00` → **`시험` · `2026-09-18 15:48`** 로 바뀜 |
+| 4 | 충돌 24개가 남은 채로도 단추가 **켜져 있고**, 누르면 `[커밋 수정] 실패: cannot create a tree from a not fully merged index` |
+| 5 | 병합 커밋을 고쳐도 **부모 둘 · 본문(충돌 해결 26개 · 29줄) 그대로** |
+| 6 | 원격에 이미 올라간 커밋도 **막지 않고** 고쳐졌고, 그 뒤 [Push] 가 `cannot push non-fastforwardable reference` 로 거부됨 |
+| 7 | 알림 · 로그에 **새 해시만**(`커밋: 61653f5 …`). 고치기 전 해시는 어디에도 없음 |
+| 8 | 커밋 메시지 칸이 한 줄짜리라 **여러 줄 메시지를 아예 칠 수 없다**. `#` 로 시작하는 글자는 그대로 들어간다 |
+
+### 고친 것 (1 · 3 · 4 · 7)
+
+| 자리 | 한 일 |
+|---|---|
+| `Services/CommitSelection.cs` | `MessageOfHead` — [마지막 메시지] 는 **HEAD 커밋**의 메시지를 읽는다(목록 순서와 무관). HEAD 가 목록에 없으면 아무것도 주지 않는다. `CanAmend` — **작업 중이면 끈다**. `AmendHint` — 꺼진 까닭을 단추 설명으로. `AmendDone` — "커밋 수정 완료 (옛 abc1234 → 새 def5678) · 파일 N개 포함" |
+| `Services/GitService.cs` | 고쳐 쓸 때 **원래 작성자 · 작성 시각을 그대로** 쓴다(git 과 같게). 커밋한 사람 · 시각만 지금 |
+| `Services/GitWorkspace.cs` | `HeadFullSha` 를 들고 있는다 — 이력 목록의 Sha 와 맞춰 보려면 짧은 해시로는 모자란다 |
+| `ViewModels/GitViewModel.cs` | 위 판단을 붙이고, 고치기 전 해시 · 함께 들어간 파일 수를 알림에. 작업이 시작 · 끝날 때 단추 설명도 다시 그린다 |
+| `Views/GitView.xaml` | [커밋 수정] 설명을 상황에 따라 바뀌는 값으로 |
+| 하네스 `MergeSaveTest` | **+15건(210 → 225)** — HEAD 메시지 읽기(맨 위가 딴 브랜치여도 · 대소문자 · HEAD 가 목록에 없음 · 빈 목록) · 켜는 조건 넷 · 꺼진 까닭 · 알림 글 셋 |
+
+### 고친 뒤 같은 순서로 다시
+
+| # | 결과 |
+|---|---|
+| 1 | 칸에 **`남이 만든 커밋`**(HEAD 것) |
+| 3 | 고친 뒤에도 작성자 `박지훈` · 작성 `2026-09-15 10:00` **그대로**, 커밋한 사람 · 시각만 지금 |
+| 4 | 충돌 24개가 남은 동안 단추 **꺼짐** — 누를 수 없고, 누른 뒤 실패 알림도 없다 |
+| 7 | 알림 **`커밋 수정 완료 (옛 bcd2c43 → 새 fd7810c) · 파일 1개 포함`** |
+
+- 하네스 22벌 **1,200건** 통과(1,185 → +15) · 빌드 경고 0 · 오류 0.
+
+### 남은 것 (결정 대기)
+
+- **2. 스테이징한 파일이 함께 들어가는 것** — 지금은 들어가되 알림에 "파일 N개 포함" 으로 드러내기만 한다. 묻기 / 메시지만 고치기(git `--only`) / 지금대로 중 정해야 한다.
+- **6. 이미 올린 커밋** — 지금도 막지 않는다(그 뒤 Push 가 거부된다). 묻기 / 막기 / 매뉴얼에만 중 정해야 한다.
+- **8. 여러 줄 커밋 메시지** — 칸이 한 줄이라 본문을 칠 수 없다. 이번 범위 밖으로 적어 둔다(병합 커밋 본문은 앱이 저절로 붙인다).
+- C-2(마지막이 아닌 커밋의 메시지 수정)는 C-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 6519a38..ef69174 100644
--- a/doc/history/changelog.md
+++ b/doc/history/changelog.md
@@ -8,6 +8,7 @@
| 날짜 | 항목 | 커밋 |
|---|---|---|
+| 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`)과 옛 주석도 그대로 무시되어야 한다는 기준까지. 코드 변경 없음 | 아래 |
| 2026-09-18 | **11차 A-3** — 변경분을 고른 폴더에 바로 쏟지 않고 **그 아래 새 폴더**(`변경분_범위_날짜`, 같은 이름이면 `_2`)를 만들어 뽑는다(사용자 결정). 예전에는 묻지 않고 덮어써 지난번에 뽑은 것과 사람이 넣어 둔 파일이 섞였다. 또 뽑기 전에 읽기 전용 · 열려 있는 파일을 **먼저 보고** 하나라도 막히면 아무것도 쓰지 않는다 — 반쯤 뽑힌 폴더가 남지 않는다. 하네스 ReportTest +12(57건) | 아래 |
| 2026-09-18 | **11차 A-2** — 변경분을 뽑으면 **폴더 옆에 안내 파일**(`…_변경분안내.txt`)을 함께 만든다(사용자 결정: 폴더 밖 · 지울 파일은 목록으로만). A-1 에서 지운 파일 · 이름 바뀐 옛 경로 · 뽑은 범위가 결과에 안 남는 것을 확인했기 때문. 안내에 범위(커밋 제목 · 날짜) · 뽑은 파일(추가 · 수정 · 이름 바뀜은 옛 이름 병기) · **장비에서 지워야 할 파일**을 적는다. 추출이 무엇을 했는지 목록으로 돌려주도록 `ExportChangedFiles` 를 고치고, 안내를 못 써도 추출은 성공으로 둔다. 하네스 ReportTest +14(45건) | 아래 |
diff --git a/doc/진행상황.md b/doc/진행상황.md
index 851d2eb..8a68cea 100644
--- a/doc/진행상황.md
+++ b/doc/진행상황.md
@@ -350,7 +350,7 @@
| 상태 | 항목 |
|---|---|
-| 대기 | C-1 있는 [커밋 수정] 을 다시 내 보고 바로잡기 — [마지막 메시지] 가 목록 맨 위(다른 브랜치일 수 있음) · 스테이징 파일이 말없이 들어감 · 원래 작성자 · 시각이 바뀜 · 작업 중에도 켜짐 · 병합 커밋 · 올린 커밋 · 옛 해시 · `#` 줄 |
+| **일부** | C-1 있는 [커밋 수정] 바로잡기 — 여덟 가지를 화면에서 재현한 뒤 **1 · 3 · 4 · 7 고침**([마지막 메시지]가 HEAD 것 · 작성자와 작성 시각 보존 · 작업 중 단추 끔 · 알림에 "옛 → 새 해시 · 파일 N개 포함"). 하네스 MergeSaveTest +15(225건). **남은 결정**: 2(스테이징 파일 함께 들어감 — 묻기/메시지만/지금대로) · 6(이미 올린 커밋 — 묻기/막기/매뉴얼). 옛 항목: — [마지막 메시지] 가 목록 맨 위(다른 브랜치일 수 있음) · 스테이징 파일이 말없이 들어감 · 원래 작성자 · 시각이 바뀜 · 작업 중에도 켜짐 · 병합 커밋 · 올린 커밋 · 옛 해시 · `#` 줄 |
| 대기 | C-2 마지막 커밋이 아닌 커밋의 메시지 수정 (기획안 "커밋 우클릭 → 이름 변경") — 할지 먼저 정함 |
### D. 시험 도구 정리
diff --git a/src/MergeHub/Services/CommitSelection.cs b/src/MergeHub/Services/CommitSelection.cs
index af0da32..c27f515 100644
--- a/src/MergeHub/Services/CommitSelection.cs
+++ b/src/MergeHub/Services/CommitSelection.cs
@@ -61,6 +61,57 @@ namespace MergeHub.Services
/// 작업이 진행 중이고 남은 충돌이 없는가.
public static bool IsResolvedOperation(bool inProgress, int conflictCount) => inProgress && conflictCount == 0;
+ ///
+ /// [마지막 메시지] 가 가져올 메시지(11차 C-1).
+ ///
+ /// 예전에는 이력 목록의 맨 위(Commits[0])를 가져왔다. [모든 브랜치] 를 켜면 맨 위가
+ /// 다른 브랜치의 더 새 커밋이라, 그 메시지를 가져와 그대로 [커밋 수정] 하면
+ /// HEAD 제목이 남의 커밋 메시지로 바뀌었다(값으로 확인). 고치는 대상은 늘 HEAD 이므로 HEAD 것을 읽는다.
+ /// HEAD 가 목록에 없으면(조회 개수 밖으로 밀렸을 때) 아무것도 주지 않는다 - 엉뚱한 것을 주느니 빈 칸이 낫다.
+ ///
+ public static string? MessageOfHead(IReadOnlyList<(string Sha, string Message)> commits, string? headSha)
+ {
+ if (commits is null || commits.Count == 0 || string.IsNullOrEmpty(headSha)) return null;
+
+ foreach (var commit in commits)
+ if (SameSha(commit.Sha, headSha)) return commit.Message;
+
+ return null;
+ }
+
+ ///
+ /// [커밋 수정] 을 켤 수 있는가(11차 C-1).
+ ///
+ /// 예전에는 저장소가 열려 있고 메시지 칸이 비어 있지 않기만 하면 켜졌다. 병합 · rebase · cherry-pick ·
+ /// revert 가 도는 중에도 켜져 있어서 누르면 cannot create a tree from a not fully merged index 로
+ /// 실패했다(값으로 확인). git 도 병합 중 --amend 를 막는다. 작업 중에는 아예 끈다.
+ ///
+ public static bool CanAmend(bool repositoryOpen, bool hasMessage, bool operationInProgress) =>
+ repositoryOpen && hasMessage && !operationInProgress;
+
+ /// 작업 중이라 [커밋 수정] 이 꺼졌을 때 단추 설명에 쓸 말.
+ public static string AmendHint(bool operationInProgress) =>
+ operationInProgress
+ ? "병합 · rebase · cherry-pick · revert 를 끝낸 뒤에 쓸 수 있습니다"
+ : "마지막 커밋을 고쳐 씁니다 (git commit --amend). 푸시하기 전에만 쓰세요";
+
+ ///
+ /// 커밋을 고친 뒤 알릴 글(11차 C-1).
+ ///
+ /// 예전에는 "커밋 수정 완료 (새해시)" 라 고치기 전 해시가 어디에도 남지 않았다. 되돌리려면
+ /// reflog 를 뒤져야 했다. 함께 들어간 파일 수도 적는다 - 스테이징해 둔 것이 조용히 들어가는 것을 보이게.
+ ///
+ public static string AmendDone(string? oldSha, string? newSha, int fileCount)
+ {
+ var from = Short(oldSha);
+ var to = Short(newSha);
+ var files = fileCount > 0 ? $" · 파일 {fileCount}개 포함" : string.Empty;
+ return $"커밋 수정 완료 (옛 {from} → 새 {to}){files}";
+ }
+
+ private static string Short(string? sha) =>
+ string.IsNullOrEmpty(sha) ? "-" : (sha!.Length > 7 ? sha.Substring(0, 7) : sha);
+
private static bool SameSha(string? a, string? b) =>
!string.IsNullOrEmpty(a) && string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
diff --git a/src/MergeHub/Services/GitService.cs b/src/MergeHub/Services/GitService.cs
index 17c9f3f..55dcbc9 100644
--- a/src/MergeHub/Services/GitService.cs
+++ b/src/MergeHub/Services/GitService.cs
@@ -483,8 +483,14 @@ namespace MergeHub.Services
if (string.IsNullOrWhiteSpace(message))
throw new ArgumentException("커밋 메시지가 비어 있습니다.", nameof(message));
- var signature = BuildSignature();
- var commit = Repo.Commit(message, signature, signature, new CommitOptions { AmendPreviousCommit = amend });
+ var now = BuildSignature();
+
+ // 고쳐 쓸 때는 원래 작성자와 작성 시각을 그대로 둔다(11차 C-1). git 도 그렇게 한다
+ // (`--reset-author` 를 줄 때만 바뀐다). 예전에는 남이 만든 커밋(cherry-pick 으로 가져온 것)의
+ // 메시지만 고쳐도 작성자가 지금 사람으로 바뀌어, 누가 쓴 코드인지가 사라졌다.
+ var author = amend && Repo.Head.Tip is { } tip ? tip.Author : now;
+
+ var commit = Repo.Commit(message, author, now, new CommitOptions { AmendPreviousCommit = amend });
Report($"커밋: {commit.Sha[..7]} {commit.MessageShort}");
return commit.Sha;
}
diff --git a/src/MergeHub/Services/GitWorkspace.cs b/src/MergeHub/Services/GitWorkspace.cs
index 25a7f84..7fa0b59 100644
--- a/src/MergeHub/Services/GitWorkspace.cs
+++ b/src/MergeHub/Services/GitWorkspace.cs
@@ -155,6 +155,9 @@ namespace MergeHub.Services
[ObservableProperty] private string _repositoryName = "-";
[ObservableProperty] private string _currentBranch = "-";
[ObservableProperty] private string _headSha = "-";
+
+ /// HEAD 의 전체 해시. 이력 목록()의 Sha 와 맞춰 볼 때 쓴다(11차 C-1).
+ public string? HeadFullSha { get; private set; }
[ObservableProperty] private string _workingTreeSummary = "저장소 없음";
[ObservableProperty] private bool _isWorkingTreeClean = true;
[ObservableProperty] private bool _isRepositoryOpen;
@@ -410,6 +413,7 @@ namespace MergeHub.Services
RepositoryName = GetRepositoryName(info.WorkingDirectory);
CurrentBranch = info.CurrentBranchName ?? "(detached)";
HeadSha = string.IsNullOrEmpty(info.HeadSha) ? "-" : info.HeadSha!.Substring(0, 7);
+ HeadFullSha = info.HeadSha;
IsRepositoryOpen = true;
IsRemoteConnected = remotes.Count > 0;
diff --git a/src/MergeHub/ViewModels/GitViewModel.cs b/src/MergeHub/ViewModels/GitViewModel.cs
index dac82f1..6b138d1 100644
--- a/src/MergeHub/ViewModels/GitViewModel.cs
+++ b/src/MergeHub/ViewModels/GitViewModel.cs
@@ -646,30 +646,67 @@ namespace MergeHub.ViewModels
// ================= 커밋 수정 (amend) =================
- private bool CanAmend() => IsOpen() && !string.IsNullOrWhiteSpace(CommitMessage);
+ ///
+ /// [커밋 수정] 을 켤 조건. 작업 중에는 끈다(11차 C-1) - 병합 · rebase · cherry-pick · revert 가
+ /// 도는 중에 누르면 "cannot create a tree from a not fully merged index" 로 실패했다.
+ ///
+ private bool CanAmend() =>
+ CommitSelection.CanAmend(IsOpen(), !string.IsNullOrWhiteSpace(CommitMessage), AmendBlockedByOperation);
+
+ /// 병합 · rebase · cherry-pick · revert 가 도는 중인가.
+ private bool AmendBlockedByOperation =>
+ _workspace.IsMergeInProgress || _workspace.IsRebaseInProgress || _workspace.IsOperationInProgress;
+
+ /// 단추 설명. 작업 중이면 왜 꺼졌는지 말한다.
+ public string AmendHint => CommitSelection.AmendHint(AmendBlockedByOperation);
///
/// 마지막 커밋을 고쳐 쓴다(`git commit --amend`).
///
/// 오타 난 메시지나 빠뜨린 파일을 새 커밋을 쌓지 않고 바로잡을 때 쓴다.
/// 이미 올린 커밋을 고치면 남의 이력과 갈라지므로, 푸시 전에만 쓰는 것이 원칙이다.
+ ///
+ /// 고치기 전 해시를 알림에 남긴다(11차 C-1) - 예전에는 새 해시만 알려 되돌릴 실마리가 없었다.
///
[RelayCommand(CanExecute = nameof(CanAmend))]
- private Task AmendAsync() => _workspace.RunAndRefreshAsync("커밋 수정", () =>
+ private Task AmendAsync()
{
- var sha = _workspace.Git.Commit(CommitMessage, amend: true);
- _workspace.OnUi(() =>
+ var oldSha = _workspace.HeadFullSha;
+
+ return _workspace.RunAndRefreshAsync("커밋 수정", () =>
{
- CommitMessage = string.Empty;
- MessageUtil.ShowSuccessToast($"커밋 수정 완료 ({sha.Substring(0, 7)})");
+ var staged = _workspace.Git.GetStatus().StagedCount;
+ var sha = _workspace.Git.Commit(CommitMessage, amend: true);
+ _workspace.OnUi(() =>
+ {
+ CommitMessage = string.Empty;
+ MessageUtil.ShowSuccessToast(CommitSelection.AmendDone(oldSha, sha, staged));
+ });
});
- });
+ }
private bool CanLoadLastMessage() => IsOpen() && _workspace.Commits.Count > 0;
- /// 고칠 메시지를 처음부터 다시 치지 않도록 마지막 커밋의 메시지를 가져온다.
+ ///
+ /// 고칠 메시지를 처음부터 다시 치지 않도록 HEAD 커밋의 메시지를 가져온다.
+ ///
+ /// 예전에는 이력 목록의 맨 위(Commits[0])를 가져왔다. [모든 브랜치] 를 켜면 맨 위가 다른 브랜치의
+ /// 더 새 커밋이라, 그대로 [커밋 수정] 하면 HEAD 제목이 남의 커밋 메시지로 바뀌었다(11차 C-1).
+ ///
[RelayCommand(CanExecute = nameof(CanLoadLastMessage))]
- private void LoadLastMessage() => CommitMessage = _workspace.Commits[0].Message.TrimEnd();
+ private void LoadLastMessage()
+ {
+ var pairs = _workspace.Commits.Select(c => (c.Sha, c.Message)).ToList();
+ var message = CommitSelection.MessageOfHead(pairs, _workspace.HeadFullSha);
+
+ if (message is null)
+ {
+ MessageUtil.ShowWarningToast("HEAD 커밋이 이력 목록에 없어 메시지를 가져오지 못했습니다.");
+ return;
+ }
+
+ CommitMessage = message.TrimEnd();
+ }
// ================= stash 목록 =================
@@ -965,6 +1002,7 @@ namespace MergeHub.ViewModels
RegisterMergeToolCommand.NotifyCanExecuteChanged();
AmendCommand.NotifyCanExecuteChanged();
+ OnPropertyChanged(nameof(AmendHint)); // 작업이 시작 · 끝나면 단추 설명도 바뀐다(11차 C-1)
LoadLastMessageCommand.NotifyCanExecuteChanged();
StashApplySelectedCommand.NotifyCanExecuteChanged();
StashDropSelectedCommand.NotifyCanExecuteChanged();
diff --git a/src/MergeHub/Views/GitView.xaml b/src/MergeHub/Views/GitView.xaml
index 01b0249..fcd17d3 100644
--- a/src/MergeHub/Views/GitView.xaml
+++ b/src/MergeHub/Views/GitView.xaml
@@ -391,8 +391,9 @@
+
+ ToolTip="{Binding AmendHint}"/>