using System; using System.Collections.Generic; using MergeHub.Models; namespace MergeHub.Services { /// /// 커밋 목록에 레인을 배정해 브랜치 그래프를 만든다. /// /// 원리: "레인"은 아직 그려지지 않은 부모 커밋 하나를 기다리는 세로줄이다. /// 목록을 위에서 아래로 훑으면서 /// 1) 지금 커밋을 기다리던 레인들을 찾아 그 중 왼쪽 것을 이 커밋의 레인으로 삼고, /// 2) 나머지는 그 레인으로 합류시키고(사선), /// 3) 첫 부모는 같은 레인이 이어받고, 두 번째 부모부터는 새 레인으로 분기시킨다. /// 커밋은 이미 시간순(topological)으로 정렬되어 들어온다는 전제다. /// public static class CommitGraphBuilder { /// 레인 색은 이 개수로 순환한다. 실제 색상값은 화면(CommitGraphCell)이 정한다. public const int ColorCount = 6; public static IReadOnlyList Build(IReadOnlyList commits) { var nodes = new List(commits.Count); // lanes[i] = 그 레인이 기다리는 커밋 SHA (없으면 null) var lanes = new List(); var laneColors = new List(); var maxLanes = 1; var nextColor = 0; foreach (var commit in commits) { // 이 행에 "위에서 내려온" 상태를 먼저 찍어둔다. var top = new List(lanes); var myLanes = new List(); 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(); // (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; } /// 비어 있는 레인을 재사용하고, 없으면 하나 늘린다. private static int TakeFreeLane(List lanes, List 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; } } }