glad-web 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/README.zh-CN.md +182 -0
- package/assets/logo.svg +40 -0
- package/bin/cli.js +65 -0
- package/lib/ai-tools/demo/enhanced-demo.js +625 -0
- package/lib/ai-tools/demo/index.js +24 -0
- package/lib/ai-tools/demo/responses.js +88 -0
- package/lib/ai-tools/detector.js +76 -0
- package/lib/ai-tools/registry.js +300 -0
- package/lib/commands/config.js +78 -0
- package/lib/commands/tools.js +128 -0
- package/lib/commands/web.js +454 -0
- package/lib/config/constants.js +17 -0
- package/lib/config/manager.js +71 -0
- package/lib/git/service.js +79 -0
- package/lib/schedule/job-runner.js +162 -0
- package/lib/schedule/job-store.js +150 -0
- package/lib/schedule/key-sequences.js +49 -0
- package/lib/schedule/scheduler-service.js +39 -0
- package/lib/session/buffer.js +102 -0
- package/lib/session/pty-manager.js +241 -0
- package/lib/session/rendered-history.js +225 -0
- package/lib/session/session-manager.js +382 -0
- package/lib/session/text-history.js +274 -0
- package/lib/utils/logger.js +74 -0
- package/lib/utils/pid.js +67 -0
- package/lib/utils/validation.js +53 -0
- package/lib/web/gitgraph.js +273 -0
- package/lib/web/index.html +1470 -0
- package/lib/workspace/service.js +76 -0
- package/package.json +60 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
const LOG_DIR = path.join(os.homedir(), '.glad', 'logs');
|
|
7
|
+
const LOG_FILE = path.join(LOG_DIR, 'cli.log');
|
|
8
|
+
|
|
9
|
+
// Ensure log directory exists
|
|
10
|
+
function ensureLogDir() {
|
|
11
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
12
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Format timestamp
|
|
17
|
+
function timestamp() {
|
|
18
|
+
return new Date().toISOString().replace('T', ' ').substring(0, 19);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Write to log file
|
|
22
|
+
function writeToFile(level, message) {
|
|
23
|
+
try {
|
|
24
|
+
ensureLogDir();
|
|
25
|
+
const entry = `[${timestamp()}] ${level}: ${message}\n`;
|
|
26
|
+
fs.appendFileSync(LOG_FILE, entry);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
// Silently fail if can't write to log
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const logger = {
|
|
33
|
+
debug: (message) => {
|
|
34
|
+
// Only log to console and file when DEBUG mode is enabled
|
|
35
|
+
if (process.env.DEBUG === '1' || process.argv.includes('--debug')) {
|
|
36
|
+
console.log(chalk.gray(`[DEBUG] ${message}`));
|
|
37
|
+
writeToFile('DEBUG', message);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Always write to file, but only show on console in DEBUG mode
|
|
42
|
+
debugInfo: (message) => {
|
|
43
|
+
writeToFile('INFO', message);
|
|
44
|
+
if (process.env.DEBUG === '1' || process.argv.includes('--debug')) {
|
|
45
|
+
console.log(chalk.gray(`[DEBUG] ${message}`));
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
info: (message) => {
|
|
50
|
+
console.log(chalk.blue(`ℹ ${message}`));
|
|
51
|
+
writeToFile('INFO', message);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
success: (message) => {
|
|
55
|
+
console.log(chalk.green(`✓ ${message}`));
|
|
56
|
+
writeToFile('INFO', message);
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
warn: (message) => {
|
|
60
|
+
console.log(chalk.yellow(`⚠️ ${message}`));
|
|
61
|
+
writeToFile('WARN', message);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
error: (message) => {
|
|
65
|
+
console.error(chalk.red(`❌ ${message}`));
|
|
66
|
+
writeToFile('ERROR', message);
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
log: (message) => {
|
|
70
|
+
console.log(message);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
module.exports = logger;
|
package/lib/utils/pid.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Check if process with PID exists
|
|
2
|
+
function isPidAlive(pid) {
|
|
3
|
+
if (!pid || typeof pid !== 'number') {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
// Signal 0 checks if process exists without sending actual signal
|
|
9
|
+
process.kill(pid, 0);
|
|
10
|
+
return true;
|
|
11
|
+
} catch (err) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Kill process by PID
|
|
17
|
+
function killProcess(pid, signal = 'SIGTERM') {
|
|
18
|
+
if (!pid || typeof pid !== 'number') {
|
|
19
|
+
throw new Error('Invalid PID');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, signal);
|
|
24
|
+
return true;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Kill process gracefully with timeout
|
|
31
|
+
async function killProcessGracefully(pid, timeout = 5000) {
|
|
32
|
+
if (!isPidAlive(pid)) {
|
|
33
|
+
return { success: true, method: 'already_dead' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Send SIGTERM
|
|
37
|
+
killProcess(pid, 'SIGTERM');
|
|
38
|
+
|
|
39
|
+
// Wait for process to exit
|
|
40
|
+
const startTime = Date.now();
|
|
41
|
+
while (Date.now() - startTime < timeout) {
|
|
42
|
+
if (!isPidAlive(pid)) {
|
|
43
|
+
return { success: true, method: 'SIGTERM' };
|
|
44
|
+
}
|
|
45
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Force kill if still alive
|
|
49
|
+
if (isPidAlive(pid)) {
|
|
50
|
+
killProcess(pid, 'SIGKILL');
|
|
51
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
52
|
+
|
|
53
|
+
if (!isPidAlive(pid)) {
|
|
54
|
+
return { success: true, method: 'SIGKILL' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { success: false, method: 'failed' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { success: true, method: 'SIGTERM' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
isPidAlive,
|
|
65
|
+
killProcess,
|
|
66
|
+
killProcessGracefully
|
|
67
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// Validate directory exists
|
|
5
|
+
function validateDirectory(dirPath) {
|
|
6
|
+
if (!dirPath) {
|
|
7
|
+
return { valid: false, error: 'Directory path is required' };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const resolvedPath = path.resolve(dirPath);
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
13
|
+
return {
|
|
14
|
+
valid: false,
|
|
15
|
+
error: `Directory does not exist: ${resolvedPath}`
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const stats = fs.statSync(resolvedPath);
|
|
20
|
+
if (!stats.isDirectory()) {
|
|
21
|
+
return {
|
|
22
|
+
valid: false,
|
|
23
|
+
error: `Path is not a directory: ${resolvedPath}`
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { valid: true, path: resolvedPath };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Validate URL
|
|
31
|
+
function validateUrl(url) {
|
|
32
|
+
try {
|
|
33
|
+
new URL(url);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate session ID format
|
|
41
|
+
function validateSessionId(sessionId) {
|
|
42
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
// UUID format
|
|
46
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
validateDirectory,
|
|
51
|
+
validateUrl,
|
|
52
|
+
validateSessionId
|
|
53
|
+
};
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
class GitGraphRenderer {
|
|
2
|
+
constructor(container) {
|
|
3
|
+
this.container = container;
|
|
4
|
+
this.colors = ['#ff5252', '#448aff', '#69f0ae', '#ffd740', '#e040fb', '#ffab40', '#18ffff'];
|
|
5
|
+
this.rowHeight = 36;
|
|
6
|
+
this.dotRadius = 5;
|
|
7
|
+
this.lineWidth = 2.5;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
render(commits) {
|
|
11
|
+
this.container.innerHTML = '';
|
|
12
|
+
if (!commits || commits.length === 0) {
|
|
13
|
+
this.container.innerHTML = '<div style="padding: 20px; text-align: center; color: #888;">No commits found.</div>';
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let tracks = [];
|
|
18
|
+
|
|
19
|
+
for (const commit of commits) {
|
|
20
|
+
const hash = commit.hash;
|
|
21
|
+
const parents = commit.parents;
|
|
22
|
+
|
|
23
|
+
let col = tracks.indexOf(hash);
|
|
24
|
+
let isNewTrack = false;
|
|
25
|
+
if (col === -1) {
|
|
26
|
+
col = tracks.findIndex(t => t === null);
|
|
27
|
+
if (col === -1) col = tracks.length;
|
|
28
|
+
tracks[col] = hash;
|
|
29
|
+
isNewTrack = true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
commit.col = col;
|
|
33
|
+
commit.tracksBefore = [...tracks];
|
|
34
|
+
if (isNewTrack) {
|
|
35
|
+
commit.tracksBefore[col] = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (parents.length > 0) {
|
|
39
|
+
tracks[col] = parents[0];
|
|
40
|
+
for (let i = 1; i < parents.length; i++) {
|
|
41
|
+
const p = parents[i];
|
|
42
|
+
let pCol = tracks.indexOf(p);
|
|
43
|
+
if (pCol === -1) {
|
|
44
|
+
let eCol = tracks.findIndex(t => t === null);
|
|
45
|
+
if (eCol === -1) eCol = tracks.length;
|
|
46
|
+
tracks[eCol] = p;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
tracks[col] = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < tracks.length; i++) {
|
|
54
|
+
for (let j = i + 1; j < tracks.length; j++) {
|
|
55
|
+
if (tracks[i] && tracks[i] === tracks[j]) {
|
|
56
|
+
tracks[j] = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
while (tracks.length > 0 && tracks[tracks.length - 1] === null) {
|
|
62
|
+
tracks.pop();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
commit.tracksAfter = [...tracks];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const maxTracks = Math.max(...commits.map(c => Math.max(c.tracksBefore.length, c.tracksAfter.length)));
|
|
69
|
+
const canvasWidth = Math.max(50, maxTracks * 16 + 24);
|
|
70
|
+
|
|
71
|
+
commits.forEach((commit, rowIndex) => {
|
|
72
|
+
const rowDiv = document.createElement('div');
|
|
73
|
+
rowDiv.className = 'gitgraph-row';
|
|
74
|
+
rowDiv.style.display = 'flex';
|
|
75
|
+
rowDiv.style.alignItems = 'center';
|
|
76
|
+
rowDiv.style.height = this.rowHeight + 'px';
|
|
77
|
+
rowDiv.style.borderBottom = '1px solid rgba(255,255,255,0.05)';
|
|
78
|
+
rowDiv.style.position = 'relative';
|
|
79
|
+
rowDiv.style.cursor = 'pointer';
|
|
80
|
+
rowDiv.onmouseover = () => rowDiv.style.backgroundColor = 'rgba(255,255,255,0.05)';
|
|
81
|
+
rowDiv.onmouseout = () => rowDiv.style.backgroundColor = 'transparent';
|
|
82
|
+
|
|
83
|
+
const canvas = document.createElement('canvas');
|
|
84
|
+
canvas.width = canvasWidth * 2; // HiDPI
|
|
85
|
+
canvas.height = this.rowHeight * 2;
|
|
86
|
+
canvas.style.flexShrink = '0';
|
|
87
|
+
canvas.style.width = canvasWidth + 'px';
|
|
88
|
+
canvas.style.height = this.rowHeight + 'px';
|
|
89
|
+
rowDiv.appendChild(canvas);
|
|
90
|
+
|
|
91
|
+
this.drawCanvas(canvas, commit);
|
|
92
|
+
|
|
93
|
+
const contentDiv = document.createElement('div');
|
|
94
|
+
contentDiv.className = 'gitgraph-content';
|
|
95
|
+
contentDiv.style.flex = '1';
|
|
96
|
+
contentDiv.style.minWidth = '0';
|
|
97
|
+
contentDiv.style.display = 'flex';
|
|
98
|
+
contentDiv.style.alignItems = 'center';
|
|
99
|
+
contentDiv.style.gap = '8px';
|
|
100
|
+
contentDiv.style.paddingRight = '14px';
|
|
101
|
+
contentDiv.style.whiteSpace = 'nowrap';
|
|
102
|
+
contentDiv.style.overflow = 'hidden';
|
|
103
|
+
|
|
104
|
+
const msgSpan = document.createElement('span');
|
|
105
|
+
msgSpan.style.color = '#fff';
|
|
106
|
+
msgSpan.style.fontSize = '13px';
|
|
107
|
+
msgSpan.style.fontWeight = '500';
|
|
108
|
+
msgSpan.style.overflow = 'hidden';
|
|
109
|
+
msgSpan.style.textOverflow = 'ellipsis';
|
|
110
|
+
msgSpan.textContent = commit.subject;
|
|
111
|
+
contentDiv.appendChild(msgSpan);
|
|
112
|
+
|
|
113
|
+
if (commit.refs) {
|
|
114
|
+
const refsSpan = document.createElement('span');
|
|
115
|
+
refsSpan.style.color = 'var(--primary)';
|
|
116
|
+
refsSpan.style.fontSize = '11px';
|
|
117
|
+
refsSpan.style.border = '1px solid rgba(0, 122, 255, 0.4)';
|
|
118
|
+
refsSpan.style.background = 'rgba(0, 122, 255, 0.1)';
|
|
119
|
+
refsSpan.style.borderRadius = '4px';
|
|
120
|
+
refsSpan.style.padding = '1px 4px';
|
|
121
|
+
refsSpan.style.flexShrink = '0';
|
|
122
|
+
refsSpan.textContent = commit.refs.replace(/[()]/g, '').trim();
|
|
123
|
+
contentDiv.appendChild(refsSpan);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const authorSpan = document.createElement('span');
|
|
127
|
+
authorSpan.style.color = 'var(--text-dim)';
|
|
128
|
+
authorSpan.style.fontSize = '12px';
|
|
129
|
+
authorSpan.style.marginLeft = 'auto';
|
|
130
|
+
authorSpan.style.flexShrink = '0';
|
|
131
|
+
authorSpan.textContent = `${commit.author} • ${commit.time}`;
|
|
132
|
+
contentDiv.appendChild(authorSpan);
|
|
133
|
+
|
|
134
|
+
const hashSpan = document.createElement('span');
|
|
135
|
+
hashSpan.style.color = 'var(--text-dim)';
|
|
136
|
+
hashSpan.style.fontSize = '12px';
|
|
137
|
+
hashSpan.style.fontFamily = 'monospace';
|
|
138
|
+
hashSpan.style.flexShrink = '0';
|
|
139
|
+
hashSpan.style.width = '60px';
|
|
140
|
+
hashSpan.style.textAlign = 'right';
|
|
141
|
+
hashSpan.textContent = commit.hash;
|
|
142
|
+
contentDiv.appendChild(hashSpan);
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
rowDiv.onclick = (e) => {
|
|
146
|
+
if (e.target.closest('.gitgraph-details-view')) return;
|
|
147
|
+
|
|
148
|
+
const existingDetails = rowDiv.nextElementSibling;
|
|
149
|
+
if (existingDetails && existingDetails.classList.contains('gitgraph-details-view')) {
|
|
150
|
+
existingDetails.remove();
|
|
151
|
+
rowDiv.style.backgroundColor = 'transparent';
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Close others
|
|
156
|
+
document.querySelectorAll('.gitgraph-details-view').forEach(el => {
|
|
157
|
+
if (el.previousElementSibling) {
|
|
158
|
+
el.previousElementSibling.style.backgroundColor = 'transparent';
|
|
159
|
+
}
|
|
160
|
+
el.remove();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
rowDiv.style.backgroundColor = 'rgba(255,255,255,0.05)';
|
|
164
|
+
|
|
165
|
+
const detailsDiv = document.createElement('div');
|
|
166
|
+
detailsDiv.className = 'gitgraph-details-view';
|
|
167
|
+
detailsDiv.style.backgroundColor = '#121212';
|
|
168
|
+
detailsDiv.style.borderLeft = '2px solid ' + this.getColor(commit.col);
|
|
169
|
+
detailsDiv.style.padding = '16px';
|
|
170
|
+
detailsDiv.style.margin = '4px 0 4px ' + (canvasWidth) + 'px';
|
|
171
|
+
detailsDiv.style.borderRadius = '0 6px 6px 0';
|
|
172
|
+
detailsDiv.style.fontSize = '13px';
|
|
173
|
+
detailsDiv.style.color = 'var(--text)';
|
|
174
|
+
detailsDiv.style.boxShadow = 'inset 0 0 10px rgba(0,0,0,0.5)';
|
|
175
|
+
|
|
176
|
+
let detailsHTML = `
|
|
177
|
+
<div style="display:flex; justify-content:space-between; margin-bottom:12px;">
|
|
178
|
+
<div>
|
|
179
|
+
<div style="font-weight:600; font-size:14px; margin-bottom:4px;">${commit.subject.replace(/</g, "<").replace(/>/g, ">")}</div>
|
|
180
|
+
<div style="color:var(--text-dim);">${commit.author} commited ${commit.time}</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div style="text-align:right;">
|
|
183
|
+
<div style="font-family:monospace; color:var(--text-dim);">Commit: ${commit.hash}</div>
|
|
184
|
+
${commit.parents.length > 0 ? `<div style="font-family:monospace; color:var(--text-dim);">Parents: ${commit.parents.join(', ')}</div>` : ''}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
`;
|
|
188
|
+
|
|
189
|
+
// Add diff placeholder
|
|
190
|
+
detailsHTML += `
|
|
191
|
+
<div style="border-top:1px solid #333; padding-top:12px; margin-top:12px;">
|
|
192
|
+
<button onclick="window.loadCommitDiff('${commit.hash}', this)" style="background:var(--primary); border:none; color:#fff; padding:6px 12px; border-radius:4px; font-size:12px; cursor:pointer;">Load Diff</button>
|
|
193
|
+
<div class="diff-container" style="margin-top:12px; font-family:monospace; font-size:12px; white-space:pre-wrap; overflow-x:auto;"></div>
|
|
194
|
+
</div>
|
|
195
|
+
`;
|
|
196
|
+
|
|
197
|
+
detailsDiv.innerHTML = detailsHTML;
|
|
198
|
+
rowDiv.parentNode.insertBefore(detailsDiv, rowDiv.nextSibling);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
rowDiv.appendChild(contentDiv);
|
|
202
|
+
|
|
203
|
+
this.container.appendChild(rowDiv);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
drawCanvas(canvas, commit) {
|
|
208
|
+
const ctx = canvas.getContext('2d');
|
|
209
|
+
ctx.scale(2, 2); // HiDPI
|
|
210
|
+
|
|
211
|
+
const h = this.rowHeight;
|
|
212
|
+
const w = canvas.width / 2;
|
|
213
|
+
const midY = h / 2;
|
|
214
|
+
const colSpacing = 16;
|
|
215
|
+
const startX = 20;
|
|
216
|
+
|
|
217
|
+
// Pass-through lines
|
|
218
|
+
commit.tracksBefore.forEach((target, i) => {
|
|
219
|
+
if (!target) return;
|
|
220
|
+
const x1 = startX + i * colSpacing;
|
|
221
|
+
|
|
222
|
+
if (target === commit.hash) {
|
|
223
|
+
const x2 = startX + commit.col * colSpacing;
|
|
224
|
+
this.drawLine(ctx, x1, 0, x2, midY, this.getColor(i));
|
|
225
|
+
} else {
|
|
226
|
+
const nextIdx = commit.tracksAfter.indexOf(target);
|
|
227
|
+
if (nextIdx !== -1) {
|
|
228
|
+
const x2 = startX + nextIdx * colSpacing;
|
|
229
|
+
this.drawLine(ctx, x1, 0, x2, h, this.getColor(i));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Lines for parents
|
|
235
|
+
commit.parents.forEach((parent, parentIdx) => {
|
|
236
|
+
const nextIdx = commit.tracksAfter.indexOf(parent);
|
|
237
|
+
if (nextIdx !== -1) {
|
|
238
|
+
const x1 = startX + commit.col * colSpacing;
|
|
239
|
+
const x2 = startX + nextIdx * colSpacing;
|
|
240
|
+
const color = parentIdx === 0 ? this.getColor(commit.col) : this.getColor(nextIdx);
|
|
241
|
+
this.drawLine(ctx, x1, midY, x2, h, color);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Commit dot
|
|
246
|
+
const cx = startX + commit.col * colSpacing;
|
|
247
|
+
ctx.beginPath();
|
|
248
|
+
ctx.arc(cx, midY, this.dotRadius, 0, 2 * Math.PI);
|
|
249
|
+
ctx.fillStyle = this.getColor(commit.col);
|
|
250
|
+
ctx.fill();
|
|
251
|
+
ctx.lineWidth = 2;
|
|
252
|
+
ctx.strokeStyle = '#121212'; // dark background match
|
|
253
|
+
ctx.stroke();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
drawLine(ctx, x1, y1, x2, y2, color) {
|
|
257
|
+
ctx.beginPath();
|
|
258
|
+
ctx.moveTo(x1, y1);
|
|
259
|
+
if (x1 === x2) {
|
|
260
|
+
ctx.lineTo(x2, y2);
|
|
261
|
+
} else {
|
|
262
|
+
ctx.bezierCurveTo(x1, (y1 + y2) / 2, x2, (y1 + y2) / 2, x2, y2);
|
|
263
|
+
}
|
|
264
|
+
ctx.strokeStyle = color;
|
|
265
|
+
ctx.lineWidth = this.lineWidth;
|
|
266
|
+
ctx.stroke();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
getColor(index) {
|
|
270
|
+
return this.colors[index % this.colors.length];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
window.GitGraphRenderer = GitGraphRenderer;
|