UbisamBase 플랫폼 위에서 도는 플러그인. 지금까지 만든 것을 그대로 담는다. - 폴더 비교 / 파일 비교(부분 수용·워드랩·찾기) / 3-Way 병합 / Git 조작 - 비교 엔진: histogram 정렬 · 이동 블록 감지 · 무시 규칙 · 13개 언어 토크나이저 - doc/ 에 작업 지시서와 진행 기록 빌드 산출물(bin/obj)은 추적하지 않는다 - 플랫폼 런타임이 빌드 때마다 D:\UbisamPlatform\bin 에서 복사되어 들어오므로 저장소에 담을 이유가 없다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
167 lines
5.9 KiB
C#
167 lines
5.9 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Text;
|
||
using System.Text.RegularExpressions;
|
||
|
||
namespace MergeHub.Services
|
||
{
|
||
/// <summary>공백을 얼마나 따질지.</summary>
|
||
public enum WhitespaceMode
|
||
{
|
||
/// <summary>공백 하나까지 그대로 비교한다.</summary>
|
||
Exact,
|
||
/// <summary>공백의 개수만 무시한다. 들여쓰기 폭을 바꿔도 같은 줄로 본다.</summary>
|
||
IgnoreAmount,
|
||
/// <summary>공백을 아예 없는 셈 친다. 탭/스페이스 정리를 무시할 때.</summary>
|
||
IgnoreAll
|
||
}
|
||
|
||
/// <summary>
|
||
/// "무엇을 차이로 볼 것인가"를 정하는 규칙.
|
||
///
|
||
/// 실무에서 diff 를 어지럽히는 것은 대개 내용이 아니라 형식이다 - 들여쓰기 정리,
|
||
/// 대소문자, 매번 바뀌는 타임스탬프 줄. 이런 것을 걸러야 진짜 변경이 눈에 들어온다.
|
||
///
|
||
/// 규칙은 두 갈래로 나뉘어 쓰인다.
|
||
/// · <see cref="KeyOf"/> — 정렬할 때 "같은 줄인가"를 판단하는 비교용 문자열.
|
||
/// 화면에 보이는 원문은 건드리지 않는다.
|
||
/// · <see cref="IsFiltered"/> — 줄 자체를 차이 계산에서 빼는 정규식 필터.
|
||
/// 정렬을 마친 뒤에 적용한다. 필터에 걸린 줄까지 같은 것으로 묶어 정렬하면
|
||
/// 엉뚱한 줄끼리 짝지어지기 때문이다.
|
||
/// </summary>
|
||
public sealed class CompareRules
|
||
{
|
||
public WhitespaceMode Whitespace { get; set; } = WhitespaceMode.Exact;
|
||
|
||
public bool IgnoreCase { get; set; }
|
||
|
||
/// <summary>이 정규식에 걸리는 줄은 차이로 세지 않는다. 잘못된 식은 조용히 버린다.</summary>
|
||
public IReadOnlyList<string> LinePatterns { get; set; } = Array.Empty<string>();
|
||
|
||
/// <summary>줄 필터를 켤지. 식은 그대로 두고 잠시 끄고 싶을 때가 있다.</summary>
|
||
public bool LineFilterEnabled { get; set; } = true;
|
||
|
||
private Regex[]? _compiled;
|
||
private string? _compiledFrom;
|
||
|
||
/// <summary>어떤 규칙도 켜져 있지 않으면 비교용 변환을 건너뛸 수 있다.</summary>
|
||
public bool IsPlain =>
|
||
Whitespace == WhitespaceMode.Exact && !IgnoreCase;
|
||
|
||
/// <summary>
|
||
/// 정렬에서 "같은 줄인가"를 따질 때 쓰는 문자열.
|
||
/// 원문은 그대로 두고 이 값만 비교하므로 화면 표시는 달라지지 않는다.
|
||
/// </summary>
|
||
public string KeyOf(string line)
|
||
{
|
||
if (line.Length == 0) return line;
|
||
|
||
var text = Whitespace switch
|
||
{
|
||
WhitespaceMode.IgnoreAll => StripSpaces(line),
|
||
WhitespaceMode.IgnoreAmount => CollapseSpaces(line),
|
||
_ => line
|
||
};
|
||
|
||
return IgnoreCase ? text.ToUpperInvariant() : text;
|
||
}
|
||
|
||
/// <summary>줄 필터에 걸리는 줄인지. 걸리면 차이로 세지 않는다.</summary>
|
||
public bool IsFiltered(string? line)
|
||
{
|
||
if (!LineFilterEnabled || line is null) return false;
|
||
|
||
foreach (var rule in Compiled())
|
||
{
|
||
if (rule.IsMatch(line)) return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>상태바에 한 줄로 요약한다. 켜진 게 없으면 null.</summary>
|
||
public string? Describe()
|
||
{
|
||
var parts = new List<string>(3);
|
||
|
||
if (Whitespace == WhitespaceMode.IgnoreAmount) parts.Add("공백 개수");
|
||
else if (Whitespace == WhitespaceMode.IgnoreAll) parts.Add("공백 전부");
|
||
|
||
if (IgnoreCase) parts.Add("대소문자");
|
||
|
||
var rules = Compiled().Length;
|
||
if (rules > 0) parts.Add($"줄필터 {rules}");
|
||
|
||
return parts.Count == 0 ? null : "무시: " + string.Join(" · ", parts);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 정규식은 쓸 때마다 다시 만들면 느리므로 캐시한다.
|
||
/// 사용자가 식을 고치면 목록 문자열이 달라지므로 그때만 다시 만든다.
|
||
/// </summary>
|
||
private Regex[] Compiled()
|
||
{
|
||
if (!LineFilterEnabled) return Array.Empty<Regex>();
|
||
|
||
var signature = string.Join("", LinePatterns);
|
||
if (_compiled is not null && _compiledFrom == signature) return _compiled;
|
||
|
||
var list = new List<Regex>(LinePatterns.Count);
|
||
|
||
foreach (var pattern in LinePatterns)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(pattern)) continue;
|
||
|
||
try
|
||
{
|
||
list.Add(new Regex(pattern, RegexOptions.CultureInvariant));
|
||
}
|
||
catch (ArgumentException)
|
||
{
|
||
// 사용자가 식을 고치는 중일 수 있다. 잘못된 식 하나 때문에 비교가 멈추면 안 된다.
|
||
}
|
||
}
|
||
|
||
_compiledFrom = signature;
|
||
_compiled = list.ToArray();
|
||
return _compiled;
|
||
}
|
||
|
||
/// <summary>연속된 공백을 하나로 줄이고 앞뒤를 다듬는다.</summary>
|
||
private static string CollapseSpaces(string line)
|
||
{
|
||
var sb = new StringBuilder(line.Length);
|
||
var pendingSpace = false;
|
||
|
||
foreach (var c in line)
|
||
{
|
||
if (char.IsWhiteSpace(c))
|
||
{
|
||
if (sb.Length > 0) pendingSpace = true;
|
||
continue;
|
||
}
|
||
|
||
if (pendingSpace)
|
||
{
|
||
sb.Append(' ');
|
||
pendingSpace = false;
|
||
}
|
||
|
||
sb.Append(c);
|
||
}
|
||
|
||
return sb.ToString();
|
||
}
|
||
|
||
private static string StripSpaces(string line)
|
||
{
|
||
var sb = new StringBuilder(line.Length);
|
||
|
||
foreach (var c in line)
|
||
if (!char.IsWhiteSpace(c)) sb.Append(c);
|
||
|
||
return sb.ToString();
|
||
}
|
||
}
|
||
}
|