using System;
using System.Collections.Generic;
namespace MergeHub.Services
{
///
/// git 이 낸 충돌 가운데 머지 주석만 다른 것을 푼다(6차 B-5).
///
/// 머지 주석(// [이름] 날짜)은 소스에 남긴다 - 누가 언제 반영했는지가 이력이다.
/// 그런데 git 은 이 규칙을 모른다. 양쪽이 같은 코드 줄 위에 서로 다른 주석을 달면 그 주석 줄끼리 충돌한다.
/// 사람이 볼 것은 코드인데 주석 때문에 파일마다 물어보게 된다.
///
/// 그래서 git 이 워킹트리에 남긴 충돌 표시(<<<<<<< · ======= · >>>>>>>)를 읽어
/// 모든 덩어리가 "주석을 빼면 양쪽 코드가 같다" 일 때만 푼다. 한 덩어리라도 코드가 다르면 손대지 않는다 -
/// 그 파일은 사람이 네 칸에서 코드 충돌만 보고 정한다(주석 차이는 비교 규칙이 가린다).
///
/// 풀 때는 양쪽 주석을 모두 남긴다. 이력은 지우지 않는다.
/// · 코드 줄 위의 주석 줄: 내 쪽 줄 다음에 상대 쪽 줄. 똑같은 줄은 한 번만.
/// · 줄 끝 주석: 내 쪽 줄 끝에 상대 주석을 이어 붙인다. 이미 들어 있으면 그대로.
/// 결과 줄끝은 \n 이다. 파일에 쓰는 쪽()이 원래 형식으로 되돌린다.
///
public static class NoteConflict
{
/// 코드와 줄 끝 주석 사이 간격. 와 같다.
private const string Gap = " ";
///
/// 충돌 표시가 든 글을 푼다. 충돌 덩어리가 없거나, 코드가 다른 덩어리가 하나라도 있으면 false.
///
/// 푼 충돌 덩어리 수.
public static bool TryResolve(string? markedText, string? path, out string resolved, out int blocks)
{
resolved = markedText ?? string.Empty;
blocks = 0;
if (string.IsNullOrEmpty(markedText)) return false;
var text = markedText!.Replace("\r\n", "\n");
var endsWithNewLine = text.EndsWith("\n", StringComparison.Ordinal);
var lines = text.Split('\n');
var count = endsWithNewLine ? lines.Length - 1 : lines.Length;
var output = new List(count);
var i = 0;
while (i < count)
{
if (!IsMarker(lines[i], "<<<<<<<"))
{
output.Add(lines[i]);
i++;
continue;
}
var ours = new List();
var theirs = new List();
var part = Part.Ours;
var closed = false;
for (i++; i < count; i++)
{
var line = lines[i];
if (part == Part.Ours && IsMarker(line, "|||||||")) { part = Part.Base; continue; }
if (part != Part.Theirs && IsMarker(line, "=======")) { part = Part.Theirs; continue; }
if (part == Part.Theirs && IsMarker(line, ">>>>>>>")) { closed = true; i++; break; }
if (part == Part.Ours) ours.Add(line);
else if (part == Part.Theirs) theirs.Add(line);
}
if (!closed) return false;
var merged = MergeBlock(ours, theirs, path);
if (merged is null) return false;
output.AddRange(merged);
blocks++;
}
if (blocks == 0) return false;
resolved = string.Join("\n", output) + (endsWithNewLine ? "\n" : string.Empty);
return true;
}
///
/// 충돌 한 덩어리(내 쪽 줄들 · 상대 쪽 줄들)를 푼다. 주석을 빼도 코드가 다르면 null.
///
public static List? MergeBlock(IReadOnlyList ours, IReadOnlyList theirs, string? path)
{
var oursCode = CodeRows(ours, path);
var theirsCode = CodeRows(theirs, path);
if (oursCode.Count != theirsCode.Count) return null;
for (var k = 0; k < oursCode.Count; k++)
{
if (!SameCode(ours[oursCode[k]], theirs[theirsCode[k]], path)) return null;
}
var result = new List(ours.Count + theirs.Count);
int o = 0, t = 0;
for (var k = 0; k <= oursCode.Count; k++)
{
var oursEnd = k < oursCode.Count ? oursCode[k] : ours.Count;
var theirsEnd = k < theirsCode.Count ? theirsCode[k] : theirs.Count;
// 이 코드 줄 앞의 주석 줄. 내 쪽 먼저, 상대 쪽은 내 쪽에 똑같은 줄이 없을 때만.
var mine = new HashSet(StringComparer.Ordinal);
for (; o < oursEnd; o++)
{
result.Add(ours[o]);
mine.Add(ours[o].Trim());
}
for (; t < theirsEnd; t++)
{
if (!mine.Contains(theirs[t].Trim())) result.Add(theirs[t]);
}
if (k == oursCode.Count) break;
result.Add(JoinTails(ours[o], theirs[t], path));
o++;
t++;
}
return result;
}
private enum Part { Ours, Base, Theirs }
/// 충돌 표시 줄인지. 표시 뒤에는 공백과 이름이 오거나 아무것도 없다.
private static bool IsMarker(string line, string marker) =>
line.StartsWith(marker, StringComparison.Ordinal) &&
(line.Length == marker.Length || line[marker.Length] == ' ');
/// 머지 주석만 있는 줄이 아닌 줄(코드 줄 · 빈 줄)의 자리.
private static List CodeRows(IReadOnlyList lines, string? path)
{
var rows = new List(lines.Count);
for (var i = 0; i < lines.Count; i++)
{
if (!MergeNote.LooksLikeNote(lines[i], path)) rows.Add(i);
}
return rows;
}
private static bool SameCode(string a, string b, string? path) =>
string.Equals(MergeNote.WithoutTail(a, path).TrimEnd(), MergeNote.WithoutTail(b, path).TrimEnd(),
StringComparison.Ordinal);
/// 코드가 같은 두 줄의 줄 끝 주석을 합친다.
private static string JoinTails(string mine, string theirs, string? path)
{
var mineTail = MergeNote.TailOf(mine, path);
var theirsTail = MergeNote.TailOf(theirs, path);
if (theirsTail.Length == 0 || mineTail.IndexOf(theirsTail, StringComparison.Ordinal) >= 0) return mine;
if (mineTail.Length == 0) return theirs;
return mine.TrimEnd() + Gap + theirsTail;
}
}
}