167 lines
7.1 KiB
C#
167 lines
7.1 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace MergeHub.Services
|
|
{
|
|
/// <summary>
|
|
/// git 이 낸 충돌 가운데 <b>머지 주석만 다른 것</b>을 푼다(6차 B-5).
|
|
///
|
|
/// 머지 주석(<c>// [이름] 날짜</c>)은 소스에 남긴다 - 누가 언제 반영했는지가 이력이다.
|
|
/// 그런데 git 은 이 규칙을 모른다. 양쪽이 같은 코드 줄 위에 서로 다른 주석을 달면 그 주석 줄끼리 충돌한다.
|
|
/// 사람이 볼 것은 코드인데 주석 때문에 파일마다 물어보게 된다.
|
|
///
|
|
/// 그래서 git 이 워킹트리에 남긴 충돌 표시(<c><<<<<<<</c> · <c>=======</c> · <c>>>>>>>></c>)를 읽어
|
|
/// <b>모든 덩어리가</b> "주석을 빼면 양쪽 코드가 같다" 일 때만 푼다. 한 덩어리라도 코드가 다르면 손대지 않는다 -
|
|
/// 그 파일은 사람이 네 칸에서 코드 충돌만 보고 정한다(주석 차이는 비교 규칙이 가린다).
|
|
///
|
|
/// 풀 때는 <b>양쪽 주석을 모두 남긴다.</b> 이력은 지우지 않는다.
|
|
/// · 코드 줄 위의 주석 줄: 내 쪽 줄 다음에 상대 쪽 줄. 똑같은 줄은 한 번만.
|
|
/// · 줄 끝 주석: 내 쪽 줄 끝에 상대 주석을 이어 붙인다. 이미 들어 있으면 그대로.
|
|
/// 결과 줄끝은 <c>\n</c> 이다. 파일에 쓰는 쪽(<see cref="TextFile.Encode"/>)이 원래 형식으로 되돌린다.
|
|
/// </summary>
|
|
public static class NoteConflict
|
|
{
|
|
/// <summary>코드와 줄 끝 주석 사이 간격. <see cref="MergeNote.Append"/> 와 같다.</summary>
|
|
private const string Gap = " ";
|
|
|
|
/// <summary>
|
|
/// 충돌 표시가 든 글을 푼다. 충돌 덩어리가 없거나, 코드가 다른 덩어리가 하나라도 있으면 false.
|
|
/// </summary>
|
|
/// <param name="blocks">푼 충돌 덩어리 수.</param>
|
|
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<string>(count);
|
|
var i = 0;
|
|
|
|
while (i < count)
|
|
{
|
|
if (!IsMarker(lines[i], "<<<<<<<"))
|
|
{
|
|
output.Add(lines[i]);
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
var ours = new List<string>();
|
|
var theirs = new List<string>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 충돌 한 덩어리(내 쪽 줄들 · 상대 쪽 줄들)를 푼다. 주석을 빼도 코드가 다르면 null.
|
|
/// </summary>
|
|
public static List<string>? MergeBlock(IReadOnlyList<string> ours, IReadOnlyList<string> 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<string>(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<string>(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 }
|
|
|
|
/// <summary>충돌 표시 줄인지. 표시 뒤에는 공백과 이름이 오거나 아무것도 없다.</summary>
|
|
private static bool IsMarker(string line, string marker) =>
|
|
line.StartsWith(marker, StringComparison.Ordinal) &&
|
|
(line.Length == marker.Length || line[marker.Length] == ' ');
|
|
|
|
/// <summary>머지 주석만 있는 줄이 아닌 줄(코드 줄 · 빈 줄)의 자리.</summary>
|
|
private static List<int> CodeRows(IReadOnlyList<string> lines, string? path)
|
|
{
|
|
var rows = new List<int>(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);
|
|
|
|
/// <summary>코드가 같은 두 줄의 줄 끝 주석을 합친다.</summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|