pan-stack-cli 0.2.1 → 0.2.2
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/README.md +37 -731
- package/bin/pan.js +4955 -269
- package/package.json +17 -5
- package/lib/ai-skills.js +0 -829
- package/lib/create.js +0 -105
- package/lib/discover.js +0 -294
- package/lib/git/git-checkout.js +0 -395
- package/lib/git/git-clone.js +0 -206
- package/lib/git/git-fetch-all.js +0 -129
- package/lib/git/git-grv.js +0 -125
- package/lib/git/git-gst.js +0 -165
- package/lib/git/git-mr.js +0 -546
- package/lib/git/git-pull.js +0 -137
- package/lib/git/git-push.js +0 -185
- package/lib/git/git-sync-main.js +0 -334
- package/lib/git/helpers/bulk-report.js +0 -202
- package/lib/git/helpers/cli.js +0 -188
- package/lib/git/helpers/colors.js +0 -19
- package/lib/git/helpers/git.js +0 -136
- package/lib/git/helpers/manifest.js +0 -223
- package/lib/git/helpers/repo-select.js +0 -416
- package/lib/git/helpers/repo-summary.js +0 -161
- package/lib/i18n.js +0 -61
- package/lib/inquirer-theme.js +0 -20
- package/lib/language.js +0 -121
- package/lib/languages.js +0 -14
- package/lib/manifest.js +0 -467
- package/lib/menu-prompt.js +0 -201
- package/lib/menu.js +0 -134
- package/lib/messages.js +0 -647
- package/lib/platform.js +0 -140
- package/lib/run-command.js +0 -334
- package/lib/select.js +0 -0
- package/lib/serve.js +0 -75
- package/lib/skill-registry-client.js +0 -173
- package/lib/stats/aggregate.js +0 -51
- package/lib/stats/identity.js +0 -20
- package/lib/stats/index.js +0 -189
- package/lib/stats/parse.js +0 -46
- package/lib/stats/range.js +0 -38
- package/lib/stats/report.js +0 -264
- package/lib/stats/review.js +0 -157
- package/lib/stats/scan.js +0 -92
- package/lib/stats/serve.js +0 -25
- package/lib/ui/api.js +0 -202
- package/lib/ui/clone.js +0 -115
- package/lib/ui/config.js +0 -182
- package/lib/ui/diff.js +0 -128
- package/lib/ui/export-gate.js +0 -79
- package/lib/ui/gen-react-mobile-code.js +0 -6
- package/lib/ui/gen-react-web-code.js +0 -75
- package/lib/ui/index.js +0 -89
- package/lib/ui/init.js +0 -252
- package/lib/ui/login.js +0 -128
- package/lib/ui/logout.js +0 -22
- package/lib/ui/palettes.js +0 -103
- package/lib/ui/published-html.js +0 -24
- package/lib/ui/sync.js +0 -137
- package/lib/ui/workspace.js +0 -213
package/lib/stats/report.js
DELETED
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Dựng file HTML self-contained (Tailwind CDN, bảng màu FPT).
|
|
5
|
-
* Nhúng commit thô → lọc thời gian + chọn thành viên chạy hoàn toàn phía client.
|
|
6
|
-
* @param {Array} commits [{e,n,t,r,a,d}] (email, name, ts giây, repo, add, del)
|
|
7
|
-
* @param {object} meta { reposTotal, reposAvailable, unavailable[], initial* }
|
|
8
|
-
*/
|
|
9
|
-
function buildReport(commits, meta = {}) {
|
|
10
|
-
const payload = JSON.stringify({ commits, meta });
|
|
11
|
-
return `<!doctype html>
|
|
12
|
-
<html lang="vi">
|
|
13
|
-
<head>
|
|
14
|
-
<meta charset="utf-8" />
|
|
15
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
16
|
-
<title>pan stats — Thống kê đóng góp</title>
|
|
17
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
18
|
-
<script>
|
|
19
|
-
tailwind.config = { theme: { extend: { colors: {
|
|
20
|
-
fptOrange: '#F37021', fptGreen: '#4EB748', fptBlue: '#0066B3', fptRed: '#E5484D'
|
|
21
|
-
} } } };
|
|
22
|
-
</script>
|
|
23
|
-
</head>
|
|
24
|
-
<body class="bg-slate-50 text-slate-800">
|
|
25
|
-
<header class="bg-gradient-to-r from-fptOrange to-fptBlue text-white">
|
|
26
|
-
<div class="max-w-7xl mx-auto px-6 py-6">
|
|
27
|
-
<h1 class="text-2xl font-bold">Thống kê đóng góp Git — pan stats</h1>
|
|
28
|
-
<p id="meta" class="text-white/90 text-sm mt-1"></p>
|
|
29
|
-
|
|
30
|
-
<div class="mt-4 flex flex-wrap gap-2 items-center">
|
|
31
|
-
<span class="text-white/80 text-sm mr-1">Khoảng:</span>
|
|
32
|
-
<div id="presets" class="flex flex-wrap gap-1"></div>
|
|
33
|
-
<span class="text-white/70 mx-1">·</span>
|
|
34
|
-
<input id="from" type="date" class="px-2 py-1.5 rounded-lg text-slate-800 text-sm outline-none" />
|
|
35
|
-
<span class="text-white/70">→</span>
|
|
36
|
-
<input id="to" type="date" class="px-2 py-1.5 rounded-lg text-slate-800 text-sm outline-none" />
|
|
37
|
-
<button id="applyDates" class="px-3 py-1.5 rounded-lg bg-white/90 text-fptBlue font-semibold text-sm hover:bg-white">Áp dụng</button>
|
|
38
|
-
</div>
|
|
39
|
-
|
|
40
|
-
<div class="mt-3 flex flex-wrap gap-2 items-center">
|
|
41
|
-
<div class="relative">
|
|
42
|
-
<button id="memberBtn" class="px-3 py-2 rounded-lg bg-white text-slate-800 text-sm font-medium min-w-56 text-left flex items-center justify-between gap-2">
|
|
43
|
-
<span id="memberLabel">Tất cả thành viên</span><span class="text-slate-400">▾</span>
|
|
44
|
-
</button>
|
|
45
|
-
<div id="memberMenu" class="hidden absolute z-20 mt-1 w-72 max-h-72 overflow-auto bg-white text-slate-800 rounded-lg shadow-xl border border-slate-200 p-2">
|
|
46
|
-
<div class="flex gap-2 mb-2">
|
|
47
|
-
<button id="mAll" class="text-xs px-2 py-1 rounded bg-slate-100 hover:bg-slate-200">Chọn tất cả</button>
|
|
48
|
-
<button id="mNone" class="text-xs px-2 py-1 rounded bg-slate-100 hover:bg-slate-200">Bỏ chọn</button>
|
|
49
|
-
</div>
|
|
50
|
-
<div id="memberList" class="space-y-1"></div>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
<button id="toggleAll" class="px-4 py-2 rounded-lg bg-white text-fptBlue font-semibold text-sm hover:bg-white/90">Mở tất cả</button>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
</header>
|
|
57
|
-
|
|
58
|
-
<main class="max-w-7xl mx-auto px-6 py-8">
|
|
59
|
-
<div id="warnBox" class="hidden mb-6 rounded-xl border border-amber-300 bg-amber-50 text-amber-900 px-4 py-3 text-sm"></div>
|
|
60
|
-
|
|
61
|
-
<section id="kpis" class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6"></section>
|
|
62
|
-
|
|
63
|
-
<section id="cards" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"></section>
|
|
64
|
-
|
|
65
|
-
<h2 class="mt-10 mb-3 text-lg font-bold text-fptBlue">Bảng tổng hợp</h2>
|
|
66
|
-
<div class="overflow-x-auto rounded-xl border border-slate-200 bg-white">
|
|
67
|
-
<table class="w-full text-sm">
|
|
68
|
-
<thead class="bg-slate-100 text-slate-600"><tr>
|
|
69
|
-
<th class="text-left px-4 py-2">Thành viên</th>
|
|
70
|
-
<th class="text-right px-4 py-2">Dự án</th>
|
|
71
|
-
<th class="text-right px-4 py-2">Commit</th>
|
|
72
|
-
<th class="text-right px-4 py-2">Thêm (+)</th>
|
|
73
|
-
<th class="text-right px-4 py-2">Xoá (−)</th>
|
|
74
|
-
</tr></thead>
|
|
75
|
-
<tbody id="summaryBody"></tbody>
|
|
76
|
-
<tfoot class="font-bold bg-slate-50 border-t-2 border-slate-200"><tr id="summaryTotal"></tr></tfoot>
|
|
77
|
-
</table>
|
|
78
|
-
</div>
|
|
79
|
-
</main>
|
|
80
|
-
|
|
81
|
-
<script>
|
|
82
|
-
const PAYLOAD = ${payload};
|
|
83
|
-
const COMMITS = PAYLOAD.commits;
|
|
84
|
-
const META = PAYLOAD.meta;
|
|
85
|
-
const nf = new Intl.NumberFormat('vi-VN');
|
|
86
|
-
const PRESETS = [1,2,3,4,5,7,14,30];
|
|
87
|
-
|
|
88
|
-
let expanded = false;
|
|
89
|
-
let range = { from: -Infinity, to: Infinity, label: 'Toàn bộ lịch sử' };
|
|
90
|
-
let selected = new Set(); // rỗng = tất cả
|
|
91
|
-
|
|
92
|
-
function initials(n){ return (n||'?').trim().split(/\\s+/).map(w=>w[0]).slice(0,2).join('').toUpperCase(); }
|
|
93
|
-
|
|
94
|
-
// Danh sách thành viên (từ toàn bộ commit) để đổ vào dropdown
|
|
95
|
-
const ALL_MEMBERS = (() => {
|
|
96
|
-
const m = new Map();
|
|
97
|
-
for (const c of COMMITS){ if(!m.has(c.e)) m.set(c.e, c.n); else m.set(c.e, c.n); }
|
|
98
|
-
return [...m.entries()].map(([e,n])=>({email:e,name:n})).sort((a,b)=>a.name.localeCompare(b.name));
|
|
99
|
-
})();
|
|
100
|
-
|
|
101
|
-
// Tổng hợp theo range + selected
|
|
102
|
-
function aggregate(){
|
|
103
|
-
const byEmail = new Map();
|
|
104
|
-
const projectSet = new Set();
|
|
105
|
-
for (const c of COMMITS){
|
|
106
|
-
if (c.t < range.from || c.t > range.to) continue;
|
|
107
|
-
if (selected.size && !selected.has(c.e)) continue;
|
|
108
|
-
projectSet.add(c.r);
|
|
109
|
-
if (!byEmail.has(c.e)) byEmail.set(c.e, { email:c.e, name:c.n, commits:0, additions:0, deletions:0, projects:new Map() });
|
|
110
|
-
const m = byEmail.get(c.e);
|
|
111
|
-
m.commits++; m.additions+=c.a; m.deletions+=c.d; if(c.n) m.name=c.n;
|
|
112
|
-
if(!m.projects.has(c.r)) m.projects.set(c.r,{name:c.r,commits:0,additions:0,deletions:0});
|
|
113
|
-
const p=m.projects.get(c.r); p.commits++; p.additions+=c.a; p.deletions+=c.d;
|
|
114
|
-
}
|
|
115
|
-
const members=[...byEmail.values()].map(m=>({...m,projects:[...m.projects.values()].sort((a,b)=>b.commits-a.commits)})).sort((a,b)=>b.commits-a.commits);
|
|
116
|
-
const totals=members.reduce((s,m)=>{s.commits+=m.commits;s.additions+=m.additions;s.deletions+=m.deletions;return s;},{members:members.length,projects:projectSet.size,commits:0,additions:0,deletions:0});
|
|
117
|
-
return {members,totals};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function kpi(label, value, sub, color){
|
|
121
|
-
return \`<div class="rounded-xl bg-white border border-slate-200 p-4 shadow-sm">
|
|
122
|
-
<div class="text-xs uppercase tracking-wide text-slate-500">\${label}</div>
|
|
123
|
-
<div class="mt-1 text-2xl font-bold \${color||''} tabular-nums">\${value}</div>
|
|
124
|
-
<div class="text-xs text-slate-400 mt-0.5">\${sub||''}</div>
|
|
125
|
-
</div>\`;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function projectRow(p){
|
|
129
|
-
return \`<div class="flex items-center justify-between text-sm py-1 border-b border-slate-100 last:border-0">
|
|
130
|
-
<span class="text-fptBlue font-medium truncate mr-2">\${p.name}</span>
|
|
131
|
-
<span class="shrink-0 tabular-nums text-slate-500">\${nf.format(p.commits)} commit ·
|
|
132
|
-
<span class="text-fptGreen">+\${nf.format(p.additions)}</span>
|
|
133
|
-
<span class="text-fptRed">−\${nf.format(p.deletions)}</span></span></div>\`;
|
|
134
|
-
}
|
|
135
|
-
function card(m){
|
|
136
|
-
return \`<article class="member-card bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
|
|
137
|
-
<header class="flex items-center gap-3 cursor-pointer select-none toggle-card">
|
|
138
|
-
<div class="w-10 h-10 rounded-full bg-fptOrange text-white grid place-items-center font-bold">\${initials(m.name)}</div>
|
|
139
|
-
<div class="min-w-0"><div class="font-semibold truncate">\${m.name}</div><div class="text-xs text-slate-500 truncate">\${m.email}</div></div>
|
|
140
|
-
</header>
|
|
141
|
-
<div class="mt-3 flex gap-4 text-sm"><span><b>\${nf.format(m.commits)}</b> commit</span>
|
|
142
|
-
<span class="text-fptGreen">+\${nf.format(m.additions)}</span>
|
|
143
|
-
<span class="text-fptRed">−\${nf.format(m.deletions)}</span>
|
|
144
|
-
<span class="text-slate-400">\${m.projects.length} dự án</span></div>
|
|
145
|
-
<div class="projects mt-3 \${expanded?'':'hidden'}">\${m.projects.map(projectRow).join('')}</div></article>\`;
|
|
146
|
-
}
|
|
147
|
-
function summaryRow(m){
|
|
148
|
-
return \`<tr class="border-t border-slate-100">
|
|
149
|
-
<td class="px-4 py-2">\${m.name} <span class="text-slate-400 text-xs">\${m.email}</span></td>
|
|
150
|
-
<td class="px-4 py-2 text-right">\${m.projects.length}</td>
|
|
151
|
-
<td class="px-4 py-2 text-right tabular-nums">\${nf.format(m.commits)}</td>
|
|
152
|
-
<td class="px-4 py-2 text-right tabular-nums text-fptGreen">+\${nf.format(m.additions)}</td>
|
|
153
|
-
<td class="px-4 py-2 text-right tabular-nums text-fptRed">−\${nf.format(m.deletions)}</td></tr>\`;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function render(){
|
|
157
|
-
const {members,totals}=aggregate();
|
|
158
|
-
const totalMembers = ALL_MEMBERS.length;
|
|
159
|
-
const filtering = selected.size > 0;
|
|
160
|
-
document.getElementById('meta').textContent =
|
|
161
|
-
\`\${range.label} · \${META.reposAvailable}/\${META.reposTotal} dự án đọc được\${filtering ? ' · đang lọc ' + selected.size + '/' + totalMembers + ' thành viên' : ''}\`;
|
|
162
|
-
|
|
163
|
-
document.getElementById('kpis').innerHTML = [
|
|
164
|
-
kpi('Thành viên', nf.format(totals.members), (filtering ? 'lọc ' + selected.size + '/' + totalMembers : 'trên tổng ' + totalMembers), 'text-fptBlue'),
|
|
165
|
-
kpi('Dự án có commit', nf.format(totals.projects), META.reposAvailable + '/' + META.reposTotal + ' đọc được', 'text-fptOrange'),
|
|
166
|
-
kpi('Commit', nf.format(totals.commits), range.label, 'text-slate-800'),
|
|
167
|
-
kpi('Dòng thay đổi', '<span class="text-fptGreen">+' + nf.format(totals.additions) + '</span> <span class="text-slate-300">/</span> <span class="text-fptRed">−' + nf.format(totals.deletions) + '</span>', 'add / del', ''),
|
|
168
|
-
].join('');
|
|
169
|
-
|
|
170
|
-
document.getElementById('cards').innerHTML = members.length ? members.map(card).join('')
|
|
171
|
-
: '<p class="text-slate-400 col-span-full">Không có dữ liệu trong khoảng/chọn hiện tại.</p>';
|
|
172
|
-
document.getElementById('summaryBody').innerHTML = members.map(summaryRow).join('');
|
|
173
|
-
document.getElementById('summaryTotal').innerHTML =
|
|
174
|
-
\`<td class="px-4 py-2">TỔNG</td><td class="px-4 py-2 text-right">—</td>
|
|
175
|
-
<td class="px-4 py-2 text-right tabular-nums">\${nf.format(totals.commits)}</td>
|
|
176
|
-
<td class="px-4 py-2 text-right tabular-nums text-fptGreen">+\${nf.format(totals.additions)}</td>
|
|
177
|
-
<td class="px-4 py-2 text-right tabular-nums text-fptRed">−\${nf.format(totals.deletions)}</td>\`;
|
|
178
|
-
document.querySelectorAll('.toggle-card').forEach(h=>h.addEventListener('click',()=>h.parentElement.querySelector('.projects').classList.toggle('hidden')));
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ── Bộ lọc thời gian ──
|
|
182
|
-
function setPreset(n){
|
|
183
|
-
const now=Math.floor(Date.now()/1000);
|
|
184
|
-
range={from:now-n*86400,to:now,label:(n===30?'1 tháng gần đây':n+' ngày gần đây')};
|
|
185
|
-
document.getElementById('from').value=''; document.getElementById('to').value='';
|
|
186
|
-
markPreset(n); render();
|
|
187
|
-
}
|
|
188
|
-
function setAllTime(){ range={from:-Infinity,to:Infinity,label:'Toàn bộ lịch sử'}; markPreset(null); render(); }
|
|
189
|
-
function setCustom(){
|
|
190
|
-
const f=document.getElementById('from').value, t=document.getElementById('to').value;
|
|
191
|
-
if(!f&&!t){ setAllTime(); return; }
|
|
192
|
-
const from=f?Math.floor(Date.parse(f+'T00:00:00')/1000):-Infinity;
|
|
193
|
-
const to=t?Math.floor(Date.parse(t+'T23:59:59')/1000):Math.floor(Date.now()/1000);
|
|
194
|
-
range={from,to,label:(f||'…')+' → '+(t||'nay')}; markPreset(null); render();
|
|
195
|
-
}
|
|
196
|
-
function markPreset(n){
|
|
197
|
-
document.querySelectorAll('#presets [data-p]').forEach(b=>{
|
|
198
|
-
const on = String(n)===b.dataset.p;
|
|
199
|
-
b.className='px-2.5 py-1.5 rounded-lg text-sm '+(on?'bg-white text-fptBlue font-semibold':'bg-white/20 text-white hover:bg-white/30');
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ── Dropdown thành viên (multi-select) ──
|
|
204
|
-
function renderMemberMenu(){
|
|
205
|
-
document.getElementById('memberList').innerHTML = ALL_MEMBERS.map(m=>\`
|
|
206
|
-
<label class="flex items-center gap-2 px-1 py-0.5 rounded hover:bg-slate-50 cursor-pointer text-sm">
|
|
207
|
-
<input type="checkbox" class="m-chk" value="\${m.email}" \${selected.has(m.email)?'checked':''}/>
|
|
208
|
-
<span class="truncate"><b>\${m.name}</b> <span class="text-slate-400 text-xs">\${m.email}</span></span>
|
|
209
|
-
</label>\`).join('');
|
|
210
|
-
document.querySelectorAll('.m-chk').forEach(chk=>chk.addEventListener('change',()=>{
|
|
211
|
-
if(chk.checked) selected.add(chk.value); else selected.delete(chk.value);
|
|
212
|
-
updateMemberLabel(); render();
|
|
213
|
-
}));
|
|
214
|
-
}
|
|
215
|
-
function updateMemberLabel(){
|
|
216
|
-
const el=document.getElementById('memberLabel');
|
|
217
|
-
el.textContent = selected.size===0 ? 'Tất cả thành viên'
|
|
218
|
-
: selected.size===1 ? (ALL_MEMBERS.find(m=>selected.has(m.email))||{}).name || '1 thành viên'
|
|
219
|
-
: selected.size+' thành viên';
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// init
|
|
223
|
-
function init(){
|
|
224
|
-
document.getElementById('presets').innerHTML =
|
|
225
|
-
'<button data-p="all" class="px-2.5 py-1.5 rounded-lg text-sm bg-white text-fptBlue font-semibold">Tất cả</button>' +
|
|
226
|
-
PRESETS.map(n=>\`<button data-p="\${n}" class="px-2.5 py-1.5 rounded-lg text-sm bg-white/20 text-white hover:bg-white/30">\${n===30?'1 tháng':n+'n'}</button>\`).join('');
|
|
227
|
-
document.querySelector('#presets [data-p="all"]').addEventListener('click',setAllTime);
|
|
228
|
-
PRESETS.forEach(n=>document.querySelector('#presets [data-p="'+n+'"]').addEventListener('click',()=>setPreset(n)));
|
|
229
|
-
document.getElementById('applyDates').addEventListener('click',setCustom);
|
|
230
|
-
|
|
231
|
-
const un=META.unavailable||[];
|
|
232
|
-
if(un.length){
|
|
233
|
-
const w=document.getElementById('warnBox'); w.classList.remove('hidden');
|
|
234
|
-
w.innerHTML='<b>⚠️ '+un.length+' dự án không thống kê được</b> (chưa clone / không đọc được — pan-stack là vỏ bọc, cần đủ quyền):<ul class="list-disc ml-5 mt-1">'+un.map(u=>'<li><b>'+u.name+'</b> — '+u.reason+'</li>').join('')+'</ul>';
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
document.getElementById('memberBtn').addEventListener('click',()=>document.getElementById('memberMenu').classList.toggle('hidden'));
|
|
238
|
-
document.addEventListener('click',(e)=>{ if(!e.target.closest('#memberBtn')&&!e.target.closest('#memberMenu')) document.getElementById('memberMenu').classList.add('hidden'); });
|
|
239
|
-
document.getElementById('mAll').addEventListener('click',()=>{selected=new Set(ALL_MEMBERS.map(m=>m.email));renderMemberMenu();updateMemberLabel();render();});
|
|
240
|
-
document.getElementById('mNone').addEventListener('click',()=>{selected=new Set();renderMemberMenu();updateMemberLabel();render();});
|
|
241
|
-
|
|
242
|
-
document.getElementById('toggleAll').addEventListener('click',(e)=>{
|
|
243
|
-
expanded=!expanded;
|
|
244
|
-
document.querySelectorAll('.projects').forEach(p=>p.classList.toggle('hidden',!expanded));
|
|
245
|
-
e.target.textContent=expanded?'Thu gọn tất cả':'Mở tất cả';
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// giá trị khởi tạo từ CLI
|
|
249
|
-
if(META.initialMembers && META.initialMembers.length){
|
|
250
|
-
for(const q of META.initialMembers){ const lo=q.toLowerCase(); ALL_MEMBERS.filter(m=>m.email.includes(lo)||m.name.toLowerCase().includes(lo)).forEach(m=>selected.add(m.email)); }
|
|
251
|
-
}
|
|
252
|
-
renderMemberMenu(); updateMemberLabel();
|
|
253
|
-
|
|
254
|
-
if(META.initialFrom||META.initialTo){ document.getElementById('from').value=META.initialFrom||''; document.getElementById('to').value=META.initialTo||''; setCustom(); }
|
|
255
|
-
else if(META.initialSince){ setPreset(META.initialSince); }
|
|
256
|
-
else { setAllTime(); }
|
|
257
|
-
}
|
|
258
|
-
init();
|
|
259
|
-
</script>
|
|
260
|
-
</body>
|
|
261
|
-
</html>`;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
module.exports = { buildReport };
|
package/lib/stats/review.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const { execFileSync } = require("child_process");
|
|
6
|
-
const { toGitDate } = require("./range");
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Xuất code change của 1 thành viên trong 1 dự án theo khoảng thời gian ra
|
|
10
|
-
* file Markdown để đưa AI review/chấm điểm.
|
|
11
|
-
* @param {{name,dir}} repo repo đã resolve (name + đường dẫn tuyệt đối)
|
|
12
|
-
* @param {string} member email hoặc tên (dùng làm --author regex của git)
|
|
13
|
-
* @param {{from,to,label}} range
|
|
14
|
-
* @param {{withDiff?:boolean, maxDiffLines?:number}} opts
|
|
15
|
-
* @returns {string} nội dung markdown
|
|
16
|
-
*/
|
|
17
|
-
function buildReviewMarkdown(repo, member, range, opts = {}) {
|
|
18
|
-
const { withDiff = true, maxDiffLines = 6000, allBranches = true } = opts;
|
|
19
|
-
const since = toGitDate(range.from);
|
|
20
|
-
const until = toGitDate(range.to);
|
|
21
|
-
|
|
22
|
-
// Mặc định quét mọi nhánh; tắt bằng { allBranches: false }.
|
|
23
|
-
const baseArgs = ["-C", repo.dir, "log", "--no-merges", `--author=${member}`];
|
|
24
|
-
if (allBranches) baseArgs.push("--all");
|
|
25
|
-
if (since) baseArgs.push(`--since=${since}`);
|
|
26
|
-
if (until) baseArgs.push(`--until=${until}`);
|
|
27
|
-
|
|
28
|
-
// 1) Danh sách commit + stat tóm tắt
|
|
29
|
-
const listRaw = run([
|
|
30
|
-
...baseArgs,
|
|
31
|
-
"--date=format:%Y-%m-%d %H:%M",
|
|
32
|
-
"--pretty=format:%h\x1f%ad\x1f%an\x1f%ae\x1f%s",
|
|
33
|
-
"--shortstat",
|
|
34
|
-
]);
|
|
35
|
-
const commits = parseListWithStat(listRaw);
|
|
36
|
-
|
|
37
|
-
const totalAdd = commits.reduce((s, c) => s + c.additions, 0);
|
|
38
|
-
const totalDel = commits.reduce((s, c) => s + c.deletions, 0);
|
|
39
|
-
|
|
40
|
-
const lines = [];
|
|
41
|
-
lines.push(`# Code review — ${repo.name}`);
|
|
42
|
-
lines.push("");
|
|
43
|
-
lines.push(`- **Thành viên:** ${member}`);
|
|
44
|
-
lines.push(`- **Khoảng thời gian:** ${range.label}`);
|
|
45
|
-
lines.push(`- **Dự án:** ${repo.name}`);
|
|
46
|
-
lines.push(`- **Sinh lúc:** ${new Date().toISOString()}`);
|
|
47
|
-
lines.push("");
|
|
48
|
-
lines.push(`## Tóm tắt`);
|
|
49
|
-
lines.push("");
|
|
50
|
-
lines.push(`- Tổng commit: **${commits.length}**`);
|
|
51
|
-
lines.push(`- Dòng thêm: **+${totalAdd}** · Dòng xoá: **−${totalDel}**`);
|
|
52
|
-
lines.push("");
|
|
53
|
-
|
|
54
|
-
if (commits.length === 0) {
|
|
55
|
-
lines.push("> Không có commit nào khớp thành viên/khoảng thời gian này.");
|
|
56
|
-
return lines.join("\n");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
lines.push(`## Danh sách commit`);
|
|
60
|
-
lines.push("");
|
|
61
|
-
lines.push("| Ngày | Commit | Mô tả | +/− |");
|
|
62
|
-
lines.push("|------|--------|-------|-----|");
|
|
63
|
-
for (const c of commits) {
|
|
64
|
-
lines.push(`| ${c.date} | \`${c.hash}\` | ${escapePipe(c.subject)} | +${c.additions}/−${c.deletions} |`);
|
|
65
|
-
}
|
|
66
|
-
lines.push("");
|
|
67
|
-
|
|
68
|
-
if (withDiff) {
|
|
69
|
-
lines.push(`## Nội dung thay đổi (diff)`);
|
|
70
|
-
lines.push("");
|
|
71
|
-
let emitted = 0;
|
|
72
|
-
for (const c of commits) {
|
|
73
|
-
if (emitted >= maxDiffLines) {
|
|
74
|
-
lines.push(`> …đã cắt bớt diff (vượt ${maxDiffLines} dòng). Dùng \`--no-diff\` hoặc thu hẹp khoảng thời gian.`);
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
const diff = run([
|
|
78
|
-
"-C",
|
|
79
|
-
repo.dir,
|
|
80
|
-
"show",
|
|
81
|
-
c.hash,
|
|
82
|
-
"--no-color",
|
|
83
|
-
"--pretty=format:",
|
|
84
|
-
]);
|
|
85
|
-
const body = diff.split("\n").slice(0, Math.max(0, maxDiffLines - emitted));
|
|
86
|
-
emitted += body.length;
|
|
87
|
-
lines.push(`### \`${c.hash}\` — ${c.subject}`);
|
|
88
|
-
lines.push("");
|
|
89
|
-
lines.push("```diff");
|
|
90
|
-
lines.push(body.join("\n").trim());
|
|
91
|
-
lines.push("```");
|
|
92
|
-
lines.push("");
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
lines.push(`---`);
|
|
97
|
-
lines.push("");
|
|
98
|
-
lines.push(`## Yêu cầu cho AI reviewer`);
|
|
99
|
-
lines.push("");
|
|
100
|
-
lines.push(`Hãy đánh giá phần code thay đổi ở trên và cho **điểm 1–10** kèm nhận xét theo các tiêu chí:`);
|
|
101
|
-
lines.push("");
|
|
102
|
-
lines.push(`1. **Tính đúng đắn** — logic có đúng, có bug tiềm ẩn không?`);
|
|
103
|
-
lines.push(`2. **Chất lượng code** — đặt tên, cấu trúc, trùng lặp, độ phức tạp.`);
|
|
104
|
-
lines.push(`3. **An toàn & xử lý lỗi** — validate input, edge case, rò rỉ dữ liệu.`);
|
|
105
|
-
lines.push(`4. **Kiểm thử** — có test kèm theo / có cần bổ sung không?`);
|
|
106
|
-
lines.push(`5. **Tính nhất quán** — bám theo quy ước dự án.`);
|
|
107
|
-
lines.push("");
|
|
108
|
-
lines.push(`Xuất kết quả: bảng điểm từng tiêu chí, điểm tổng, và top 3 điểm cần cải thiện.`);
|
|
109
|
-
|
|
110
|
-
return lines.join("\n");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function run(args) {
|
|
114
|
-
try {
|
|
115
|
-
return execFileSync("git", args, { encoding: "utf8", maxBuffer: 512 * 1024 * 1024 });
|
|
116
|
-
} catch (err) {
|
|
117
|
-
throw new Error(`git lỗi: ${(err.message || "").split("\n")[0]}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Parse output của `git log --shortstat --pretty=hash..subject`. */
|
|
122
|
-
function parseListWithStat(raw) {
|
|
123
|
-
const commits = [];
|
|
124
|
-
let cur = null;
|
|
125
|
-
for (const line of raw.split("\n")) {
|
|
126
|
-
if (line.includes("\x1f")) {
|
|
127
|
-
if (cur) commits.push(cur);
|
|
128
|
-
const [hash, date, name, email, subject] = line.split("\x1f");
|
|
129
|
-
cur = { hash, date, name, email, subject: subject || "", additions: 0, deletions: 0 };
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
const m = line.match(/(\d+) insertion[s]?\(\+\)|(\d+) deletion[s]?\(-\)/g);
|
|
133
|
-
if (m && cur) {
|
|
134
|
-
for (const part of m) {
|
|
135
|
-
const add = part.match(/(\d+) insertion/);
|
|
136
|
-
const del = part.match(/(\d+) deletion/);
|
|
137
|
-
if (add) cur.additions += parseInt(add[1], 10);
|
|
138
|
-
if (del) cur.deletions += parseInt(del[1], 10);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (cur) commits.push(cur);
|
|
143
|
-
return commits;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function escapePipe(s) {
|
|
147
|
-
return String(s || "").replace(/\|/g, "\\|");
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Ghi ra file, tạo thư mục nếu cần. */
|
|
151
|
-
function writeReview(outFile, content) {
|
|
152
|
-
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
153
|
-
fs.writeFileSync(outFile, content);
|
|
154
|
-
return outFile;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
module.exports = { buildReviewMarkdown, writeReview };
|
package/lib/stats/scan.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const { execFileSync } = require("child_process");
|
|
6
|
-
const { parseGitLog, PRETTY_FORMAT } = require("./parse");
|
|
7
|
-
const { toGitDate } = require("./range");
|
|
8
|
-
const { validateRepo } = require("../manifest");
|
|
9
|
-
const { discoverGitRepos } = require("../discover");
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Từ workspace (.project.yaml) → danh sách repo cần quét kèm đường dẫn tuyệt đối.
|
|
13
|
-
*/
|
|
14
|
-
function reposFromWorkspace(workspace) {
|
|
15
|
-
const out = [];
|
|
16
|
-
for (const repo of workspace.repositories) {
|
|
17
|
-
let name;
|
|
18
|
-
let repoPath;
|
|
19
|
-
try {
|
|
20
|
-
({ name, repoPath } = validateRepo(repo));
|
|
21
|
-
} catch {
|
|
22
|
-
continue; // entry lỗi trong yaml
|
|
23
|
-
}
|
|
24
|
-
out.push({ name, dir: path.resolve(workspace.root, repoPath) });
|
|
25
|
-
}
|
|
26
|
-
return out;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function isGitRepo(dir) {
|
|
30
|
-
try {
|
|
31
|
-
return fs.existsSync(path.join(dir, ".git"));
|
|
32
|
-
} catch {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Quét 1 repo trong khoảng thời gian. Trả trạng thái sẵn có (available/reason).
|
|
39
|
-
* pan-stack là vỏ bọc → repo có thể chưa clone / không đọc được.
|
|
40
|
-
*/
|
|
41
|
-
function scanRepo(repo, range, { allBranches = true } = {}) {
|
|
42
|
-
if (!fs.existsSync(repo.dir)) {
|
|
43
|
-
return { name: repo.name, available: false, reason: "chưa clone (không có thư mục)", commits: [] };
|
|
44
|
-
}
|
|
45
|
-
if (!isGitRepo(repo.dir)) {
|
|
46
|
-
return { name: repo.name, available: false, reason: "không phải git repo", commits: [] };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Mặc định `--all` → gộp commit trên MỌI nhánh (local + remote-tracking + tags),
|
|
50
|
-
// git tự dedupe theo hash. Tắt bằng { allBranches: false } (chỉ HEAD hiện tại).
|
|
51
|
-
const args = ["-C", repo.dir, "log", "--no-merges", "--numstat", "--date=unix", `--pretty=format:${PRETTY_FORMAT}`];
|
|
52
|
-
if (allBranches) args.push("--all");
|
|
53
|
-
const since = toGitDate(range.from);
|
|
54
|
-
const until = toGitDate(range.to);
|
|
55
|
-
if (since) args.push(`--since=${since}`);
|
|
56
|
-
if (until) args.push(`--until=${until}`);
|
|
57
|
-
|
|
58
|
-
let raw = "";
|
|
59
|
-
try {
|
|
60
|
-
raw = execFileSync("git", args, { encoding: "utf8", maxBuffer: 512 * 1024 * 1024 });
|
|
61
|
-
} catch (err) {
|
|
62
|
-
return {
|
|
63
|
-
name: repo.name,
|
|
64
|
-
available: false,
|
|
65
|
-
reason: `không đọc được: ${(err.message || "").split("\n")[0]}`,
|
|
66
|
-
commits: [],
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const commits = parseGitLog(raw).map((c) => ({ ...c, repoName: repo.name }));
|
|
71
|
-
return { name: repo.name, available: true, reason: "", commits };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Quét toàn bộ repo. Trả { commits, repoStatus } — repoStatus gồm cả repo lỗi.
|
|
76
|
-
*/
|
|
77
|
-
function scanAll(repos, range, { log = true, allBranches = true } = {}) {
|
|
78
|
-
const commits = [];
|
|
79
|
-
const repoStatus = [];
|
|
80
|
-
for (const repo of repos) {
|
|
81
|
-
const res = scanRepo(repo, range, { allBranches });
|
|
82
|
-
repoStatus.push({ name: res.name, available: res.available, reason: res.reason, commits: res.commits.length });
|
|
83
|
-
if (log) {
|
|
84
|
-
const tag = res.available ? `${res.commits.length} commit` : `⚠️ ${res.reason}`;
|
|
85
|
-
console.log(` • ${repo.name} ... ${tag}`);
|
|
86
|
-
}
|
|
87
|
-
commits.push(...res.commits);
|
|
88
|
-
}
|
|
89
|
-
return { commits, repoStatus };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
module.exports = { reposFromWorkspace, discoverGitRepos, scanRepo, scanAll, isGitRepo };
|
package/lib/stats/serve.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const http = require("http");
|
|
4
|
-
const { execFile } = require("child_process");
|
|
5
|
-
|
|
6
|
-
/** Serve HTML tại localhost và (tuỳ chọn) mở trình duyệt. */
|
|
7
|
-
function serveHtml(html, { port = 4321, open = true } = {}) {
|
|
8
|
-
const server = http.createServer((req, res) => {
|
|
9
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
10
|
-
res.end(html);
|
|
11
|
-
});
|
|
12
|
-
server.listen(port, () => {
|
|
13
|
-
const url = `http://localhost:${port}`;
|
|
14
|
-
console.log(`\n✅ Báo cáo đang chạy tại ${url} (Ctrl+C để dừng)`);
|
|
15
|
-
if (open) openBrowser(url);
|
|
16
|
-
});
|
|
17
|
-
return server;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function openBrowser(url) {
|
|
21
|
-
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
22
|
-
execFile(cmd, [url], () => {});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
module.exports = { serveHtml };
|