Fix commit graph rendering for branching history

Track active lanes via explicit state (openLanes set) rather than
relying on PlotCommit.getPassingLanes() which is not available. Draw
'-' between passing lanes and a commit when the commit's parent is
on a lane to the left (branch convergence), giving output like:

  *     <- C (lane 0)
  | *   <- D (lane 0 passing, D on lane 1, no convergence)
  * |   <- C (lane 0, lane 1 passing)
  |-*   <- B (lane 1, converges to A on lane 0)
  *     <- A (lane 0)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-02-27 10:48:34 +01:00
parent eb222716cd
commit 04a69c323e
2 changed files with 97 additions and 21 deletions
@@ -36,8 +36,10 @@ import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
@Service
@@ -349,17 +351,25 @@ public class GitService
plotCommitList.source(plotWalk);
plotCommitList.fillTo(Integer.MAX_VALUE);
int maxLanes = 0;
for (PlotCommit<PlotLane> pc : plotCommitList)
{
if (pc.getLane() != null && pc.getLane().getPosition() + 1 > maxLanes)
maxLanes = pc.getLane().getPosition() + 1;
}
// Track which lane positions are currently "open" (started but parent not yet seen)
Set<Integer> openLanes = new LinkedHashSet<>();
List<CommitInfo> commits = new ArrayList<>();
for (PlotCommit<PlotLane> pc : plotCommitList)
{
String graphLine = buildGraphLine(pc, maxLanes);
int lane = pc.getLane() != null ? pc.getLane().getPosition() : 0;
openLanes.add(lane); // ensure this lane is marked active
String graphLine = buildGraphLine(pc, new LinkedHashSet<>(openLanes));
// Advance lane state: close this lane, open parent lanes
openLanes.remove(lane);
for (RevCommit parent : pc.getParents())
{
if (parent instanceof PlotCommit<?> pp && pp.getLane() != null)
openLanes.add(pp.getLane().getPosition());
}
List<String> parents = Arrays.stream(pc.getParents())
.map(p -> p.getId().abbreviate(7).name())
.toList();
@@ -382,27 +392,46 @@ public class GitService
}
}
private String buildGraphLine(PlotCommit<PlotLane> pc, int maxLanes)
private String buildGraphLine(PlotCommit<PlotLane> pc, Set<Integer> activeLanes)
{
int lanePos = pc.getLane() != null ? pc.getLane().getPosition() : 0;
int width = Math.max(maxLanes, lanePos + 1);
char[] line = new char[width];
Arrays.fill(line, ' ');
int commitLane = pc.getLane() != null ? pc.getLane().getPosition() : 0;
// Mark passing-through lanes
for (int i = 0; i < pc.getChildCount(); i++)
// Find the leftmost parent lane that is to the left of this commit's lane.
// Dashes are drawn from that lane to this commit to show convergence.
int mergeFromLane = commitLane;
for (RevCommit parent : pc.getParents())
{
PlotCommit child = (PlotCommit) pc.getChild(i);
if (child.getLane() != null)
if (parent instanceof PlotCommit<?> pp && pp.getLane() != null)
{
int childPos = child.getLane().getPosition();
if (childPos < width)
line[childPos] = '|';
int pLane = pp.getLane().getPosition();
if (pLane < commitLane)
mergeFromLane = Math.min(mergeFromLane, pLane);
}
}
line[lanePos] = '*';
return new String(line);
int width = activeLanes.stream().mapToInt(Integer::intValue).max().orElse(0) + 1;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < width; i++)
{
if (i == commitLane)
sb.append('*');
else if (activeLanes.contains(i))
sb.append('|');
else
sb.append(' ');
if (i < width - 1)
{
// Draw '-' between mergeFromLane and commitLane to show branch convergence
if (i >= mergeFromLane && i < commitLane)
sb.append('-');
else
sb.append(' ');
}
}
return sb.toString().stripTrailing();
}
public List<DiffInfo> getCommitDiff(String name, String commitHash) throws IOException