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 는 지금처럼 브랜치 이름만 쓴다. /// 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 로그. 상대 쪽에 없는 줄이면 지운 것으로 적는다. public static string TakeTheirsLog(string? number, string? theirs, bool hasLine, bool mergeSides) { var label = SideLabel(mine: false, theirs, mergeSides); return hasLine ? $"{number}행 {label} 반영" : $"{number}행 삭제({label} 에 없음)"; } /// /// 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); } } }