gitfamiliar 0.9.0 → 0.10.0
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/dist/bin/gitfamiliar.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
processBatch,
|
|
9
9
|
resolveUser,
|
|
10
10
|
walkFiles
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-6XUJIHN3.js";
|
|
12
12
|
|
|
13
13
|
// src/cli/index.ts
|
|
14
14
|
import { createRequire } from "module";
|
|
@@ -25,7 +25,7 @@ var DEFAULT_EXPIRATION = {
|
|
|
25
25
|
|
|
26
26
|
// src/cli/options.ts
|
|
27
27
|
function parseOptions(raw, repoPath) {
|
|
28
|
-
const mode = validateMode(raw.mode || "
|
|
28
|
+
const mode = validateMode(raw.mode || "committed");
|
|
29
29
|
let weights = DEFAULT_WEIGHTS;
|
|
30
30
|
if (raw.weights) {
|
|
31
31
|
weights = parseWeights(raw.weights);
|
|
@@ -45,7 +45,8 @@ function parseOptions(raw, repoPath) {
|
|
|
45
45
|
hotspot = "personal";
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
const
|
|
48
|
+
const sinceRaw = raw.since || raw.window;
|
|
49
|
+
const sinceDays = sinceRaw ? parseInt(sinceRaw, 10) : void 0;
|
|
49
50
|
return {
|
|
50
51
|
mode,
|
|
51
52
|
user,
|
|
@@ -54,13 +55,20 @@ function parseOptions(raw, repoPath) {
|
|
|
54
55
|
weights,
|
|
55
56
|
repoPath,
|
|
56
57
|
team: raw.team || false,
|
|
57
|
-
|
|
58
|
+
contributorsPerFile: raw.contributorsPerFile || raw.contributors || raw.teamCoverage || false,
|
|
58
59
|
hotspot,
|
|
59
|
-
|
|
60
|
+
since: sinceDays
|
|
60
61
|
};
|
|
61
62
|
}
|
|
63
|
+
var MODE_ALIASES = {
|
|
64
|
+
binary: "committed",
|
|
65
|
+
authorship: "code-coverage"
|
|
66
|
+
};
|
|
62
67
|
function validateMode(mode) {
|
|
63
|
-
|
|
68
|
+
if (mode in MODE_ALIASES) {
|
|
69
|
+
return MODE_ALIASES[mode];
|
|
70
|
+
}
|
|
71
|
+
const valid = ["committed", "code-coverage", "weighted"];
|
|
64
72
|
if (!valid.includes(mode)) {
|
|
65
73
|
throw new Error(
|
|
66
74
|
`Invalid mode: "${mode}". Valid modes: ${valid.join(", ")}`
|
|
@@ -99,10 +107,10 @@ function formatPercent(score) {
|
|
|
99
107
|
}
|
|
100
108
|
function getModeLabel(mode) {
|
|
101
109
|
switch (mode) {
|
|
102
|
-
case "
|
|
103
|
-
return "
|
|
104
|
-
case "
|
|
105
|
-
return "
|
|
110
|
+
case "committed":
|
|
111
|
+
return "Committed mode";
|
|
112
|
+
case "code-coverage":
|
|
113
|
+
return "Code Coverage mode";
|
|
106
114
|
case "weighted":
|
|
107
115
|
return "Weighted mode";
|
|
108
116
|
default:
|
|
@@ -129,7 +137,7 @@ function renderFolder(node, indent, mode, maxDepth) {
|
|
|
129
137
|
NAME_COLUMN_WIDTH - prefixWidth - name.length
|
|
130
138
|
);
|
|
131
139
|
const padding = " ".repeat(padWidth);
|
|
132
|
-
if (mode === "
|
|
140
|
+
if (mode === "committed") {
|
|
133
141
|
const readCount = folder.readCount || 0;
|
|
134
142
|
lines.push(
|
|
135
143
|
`${prefix}${chalk.bold(name)}${padding} ${bar} ${pct.padStart(4)} (${readCount}/${folder.fileCount} files)`
|
|
@@ -153,7 +161,7 @@ function renderTerminal(result) {
|
|
|
153
161
|
chalk.bold(`GitFamiliar \u2014 ${repoName} (${getModeLabel(mode)})`)
|
|
154
162
|
);
|
|
155
163
|
console.log("");
|
|
156
|
-
if (mode === "
|
|
164
|
+
if (mode === "committed") {
|
|
157
165
|
const readCount = tree.readCount || 0;
|
|
158
166
|
const pct = formatPercent(tree.score);
|
|
159
167
|
console.log(`Overall: ${readCount}/${tree.fileCount} files (${pct})`);
|
|
@@ -167,7 +175,7 @@ function renderTerminal(result) {
|
|
|
167
175
|
console.log(line);
|
|
168
176
|
}
|
|
169
177
|
console.log("");
|
|
170
|
-
if (mode === "
|
|
178
|
+
if (mode === "committed") {
|
|
171
179
|
const { writtenCount } = result;
|
|
172
180
|
console.log(`Written: ${writtenCount} files`);
|
|
173
181
|
console.log("");
|
|
@@ -1087,7 +1095,7 @@ async function computeMultiUser(options) {
|
|
|
1087
1095
|
...options,
|
|
1088
1096
|
user: userName,
|
|
1089
1097
|
team: false,
|
|
1090
|
-
|
|
1098
|
+
contributorsPerFile: false
|
|
1091
1099
|
};
|
|
1092
1100
|
const result = await computeFamiliarity(userOptions);
|
|
1093
1101
|
results.push({ userName, result });
|
|
@@ -1209,10 +1217,10 @@ function formatPercent2(score) {
|
|
|
1209
1217
|
}
|
|
1210
1218
|
function getModeLabel2(mode) {
|
|
1211
1219
|
switch (mode) {
|
|
1212
|
-
case "
|
|
1213
|
-
return "
|
|
1214
|
-
case "
|
|
1215
|
-
return "
|
|
1220
|
+
case "committed":
|
|
1221
|
+
return "Committed mode";
|
|
1222
|
+
case "code-coverage":
|
|
1223
|
+
return "Code Coverage mode";
|
|
1216
1224
|
case "weighted":
|
|
1217
1225
|
return "Weighted mode";
|
|
1218
1226
|
default:
|
|
@@ -1257,7 +1265,7 @@ function renderMultiUserTerminal(result) {
|
|
|
1257
1265
|
const name = truncateName(summary.user.name, 14).padEnd(14);
|
|
1258
1266
|
const bar = makeBar2(summary.overallScore);
|
|
1259
1267
|
const pct = formatPercent2(summary.overallScore);
|
|
1260
|
-
if (mode === "
|
|
1268
|
+
if (mode === "committed") {
|
|
1261
1269
|
console.log(
|
|
1262
1270
|
` ${name} ${bar} ${pct.padStart(4)} (${summary.writtenCount}/${totalFiles} files)`
|
|
1263
1271
|
);
|
|
@@ -1627,7 +1635,7 @@ async function computeHotspots(options) {
|
|
|
1627
1635
|
const repoRoot = await gitClient.getRepoRoot();
|
|
1628
1636
|
const filter = createFilter(repoRoot);
|
|
1629
1637
|
const tree = await buildFileTree(gitClient, filter);
|
|
1630
|
-
const timeWindow = options.
|
|
1638
|
+
const timeWindow = options.since || DEFAULT_WINDOW;
|
|
1631
1639
|
const isTeamMode = options.hotspot === "team";
|
|
1632
1640
|
const trackedFiles = /* @__PURE__ */ new Set();
|
|
1633
1641
|
walkFiles(tree, (f) => trackedFiles.add(f.path));
|
|
@@ -1649,7 +1657,7 @@ async function computeHotspots(options) {
|
|
|
1649
1657
|
const result = await computeFamiliarity({
|
|
1650
1658
|
...options,
|
|
1651
1659
|
team: false,
|
|
1652
|
-
|
|
1660
|
+
contributorsPerFile: false
|
|
1653
1661
|
});
|
|
1654
1662
|
userName = result.userName;
|
|
1655
1663
|
familiarityMap = /* @__PURE__ */ new Map();
|
|
@@ -2104,10 +2112,10 @@ async function generateAndOpenHotspotHTML(result, repoPath) {
|
|
|
2104
2112
|
// src/core/unified.ts
|
|
2105
2113
|
async function computeUnified(options) {
|
|
2106
2114
|
console.log("Computing unified dashboard data...");
|
|
2107
|
-
console.log(" [1/4] Scoring (
|
|
2108
|
-
const [
|
|
2109
|
-
computeFamiliarity({ ...options, mode: "
|
|
2110
|
-
computeFamiliarity({ ...options, mode: "
|
|
2115
|
+
console.log(" [1/4] Scoring (committed, code-coverage, weighted)...");
|
|
2116
|
+
const [committed, codeCoverage, weighted] = await Promise.all([
|
|
2117
|
+
computeFamiliarity({ ...options, mode: "committed" }),
|
|
2118
|
+
computeFamiliarity({ ...options, mode: "code-coverage" }),
|
|
2111
2119
|
computeFamiliarity({ ...options, mode: "weighted" })
|
|
2112
2120
|
]);
|
|
2113
2121
|
console.log(" [2/4] Team coverage...");
|
|
@@ -2138,9 +2146,9 @@ async function computeUnified(options) {
|
|
|
2138
2146
|
});
|
|
2139
2147
|
console.log("Done.");
|
|
2140
2148
|
return {
|
|
2141
|
-
repoName:
|
|
2142
|
-
userName:
|
|
2143
|
-
scoring: {
|
|
2149
|
+
repoName: committed.repoName,
|
|
2150
|
+
userName: committed.userName,
|
|
2151
|
+
scoring: { committed, codeCoverage, weighted },
|
|
2144
2152
|
coverage,
|
|
2145
2153
|
hotspot,
|
|
2146
2154
|
hotspotTeamFamiliarity,
|
|
@@ -2152,8 +2160,10 @@ async function computeUnified(options) {
|
|
|
2152
2160
|
import { writeFileSync as writeFileSync5 } from "fs";
|
|
2153
2161
|
import { join as join5 } from "path";
|
|
2154
2162
|
function generateUnifiedHTML(data) {
|
|
2155
|
-
const
|
|
2156
|
-
const
|
|
2163
|
+
const scoringCommittedJson = JSON.stringify(data.scoring.committed.tree);
|
|
2164
|
+
const scoringCodeCoverageJson = JSON.stringify(
|
|
2165
|
+
data.scoring.codeCoverage.tree
|
|
2166
|
+
);
|
|
2157
2167
|
const scoringWeightedJson = JSON.stringify(data.scoring.weighted.tree);
|
|
2158
2168
|
const coverageTreeJson = JSON.stringify(data.coverage.tree);
|
|
2159
2169
|
const coverageRiskJson = JSON.stringify(data.coverage.riskFiles);
|
|
@@ -2530,12 +2540,12 @@ function generateUnifiedHTML(data) {
|
|
|
2530
2540
|
<body>
|
|
2531
2541
|
<div id="header">
|
|
2532
2542
|
<h1>GitFamiliar \u2014 ${data.repoName}</h1>
|
|
2533
|
-
<div class="info">${data.userName} | ${data.scoring.
|
|
2543
|
+
<div class="info">${data.userName} | ${data.scoring.committed.totalFiles} files</div>
|
|
2534
2544
|
</div>
|
|
2535
2545
|
|
|
2536
2546
|
<div id="tabs">
|
|
2537
2547
|
<div class="tab active" onclick="switchTab('scoring')">Scoring</div>
|
|
2538
|
-
<div class="tab" onclick="switchTab('coverage')">
|
|
2548
|
+
<div class="tab" onclick="switchTab('coverage')">Contributors</div>
|
|
2539
2549
|
<div class="tab" onclick="switchTab('multiuser')">Multi-User</div>
|
|
2540
2550
|
<div class="tab" onclick="switchTab('hotspots')">Hotspots</div>
|
|
2541
2551
|
</div>
|
|
@@ -2544,7 +2554,7 @@ function generateUnifiedHTML(data) {
|
|
|
2544
2554
|
Your personal familiarity with each file, based on Git history. Larger blocks = more lines of code. Color shows how well you know each file.
|
|
2545
2555
|
</div>
|
|
2546
2556
|
<div id="tab-desc-coverage" class="tab-desc">
|
|
2547
|
-
|
|
2557
|
+
Contributors per file: how many people have committed to each file. Low contributor count = high bus factor risk.
|
|
2548
2558
|
</div>
|
|
2549
2559
|
<div id="tab-desc-multiuser" class="tab-desc">
|
|
2550
2560
|
Compare familiarity scores across team members. Select a user to see the codebase colored by their knowledge.
|
|
@@ -2554,9 +2564,9 @@ function generateUnifiedHTML(data) {
|
|
|
2554
2564
|
</div>
|
|
2555
2565
|
|
|
2556
2566
|
<div id="scoring-controls" class="visible">
|
|
2557
|
-
<button class="subtab active" onclick="switchScoringMode('
|
|
2558
|
-
<button class="subtab" onclick="switchScoringMode('
|
|
2559
|
-
<button class="subtab" onclick="switchScoringMode('weighted')">Weighted</button>
|
|
2567
|
+
<button class="subtab active" data-mode="committed" onclick="switchScoringMode('committed')">Committed</button>
|
|
2568
|
+
<button class="subtab" data-mode="code-coverage" onclick="switchScoringMode('code-coverage')">Code Coverage</button>
|
|
2569
|
+
<button class="subtab" data-mode="weighted" onclick="switchScoringMode('weighted')">Weighted</button>
|
|
2560
2570
|
<div id="weight-controls">
|
|
2561
2571
|
<span>Blame:</span>
|
|
2562
2572
|
<span class="weight-label" id="blame-label">50%</span>
|
|
@@ -2576,13 +2586,13 @@ function generateUnifiedHTML(data) {
|
|
|
2576
2586
|
|
|
2577
2587
|
<div id="hotspot-controls">
|
|
2578
2588
|
<label>Mode:</label>
|
|
2579
|
-
<button class="subtab active" onclick="switchHotspotMode('personal')">Personal</button>
|
|
2580
|
-
<button class="subtab" onclick="switchHotspotMode('team')">Team</button>
|
|
2589
|
+
<button class="subtab active" data-mode="personal" onclick="switchHotspotMode('personal')">Personal</button>
|
|
2590
|
+
<button class="subtab" data-mode="team" onclick="switchHotspotMode('team')">Team</button>
|
|
2581
2591
|
<span class="sep-v"></span>
|
|
2582
2592
|
<label>Scoring:</label>
|
|
2583
|
-
<button class="subtab hs-scoring active" onclick="switchHotspotScoring('
|
|
2584
|
-
<button class="subtab hs-scoring" onclick="switchHotspotScoring('
|
|
2585
|
-
<button class="subtab hs-scoring" onclick="switchHotspotScoring('weighted')">Weighted</button>
|
|
2593
|
+
<button class="subtab hs-scoring active" data-mode="committed" onclick="switchHotspotScoring('committed')">Committed</button>
|
|
2594
|
+
<button class="subtab hs-scoring" data-mode="code-coverage" onclick="switchHotspotScoring('code-coverage')">Code Coverage</button>
|
|
2595
|
+
<button class="subtab hs-scoring" data-mode="weighted" onclick="switchHotspotScoring('weighted')">Weighted</button>
|
|
2586
2596
|
</div>
|
|
2587
2597
|
|
|
2588
2598
|
<div id="breadcrumb"><span onclick="zoomTo('')">root</span></div>
|
|
@@ -2632,8 +2642,8 @@ function generateUnifiedHTML(data) {
|
|
|
2632
2642
|
<script>
|
|
2633
2643
|
// \u2500\u2500 Data \u2500\u2500
|
|
2634
2644
|
const scoringData = {
|
|
2635
|
-
|
|
2636
|
-
|
|
2645
|
+
committed: ${scoringCommittedJson},
|
|
2646
|
+
'code-coverage': ${scoringCodeCoverageJson},
|
|
2637
2647
|
weighted: ${scoringWeightedJson},
|
|
2638
2648
|
};
|
|
2639
2649
|
const coverageData = ${coverageTreeJson};
|
|
@@ -2646,14 +2656,14 @@ const multiUserSummaries = ${multiUserSummariesJson};
|
|
|
2646
2656
|
|
|
2647
2657
|
// \u2500\u2500 State \u2500\u2500
|
|
2648
2658
|
let activeTab = 'scoring';
|
|
2649
|
-
let scoringMode = '
|
|
2659
|
+
let scoringMode = 'committed';
|
|
2650
2660
|
let blameWeight = 0.5;
|
|
2651
2661
|
let scoringPath = '';
|
|
2652
2662
|
let coveragePath = '';
|
|
2653
2663
|
let multiuserPath = '';
|
|
2654
2664
|
let currentUser = 0;
|
|
2655
2665
|
let hotspotMode = 'personal';
|
|
2656
|
-
let hotspotScoring = '
|
|
2666
|
+
let hotspotScoring = 'committed';
|
|
2657
2667
|
const rendered = { scoring: false, coverage: false, hotspots: false, multiuser: false };
|
|
2658
2668
|
|
|
2659
2669
|
// \u2500\u2500 Hotspot recalculation utilities \u2500\u2500
|
|
@@ -2668,8 +2678,8 @@ function extractFlatScores(node) {
|
|
|
2668
2678
|
}
|
|
2669
2679
|
|
|
2670
2680
|
const personalScores = {
|
|
2671
|
-
|
|
2672
|
-
|
|
2681
|
+
committed: extractFlatScores(scoringData.committed),
|
|
2682
|
+
'code-coverage': extractFlatScores(scoringData['code-coverage']),
|
|
2673
2683
|
weighted: extractFlatScores(scoringData.weighted),
|
|
2674
2684
|
};
|
|
2675
2685
|
|
|
@@ -2691,7 +2701,7 @@ function recalculateHotspotData() {
|
|
|
2691
2701
|
function switchHotspotMode(mode) {
|
|
2692
2702
|
hotspotMode = mode;
|
|
2693
2703
|
document.querySelectorAll('#hotspot-controls .subtab:not(.hs-scoring)').forEach(el => {
|
|
2694
|
-
el.classList.toggle('active', el.
|
|
2704
|
+
el.classList.toggle('active', el.dataset.mode === mode);
|
|
2695
2705
|
});
|
|
2696
2706
|
// Disable scoring buttons in team mode
|
|
2697
2707
|
const isTeam = mode === 'team';
|
|
@@ -2706,7 +2716,7 @@ function switchHotspotScoring(mode) {
|
|
|
2706
2716
|
if (hotspotMode === 'team') return;
|
|
2707
2717
|
hotspotScoring = mode;
|
|
2708
2718
|
document.querySelectorAll('#hotspot-controls .hs-scoring').forEach(el => {
|
|
2709
|
-
el.classList.toggle('active', el.
|
|
2719
|
+
el.classList.toggle('active', el.dataset.mode === mode);
|
|
2710
2720
|
});
|
|
2711
2721
|
renderHotspot();
|
|
2712
2722
|
renderHotspotSidebar();
|
|
@@ -2806,8 +2816,8 @@ function truncateLabel(name, w, h) {
|
|
|
2806
2816
|
|
|
2807
2817
|
// \u2500\u2500 Tab switching \u2500\u2500
|
|
2808
2818
|
const modeDescriptions = {
|
|
2809
|
-
|
|
2810
|
-
|
|
2819
|
+
committed: 'Committed: Have you ever committed to this file? Yes (green) or No (red).',
|
|
2820
|
+
'code-coverage': 'Code Coverage: How much of the current code did you write? Based on git blame line ownership.',
|
|
2811
2821
|
weighted: 'Weighted: Combines blame ownership and commit history with adjustable weights. Use the sliders to tune.',
|
|
2812
2822
|
};
|
|
2813
2823
|
|
|
@@ -2883,7 +2893,7 @@ function switchScoringMode(mode) {
|
|
|
2883
2893
|
scoringPath = '';
|
|
2884
2894
|
updateBreadcrumb('');
|
|
2885
2895
|
document.querySelectorAll('#scoring-controls .subtab').forEach(el => {
|
|
2886
|
-
el.classList.toggle('active', el.
|
|
2896
|
+
el.classList.toggle('active', el.dataset.mode === mode);
|
|
2887
2897
|
});
|
|
2888
2898
|
document.getElementById('weight-controls').classList.toggle('visible', mode === 'weighted');
|
|
2889
2899
|
document.getElementById('mode-desc-text').textContent = modeDescriptions[mode];
|
|
@@ -3337,8 +3347,8 @@ function createProgram() {
|
|
|
3337
3347
|
const program2 = new Command();
|
|
3338
3348
|
program2.name("gitfamiliar").description("Visualize your code familiarity from Git history").version(pkg.version).option(
|
|
3339
3349
|
"-m, --mode <mode>",
|
|
3340
|
-
"Scoring mode:
|
|
3341
|
-
"
|
|
3350
|
+
"Scoring mode: committed, code-coverage, weighted",
|
|
3351
|
+
"committed"
|
|
3342
3352
|
).option(
|
|
3343
3353
|
"-u, --user <user>",
|
|
3344
3354
|
"Git user name or email (repeatable for comparison)",
|
|
@@ -3352,18 +3362,15 @@ function createProgram() {
|
|
|
3352
3362
|
"-w, --weights <weights>",
|
|
3353
3363
|
'Weights for weighted mode: blame,commit (e.g., "0.5,0.5")'
|
|
3354
3364
|
).option("--team", "Compare all contributors", false).option(
|
|
3355
|
-
"--
|
|
3356
|
-
"
|
|
3365
|
+
"--contributors-per-file",
|
|
3366
|
+
"Analyze number of contributors per file (bus factor)",
|
|
3357
3367
|
false
|
|
3358
|
-
).option("--hotspot [mode]", "Hotspot analysis: personal (default) or team").option(
|
|
3359
|
-
"--window <days>",
|
|
3360
|
-
"Time window for hotspot analysis in days (default: 90)"
|
|
3361
|
-
).action(async (rawOptions) => {
|
|
3368
|
+
).option("--contributors", "Alias for --contributors-per-file").option("--team-coverage", "Deprecated alias for --contributors-per-file").option("--hotspot [mode]", "Hotspot analysis: personal (default) or team").option("--since <days>", "Hotspot analysis period in days (default: 90)").option("--window <days>", "Deprecated alias for --since").action(async (rawOptions) => {
|
|
3362
3369
|
try {
|
|
3363
3370
|
const repoPath = process.cwd();
|
|
3364
3371
|
const options = parseOptions(rawOptions, repoPath);
|
|
3365
3372
|
const isMultiUserCheck = options.team || Array.isArray(options.user) && options.user.length > 1;
|
|
3366
|
-
if (options.html && !options.hotspot && !options.
|
|
3373
|
+
if (options.html && !options.hotspot && !options.contributorsPerFile && !isMultiUserCheck) {
|
|
3367
3374
|
const data = await computeUnified(options);
|
|
3368
3375
|
await generateAndOpenUnifiedHTML(data, repoPath);
|
|
3369
3376
|
return;
|
|
@@ -3377,7 +3384,7 @@ function createProgram() {
|
|
|
3377
3384
|
}
|
|
3378
3385
|
return;
|
|
3379
3386
|
}
|
|
3380
|
-
if (options.
|
|
3387
|
+
if (options.contributorsPerFile) {
|
|
3381
3388
|
const result2 = await computeTeamCoverage(options);
|
|
3382
3389
|
if (options.html) {
|
|
3383
3390
|
await generateAndOpenCoverageHTML(result2, repoPath);
|