Ubisam-MergeTool/src/MergeHub/Services/CommitGraphBuilder.cs
ksj09406 7719094edf 최초 커밋: MergeHub (사내 Git Merge/Diff 도구)
UbisamBase 플랫폼 위에서 도는 플러그인. 지금까지 만든 것을 그대로 담는다.
- 폴더 비교 / 파일 비교(부분 수용·워드랩·찾기) / 3-Way 병합 / Git 조작
- 비교 엔진: histogram 정렬 · 이동 블록 감지 · 무시 규칙 · 13개 언어 토크나이저
- doc/ 에 작업 지시서와 진행 기록

빌드 산출물(bin/obj)은 추적하지 않는다 - 플랫폼 런타임이 빌드 때마다
D:\UbisamPlatform\bin 에서 복사되어 들어오므로 저장소에 담을 이유가 없다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 10:13:27 +09:00

134 lines
5.3 KiB
C#

using System;
using System.Collections.Generic;
using MergeHub.Models;
namespace MergeHub.Services
{
/// <summary>
/// 커밋 목록에 레인을 배정해 브랜치 그래프를 만든다.
///
/// 원리: "레인"은 아직 그려지지 않은 부모 커밋 하나를 기다리는 세로줄이다.
/// 목록을 위에서 아래로 훑으면서
/// 1) 지금 커밋을 기다리던 레인들을 찾아 그 중 왼쪽 것을 이 커밋의 레인으로 삼고,
/// 2) 나머지는 그 레인으로 합류시키고(사선),
/// 3) 첫 부모는 같은 레인이 이어받고, 두 번째 부모부터는 새 레인으로 분기시킨다.
/// 커밋은 이미 시간순(topological)으로 정렬되어 들어온다는 전제다.
/// </summary>
public static class CommitGraphBuilder
{
/// <summary>레인 색은 이 개수로 순환한다. 실제 색상값은 화면(CommitGraphCell)이 정한다.</summary>
public const int ColorCount = 6;
public static IReadOnlyList<CommitNode> Build(IReadOnlyList<CommitInfo> commits)
{
var nodes = new List<CommitNode>(commits.Count);
// lanes[i] = 그 레인이 기다리는 커밋 SHA (없으면 null)
var lanes = new List<string?>();
var laneColors = new List<int>();
var maxLanes = 1;
var nextColor = 0;
foreach (var commit in commits)
{
// 이 행에 "위에서 내려온" 상태를 먼저 찍어둔다.
var top = new List<string?>(lanes);
var myLanes = new List<int>();
for (var i = 0; i < lanes.Count; i++)
if (lanes[i] == commit.Sha) myLanes.Add(i);
int lane;
if (myLanes.Count == 0)
{
// 아무도 기다리지 않던 커밋 = 새 가지의 시작(브랜치 팁)
lane = TakeFreeLane(lanes, laneColors, ref nextColor);
lanes[lane] = commit.Sha;
myLanes.Add(lane);
}
else
{
lane = myLanes[0];
}
var colorIndex = laneColors[lane];
// 이 커밋을 기다리던 레인은 전부 비운다.
foreach (var i in myLanes) lanes[i] = null;
// 첫 부모는 같은 레인이 이어받고, 나머지는 새 레인으로 분기한다.
for (var p = 0; p < commit.ParentShas.Count; p++)
{
var parent = commit.ParentShas[p];
if (p == 0)
{
lanes[lane] = parent;
continue;
}
// 이미 그 부모를 기다리는 레인이 있으면 그리로 합친다.
var existing = lanes.IndexOf(parent);
if (existing >= 0) continue;
var branchLane = TakeFreeLane(lanes, laneColors, ref nextColor);
lanes[branchLane] = parent;
}
var edges = new List<GraphEdge>();
// (1) 위에서 내려온 선들
for (var i = 0; i < top.Count; i++)
{
if (top[i] is null) continue;
if (myLanes.Contains(i))
edges.Add(new GraphEdge(i, lane, laneColors[i])); // 이 커밋으로 합류
else if (i < lanes.Count && lanes[i] is not null)
edges.Add(new GraphEdge(i, i, laneColors[i])); // 그냥 통과
}
// (2) 이 커밋에서 새로 시작하는 선들 (첫 부모의 수직 연장 + 분기)
for (var i = 0; i < lanes.Count; i++)
{
if (lanes[i] is null) continue;
// 위에서 이미 내려온 레인은 (1)에서 그렸다.
if (i < top.Count && top[i] is not null && !myLanes.Contains(i)) continue;
// 이 커밋의 레인이 위에서도 이어져 왔다면 (1)의 i→lane 선과 겹친다.
if (i == lane && lane < top.Count && top[lane] is not null) continue;
edges.Add(new GraphEdge(lane, i, laneColors[i]));
}
if (lanes.Count > maxLanes) maxLanes = lanes.Count;
if (top.Count > maxLanes) maxLanes = top.Count;
nodes.Add(new CommitNode(commit, new GraphRow(lane, colorIndex, edges, commit.IsMerge)));
}
// 열 너비를 전체에서 가장 넓은 행에 맞춘다.
foreach (var node in nodes) node.Graph.LaneCount = maxLanes;
return nodes;
}
/// <summary>비어 있는 레인을 재사용하고, 없으면 하나 늘린다.</summary>
private static int TakeFreeLane(List<string?> lanes, List<int> laneColors, ref int nextColor)
{
for (var i = 0; i < lanes.Count; i++)
{
if (lanes[i] is not null) continue;
laneColors[i] = nextColor++ % ColorCount;
return i;
}
lanes.Add(null);
laneColors.Add(nextColor++ % ColorCount);
return lanes.Count - 1;
}
}
}