Files
PR-Helper/static/js/graph.js
T

240 lines
7.5 KiB
JavaScript
Raw Normal View History

// Git Graph visualization using D3.js
const GitGraph = {
init(containerId, repoId) {
this.container = document.getElementById(containerId);
this.repoId = repoId;
this.commits = [];
this.refs = [];
this.edges = [];
if (!this.container) {
console.error('Graph container not found:', containerId);
return;
}
this.load();
},
async load() {
try {
const resp = await fetch(`/api/repos/${this.repoId}/graph`);
if (!resp.ok) throw new Error('Failed to load graph');
const data = await resp.json();
this.commits = data.commits || [];
this.refs = data.refs || [];
this.edges = data.edges || [];
this.render();
} catch (err) {
this.container.innerHTML = `<div class="text-red-500 p-4">Error loading graph: ${err.message}</div>`;
}
},
render() {
if (this.commits.length === 0) {
this.container.innerHTML = '<div class="text-gray-500 p-4">No commits found</div>';
return;
}
// Clear container
this.container.innerHTML = '';
const width = this.container.clientWidth || 800;
const nodeHeight = 40;
const height = Math.max(400, this.commits.length * nodeHeight + 60);
const margin = { top: 20, right: 120, bottom: 20, left: 60 };
const svg = d3.select(this.container)
.append('svg')
.attr('width', width)
.attr('height', height);
const g = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// Build node positions
const nodeMap = {};
const laneWidth = 24;
// Assign lanes to commits (simple algorithm: each branch gets its own lane)
const lanes = this.assignLanes();
this.commits.forEach((commit, i) => {
nodeMap[commit.hash] = {
x: (lanes[commit.hash] || 0) * laneWidth + 20,
y: i * nodeHeight + 20,
commit
};
});
// Draw edges
g.selectAll('.edge')
.data(this.edges.filter(e => nodeMap[e.source] && nodeMap[e.target]))
.enter()
.append('path')
.attr('class', 'edge')
.attr('d', d => {
const s = nodeMap[d.source];
const t = nodeMap[d.target];
if (!s || !t) return '';
const midY = (s.y + t.y) / 2;
return `M ${s.x} ${s.y} C ${s.x} ${midY}, ${t.x} ${midY}, ${t.x} ${t.y}`;
})
.attr('fill', 'none')
.attr('stroke', '#94a3b8')
.attr('stroke-width', 2);
// Draw commit nodes
const nodes = g.selectAll('.node')
.data(this.commits)
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', (d, i) => `translate(${nodeMap[d.hash]?.x || 20},${nodeMap[d.hash]?.y || i * nodeHeight + 20})`);
nodes.append('circle')
.attr('r', 6)
.attr('fill', '#3b82f6')
.attr('stroke', '#1e40af')
.attr('stroke-width', 2);
// Add commit message
nodes.append('text')
.attr('x', 16)
.attr('y', 4)
.text(d => d.message.substring(0, 60) + (d.message.length > 60 ? '...' : ''))
.attr('font-size', '12px')
.attr('fill', '#374151');
// Add short hash
nodes.append('text')
.attr('x', 16)
.attr('y', -8)
.text(d => d.short_hash)
.attr('font-size', '10px')
.attr('fill', '#6b7280')
.attr('font-family', 'monospace');
// Draw refs
const refGroups = g.selectAll('.ref')
.data(this.refs)
.enter()
.append('g')
.attr('class', 'ref');
refGroups.each((ref, i, elements) => {
const commitNode = nodeMap[ref.hash];
if (!commitNode) return;
const group = d3.select(elements[i]);
const isTag = ref.is_tag;
const color = isTag ? '#10b981' : (ref.is_head ? '#8b5cf6' : '#3b82f6');
const bbox = { x: commitNode.x + 30, y: commitNode.y - 10 };
group.append('rect')
.attr('x', bbox.x)
.attr('y', bbox.y)
.attr('rx', 4)
.attr('ry', 4)
.attr('fill', color)
.attr('opacity', 0.1)
.attr('stroke', color)
.attr('stroke-width', 1);
const text = group.append('text')
.attr('x', bbox.x + 6)
.attr('y', bbox.y + 14)
.text(ref.name)
.attr('font-size', '11px')
.attr('fill', color)
.attr('font-weight', '500');
// Size rect to text
const textBBox = text.node().getBBox();
group.select('rect')
.attr('width', textBBox.width + 12)
.attr('height', textBBox.height + 6);
});
// Add hover effect
nodes.on('mouseover', function(event, d) {
d3.select(this).select('circle')
.attr('r', 8)
.attr('fill', '#2563eb');
}).on('mouseout', function() {
d3.select(this).select('circle')
.attr('r', 6)
.attr('fill', '#3b82f6');
});
},
assignLanes() {
// Simple lane assignment: find branches and assign lanes
const lanes = {};
const branchHeads = {};
// Find branch head commits
this.refs.forEach(ref => {
if (!ref.is_tag) {
branchHeads[ref.hash] = ref.name;
}
});
// BFS from branch heads to assign lanes
let nextLane = 0;
const visited = new Set();
// First assign HEAD branch
const headRef = this.refs.find(r => r.is_head);
if (headRef) {
const queue = [headRef.hash];
while (queue.length > 0) {
const hash = queue.shift();
if (visited.has(hash)) continue;
visited.add(hash);
lanes[hash] = 0;
// Find children (commits that have this as parent)
const children = this.edges.filter(e => e.target === hash).map(e => e.source);
children.forEach(child => {
if (!visited.has(child)) queue.push(child);
});
}
nextLane = 1;
}
// Assign lanes to other branches
this.refs.filter(r => !r.is_head && !r.is_tag).forEach(ref => {
if (visited.has(ref.hash)) return;
const queue = [ref.hash];
while (queue.length > 0) {
const hash = queue.shift();
if (visited.has(hash)) continue;
visited.add(hash);
lanes[hash] = nextLane;
const children = this.edges.filter(e => e.target === hash).map(e => e.source);
children.forEach(child => {
if (!visited.has(child)) queue.push(child);
});
}
nextLane++;
});
// Assign any remaining commits
this.commits.forEach(commit => {
if (!lanes[commit.hash]) {
lanes[commit.hash] = 0;
}
});
return lanes;
}
};
// Export for use
window.GitGraph = GitGraph;