using System; using System.Linq; using System.Text.RegularExpressions; namespace MergeHub.Services { /// /// 병합 중에 "상대가 누구인지" 를 사람이 읽는 이름으로 만든다. /// /// 3분할의 REMOTE 칸 제목에 쓴다(6차 A-3). 예전에는 늘 "REMOTE · THEIRS" 라고만 적어서 /// 어느 브랜치를 끌어오는 중인지 화면에서 알 수 없었다. /// git 이 남기는 병합 메시지만 보고 판단하므로 저장소 없이도 검사할 수 있게 따로 뺐다. /// public static class MergeNames { private static readonly Regex FirstLine = new Regex( @"^Merge\s+(?:remote-tracking\s+)?(?:branch|tag)\s+'([^']+)'", RegexOptions.Compiled | RegexOptions.CultureInvariant); /// /// MERGE_MSG 의 첫 줄에서 브랜치(또는 태그) 이름을 꺼낸다. 못 꺼내면 null. /// /// Merge commit '1742d8f...' 처럼 커밋으로 병합한 경우도 null 이다 - /// 해시는 이름이 아니다. 그때는 부르는 쪽이 짧은 해시로 대신한다. /// public static string? TheirsFromMergeMessage(string? message) { if (string.IsNullOrWhiteSpace(message)) return null; var first = message!.Replace("\r\n", "\n").Split('\n')[0].Trim(); var match = FirstLine.Match(first); if (!match.Success) return null; var name = match.Groups[1].Value.Trim(); return name.Length == 0 ? null : name; } /// /// 병합 커밋의 기본 메시지. git 명령으로 브랜치를 병합할 때와 같은 모양이다(6차 B-3). /// 상대 이름을 모르면 Merge into 브랜치 로 줄인다. /// /// 파일 이름을 넣지 않는 까닭 - 병합 커밋은 파일 하나가 아니라 병합 전체를 담는다. 예전에는 파일 비교 탭이 /// "Merge: 첫 충돌 파일 충돌 해결" 을 채우고, 다른 파일로 옮겨도 그 이름이 굳어 남았다. /// public static string BranchMergeMessage(string? theirs, string? head) { var into = string.IsNullOrWhiteSpace(head) ? string.Empty : $" into {head!.Trim()}"; return string.IsNullOrWhiteSpace(theirs) ? $"Merge{(into.Length == 0 ? " commit" : into)}" : $"Merge branch '{theirs!.Trim()}'{into}"; } /// /// pull 로 병합했을 때의 기본 메시지. git 명령으로 추적 브랜치를 병합할 때와 같은 모양이다(7차 C-1). /// Merge remote-tracking branch 'origin/feature/mine' into feature/mine /// /// 예전에는 pull 뒤 커밋 메시지 칸이 비어 있었고, 병합 커밋이 들어오는 커밋을 가리키는 로컬 브랜치 이름을 대신 썼다 - /// 받아 온 원격 브랜치가 무엇인지 이력에 남지 않았다. 추적 브랜치 이름을 모르면 와 같게 줄인다. /// public static string PullMergeMessage(string? trackedBranch, string? head) { if (string.IsNullOrWhiteSpace(trackedBranch)) return BranchMergeMessage(null, head); var into = string.IsNullOrWhiteSpace(head) ? string.Empty : $" into {head!.Trim()}"; return $"Merge remote-tracking branch '{trackedBranch!.Trim()}'{into}"; } /// /// git 이 남긴 병합 메시지(MERGE_MSG)를 커밋에 쓸 모양으로 다듬는다(6차 B-3). /// /// /// # 로 시작하는 줄은 뺀다. git 명령은 커밋할 때 이 주석 줄(#Conflicts: 와 파일 목록)을 /// 지우는데, MergeHub 는 파일을 통째로 써서 커밋 이력에 그대로 남았다. /// 첫 줄이 Merge commit '해시' 이고 상대 이름을 알면 Merge branch '이름' into 브랜치 로 바꾼다. /// MergeHub 는 브랜치를 커밋으로 풀어 병합해서 git 이 해시로 적는다 - 이력에서 무엇을 합쳤는지 안 보였다. /// 남는 것이 없으면 를 쓴다. /// /// /// /// 네 칸에서 줄을 고를 때 로그에 적을 쪽 이름(7차 A-2). /// /// 병합 충돌(또는 git mergetool 세 벌)이면 Theirs(feature/theirs) · Mine(feature/mine) 처럼 어느 쪽인지와 이름을 함께. /// 예전에는 3-Way 콤보 값을 그대로 써서, 콤보를 안 건드리고 네 칸을 띄우는 충돌 파일에서 "(3-Way 안 씀) 반영" 이라고 적혔다. /// 콤보로 고른 브랜치 3-Way 는 브랜치 이름만 돌려준다 - 로그 · 주석은 그 앞에 REMOTE 를 붙여 쓴다(, 11차 F-1). /// public static string SideLabel(bool mine, string? name, bool mergeSides) { var trimmed = string.IsNullOrWhiteSpace(name) ? null : name!.Trim(); if (!mergeSides) return trimmed ?? (mine ? "내 원본" : "REMOTE"); var side = mine ? "Mine" : "Theirs"; return trimmed is null ? side : $"{side}({trimmed})"; } /// /// Take Theirs 로그. 상대 쪽에 없는 줄이면 지운 것으로 적는다. /// 쪽 이름은 머지 주석의 출처와 같은 말이다(, 11차 F-1). /// public static string TakeTheirsLog(string? number, string? theirs, bool hasLine, bool 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 에서는 지금 문구("내 원본으로 되돌림")를 그대로 쓴다. /// public static string TakeMineLog(string? number, string? head, bool hasLine, bool alreadySame, bool mergeSides) { if (!mergeSides) return hasLine ? $"{number}행 내 원본으로 되돌림" : $"{number}행 삭제(내 원본에 없음)"; var label = SideLabel(mine: true, head, mergeSides: true); if (alreadySame) return $"{number}행 {label} 으로 정함"; return hasLine ? $"{number}행 {label} 으로 되돌림" : $"{number}행 삭제({label} 에 없음)"; } public static string CleanMergeMessage(string? raw, string? theirs, string? head) { var lines = (raw ?? string.Empty).Replace("\r\n", "\n").Split('\n') .Where(l => !l.TrimStart().StartsWith("#", StringComparison.Ordinal)) .ToList(); // 앞뒤 빈 줄 정리 while (lines.Count > 0 && lines[0].Trim().Length == 0) lines.RemoveAt(0); while (lines.Count > 0 && lines[lines.Count - 1].Trim().Length == 0) lines.RemoveAt(lines.Count - 1); if (lines.Count == 0) return BranchMergeMessage(theirs, head); var byCommit = Regex.IsMatch(lines[0].Trim(), @"^Merge\s+commit\s+'[0-9a-fA-F]{7,40}'"); if (byCommit && !string.IsNullOrWhiteSpace(theirs) && !Regex.IsMatch(theirs!.Trim(), @"^[0-9a-fA-F]{7,40}$")) lines[0] = BranchMergeMessage(theirs, head); return string.Join("\n", lines); } } }